Skip to content

Commit

Permalink
Pull request #2323: AGDNS-2598-clients-search
Browse files Browse the repository at this point in the history
Merge in DNS/adguard-home from AGDNS-2598-clients-search to master

Squashed commit of the following:

commit 9df3c19
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Dec 16 19:11:43 2024 +0300

    home: imp code

commit 7bf8f0a
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Dec 16 18:34:06 2024 +0300

    all: imp code

commit 2dd1c94
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Dec 13 17:35:11 2024 +0300

    all: clients search
  • Loading branch information
schzhn committed Dec 17, 2024
1 parent dedbada commit fe07786
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 25 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
-->
Expand Down
2 changes: 1 addition & 1 deletion client/src/actions/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
13 changes: 7 additions & 6 deletions client/src/api/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' };

Expand Down Expand Up @@ -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
Expand Down
9 changes: 3 additions & 6 deletions client/src/helpers/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 })),
};
};

/**
Expand Down
66 changes: 55 additions & 11 deletions internal/home/clientshttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand All @@ -433,19 +435,58 @@ func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http
break
}

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: 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)
c, ok := clients.storage.Find(idStr)
if !ok {
return clients.findRuntime(ip, idStr)
}

cj = clientToJSON(c)
disallowed, rule := clients.clientChecker.IsBlockedClient(ip, idStr)
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{
idStr: cj,
idStr: clients.findClient(idStr),
})
}

Expand Down Expand Up @@ -493,5 +534,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)
}
143 changes: 143 additions & 0 deletions internal/home/clientshttp_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/schedule"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -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)
})
}
}
27 changes: 26 additions & 1 deletion openapi/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,32 @@

## 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`

Expand Down
Loading

0 comments on commit fe07786

Please sign in to comment.