-
-
+
+
+
+
+
+
+
+
+ client_identifier
+
+
+
+
+ link
+ ,
+ ]}
+ >
+ client_identifier_desc
+
-
-
- {clientIdentifier === CLIENT_ID.IP && (
-
-
-
- )}
- {clientIdentifier === CLIENT_ID.MAC && (
-
-
-
- )}
-
-
-
-
-
-
-
- link
- ,
- ]}
- >
- client_identifier_desc
-
+
+
+
@@ -140,7 +168,11 @@ let Form = (props) => {
type="checkbox"
component={renderSelectField}
placeholder={t(setting.placeholder)}
- disabled={setting.name !== 'use_global_settings' ? useGlobalSettings : false}
+ disabled={
+ setting.name !== 'use_global_settings'
+ ? useGlobalSettings
+ : false
+ }
/>
))}
@@ -210,7 +242,13 @@ let Form = (props) => {
@@ -227,22 +265,20 @@ Form.propTypes = {
change: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
toggleClientModal: PropTypes.func.isRequired,
- clientIdentifier: PropTypes.string,
useGlobalSettings: PropTypes.bool,
useGlobalServices: PropTypes.bool,
t: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired,
+ invalid: PropTypes.bool.isRequired,
};
const selector = formValueSelector('clientForm');
Form = connect((state) => {
- const clientIdentifier = selector(state, 'identifier');
const useGlobalSettings = selector(state, 'use_global_settings');
const useGlobalServices = selector(state, 'use_global_blocked_services');
return {
- clientIdentifier,
useGlobalSettings,
useGlobalServices,
};
@@ -253,5 +289,6 @@ export default flow([
reduxForm({
form: 'clientForm',
enableReinitialize: true,
+ validate,
}),
])(Form);
diff --git a/client/src/components/ui/Icons.css b/client/src/components/ui/Icons.css
index 17d608fd..da2c5f4e 100644
--- a/client/src/components/ui/Icons.css
+++ b/client/src/components/ui/Icons.css
@@ -3,3 +3,8 @@
vertical-align: middle;
height: 100%;
}
+
+.icon--close {
+ width: 24px;
+ height: 24px;
+}
diff --git a/client/src/components/ui/Icons.js b/client/src/components/ui/Icons.js
index 100e74de..24d05e65 100644
--- a/client/src/components/ui/Icons.js
+++ b/client/src/components/ui/Icons.js
@@ -167,6 +167,14 @@ const Icons = () => (
+
+
+
+
+
+
+
+
);
diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js
index fc0286a1..55e0b0a0 100644
--- a/client/src/helpers/form.js
+++ b/client/src/helpers/form.js
@@ -29,6 +29,50 @@ export const renderField = ({
);
+export const renderGroupField = ({
+ input,
+ id,
+ className,
+ placeholder,
+ type,
+ disabled,
+ autoComplete,
+ isActionAvailable,
+ removeField,
+ meta: { touched, error },
+}) => (
+
+
+
+ {isActionAvailable &&
+
+
+
+ }
+
+
+ {!disabled &&
+ touched &&
+ (error && {error})}
+
+);
+
export const renderRadioField = ({
input, placeholder, disabled, meta: { touched, error },
}) => (
@@ -102,6 +146,7 @@ export const renderServiceField = ({
);
+// Validation functions
export const required = (value) => {
if (value || value === 0) {
return false;
diff --git a/client/src/helpers/formatClientCell.js b/client/src/helpers/formatClientCell.js
index 931210a7..c5626061 100644
--- a/client/src/helpers/formatClientCell.js
+++ b/client/src/helpers/formatClientCell.js
@@ -1,5 +1,5 @@
import React, { Fragment } from 'react';
-import { getClientInfo, normalizeWhois } from './helpers';
+import { normalizeWhois } from './helpers';
import { WHOIS_ICONS } from './constants';
const getFormattedWhois = (whois, t) => {
@@ -22,26 +22,29 @@ const getFormattedWhois = (whois, t) => {
);
};
-export const formatClientCell = (value, clients, autoClients, t) => {
- const clientInfo = getClientInfo(clients, value) || getClientInfo(autoClients, value);
- const { name, whois } = clientInfo;
+export const formatClientCell = (row, t) => {
+ const { value, original: { info } } = row;
let whoisContainer = '';
let nameContainer = value;
- if (name) {
- nameContainer = (
-
- {name} ({value})
-
- );
- }
+ if (info) {
+ const { name, whois } = info;
- if (whois) {
- whoisContainer = (
-
- {getFormattedWhois(whois, t)}
-
- );
+ if (name) {
+ nameContainer = (
+
+ {name} ({value})
+
+ );
+ }
+
+ if (whois) {
+ whoisContainer = (
+
+ {getFormattedWhois(whois, t)}
+
+ );
+ }
}
return (
diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js
index ced2e9ad..089f0604 100644
--- a/client/src/helpers/helpers.js
+++ b/client/src/helpers/helpers.js
@@ -8,6 +8,7 @@ import subDays from 'date-fns/sub_days';
import round from 'lodash/round';
import axios from 'axios';
import i18n from 'i18next';
+import uniqBy from 'lodash/uniqBy';
import versionCompare from './versionCompare';
import {
@@ -92,6 +93,17 @@ export const normalizeTopStats = stats => (
}))
);
+export const addClientInfo = (data, clients, param) => (
+ data.map((row) => {
+ const clientIp = row[param];
+ const info = clients.find(item => item[clientIp]) || '';
+ return {
+ ...row,
+ info: (info && info[clientIp]) || '',
+ };
+ })
+);
+
export const normalizeFilteringStatus = (filteringStatus) => {
const {
enabled, filters, user_rules: userRules, interval,
@@ -248,6 +260,20 @@ export const redirectToCurrentProtocol = (values, httpPort = 80) => {
export const normalizeTextarea = text => text && text.replace(/[;, ]/g, '\n').split('\n').filter(n => n);
export const getClientInfo = (clients, ip) => {
+ const client = clients
+ .find(item => item.ip_addrs && item.ip_addrs.find(clientIp => clientIp === ip));
+
+ if (!client) {
+ return '';
+ }
+
+ const { name, whois_info } = client;
+ const whois = Object.keys(whois_info).length > 0 ? whois_info : '';
+
+ return { name, whois };
+};
+
+export const getAutoClientInfo = (clients, ip) => {
const client = clients.find(item => ip === item.ip);
if (!client) {
@@ -328,3 +354,13 @@ export const getPathWithQueryString = (path, params) => {
return `${path}?${searchParams.toString()}`;
};
+
+export const getParamsForClientsSearch = (data, param) => {
+ const uniqueClients = uniqBy(data, param);
+ return uniqueClients
+ .reduce((acc, item, idx) => {
+ const key = `ip${idx}`;
+ acc[key] = item[param];
+ return acc;
+ }, {});
+};
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..cf407a00 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)
@@ -370,11 +372,13 @@ func cleanup() {
// Stop HTTP server, possibly waiting for all active connections to be closed
func stopHTTPServer() {
+ log.Info("Stopping HTTP server...")
config.httpsServer.shutdown = true
if config.httpsServer.server != nil {
config.httpsServer.server.Shutdown(context.TODO())
}
config.httpServer.Shutdown(context.TODO())
+ log.Info("Stopped HTTP server")
}
// This function is called before application exits
diff --git a/home/upgrade.go b/home/upgrade.go
index 9445e8b1..3a703ebc 100644
--- a/home/upgrade.go
+++ b/home/upgrade.go
@@ -11,7 +11,7 @@ import (
yaml "gopkg.in/yaml.v2"
)
-const currentSchemaVersion = 5 // used for upgrading from old configs to new config
+const currentSchemaVersion = 6 // used for upgrading from old configs to new config
// Performs necessary upgrade operations if needed
func upgradeConfig() error {
@@ -82,6 +82,12 @@ func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) err
if err != nil {
return err
}
+ fallthrough
+ case 5:
+ err := upgradeSchema5to6(diskConfig)
+ if err != nil {
+ return err
+ }
default:
err := fmt.Errorf("configuration file contains unknown schema_version, abort")
log.Println(err)
@@ -268,3 +274,72 @@ func upgradeSchema4to5(diskConfig *map[string]interface{}) error {
(*diskConfig)["users"] = users
return nil
}
+
+// clients:
+// ...
+// ip: 127.0.0.1
+// mac: ...
+//
+// ->
+//
+// clients:
+// ...
+// ids:
+// - 127.0.0.1
+// - ...
+func upgradeSchema5to6(diskConfig *map[string]interface{}) error {
+ log.Printf("%s(): called", _Func())
+
+ (*diskConfig)["schema_version"] = 6
+
+ clients, ok := (*diskConfig)["clients"]
+ if !ok {
+ return nil
+ }
+
+ switch arr := clients.(type) {
+ case []interface{}:
+
+ for i := range arr {
+
+ switch c := arr[i].(type) {
+
+ case map[interface{}]interface{}:
+ _ip, ok := c["ip"]
+ ids := []string{}
+ if ok {
+ ip, ok := _ip.(string)
+ if !ok {
+ log.Fatalf("client.ip is not a string: %v", _ip)
+ return nil
+ }
+ if len(ip) != 0 {
+ ids = append(ids, ip)
+ }
+ }
+
+ _mac, ok := c["mac"]
+ if ok {
+ mac, ok := _mac.(string)
+ if !ok {
+ log.Fatalf("client.mac is not a string: %v", _mac)
+ return nil
+ }
+ if len(mac) != 0 {
+ ids = append(ids, mac)
+ }
+ }
+
+ c["ids"] = ids
+
+ default:
+ continue
+ }
+ }
+
+ default:
+ return nil
+ }
+
+ return nil
+}
diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md
index 3172be07..c6e79b8a 100644
--- a/openapi/CHANGELOG.md
+++ b/openapi/CHANGELOG.md
@@ -1,6 +1,95 @@
# AdGuard Home API Change Log
+## v0.100: API changes
+
+### API: Get list of clients: GET /control/clients
+
+* "ip" and "mac" fields are removed
+* "ids" and "ip_addrs" fields are added
+
+Response:
+
+ 200 OK
+
+ {
+ clients: [
+ {
+ name: "client1"
+ ids: ["...", ...] // IP or MAC
+ ip_addrs: ["...", ...] // all IP addresses (set by user and resolved by 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"
+ ...
+ }
+ }
+ ]
+ auto_clients: [
+ {
+ name: "host"
+ ip: "..."
+ source: "etc/hosts" || "rDNS"
+ whois_info: {
+ key: "value"
+ ...
+ }
+ }
+ ]
+ }
+
+### API: Add client: POST /control/clients/add
+
+* "ip" and "mac" fields are removed
+* "ids" field is added
+
+Request:
+
+ POST /control/clients/add
+
+ {
+ name: "client1"
+ ids: ["...", ...] // IP 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", ... ]
+ }
+
+### API: Update client: POST /control/clients/update
+
+* "ip" and "mac" fields are removed
+* "ids" field is added
+
+Request:
+
+ POST /control/clients/update
+
+ {
+ name: "client1"
+ data: {
+ name: "client1"
+ ids: ["...", ...] // IP 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", ... ]
+ }
+ }
+
+
## v0.99.3: API changes
### API: Get query log: GET /control/querylog
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index 406194b2..6514922c 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -772,6 +772,22 @@ paths:
200:
description: OK
+ /clients/find:
+ get:
+ tags:
+ - clients
+ operationId: clientsFind
+ summary: 'Get information about selected clients by their IP address'
+ parameters:
+ - name: ip0
+ in: query
+ type: string
+ responses:
+ 200:
+ description: OK
+ schema:
+ $ref: "#/definitions/ClientsFindResponse"
+
/blocked_services/list:
get:
@@ -1589,16 +1605,15 @@ definitions:
type: "object"
description: "Client information"
properties:
- ip:
- type: "string"
- description: "IP address"
- example: "127.0.0.1"
name:
type: "string"
description: "Name"
example: "localhost"
- mac:
- type: "string"
+ ids:
+ type: "array"
+ description: "IP, CIDR or MAC address"
+ items:
+ type: "string"
use_global_settings:
type: "boolean"
filtering_enabled:
@@ -1645,6 +1660,20 @@ definitions:
properties:
name:
type: "string"
+
+ ClientsFindResponse:
+ type: "array"
+ description: "Response to clients find operation"
+ items:
+ $ref: "#/definitions/ClientsFindEntry"
+
+ ClientsFindEntry:
+ type: "object"
+ properties:
+ "1.2.3.4":
+ items:
+ $ref: "#/definitions/Client"
+
Clients:
type: "object"
properties: