diff --git a/CHANGELOG.md b/CHANGELOG.md index fbffff1f..c4b9f685 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. + diff --git a/client/src/actions/stats.ts b/client/src/actions/stats.ts index d0dcc852..05b189ed 100644 --- a/client/src/actions/stats.ts +++ b/client/src/actions/stats.ts @@ -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 = { diff --git a/client/src/api/Api.ts b/client/src/api/Api.ts index 40423ab0..c77264dd 100644 --- a/client/src/api/Api.ts +++ b/client/src/api/Api.ts @@ -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 diff --git a/client/src/helpers/helpers.tsx b/client/src/helpers/helpers.tsx index 1a0f368b..0ad9e9e2 100644 --- a/client/src/helpers/helpers.tsx +++ b/client/src/helpers/helpers.tsx @@ -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 })), + }; }; /** diff --git a/internal/home/clientshttp.go b/internal/home/clientshttp.go index 8cea8650..e53cf1a8 100644 --- a/internal/home/clientshttp.go +++ b/internal/home/clientshttp.go @@ -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) } diff --git a/internal/home/clientshttp_internal_test.go b/internal/home/clientshttp_internal_test.go index a10ca8d1..95e5950d 100644 --- a/internal/home/clientshttp_internal_test.go +++ b/internal/home/clientshttp_internal_test.go @@ -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) + }) + } +} diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md index e9894c37..8911b3a2 100644 --- a/openapi/CHANGELOG.md +++ b/openapi/CHANGELOG.md @@ -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` diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 32c691d1..06173426 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -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.'