+ clients API

* /clients handler: new format
+ /clients/add handler
+ /clients/delete handler
+ /clients/update handler
This commit is contained in:
Simon Zolin 2019-04-26 15:10:29 +03:00
parent 6a7b1aba8b
commit 5fb7e44e79
2 changed files with 535 additions and 23 deletions

View File

@ -2,11 +2,14 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io/ioutil" "io/ioutil"
"net"
"net/http" "net/http"
"os" "os"
"runtime" "runtime"
"strings" "strings"
"sync"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
@ -14,20 +17,249 @@ import (
// Client information // Client information
type Client struct { type Client struct {
IP string IP string
MAC string
Name string Name string
//Source source // Hosts file / User settings / DHCP UseOwnSettings bool // false: use global settings
FilteringEnabled bool
SafeSearchEnabled bool
SafeBrowsingEnabled bool
ParentalEnabled bool
} }
type clientJSON struct { type clientJSON struct {
IP string `json:"ip"` IP string `json:"ip"`
MAC string `json:"mac"`
Name string `json:"name"` Name string `json:"name"`
UseGlobalSettings bool `json:"use_global_settings"`
FilteringEnabled bool `json:"filtering_enabled"`
ParentalEnabled bool `json:"parental_enabled"`
SafeSearchEnabled bool `json:"safebrowsing_enabled"`
SafeBrowsingEnabled bool `json:"safesearch_enabled"`
} }
var clients []Client type clientSource uint
var clientsFilled bool
const (
ClientSourceHostsFile clientSource = 0 // from /etc/hosts
ClientSourceRDNS clientSource = 1 // from rDNS
)
// ClientHost information
type ClientHost struct {
Host string
Source clientSource
}
type clientsContainer struct {
list map[string]*Client
ipIndex map[string]*Client
ipHost map[string]ClientHost // IP -> Hostname
lock sync.Mutex
}
var clients clientsContainer
// Initialize clients container
func clientsInit() {
if clients.list != nil {
log.Fatal("clients.list != nil")
}
clients.list = make(map[string]*Client)
clients.ipIndex = make(map[string]*Client)
clients.ipHost = make(map[string]ClientHost)
clientsAddFromHostsFile()
}
func clientsGetList() map[string]*Client {
return clients.list
}
func clientExists(ip string) bool {
clients.lock.Lock()
defer clients.lock.Unlock()
_, ok := clients.ipIndex[ip]
if ok {
return true
}
_, ok = clients.ipHost[ip]
return ok
}
// Search for a client by IP
func clientFind(ip string) (Client, bool) {
clients.lock.Lock()
defer clients.lock.Unlock()
c, ok := clients.ipIndex[ip]
if ok {
return *c, true
}
for _, c = range clients.list {
if len(c.MAC) != 0 {
mac, err := net.ParseMAC(c.MAC)
if err != nil {
continue
}
ipAddr := dhcpServer.FindIPbyMAC(mac)
if ipAddr == nil {
continue
}
if ip == ipAddr.String() {
return *c, true
}
}
}
return Client{}, false
}
// Check if Client object's fields are correct
func clientCheck(c *Client) error {
if len(c.Name) == 0 {
return fmt.Errorf("Invalid Name")
}
if (len(c.IP) == 0 && len(c.MAC) == 0) ||
(len(c.IP) != 0 && len(c.MAC) != 0) {
return fmt.Errorf("IP or MAC required")
}
if len(c.IP) != 0 {
ip := net.ParseIP(c.IP)
if ip == nil {
return fmt.Errorf("Invalid IP")
}
c.IP = ip.String()
} else {
_, err := net.ParseMAC(c.MAC)
if err != nil {
return fmt.Errorf("Invalid MAC: %s", err)
}
}
return nil
}
// Add a new client object
// Return true: success; false: client exists.
func clientAdd(c Client) (bool, error) {
e := clientCheck(&c)
if e != nil {
return false, e
}
clients.lock.Lock()
defer clients.lock.Unlock()
// check Name index
_, ok := clients.list[c.Name]
if ok {
return false, nil
}
// check IP index
if len(c.IP) != 0 {
c2, ok := clients.ipIndex[c.IP]
if ok {
return false, fmt.Errorf("Another client uses the same IP address: %s", c2.Name)
}
}
clients.list[c.Name] = &c
if len(c.IP) != 0 {
clients.ipIndex[c.IP] = &c
}
log.Tracef("'%s': '%s' | '%s' -> [%d]", c.Name, c.IP, c.MAC, len(clients.list))
return true, nil
}
// Remove a client
func clientDel(name string) bool {
clients.lock.Lock()
defer clients.lock.Unlock()
c, ok := clients.list[name]
if !ok {
return false
}
delete(clients.list, name)
delete(clients.ipIndex, c.IP)
return true
}
// Update a client
func clientUpdate(name string, c Client) error {
err := clientCheck(&c)
if err != nil {
return err
}
clients.lock.Lock()
defer clients.lock.Unlock()
old, ok := clients.list[name]
if !ok {
return fmt.Errorf("Client not found")
}
// check Name index
if old.Name != c.Name {
_, ok = clients.list[c.Name]
if ok {
return fmt.Errorf("Client already exists")
}
}
// check IP index
if old.IP != c.IP && len(c.IP) != 0 {
c2, ok := clients.ipIndex[c.IP]
if ok {
return fmt.Errorf("Another client uses the same IP address: %s", c2.Name)
}
}
// update Name index
if old.Name != c.Name {
delete(clients.list, old.Name)
}
clients.list[c.Name] = &c
// update IP index
if old.IP != c.IP {
delete(clients.ipIndex, old.IP)
}
if len(c.IP) != 0 {
clients.ipIndex[c.IP] = &c
}
return nil
}
func clientAddHost(ip, host string, source clientSource) (bool, error) {
clients.lock.Lock()
defer clients.lock.Unlock()
// check index
_, ok := clients.ipHost[ip]
if ok {
return false, nil
}
clients.ipHost[ip] = ClientHost{
Host: host,
Source: source,
}
log.Tracef("'%s': '%s' -> [%d]", host, ip, len(clients.ipHost))
return true, nil
}
// Parse system 'hosts' file and fill clients array // Parse system 'hosts' file and fill clients array
func fillClientInfo() { func clientsAddFromHostsFile() {
hostsFn := "/etc/hosts" hostsFn := "/etc/hosts"
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
hostsFn = os.ExpandEnv("$SystemRoot\\system32\\drivers\\etc\\hosts") hostsFn = os.ExpandEnv("$SystemRoot\\system32\\drivers\\etc\\hosts")
@ -40,6 +272,7 @@ func fillClientInfo() {
} }
lines := strings.Split(string(d), "\n") lines := strings.Split(string(d), "\n")
n := 0
for _, ln := range lines { for _, ln := range lines {
ln = strings.TrimSpace(ln) ln = strings.TrimSpace(ln)
if len(ln) == 0 || ln[0] == '#' { if len(ln) == 0 || ln[0] == '#' {
@ -51,33 +284,71 @@ func fillClientInfo() {
continue continue
} }
var c Client ok, e := clientAddHost(fields[0], fields[1], ClientSourceHostsFile)
c.IP = fields[0] if e != nil {
c.Name = fields[1] log.Tracef("%s", e)
clients = append(clients, c) }
log.Tracef("%s -> %s", c.IP, c.Name) if ok {
n++
}
} }
log.Info("Added %d client aliases from %s", len(clients), hostsFn) log.Info("Added %d client aliases from %s", n, hostsFn)
clientsFilled = true }
type clientHostJSON struct {
IP string `json:"ip"`
Name string `json:"name"`
Source string `json:"source"`
}
type clientListJSON struct {
Clients []clientJSON `json:"clients"`
AutoClients []clientHostJSON `json:"auto_clients"`
} }
// respond with information about configured clients // respond with information about configured clients
func handleGetClients(w http.ResponseWriter, r *http.Request) { func handleGetClients(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL) log.Tracef("%s %v", r.Method, r.URL)
if !clientsFilled { data := clientListJSON{}
fillClientInfo()
}
data := []clientJSON{} clients.lock.Lock()
for _, c := range clients { for _, c := range clients.list {
cj := clientJSON{ cj := clientJSON{
IP: c.IP, IP: c.IP,
MAC: c.MAC,
Name: c.Name, Name: c.Name,
UseGlobalSettings: !c.UseOwnSettings,
FilteringEnabled: c.FilteringEnabled,
ParentalEnabled: c.ParentalEnabled,
SafeSearchEnabled: c.SafeSearchEnabled,
SafeBrowsingEnabled: c.SafeBrowsingEnabled,
} }
data = append(data, cj)
if len(c.MAC) != 0 {
hwAddr, _ := net.ParseMAC(c.MAC)
ipAddr := dhcpServer.FindIPbyMAC(hwAddr)
if ipAddr != nil {
cj.IP = ipAddr.String()
} }
}
data.Clients = append(data.Clients, cj)
}
for ip, ch := range clients.ipHost {
cj := clientHostJSON{
IP: ip,
Name: ch.Host,
}
cj.Source = "etc/hosts"
if ch.Source == ClientSourceRDNS {
cj.Source = "rDNS"
}
data.AutoClients = append(data.AutoClients, cj)
}
clients.lock.Unlock()
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w).Encode(data) e := json.NewEncoder(w).Encode(data)
if e != nil { if e != nil {
@ -86,7 +357,126 @@ func handleGetClients(w http.ResponseWriter, r *http.Request) {
} }
} }
// Convert JSON object to Client object
func jsonToClient(cj clientJSON) (*Client, error) {
c := Client{
IP: cj.IP,
MAC: cj.MAC,
Name: cj.Name,
UseOwnSettings: !cj.UseGlobalSettings,
FilteringEnabled: cj.FilteringEnabled,
ParentalEnabled: cj.ParentalEnabled,
SafeSearchEnabled: cj.SafeSearchEnabled,
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
}
return &c, nil
}
// Add a new client
func handleAddClient(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
return
}
cj := clientJSON{}
err = json.Unmarshal(body, &cj)
if err != nil {
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
return
}
c, err := jsonToClient(cj)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
ok, err := clientAdd(*c)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
if !ok {
httpError(w, http.StatusBadRequest, "Client already exists")
return
}
_ = writeAllConfigsAndReloadDNS()
returnOK(w)
}
// Remove client
func handleDelClient(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
return
}
cj := clientJSON{}
err = json.Unmarshal(body, &cj)
if err != nil || len(cj.Name) == 0 {
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
return
}
if !clientDel(cj.Name) {
httpError(w, http.StatusBadRequest, "Client not found")
return
}
_ = writeAllConfigsAndReloadDNS()
returnOK(w)
}
type clientUpdateJSON struct {
Name string `json:"name"`
Data clientJSON `json:"data"`
}
// Update client's properties
func handleUpdateClient(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
return
}
var dj clientUpdateJSON
err = json.Unmarshal(body, &dj)
if err != nil {
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
return
}
if len(dj.Name) == 0 {
httpError(w, http.StatusBadRequest, "Invalid request")
return
}
c, err := jsonToClient(dj.Data)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
err = clientUpdate(dj.Name, *c)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
_ = writeAllConfigsAndReloadDNS()
returnOK(w)
}
// RegisterClientsHandlers registers HTTP handlers // RegisterClientsHandlers registers HTTP handlers
func RegisterClientsHandlers() { func RegisterClientsHandlers() {
http.HandleFunc("/control/clients", postInstall(optionalAuth(ensureGET(handleGetClients)))) http.HandleFunc("/control/clients", postInstall(optionalAuth(ensureGET(handleGetClients))))
http.HandleFunc("/control/clients/add", postInstall(optionalAuth(ensurePOST(handleAddClient))))
http.HandleFunc("/control/clients/delete", postInstall(optionalAuth(ensurePOST(handleDelClient))))
http.HandleFunc("/control/clients/update", postInstall(optionalAuth(ensurePOST(handleUpdateClient))))
} }

122
clients_test.go Normal file
View File

@ -0,0 +1,122 @@
package main
import "testing"
func TestClients(t *testing.T) {
var c Client
var e error
var b bool
clientsInit()
// add
c = Client{
IP: "1.1.1.1",
Name: "client1",
}
b, e = clientAdd(c)
if !b || e != nil {
t.Fatalf("clientAdd #1")
}
// add #2
c = Client{
IP: "2.2.2.2",
Name: "client2",
}
b, e = clientAdd(c)
if !b || e != nil {
t.Fatalf("clientAdd #2")
}
// failed add - name in use
c = Client{
IP: "1.2.3.5",
Name: "client1",
}
b, e = clientAdd(c)
if b {
t.Fatalf("clientAdd - name in use")
}
// failed add - ip in use
c = Client{
IP: "2.2.2.2",
Name: "client3",
}
b, e = clientAdd(c)
if b || e == nil {
t.Fatalf("clientAdd - ip in use")
}
// get
if clientExists("1.2.3.4") {
t.Fatalf("clientExists")
}
if !clientExists("1.1.1.1") {
t.Fatalf("clientExists #1")
}
if !clientExists("2.2.2.2") {
t.Fatalf("clientExists #2")
}
// failed update - no such name
c.IP = "1.2.3.0"
c.Name = "client3"
if clientUpdate("client3", c) == nil {
t.Fatalf("clientUpdate")
}
// failed update - name in use
c.IP = "1.2.3.0"
c.Name = "client2"
if clientUpdate("client1", c) == nil {
t.Fatalf("clientUpdate - name in use")
}
// failed update - ip in use
c.IP = "2.2.2.2"
c.Name = "client1"
if clientUpdate("client1", c) == nil {
t.Fatalf("clientUpdate - ip in use")
}
// update
c.IP = "1.1.1.2"
c.Name = "client1"
if clientUpdate("client1", c) != nil {
t.Fatalf("clientUpdate")
}
// get after update
if clientExists("1.1.1.1") || !clientExists("1.1.1.2") {
t.Fatalf("clientExists - get after update")
}
// failed remove - no such name
if clientDel("client3") {
t.Fatalf("clientDel - no such name")
}
// remove
if !clientDel("client1") || clientExists("1.1.1.2") {
t.Fatalf("clientDel")
}
// add host client
b, e = clientAddHost("1.1.1.1", "host", ClientSourceHostsFile)
if !b || e != nil {
t.Fatalf("clientAddHost")
}
// failed add - ip exists
b, e = clientAddHost("1.1.1.1", "host", ClientSourceHostsFile)
if b {
t.Fatalf("clientAddHost - ip exists")
}
// get
if !clientExists("1.1.1.1") {
t.Fatalf("clientAddHost")
}
}