Pull request #2323: AGDNS-2598-clients-search
Merge in DNS/adguard-home from AGDNS-2598-clients-search to master Squashed commit of the following: commit9df3c19aca
Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Dec 16 19:11:43 2024 +0300 home: imp code commit7bf8f0a516
Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Dec 16 18:34:06 2024 +0300 all: imp code commit2dd1c94123
Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Fri Dec 13 17:35:11 2024 +0300 all: clients search
This commit is contained in:
parent
dedbadafc4
commit
fe07786d2d
|
@ -18,6 +18,14 @@ See also the [v0.107.56 GitHub milestone][ms-v0.107.56].
|
||||||
NOTE: Add new changes BELOW THIS COMMENT.
|
NOTE: Add new changes BELOW THIS COMMENT.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- The new HTTP API `POST /clients/search` that finds clients by their IP addresses, CIDRs, MAC addresses, or ClientIDs. See `openapi/openapi.yaml` for the full description.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
- The `GET /clients/find` HTTP API is deprecated. Use the new `POST /clients/search` API.
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||||
-->
|
-->
|
||||||
|
|
|
@ -46,7 +46,7 @@ export const getStats = () => async (dispatch: any) => {
|
||||||
const normalizedTopClients = normalizeTopStats(stats.top_clients);
|
const normalizedTopClients = normalizeTopStats(stats.top_clients);
|
||||||
|
|
||||||
const clientsParams = getParamsForClientsSearch(normalizedTopClients, 'name');
|
const clientsParams = getParamsForClientsSearch(normalizedTopClients, 'name');
|
||||||
const clients = await apiClient.findClients(clientsParams);
|
const clients = await apiClient.searchClients(clientsParams);
|
||||||
const topClientsWithInfo = addClientInfo(normalizedTopClients, clients, 'name');
|
const topClientsWithInfo = addClientInfo(normalizedTopClients, clients, 'name');
|
||||||
|
|
||||||
const normalizedStats = {
|
const normalizedStats = {
|
||||||
|
|
|
@ -415,7 +415,7 @@ class Api {
|
||||||
// Per-client settings
|
// Per-client settings
|
||||||
GET_CLIENTS = { path: 'clients', method: 'GET' };
|
GET_CLIENTS = { path: 'clients', method: 'GET' };
|
||||||
|
|
||||||
FIND_CLIENTS = { path: 'clients/find', method: 'GET' };
|
SEARCH_CLIENTS = { path: 'clients/search', method: 'POST' };
|
||||||
|
|
||||||
ADD_CLIENT = { path: 'clients/add', method: 'POST' };
|
ADD_CLIENT = { path: 'clients/add', method: 'POST' };
|
||||||
|
|
||||||
|
@ -453,11 +453,12 @@ class Api {
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
findClients(params: any) {
|
searchClients(config: any) {
|
||||||
const { path, method } = this.FIND_CLIENTS;
|
const { path, method } = this.SEARCH_CLIENTS;
|
||||||
const url = getPathWithQueryString(path, params);
|
const parameters = {
|
||||||
|
data: config,
|
||||||
return this.makeRequest(url, method);
|
};
|
||||||
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNS access settings
|
// DNS access settings
|
||||||
|
|
|
@ -451,13 +451,10 @@ export const getParamsForClientsSearch = (data: any, param: any, additionalParam
|
||||||
clients.add(e[additionalParam]);
|
clients.add(e[additionalParam]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const params = {};
|
|
||||||
const ids = Array.from(clients.values());
|
|
||||||
ids.forEach((id, i) => {
|
|
||||||
params[`ip${i}`] = id;
|
|
||||||
});
|
|
||||||
|
|
||||||
return params;
|
return {
|
||||||
|
clients: Array.from(clients).map(id => ({ id })),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -424,6 +424,8 @@ func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *ht
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleFindClient is the handler for GET /control/clients/find HTTP API.
|
// handleFindClient is the handler for GET /control/clients/find HTTP API.
|
||||||
|
//
|
||||||
|
// Deprecated: Remove it when migration to the new API is over.
|
||||||
func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http.Request) {
|
func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http.Request) {
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
data := []map[string]*clientJSON{}
|
data := []map[string]*clientJSON{}
|
||||||
|
@ -433,19 +435,58 @@ func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data = append(data, map[string]*clientJSON{
|
||||||
|
idStr: clients.findClient(idStr),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
aghhttp.WriteJSONResponseOK(w, r, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// findClient returns available information about a client by idStr from the
|
||||||
|
// client's storage or access settings. cj is guaranteed to be non-nil.
|
||||||
|
func (clients *clientsContainer) findClient(idStr string) (cj *clientJSON) {
|
||||||
ip, _ := netip.ParseAddr(idStr)
|
ip, _ := netip.ParseAddr(idStr)
|
||||||
c, ok := clients.storage.Find(idStr)
|
c, ok := clients.storage.Find(idStr)
|
||||||
var cj *clientJSON
|
|
||||||
if !ok {
|
if !ok {
|
||||||
cj = clients.findRuntime(ip, idStr)
|
return clients.findRuntime(ip, idStr)
|
||||||
} else {
|
}
|
||||||
|
|
||||||
cj = clientToJSON(c)
|
cj = clientToJSON(c)
|
||||||
disallowed, rule := clients.clientChecker.IsBlockedClient(ip, idStr)
|
disallowed, rule := clients.clientChecker.IsBlockedClient(ip, idStr)
|
||||||
cj.Disallowed, cj.DisallowedRule = &disallowed, &rule
|
cj.Disallowed, cj.DisallowedRule = &disallowed, &rule
|
||||||
|
|
||||||
|
return cj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// searchQueryJSON is a request to the POST /control/clients/search HTTP API.
|
||||||
|
//
|
||||||
|
// TODO(s.chzhen): Add UIDs.
|
||||||
|
type searchQueryJSON struct {
|
||||||
|
Clients []searchClientJSON `json:"clients"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// searchClientJSON is a part of [searchQueryJSON] that contains a string
|
||||||
|
// representation of the client's IP address, CIDR, MAC address, or ClientID.
|
||||||
|
type searchClientJSON struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSearchClient is the handler for the POST /control/clients/search HTTP API.
|
||||||
|
func (clients *clientsContainer) handleSearchClient(w http.ResponseWriter, r *http.Request) {
|
||||||
|
q := searchQueryJSON{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&q)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusBadRequest, "failed to process request body: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := []map[string]*clientJSON{}
|
||||||
|
for _, c := range q.Clients {
|
||||||
|
idStr := c.ID
|
||||||
data = append(data, map[string]*clientJSON{
|
data = append(data, map[string]*clientJSON{
|
||||||
idStr: cj,
|
idStr: clients.findClient(idStr),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -493,5 +534,8 @@ func (clients *clientsContainer) registerWebHandlers() {
|
||||||
httpRegister(http.MethodPost, "/control/clients/add", clients.handleAddClient)
|
httpRegister(http.MethodPost, "/control/clients/add", clients.handleAddClient)
|
||||||
httpRegister(http.MethodPost, "/control/clients/delete", clients.handleDelClient)
|
httpRegister(http.MethodPost, "/control/clients/delete", clients.handleDelClient)
|
||||||
httpRegister(http.MethodPost, "/control/clients/update", clients.handleUpdateClient)
|
httpRegister(http.MethodPost, "/control/clients/update", clients.handleUpdateClient)
|
||||||
|
httpRegister(http.MethodPost, "/control/clients/search", clients.handleSearchClient)
|
||||||
|
|
||||||
|
// Deprecated handler.
|
||||||
httpRegister(http.MethodGet, "/control/clients/find", clients.handleFindClient)
|
httpRegister(http.MethodGet, "/control/clients/find", clients.handleFindClient)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/client"
|
"github.com/AdguardTeam/AdGuardHome/internal/client"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/schedule"
|
"github.com/AdguardTeam/AdGuardHome/internal/schedule"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -408,3 +409,145 @@ func TestClientsContainer_HandleFindClient(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClientsContainer_HandleSearchClient(t *testing.T) {
|
||||||
|
var (
|
||||||
|
runtimeCli = "runtime_client1"
|
||||||
|
|
||||||
|
runtimeCliIP = "3.3.3.3"
|
||||||
|
blockedCliIP = "4.4.4.4"
|
||||||
|
nonExistentCliIP = "5.5.5.5"
|
||||||
|
|
||||||
|
allowed = false
|
||||||
|
dissallowed = true
|
||||||
|
|
||||||
|
emptyRule = ""
|
||||||
|
disallowedRule = "disallowed_rule"
|
||||||
|
)
|
||||||
|
|
||||||
|
clients := newClientsContainer(t)
|
||||||
|
clients.clientChecker = &testBlockedClientChecker{
|
||||||
|
onIsBlockedClient: func(ip netip.Addr, _ string) (ok bool, rule string) {
|
||||||
|
if ip == netip.MustParseAddr(blockedCliIP) {
|
||||||
|
return true, disallowedRule
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, emptyRule
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
|
||||||
|
clientOne := newPersistentClientWithIDs(t, "client1", []string{testClientIP1})
|
||||||
|
err := clients.storage.Add(ctx, clientOne)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
clientTwo := newPersistentClientWithIDs(t, "client2", []string{testClientIP2})
|
||||||
|
err = clients.storage.Add(ctx, clientTwo)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assertPersistentClients(t, clients, []*client.Persistent{clientOne, clientTwo})
|
||||||
|
|
||||||
|
clients.UpdateAddress(ctx, netip.MustParseAddr(runtimeCliIP), runtimeCli, nil)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
query *searchQueryJSON
|
||||||
|
wantPersistent []*client.Persistent
|
||||||
|
wantRuntime *clientJSON
|
||||||
|
}{{
|
||||||
|
name: "single",
|
||||||
|
query: &searchQueryJSON{
|
||||||
|
Clients: []searchClientJSON{{
|
||||||
|
ID: testClientIP1,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
wantPersistent: []*client.Persistent{clientOne},
|
||||||
|
}, {
|
||||||
|
name: "multiple",
|
||||||
|
query: &searchQueryJSON{
|
||||||
|
Clients: []searchClientJSON{{
|
||||||
|
ID: testClientIP1,
|
||||||
|
}, {
|
||||||
|
ID: testClientIP2,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
wantPersistent: []*client.Persistent{clientOne, clientTwo},
|
||||||
|
}, {
|
||||||
|
name: "runtime",
|
||||||
|
query: &searchQueryJSON{
|
||||||
|
Clients: []searchClientJSON{{
|
||||||
|
ID: runtimeCliIP,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
wantRuntime: &clientJSON{
|
||||||
|
Name: runtimeCli,
|
||||||
|
IDs: []string{runtimeCliIP},
|
||||||
|
Disallowed: &allowed,
|
||||||
|
DisallowedRule: &emptyRule,
|
||||||
|
WHOIS: &whois.Info{},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "blocked_access",
|
||||||
|
query: &searchQueryJSON{
|
||||||
|
Clients: []searchClientJSON{{
|
||||||
|
ID: blockedCliIP,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
wantRuntime: &clientJSON{
|
||||||
|
IDs: []string{blockedCliIP},
|
||||||
|
Disallowed: &dissallowed,
|
||||||
|
DisallowedRule: &disallowedRule,
|
||||||
|
WHOIS: &whois.Info{},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "non_existing_client",
|
||||||
|
query: &searchQueryJSON{
|
||||||
|
Clients: []searchClientJSON{{
|
||||||
|
ID: nonExistentCliIP,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
wantRuntime: &clientJSON{
|
||||||
|
IDs: []string{nonExistentCliIP},
|
||||||
|
Disallowed: &allowed,
|
||||||
|
DisallowedRule: &emptyRule,
|
||||||
|
WHOIS: &whois.Info{},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var body []byte
|
||||||
|
body, err = json.Marshal(tc.query)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var r *http.Request
|
||||||
|
r, err = http.NewRequest(http.MethodPost, "", bytes.NewReader(body))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
clients.handleSearchClient(rw, r)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusOK, rw.Code)
|
||||||
|
|
||||||
|
body, err = io.ReadAll(rw.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
clientData := []map[string]*clientJSON{}
|
||||||
|
err = json.Unmarshal(body, &clientData)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if tc.wantPersistent != nil {
|
||||||
|
assertPersistentClientsData(t, clients, clientData, tc.wantPersistent)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Len(t, clientData, 1)
|
||||||
|
require.Len(t, clientData[0], 1)
|
||||||
|
|
||||||
|
rc := clientData[0][tc.wantRuntime.IDs[0]]
|
||||||
|
assert.Equal(t, tc.wantRuntime, rc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,32 @@
|
||||||
|
|
||||||
## v0.108.0: API changes
|
## v0.108.0: API changes
|
||||||
|
|
||||||
## v0.107.55: API changes
|
## v0.107.56: API changes
|
||||||
|
|
||||||
|
### Deprecated client APIs
|
||||||
|
|
||||||
|
* The `GET /control/clients/find` HTTP API; use the new `POST
|
||||||
|
/control/clients/search` API instead.
|
||||||
|
|
||||||
|
### New client APIs
|
||||||
|
|
||||||
|
* The new `POST /control/clients/search` HTTP API allows config updates. It
|
||||||
|
accepts a JSON object with the following format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"clients": [
|
||||||
|
{
|
||||||
|
"id": "192.0.2.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "test"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## v0.107.53: API changes
|
||||||
|
|
||||||
### The new field `"ecosia"` in `SafeSearchConfig`
|
### The new field `"ecosia"` in `SafeSearchConfig`
|
||||||
|
|
||||||
|
|
|
@ -934,6 +934,9 @@
|
||||||
'description': 'OK.'
|
'description': 'OK.'
|
||||||
'/clients/find':
|
'/clients/find':
|
||||||
'get':
|
'get':
|
||||||
|
'deprecated': true
|
||||||
|
'description': >
|
||||||
|
Deprecated: Use `POST /clients/search` instead.
|
||||||
'tags':
|
'tags':
|
||||||
- 'clients'
|
- 'clients'
|
||||||
'operationId': 'clientsFind'
|
'operationId': 'clientsFind'
|
||||||
|
@ -957,6 +960,26 @@
|
||||||
'application/json':
|
'application/json':
|
||||||
'schema':
|
'schema':
|
||||||
'$ref': '#/components/schemas/ClientsFindResponse'
|
'$ref': '#/components/schemas/ClientsFindResponse'
|
||||||
|
'/clients/search':
|
||||||
|
'post':
|
||||||
|
'tags':
|
||||||
|
- 'clients'
|
||||||
|
'operationId': 'clientsSearch'
|
||||||
|
'summary': >
|
||||||
|
Get information about clients by their IP addresses, CIDRs, MAC addresses, or ClientIDs.
|
||||||
|
'requestBody':
|
||||||
|
'content':
|
||||||
|
'application/json':
|
||||||
|
'schema':
|
||||||
|
'$ref': '#/components/schemas/ClientsSearchRequest'
|
||||||
|
'required': true
|
||||||
|
'responses':
|
||||||
|
'200':
|
||||||
|
'description': 'OK.'
|
||||||
|
'content':
|
||||||
|
'application/json':
|
||||||
|
'schema':
|
||||||
|
'$ref': '#/components/schemas/ClientsFindResponse'
|
||||||
'/access/list':
|
'/access/list':
|
||||||
'get':
|
'get':
|
||||||
'operationId': 'accessList'
|
'operationId': 'accessList'
|
||||||
|
@ -2749,6 +2772,20 @@
|
||||||
'properties':
|
'properties':
|
||||||
'name':
|
'name':
|
||||||
'type': 'string'
|
'type': 'string'
|
||||||
|
'ClientsSearchRequest':
|
||||||
|
'type': 'object'
|
||||||
|
'description': 'Client search request'
|
||||||
|
'properties':
|
||||||
|
'clients':
|
||||||
|
'type': 'array'
|
||||||
|
'items':
|
||||||
|
'$ref': '#/components/schemas/ClientsSearchRequestItem'
|
||||||
|
'ClientsSearchRequestItem':
|
||||||
|
'type': 'object'
|
||||||
|
'properties':
|
||||||
|
'id':
|
||||||
|
'type': 'string'
|
||||||
|
'description': 'Client IP address, CIDR, MAC address, or ClientID'
|
||||||
'ClientsFindResponse':
|
'ClientsFindResponse':
|
||||||
'type': 'array'
|
'type': 'array'
|
||||||
'description': 'Client search results.'
|
'description': 'Client search results.'
|
||||||
|
|
Loading…
Reference in New Issue