Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Limit number of clients returned by /api/history/clients #1832

Merged
merged 9 commits into from
Jan 16, 2024
1 change: 1 addition & 0 deletions src/api/api.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
int api_handler(struct mg_connection *conn, void *ignored);

// Statistic methods
int __attribute__((pure)) cmpdesc(const void *a, const void *b);
int api_stats_summary(struct ftl_conn *api);
int api_stats_query_types(struct ftl_conn *api);
int api_stats_upstreams(struct ftl_conn *api);
Expand Down
36 changes: 35 additions & 1 deletion src/api/docs/content/specs/history.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,15 @@ components:
- Metrics
operationId: "get_client_metrics"
description: |
Request data needed to generate the \"Client activity over last 24 hours\" graph
Request data needed to generate the \"Client activity over last 24 hours\" graph.
This endpoint returns the top N clients, sorted by total number of queries within 24 hours. If N is set to 0, all clients will be returned.
The client name is only available if the client's IP address can be resolved to a hostname.

The last client returned is a special client that contains the total number of queries that were not sent by any of the other shown clients , i.e. queries that were sent by clients that are not in the top N. This client is always present, even if it has 0 queries and can be identified by the special name "other clients" (mind the space in the hostname) and the IP address "0.0.0.0".

Note that, due to privacy settings, the returned data may also be empty.
parameters:
- $ref: 'history.yaml#/components/parameters/clients/N'
responses:
'200':
description: OK
Expand Down Expand Up @@ -162,11 +170,15 @@ components:
- 12
- 65
- 67
- 9
- 5
- timestamp: 1511820500.583821
data:
- 1
- 35
- 63
- 20
- 9
clients:
type: array
description: Data array
Expand All @@ -180,10 +192,32 @@ components:
ip:
type: string
description: Client IP address
total:
type: integer
description: Total number of queries
example:
- name: localhost
ip: "127.0.0.1"
total: 13428
- name: ip6-localnet
ip: "::1"
total: 2100
- name: null
ip: "192.168.1.1"
total: 254
- name: "pi.hole"
ip: "::"
total: 29
- name: "other clients"
ip: "0.0.0.0"
total: 14
parameters:
clients:
N:
in: query
description: Maximum number of clients to return, setting this to 0 will return all clients
name: N
schema:
type: integer
required: false
example: 20
98 changes: 85 additions & 13 deletions src/api/history.c
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ int api_history(struct ftl_conn *api)
JSON_SEND_OBJECT(json);
}

#define DEFAULT_MAX_CLIENTS 10
int api_history_clients(struct ftl_conn *api)
{
// Exit before processing any data if requested via config setting
Expand All @@ -66,13 +67,38 @@ int api_history_clients(struct ftl_conn *api)
JSON_SEND_OBJECT_UNLOCK(json);
}

// Get number of clients to return´
unsigned int Nc = min(counters->clients, DEFAULT_MAX_CLIENTS);
if(api->request->query_string != NULL)
{
// Does the user request a non-default number of clients
get_uint_var(api->request->query_string, "N", &Nc);

// Limit the number of clients to return to the number of
// clients to avoid possible overflows for very large N
// Also allow N=0 to return all clients
if((int)Nc > counters->clients || Nc == 0)
Nc = counters->clients;
}

// Lock shared memory
lock_shm();

// Get clients which the user doesn't want to see
// if skipclient[i] == true then this client should be hidden from
// returned data. We initialize it with false
bool *skipclient = calloc(counters->clients, sizeof(bool));
int *temparray = calloc(2*counters->clients, sizeof(int));
if(skipclient == NULL || temparray == NULL)
{
unlock_shm();
return send_json_error(api, 500, "internal_error",
"Failed to allocate memory for client history", NULL);
}

// Check if the user wants to exclude any clients, this code path is
// only taken if the user has configured the web interface to exclude
// clients (it will most often be skipped)
unsigned int excludeClients = cJSON_GetArraySize(config.webserver.api.excludeClients.v.json);
if(excludeClients > 0)
{
Expand All @@ -93,42 +119,72 @@ int api_history_clients(struct ftl_conn *api)
}
}

// Also skip clients included in others (in alias-clients)
// Skip clients included in others (in alias-clients)
for(int clientID = 0; clientID < counters->clients; clientID++)
{
// Get client pointer
const clientsData* client = getClient(clientID, true);
if(client == NULL)
continue;

// Check if this client should be skipped
if(!client->flags.aliasclient && client->aliasclient_id > -1)
skipclient[clientID] = true;
}

// Get MAX_CLIENTS clients with the highest number of queries
for(int clientID = 0; clientID < counters->clients; clientID++)
{
// Get client pointer
const clientsData* client = getClient(clientID, true);

// Skip invalid clients
if(client == NULL)
continue;

// Store clientID and number of queries in temporary array
temparray[2*clientID + 0] = clientID;
temparray[2*clientID + 1] = client->count;
}

// Sort temporary array
qsort(temparray, counters->clients, sizeof(int[2]), cmpdesc);

// Main return loop
cJSON *history = JSON_NEW_ARRAY();
int others_total = 0;
for(unsigned int slot = 0; slot < OVERTIME_SLOTS; slot++)
{
cJSON *item = JSON_NEW_OBJECT();
JSON_ADD_NUMBER_TO_OBJECT(item, "timestamp", overTime[slot].timestamp);

// Loop over clients to generate output to be sent to the client
cJSON *data = JSON_NEW_ARRAY();
for(int clientID = 0; clientID < counters->clients; clientID++)
int others = 0;
for(int id = 0; id < counters->clients; id++)
{
if(skipclient[clientID])
continue;

// Get client pointer
const int clientID = temparray[2*id + 0];
const clientsData* client = getClient(clientID, true);

// Skip invalid clients and also those managed by alias clients
if(client == NULL || client->aliasclient_id >= 0)
// Skip invalid (recycled) clients
if(client == NULL)
continue;

const int thisclient = client->overTime[slot];
// Skip clients which should be hidden and add them to the "others" counter.
// Also skip clients when we reached the maximum number of clients to return
if(skipclient[clientID] || id >= (int)Nc)
{
others += client->overTime[slot];
continue;
}

JSON_ADD_NUMBER_TO_ARRAY(data, thisclient);
JSON_ADD_NUMBER_TO_ARRAY(data, client->overTime[slot]);
}
// Add others as last element in the array
others_total += others;
JSON_ADD_NUMBER_TO_ARRAY(data, others);

JSON_ADD_ITEM_TO_OBJECT(item, "data", data);
JSON_ADD_ITEM_TO_ARRAY(history, item);
}
Expand All @@ -137,25 +193,40 @@ int api_history_clients(struct ftl_conn *api)

// Loop over clients to generate output to be sent to the client
cJSON *clients = JSON_NEW_ARRAY();
for(int clientID = 0; clientID < counters->clients; clientID++)
for(int id = 0; id < counters->clients; id++)
{
if(skipclient[clientID])
continue;

// Get client pointer
const int clientID = temparray[2*id + 0];
const clientsData* client = getClient(clientID, true);

// Skip invalid (recycled) clients
if(client == NULL)
continue;

// Skip clients which should be hidden. Also skip clients when
// we reached the maximum number of clients to return
if(skipclient[clientID] || id >= (int)Nc)
continue;

// Get client name and IP address
const char *client_ip = getstr(client->ippos);
const char *client_name = client->namepos != 0 ? getstr(client->namepos) : NULL;

// Create JSON object for this client
cJSON *item = JSON_NEW_OBJECT();
JSON_REF_STR_IN_OBJECT(item, "name", client_name);
JSON_REF_STR_IN_OBJECT(item, "ip", client_ip);
JSON_ADD_NUMBER_TO_OBJECT(item, "total", client->count);
JSON_ADD_ITEM_TO_ARRAY(clients, item);
}

// Add "others" client
cJSON *item = JSON_NEW_OBJECT();
JSON_REF_STR_IN_OBJECT(item, "name", "other clients");
JSON_REF_STR_IN_OBJECT(item, "ip", "0.0.0.0");
JSON_ADD_NUMBER_TO_OBJECT(item, "total", others_total);
JSON_ADD_ITEM_TO_ARRAY(clients, item);

// Unlock already here to avoid keeping the lock during JSON generation
// This is safe because we don't access any shared memory after this
// point and all strings in the JSON are references to idempotent shared
Expand All @@ -164,6 +235,7 @@ int api_history_clients(struct ftl_conn *api)

// Free memory
free(skipclient);
free(temparray);

JSON_ADD_ITEM_TO_OBJECT(json, "clients", clients);
JSON_SEND_OBJECT(json);
Expand Down
3 changes: 2 additions & 1 deletion src/api/stats.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ static int __attribute__((pure)) cmpasc(const void *a, const void *b)
} */

// qsort subroutine, sort DESC
static int __attribute__((pure)) cmpdesc(const void *a, const void *b)
int __attribute__((pure)) cmpdesc(const void *a, const void *b)
{
const int *elem1 = (int*)a;
const int *elem2 = (int*)b;
Expand Down Expand Up @@ -163,6 +163,7 @@ int api_stats_top_domains(struct ftl_conn *api)
return 0;
}


bool blocked = false; // Can be overwritten by query string
int count = 10;
// /api/stats/top_domains?blocked=true
Expand Down
2 changes: 1 addition & 1 deletion src/datastructure.c
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ int _findClientID(const char *clientIP, const bool count, const bool aliasclient
// Set all MAC address bytes to zero
client->hwlen = -1;
memset(client->hwaddr, 0, sizeof(client->hwaddr));
// This may be a alias-client, the ID is set elsewhere
// This may be an alias-client, the ID is set elsewhere
client->flags.aliasclient = aliasclient;
client->aliasclient_id = -1;

Expand Down