all: clients search

This commit is contained in:
Stanislav Chzhen 2024-12-13 17:35:11 +03:00
parent dedbadafc4
commit 2dd1c94123
8 changed files with 207 additions and 14 deletions

View file

@ -18,6 +18,14 @@ See also the [v0.107.56 GitHub milestone][ms-v0.107.56].
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.
-->

View file

@ -46,7 +46,7 @@ export const getStats = () => async (dispatch: any) => {
const normalizedTopClients = normalizeTopStats(stats.top_clients);
const clientsParams = getParamsForClientsSearch(normalizedTopClients, 'name');
const clients = await apiClient.findClients(clientsParams);
const clients = await apiClient.searchClients(clientsParams);
const topClientsWithInfo = addClientInfo(normalizedTopClients, clients, 'name');
const normalizedStats = {

View file

@ -415,7 +415,7 @@ class Api {
// Per-client settings
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' };
@ -453,11 +453,12 @@ class Api {
return this.makeRequest(path, method, parameters);
}
findClients(params: any) {
const { path, method } = this.FIND_CLIENTS;
const url = getPathWithQueryString(path, params);
return this.makeRequest(url, method);
searchClients(config: any) {
const { path, method } = this.SEARCH_CLIENTS;
const parameters = {
data: config,
};
return this.makeRequest(path, method, parameters);
}
// DNS access settings

View file

@ -451,13 +451,10 @@ export const getParamsForClientsSearch = (data: any, param: any, 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 })),
};
};
/**

View file

@ -424,6 +424,8 @@ func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *ht
}
// 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) {
q := r.URL.Query()
data := []map[string]*clientJSON{}
@ -452,6 +454,51 @@ func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http
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
// /etc/hosts tables, DHCP leases, or blocklists. cj is guaranteed to be
// non-nil.
@ -493,5 +540,8 @@ func (clients *clientsContainer) registerWebHandlers() {
httpRegister(http.MethodPost, "/control/clients/add", clients.handleAddClient)
httpRegister(http.MethodPost, "/control/clients/delete", clients.handleDelClient)
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)
}

View file

@ -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)
})
}
}

View file

@ -4,7 +4,33 @@
## 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`

View file

@ -934,6 +934,9 @@
'description': 'OK.'
'/clients/find':
'get':
'deprecated': true
'description': >
Deprecated: Use `POST /clients/search` instead.
'tags':
- 'clients'
'operationId': 'clientsFind'
@ -957,6 +960,26 @@
'application/json':
'schema':
'$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':
'get':
'operationId': 'accessList'
@ -2749,6 +2772,20 @@
'properties':
'name':
'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':
'type': 'array'
'description': 'Client search results.'