mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-12-18 13:01:51 +03:00
Merge branch 'master' into 7505-format-values
This commit is contained in:
commit
5c15d1bb16
9 changed files with 287 additions and 26 deletions
|
@ -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.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The formatting of large numbers on the dashboard ([#7329]).
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 })),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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{}
|
||||
|
@ -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: cj,
|
||||
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: clients.findClient(idStr),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
@ -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.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`
|
||||
|
||||
|
|
|
@ -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/ClientsSearchRequestItem'
|
||||
'ClientsSearchRequestItem':
|
||||
'type': 'object'
|
||||
'properties':
|
||||
'id':
|
||||
'type': 'string'
|
||||
'description': 'Client IP address, CIDR, MAC address, or ClientID'
|
||||
'ClientsFindResponse':
|
||||
'type': 'array'
|
||||
'description': 'Client search results.'
|
||||
|
|
|
@ -223,6 +223,12 @@ run_linter ineffassign ./...
|
|||
run_linter unparam ./...
|
||||
|
||||
find . \
|
||||
'(' \
|
||||
-name 'node_modules' \
|
||||
-type 'd' \
|
||||
-prune \
|
||||
')' \
|
||||
-o \
|
||||
-type 'f' \
|
||||
'(' \
|
||||
-name 'Makefile' \
|
||||
|
|
Loading…
Reference in a new issue