diff --git a/AGHTechDoc.md b/AGHTechDoc.md index dafb44c0..93ae634c 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -21,6 +21,7 @@ Contents: * Add client * Update client * Delete client + * API: Find clients by IP * Enable DHCP server * "Show DHCP status" command * "Check DHCP" command @@ -618,8 +619,6 @@ Notes: * `name`, `ip` and `mac` values are unique. -* `ip` & `mac` values can't be set both at the same time. - * If `mac` is set and DHCP server is enabled, IP is taken from DHCP lease table. * If `use_global_settings` is true, then DNS responses for this client are processed and filtered using global settings. @@ -643,8 +642,7 @@ Response: clients: [ { name: "client1" - ip: "..." - mac: "..." + ids: ["...", ...] // IP, CIDR or MAC use_global_settings: true filtering_enabled: false parental_enabled: false @@ -682,8 +680,7 @@ Request: { name: "client1" - ip: "..." - mac: "..." + ids: ["...", ...] // IP, CIDR or MAC use_global_settings: true filtering_enabled: false parental_enabled: false @@ -712,8 +709,7 @@ Request: name: "client1" data: { name: "client1" - ip: "..." - mac: "..." + ids: ["...", ...] // IP, CIDR or MAC use_global_settings: true filtering_enabled: false parental_enabled: false @@ -752,6 +748,41 @@ Error response (Client not found): 400 +### API: Find clients by IP + +This method returns the list of clients (manual and auto-clients) matching the IP list. +For auto-clients only `name`, `ids` and `whois_info` fields are set. Other fields are empty. + +Request: + + GET /control/clients/find?ip0=...&ip1=...&ip2=... + +Response: + + 200 OK + + [ + { + "1.2.3.4": { + name: "client1" + ids: ["...", ...] // IP, CIDR or MAC + use_global_settings: true + filtering_enabled: false + parental_enabled: false + safebrowsing_enabled: false + safesearch_enabled: false + use_global_blocked_services: true + blocked_services: [ "name1", ... ] + whois_info: { + key: "value" + ... + } + } + } + ... + ] + + ## DNS access settings There are low-level settings that can block undesired DNS requests. "Blocking" means not responding to request. diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index 3163c753..1231063f 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -684,6 +684,21 @@ func (s *Server) FindIPbyMAC(mac net.HardwareAddr) net.IP { return nil } +// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases +func (s *Server) FindMACbyIP(ip net.IP) net.HardwareAddr { + now := time.Now().Unix() + + s.leasesLock.RLock() + defer s.leasesLock.RUnlock() + + for _, l := range s.leases { + if l.Expiry.Unix() > now && l.IP.Equal(ip) { + return l.HWAddr + } + } + return nil +} + // Reset internal state func (s *Server) reset() { s.leasesLock.Lock() diff --git a/home/clients.go b/home/clients.go index eacdadb9..2b92c4d2 100644 --- a/home/clients.go +++ b/home/clients.go @@ -1,11 +1,10 @@ package home import ( - "encoding/json" + "bytes" "fmt" "io/ioutil" "net" - "net/http" "os" "os/exec" "runtime" @@ -23,8 +22,7 @@ const ( // Client information type Client struct { - IP string - MAC string + IDs []string Name string UseOwnSettings bool // false: use global settings FilteringEnabled bool @@ -37,22 +35,6 @@ type Client struct { BlockedServices []string } -type clientJSON struct { - IP string `json:"ip"` - MAC string `json:"mac"` - 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"` - - WhoisInfo map[string]interface{} `json:"whois_info"` - - UseGlobalBlockedServices bool `json:"use_global_blocked_services"` - BlockedServices []string `json:"blocked_services"` -} - type clientSource uint // Client sources @@ -74,24 +56,79 @@ type ClientHost struct { type clientsContainer struct { list map[string]*Client // name -> client - ipIndex map[string]*Client // IP -> client + idIndex map[string]*Client // IP -> client ipHost map[string]*ClientHost // IP -> Hostname lock sync.Mutex } // Init initializes clients container // Note: this function must be called only once -func (clients *clientsContainer) Init() { +func (clients *clientsContainer) Init(objects []clientObject) { if clients.list != nil { log.Fatal("clients.list != nil") } clients.list = make(map[string]*Client) - clients.ipIndex = make(map[string]*Client) + clients.idIndex = make(map[string]*Client) clients.ipHost = make(map[string]*ClientHost) + clients.addFromConfig(objects) go clients.periodicUpdate() } +type clientObject struct { + Name string `yaml:"name"` + IDs []string `yaml:"ids"` + UseGlobalSettings bool `yaml:"use_global_settings"` + FilteringEnabled bool `yaml:"filtering_enabled"` + ParentalEnabled bool `yaml:"parental_enabled"` + SafeSearchEnabled bool `yaml:"safebrowsing_enabled"` + SafeBrowsingEnabled bool `yaml:"safesearch_enabled"` + + UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"` + BlockedServices []string `yaml:"blocked_services"` +} + +func (clients *clientsContainer) addFromConfig(objects []clientObject) { + for _, cy := range objects { + cli := Client{ + Name: cy.Name, + IDs: cy.IDs, + UseOwnSettings: !cy.UseGlobalSettings, + FilteringEnabled: cy.FilteringEnabled, + ParentalEnabled: cy.ParentalEnabled, + SafeSearchEnabled: cy.SafeSearchEnabled, + SafeBrowsingEnabled: cy.SafeBrowsingEnabled, + + UseOwnBlockedServices: !cy.UseGlobalBlockedServices, + BlockedServices: cy.BlockedServices, + } + _, err := clients.Add(cli) + if err != nil { + log.Tracef("clientAdd: %s", err) + } + } +} + +// WriteDiskConfig - write configuration +func (clients *clientsContainer) WriteDiskConfig(objects *[]clientObject) { + clientsList := clients.GetList() + for _, cli := range clientsList { + cy := clientObject{ + Name: cli.Name, + IDs: cli.IDs, + UseGlobalSettings: !cli.UseOwnSettings, + FilteringEnabled: cli.FilteringEnabled, + ParentalEnabled: cli.ParentalEnabled, + SafeSearchEnabled: cli.SafeSearchEnabled, + SafeBrowsingEnabled: cli.SafeBrowsingEnabled, + + UseGlobalBlockedServices: !cli.UseOwnBlockedServices, + BlockedServices: cli.BlockedServices, + } + *objects = append(*objects, cy) + } +} + func (clients *clientsContainer) periodicUpdate() { for { clients.addFromHostsFile() @@ -111,7 +148,7 @@ func (clients *clientsContainer) Exists(ip string, source clientSource) bool { clients.lock.Lock() defer clients.lock.Unlock() - _, ok := clients.ipIndex[ip] + _, ok := clients.idIndex[ip] if ok { return true } @@ -128,25 +165,42 @@ func (clients *clientsContainer) Exists(ip string, source clientSource) bool { // Find searches for a client by IP func (clients *clientsContainer) Find(ip string) (Client, bool) { + ipAddr := net.ParseIP(ip) + if ipAddr == nil { + return Client{}, false + } + clients.lock.Lock() defer clients.lock.Unlock() - c, ok := clients.ipIndex[ip] + c, ok := clients.idIndex[ip] if ok { return *c, true } for _, c = range clients.list { - if len(c.MAC) != 0 { - mac, err := net.ParseMAC(c.MAC) + for _, id := range c.IDs { + _, ipnet, err := net.ParseCIDR(id) if err != nil { continue } - ipAddr := config.dhcpServer.FindIPbyMAC(mac) - if ipAddr == nil { + if ipnet.Contains(ipAddr) { + return *c, true + } + } + } + + macFound := config.dhcpServer.FindMACbyIP(ipAddr) + if macFound == nil { + return Client{}, false + } + for _, c = range clients.list { + for _, id := range c.IDs { + hwAddr, err := net.ParseMAC(id) + if err != nil { continue } - if ip == ipAddr.String() { + if bytes.Equal(hwAddr, macFound) { return *c, true } } @@ -155,28 +209,51 @@ func (clients *clientsContainer) Find(ip string) (Client, bool) { return Client{}, false } +// FindAutoClient - search for an auto-client by IP +func (clients *clientsContainer) FindAutoClient(ip string) (ClientHost, bool) { + ipAddr := net.ParseIP(ip) + if ipAddr == nil { + return ClientHost{}, false + } + + clients.lock.Lock() + defer clients.lock.Unlock() + + ch, ok := clients.ipHost[ip] + if ok { + return *ch, true + } + return ClientHost{}, false +} + // Check if Client object's fields are correct func (c *Client) check() 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.IDs) == 0 { + return fmt.Errorf("ID required") } - if len(c.IP) != 0 { - ip := net.ParseIP(c.IP) - if ip == nil { - return fmt.Errorf("Invalid IP") + for i, id := range c.IDs { + ip := net.ParseIP(id) + if ip != nil { + c.IDs[i] = ip.String() // normalize IP address + continue } - c.IP = ip.String() - } else { - _, err := net.ParseMAC(c.MAC) - if err != nil { - return fmt.Errorf("Invalid MAC: %s", err) + + _, _, err := net.ParseCIDR(id) + if err == nil { + continue } + + _, err = net.ParseMAC(id) + if err == nil { + continue + } + + return fmt.Errorf("Invalid ID: %s", id) } return nil } @@ -198,26 +275,34 @@ func (clients *clientsContainer) Add(c Client) (bool, error) { return false, nil } - // check IP index - if len(c.IP) != 0 { - c2, ok := clients.ipIndex[c.IP] + // check ID index + for _, id := range c.IDs { + c2, ok := clients.idIndex[id] if ok { - return false, fmt.Errorf("Another client uses the same IP address: %s", c2.Name) + return false, fmt.Errorf("Another client uses the same ID (%s): %s", id, c2.Name) } } - ch, ok := clients.ipHost[c.IP] - if ok { - c.WhoisInfo = ch.WhoisInfo - delete(clients.ipHost, c.IP) + // remove auto-clients with the same IP address, keeping WHOIS info if possible + for _, id := range c.IDs { + ch, ok := clients.ipHost[id] + if ok { + if len(c.WhoisInfo) == 0 { + c.WhoisInfo = ch.WhoisInfo + } + delete(clients.ipHost, id) + } } + // update Name index clients.list[c.Name] = &c - if len(c.IP) != 0 { - clients.ipIndex[c.IP] = &c + + // update ID index + for _, id := range c.IDs { + clients.idIndex[id] = &c } - log.Tracef("'%s': '%s' | '%s' -> [%d]", c.Name, c.IP, c.MAC, len(clients.list)) + log.Tracef("'%s': ID:%v [%d]", c.Name, c.IDs, len(clients.list)) return true, nil } @@ -231,8 +316,26 @@ func (clients *clientsContainer) Del(name string) bool { return false } + // update Name index delete(clients.list, name) - delete(clients.ipIndex, c.IP) + + // update ID index + for _, id := range c.IDs { + delete(clients.idIndex, id) + } + return true +} + +// Return TRUE if arrays are equal +func arraysEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := 0; i != len(a); i++ { + if a[i] != b[i] { + return false + } + } return true } @@ -260,27 +363,30 @@ func (clients *clientsContainer) Update(name string, c Client) error { } // 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) + if !arraysEqual(old.IDs, c.IDs) { + for _, id := range c.IDs { + c2, ok := clients.idIndex[id] + if ok && c2 != old { + return fmt.Errorf("Another client uses the same ID (%s): %s", id, c2.Name) + } + } + + // update ID index + for _, id := range old.IDs { + delete(clients.idIndex, id) + } + for _, id := range c.IDs { + clients.idIndex[id] = old } } // 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 + clients.list[c.Name] = old } + *old = c return nil } @@ -289,7 +395,7 @@ func (clients *clientsContainer) SetWhoisInfo(ip string, info [][]string) { clients.lock.Lock() defer clients.lock.Unlock() - c, ok := clients.ipIndex[ip] + c, ok := clients.idIndex[ip] if ok { c.WhoisInfo = info log.Debug("Clients: set WHOIS info for client %s: %v", c.Name, c.WhoisInfo) @@ -319,7 +425,7 @@ func (clients *clientsContainer) AddHost(ip, host string, source clientSource) ( defer clients.lock.Unlock() // check index - _, ok := clients.ipIndex[ip] + _, ok := clients.idIndex[ip] if ok { return false, nil } @@ -440,210 +546,3 @@ func (clients *clientsContainer) addFromDHCP() { } log.Debug("Added %d client aliases from DHCP", n) } - -type clientHostJSON struct { - IP string `json:"ip"` - Name string `json:"name"` - Source string `json:"source"` - - WhoisInfo map[string]interface{} `json:"whois_info"` -} - -type clientListJSON struct { - Clients []clientJSON `json:"clients"` - AutoClients []clientHostJSON `json:"auto_clients"` -} - -// respond with information about configured clients -func handleGetClients(w http.ResponseWriter, r *http.Request) { - data := clientListJSON{} - - config.clients.lock.Lock() - for _, c := range config.clients.list { - cj := clientJSON{ - IP: c.IP, - MAC: c.MAC, - Name: c.Name, - UseGlobalSettings: !c.UseOwnSettings, - FilteringEnabled: c.FilteringEnabled, - ParentalEnabled: c.ParentalEnabled, - SafeSearchEnabled: c.SafeSearchEnabled, - SafeBrowsingEnabled: c.SafeBrowsingEnabled, - - UseGlobalBlockedServices: !c.UseOwnBlockedServices, - BlockedServices: c.BlockedServices, - } - - if len(c.MAC) != 0 { - hwAddr, _ := net.ParseMAC(c.MAC) - ipAddr := config.dhcpServer.FindIPbyMAC(hwAddr) - if ipAddr != nil { - cj.IP = ipAddr.String() - } - } - - cj.WhoisInfo = make(map[string]interface{}) - for _, wi := range c.WhoisInfo { - cj.WhoisInfo[wi[0]] = wi[1] - } - - data.Clients = append(data.Clients, cj) - } - for ip, ch := range config.clients.ipHost { - cj := clientHostJSON{ - IP: ip, - Name: ch.Host, - } - - cj.Source = "etc/hosts" - switch ch.Source { - case ClientSourceDHCP: - cj.Source = "DHCP" - case ClientSourceRDNS: - cj.Source = "rDNS" - case ClientSourceARP: - cj.Source = "ARP" - case ClientSourceWHOIS: - cj.Source = "WHOIS" - } - - cj.WhoisInfo = make(map[string]interface{}) - for _, wi := range ch.WhoisInfo { - cj.WhoisInfo[wi[0]] = wi[1] - } - - data.AutoClients = append(data.AutoClients, cj) - } - config.clients.lock.Unlock() - - w.Header().Set("Content-Type", "application/json") - e := json.NewEncoder(w).Encode(data) - if e != nil { - httpError(w, http.StatusInternalServerError, "Failed to encode to json: %v", e) - return - } -} - -// 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, - - UseOwnBlockedServices: !cj.UseGlobalBlockedServices, - BlockedServices: cj.BlockedServices, - } - return &c, nil -} - -// Add a new client -func handleAddClient(w http.ResponseWriter, r *http.Request) { - 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 := config.clients.Add(*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) { - 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 !config.clients.Del(cj.Name) { - httpError(w, http.StatusBadRequest, "Client not found") - return - } - - _ = writeAllConfigsAndReloadDNS() - returnOK(w) -} - -type updateJSON struct { - Name string `json:"name"` - Data clientJSON `json:"data"` -} - -// Update client's properties -func handleUpdateClient(w http.ResponseWriter, r *http.Request) { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - httpError(w, http.StatusBadRequest, "failed to read request body: %s", err) - return - } - - var dj updateJSON - 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 = config.clients.Update(dj.Name, *c) - if err != nil { - httpError(w, http.StatusBadRequest, "%s", err) - return - } - - _ = writeAllConfigsAndReloadDNS() - returnOK(w) -} - -// RegisterClientsHandlers registers HTTP handlers -func RegisterClientsHandlers() { - httpRegister(http.MethodGet, "/control/clients", handleGetClients) - httpRegister(http.MethodPost, "/control/clients/add", handleAddClient) - httpRegister(http.MethodPost, "/control/clients/delete", handleDelClient) - httpRegister(http.MethodPost, "/control/clients/update", handleUpdateClient) -} diff --git a/home/clients_http.go b/home/clients_http.go new file mode 100644 index 00000000..dbfbf873 --- /dev/null +++ b/home/clients_http.go @@ -0,0 +1,286 @@ +package home + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" +) + +type clientJSON struct { + IDs []string `json:"ids"` + 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"` + + WhoisInfo map[string]interface{} `json:"whois_info"` + + UseGlobalBlockedServices bool `json:"use_global_blocked_services"` + BlockedServices []string `json:"blocked_services"` +} + +type clientHostJSON struct { + IP string `json:"ip"` + Name string `json:"name"` + Source string `json:"source"` + + WhoisInfo map[string]interface{} `json:"whois_info"` +} + +type clientListJSON struct { + Clients []clientJSON `json:"clients"` + AutoClients []clientHostJSON `json:"auto_clients"` +} + +// respond with information about configured clients +func handleGetClients(w http.ResponseWriter, r *http.Request) { + data := clientListJSON{} + + config.clients.lock.Lock() + for _, c := range config.clients.list { + cj := clientToJSON(c) + data.Clients = append(data.Clients, cj) + } + for ip, ch := range config.clients.ipHost { + cj := clientHostJSON{ + IP: ip, + Name: ch.Host, + } + + cj.Source = "etc/hosts" + switch ch.Source { + case ClientSourceDHCP: + cj.Source = "DHCP" + case ClientSourceRDNS: + cj.Source = "rDNS" + case ClientSourceARP: + cj.Source = "ARP" + case ClientSourceWHOIS: + cj.Source = "WHOIS" + } + + cj.WhoisInfo = make(map[string]interface{}) + for _, wi := range ch.WhoisInfo { + cj.WhoisInfo[wi[0]] = wi[1] + } + + data.AutoClients = append(data.AutoClients, cj) + } + config.clients.lock.Unlock() + + w.Header().Set("Content-Type", "application/json") + e := json.NewEncoder(w).Encode(data) + if e != nil { + httpError(w, http.StatusInternalServerError, "Failed to encode to json: %v", e) + return + } +} + +// Convert JSON object to Client object +func jsonToClient(cj clientJSON) (*Client, error) { + c := Client{ + Name: cj.Name, + IDs: cj.IDs, + UseOwnSettings: !cj.UseGlobalSettings, + FilteringEnabled: cj.FilteringEnabled, + ParentalEnabled: cj.ParentalEnabled, + SafeSearchEnabled: cj.SafeSearchEnabled, + SafeBrowsingEnabled: cj.SafeBrowsingEnabled, + + UseOwnBlockedServices: !cj.UseGlobalBlockedServices, + BlockedServices: cj.BlockedServices, + } + return &c, nil +} + +// Convert Client object to JSON +func clientToJSON(c *Client) clientJSON { + cj := clientJSON{ + Name: c.Name, + IDs: c.IDs, + UseGlobalSettings: !c.UseOwnSettings, + FilteringEnabled: c.FilteringEnabled, + ParentalEnabled: c.ParentalEnabled, + SafeSearchEnabled: c.SafeSearchEnabled, + SafeBrowsingEnabled: c.SafeBrowsingEnabled, + + UseGlobalBlockedServices: !c.UseOwnBlockedServices, + BlockedServices: c.BlockedServices, + } + + cj.WhoisInfo = make(map[string]interface{}) + for _, wi := range c.WhoisInfo { + cj.WhoisInfo[wi[0]] = wi[1] + } + return cj +} + +type clientHostJSONWithID struct { + IDs []string `json:"ids"` + Name string `json:"name"` + WhoisInfo map[string]interface{} `json:"whois_info"` +} + +// Convert ClientHost object to JSON +func clientHostToJSON(ip string, ch ClientHost) clientHostJSONWithID { + cj := clientHostJSONWithID{ + Name: ch.Host, + IDs: []string{ip}, + } + + cj.WhoisInfo = make(map[string]interface{}) + for _, wi := range ch.WhoisInfo { + cj.WhoisInfo[wi[0]] = wi[1] + } + return cj +} + +// Add a new client +func handleAddClient(w http.ResponseWriter, r *http.Request) { + 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 := config.clients.Add(*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) { + 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 !config.clients.Del(cj.Name) { + httpError(w, http.StatusBadRequest, "Client not found") + return + } + + _ = writeAllConfigsAndReloadDNS() + returnOK(w) +} + +type updateJSON struct { + Name string `json:"name"` + Data clientJSON `json:"data"` +} + +// Update client's properties +func handleUpdateClient(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + httpError(w, http.StatusBadRequest, "failed to read request body: %s", err) + return + } + + var dj updateJSON + 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 = config.clients.Update(dj.Name, *c) + if err != nil { + httpError(w, http.StatusBadRequest, "%s", err) + return + } + + _ = writeAllConfigsAndReloadDNS() + returnOK(w) +} + +// Get the list of clients by IP address list +func handleFindClient(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + data := []map[string]interface{}{} + for i := 0; ; i++ { + ip := q.Get(fmt.Sprintf("ip%d", i)) + if len(ip) == 0 { + break + } + el := map[string]interface{}{} + c, ok := config.clients.Find(ip) + if !ok { + ch, ok := config.clients.FindAutoClient(ip) + if !ok { + continue // a client with this IP isn't found + } + cj := clientHostToJSON(ip, ch) + el[ip] = cj + + } else { + cj := clientToJSON(&c) + el[ip] = cj + } + + data = append(data, el) + } + + js, err := json.Marshal(data) + if err != nil { + httpError(w, http.StatusInternalServerError, "json.Marshal: %s", err) + return + } + + w.Header().Set("Content-Type", "application/json") + _, err = w.Write(js) + if err != nil { + httpError(w, http.StatusInternalServerError, "Couldn't write response: %s", err) + } +} + +// RegisterClientsHandlers registers HTTP handlers +func RegisterClientsHandlers() { + httpRegister("GET", "/control/clients", handleGetClients) + httpRegister("POST", "/control/clients/add", handleAddClient) + httpRegister("POST", "/control/clients/delete", handleDelClient) + httpRegister("POST", "/control/clients/update", handleUpdateClient) + httpRegister("GET", "/control/clients/find", handleFindClient) +} diff --git a/home/clients_test.go b/home/clients_test.go index f535d69f..70493a19 100644 --- a/home/clients_test.go +++ b/home/clients_test.go @@ -12,11 +12,11 @@ func TestClients(t *testing.T) { var b bool clients := clientsContainer{} - clients.Init() + clients.Init(nil) // add c = Client{ - IP: "1.1.1.1", + IDs: []string{"1.1.1.1", "aa:aa:aa:aa:aa:aa"}, Name: "client1", } b, e = clients.Add(c) @@ -26,7 +26,7 @@ func TestClients(t *testing.T) { // add #2 c = Client{ - IP: "2.2.2.2", + IDs: []string{"2.2.2.2"}, Name: "client2", } b, e = clients.Add(c) @@ -46,7 +46,7 @@ func TestClients(t *testing.T) { // failed add - name in use c = Client{ - IP: "1.2.3.5", + IDs: []string{"1.2.3.5"}, Name: "client1", } b, _ = clients.Add(c) @@ -56,7 +56,7 @@ func TestClients(t *testing.T) { // failed add - ip in use c = Client{ - IP: "2.2.2.2", + IDs: []string{"2.2.2.2"}, Name: "client3", } b, e = clients.Add(c) @@ -70,35 +70,45 @@ func TestClients(t *testing.T) { assert.True(t, clients.Exists("2.2.2.2", ClientSourceHostsFile)) // failed update - no such name - c.IP = "1.2.3.0" + c.IDs = []string{"1.2.3.0"} c.Name = "client3" if clients.Update("client3", c) == nil { t.Fatalf("Update") } // failed update - name in use - c.IP = "1.2.3.0" + c.IDs = []string{"1.2.3.0"} c.Name = "client2" if clients.Update("client1", c) == nil { t.Fatalf("Update - name in use") } // failed update - ip in use - c.IP = "2.2.2.2" + c.IDs = []string{"2.2.2.2"} c.Name = "client1" if clients.Update("client1", c) == nil { t.Fatalf("Update - ip in use") } // update - c.IP = "1.1.1.2" + c.IDs = []string{"1.1.1.2"} c.Name = "client1" if clients.Update("client1", c) != nil { t.Fatalf("Update") } // get after update - assert.True(t, !(clients.Exists("1.1.1.1", ClientSourceHostsFile) || !clients.Exists("1.1.1.2", ClientSourceHostsFile))) + assert.True(t, !clients.Exists("1.1.1.1", ClientSourceHostsFile)) + assert.True(t, clients.Exists("1.1.1.2", ClientSourceHostsFile)) + + // update - rename + c.IDs = []string{"1.1.1.2"} + c.Name = "client1-renamed" + c.UseOwnSettings = true + assert.True(t, clients.Update("client1", c) == nil) + c = Client{} + c, b = clients.Find("1.1.1.2") + assert.True(t, b && c.Name == "client1-renamed" && c.IDs[0] == "1.1.1.2" && c.UseOwnSettings) // failed remove - no such name if clients.Del("client3") { @@ -106,7 +116,7 @@ func TestClients(t *testing.T) { } // remove - assert.True(t, !(!clients.Del("client1") || clients.Exists("1.1.1.2", ClientSourceHostsFile))) + assert.True(t, !(!clients.Del("client1-renamed") || clients.Exists("1.1.1.2", ClientSourceHostsFile))) // add host client b, e = clients.AddHost("1.1.1.1", "host", ClientSourceARP) @@ -139,7 +149,7 @@ func TestClients(t *testing.T) { func TestClientsWhois(t *testing.T) { var c Client clients := clientsContainer{} - clients.Init() + clients.Init(nil) whois := [][]string{{"orgname", "orgname-val"}, {"country", "country-val"}} // set whois info on new client @@ -153,11 +163,11 @@ func TestClientsWhois(t *testing.T) { // set whois info on existing client c = Client{ - IP: "1.1.1.2", + IDs: []string{"1.1.1.2"}, Name: "client1", } _, _ = clients.Add(c) clients.SetWhoisInfo("1.1.1.2", whois) - assert.True(t, clients.ipIndex["1.1.1.2"].WhoisInfo[0][1] == "orgname-val") + assert.True(t, clients.idIndex["1.1.1.2"].WhoisInfo[0][1] == "orgname-val") _ = clients.Del("client1") } diff --git a/home/config.go b/home/config.go index 8c53574a..c7bbb269 100644 --- a/home/config.go +++ b/home/config.go @@ -30,20 +30,6 @@ type logSettings struct { Verbose bool `yaml:"verbose"` // If true, verbose logging is enabled } -type clientObject struct { - Name string `yaml:"name"` - IP string `yaml:"ip"` - MAC string `yaml:"mac"` - UseGlobalSettings bool `yaml:"use_global_settings"` - FilteringEnabled bool `yaml:"filtering_enabled"` - ParentalEnabled bool `yaml:"parental_enabled"` - SafeSearchEnabled bool `yaml:"safebrowsing_enabled"` - SafeBrowsingEnabled bool `yaml:"safesearch_enabled"` - - UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"` - BlockedServices []string `yaml:"blocked_services"` -} - type HTTPSServer struct { server *http.Server cond *sync.Cond // reacts to config.TLS.Enabled, PortHTTPS, CertificateChain and PrivateKey @@ -285,27 +271,6 @@ func parseConfig() error { config.DNS.FiltersUpdateIntervalHours = 24 } - for _, cy := range config.Clients { - cli := Client{ - Name: cy.Name, - IP: cy.IP, - MAC: cy.MAC, - UseOwnSettings: !cy.UseGlobalSettings, - FilteringEnabled: cy.FilteringEnabled, - ParentalEnabled: cy.ParentalEnabled, - SafeSearchEnabled: cy.SafeSearchEnabled, - SafeBrowsingEnabled: cy.SafeBrowsingEnabled, - - UseOwnBlockedServices: !cy.UseGlobalBlockedServices, - BlockedServices: cy.BlockedServices, - } - _, err = config.clients.Add(cli) - if err != nil { - log.Tracef("clientAdd: %s", err) - } - } - config.Clients = nil - status := tlsConfigStatus{} if !tlsLoadConfig(&config.TLS, &status) { log.Error("%s", status.WarningValidation) @@ -335,27 +300,7 @@ func (c *configuration) write() error { c.Lock() defer c.Unlock() - clientsList := config.clients.GetList() - for _, cli := range clientsList { - ip := cli.IP - if len(cli.MAC) != 0 { - ip = "" - } - cy := clientObject{ - Name: cli.Name, - IP: ip, - MAC: cli.MAC, - UseGlobalSettings: !cli.UseOwnSettings, - FilteringEnabled: cli.FilteringEnabled, - ParentalEnabled: cli.ParentalEnabled, - SafeSearchEnabled: cli.SafeSearchEnabled, - SafeBrowsingEnabled: cli.SafeBrowsingEnabled, - - UseGlobalBlockedServices: !cli.UseOwnBlockedServices, - BlockedServices: cli.BlockedServices, - } - config.Clients = append(config.Clients, cy) - } + config.clients.WriteDiskConfig(&config.Clients) if config.auth != nil { config.Users = config.auth.GetUsers() diff --git a/home/home.go b/home/home.go index 1569e796..f9257002 100644 --- a/home/home.go +++ b/home/home.go @@ -98,7 +98,6 @@ func run(args options) { }() initConfig() - config.clients.Init() initServices() if !config.firstRun { @@ -119,6 +118,9 @@ func run(args options) { } } + config.clients.Init(config.Clients) + config.Clients = nil + if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && config.RlimitNoFile != 0 { setRlimit(config.RlimitNoFile)