mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-12-19 05:21:49 +03:00
all: clients search
This commit is contained in:
parent
dedbadafc4
commit
2dd1c94123
8 changed files with 207 additions and 14 deletions
|
@ -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{}
|
||||||
|
@ -452,6 +454,51 @@ func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http
|
||||||
aghhttp.WriteJSONResponseOK(w, r, data)
|
aghhttp.WriteJSONResponseOK(w, r, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
ip, _ := netip.ParseAddr(idStr)
|
||||||
|
c, ok := clients.storage.Find(idStr)
|
||||||
|
var cj *clientJSON
|
||||||
|
if !ok {
|
||||||
|
cj = clients.findRuntime(ip, idStr)
|
||||||
|
} else {
|
||||||
|
cj = clientToJSON(c)
|
||||||
|
disallowed, rule := clients.clientChecker.IsBlockedClient(ip, idStr)
|
||||||
|
cj.Disallowed, cj.DisallowedRule = &disallowed, &rule
|
||||||
|
}
|
||||||
|
|
||||||
|
data = append(data, map[string]*clientJSON{
|
||||||
|
idStr: cj,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
aghhttp.WriteJSONResponseOK(w, r, data)
|
||||||
|
}
|
||||||
|
|
||||||
// findRuntime looks up the IP in runtime and temporary storages, like
|
// findRuntime looks up the IP in runtime and temporary storages, like
|
||||||
// /etc/hosts tables, DHCP leases, or blocklists. cj is guaranteed to be
|
// /etc/hosts tables, DHCP leases, or blocklists. cj is guaranteed to be
|
||||||
// non-nil.
|
// non-nil.
|
||||||
|
@ -493,5 +540,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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -408,3 +408,77 @@ func TestClientsContainer_HandleFindClient(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClientsContainer_HandleSearchClient(t *testing.T) {
|
||||||
|
clients := newClientsContainer(t)
|
||||||
|
clients.clientChecker = &testBlockedClientChecker{
|
||||||
|
onIsBlockedClient: func(ip netip.Addr, clientID string) (ok bool, rule string) {
|
||||||
|
return false, ""
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
query *searchQueryJSON
|
||||||
|
wantCode int
|
||||||
|
wantClient []*client.Persistent
|
||||||
|
}{{
|
||||||
|
name: "single",
|
||||||
|
query: &searchQueryJSON{
|
||||||
|
Clients: []searchClientJSON{{
|
||||||
|
ID: testClientIP1,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
wantCode: http.StatusOK,
|
||||||
|
wantClient: []*client.Persistent{clientOne},
|
||||||
|
}, {
|
||||||
|
name: "multiple",
|
||||||
|
query: &searchQueryJSON{
|
||||||
|
Clients: []searchClientJSON{{
|
||||||
|
ID: testClientIP1,
|
||||||
|
}, {
|
||||||
|
ID: testClientIP2,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
wantCode: http.StatusOK,
|
||||||
|
wantClient: []*client.Persistent{clientOne, clientTwo},
|
||||||
|
}}
|
||||||
|
|
||||||
|
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, tc.wantCode, 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)
|
||||||
|
|
||||||
|
assertPersistentClientsData(t, clients, clientData, tc.wantClient)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,33 @@
|
||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||
|
These APIs accept and return 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/ClientsIDEntry'
|
||||||
|
'ClientsIDEntry':
|
||||||
|
'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 a new issue