From 23c26071654f6ca80600f2f899610cee6faf6af4 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Mon, 5 Aug 2024 19:05:13 +0200 Subject: [PATCH] Add new GET /api/padd endpoint Signed-off-by: DL6ER --- .github/.codespellignore | 1 + src/api/CMakeLists.txt | 1 + src/api/api.c | 1 + src/api/api.h | 15 +- src/api/config.c | 40 ++-- src/api/dns.c | 17 +- src/api/docs/CMakeLists.txt | 1 + src/api/docs/content/specs/main.yaml | 5 + src/api/docs/content/specs/padd.yaml | 248 +++++++++++++++++++++ src/api/docs/docs.h | 5 + src/api/info.c | 35 ++- src/api/network.c | 18 +- src/api/padd.c | 308 +++++++++++++++++++++++++++ src/api/queries.c | 4 +- src/api/stats.c | 79 ++++--- src/datastructure.c | 16 ++ src/datastructure.h | 1 + 17 files changed, 717 insertions(+), 78 deletions(-) create mode 100644 src/api/docs/content/specs/padd.yaml create mode 100644 src/api/padd.c diff --git a/.github/.codespellignore b/.github/.codespellignore index 5ca20bf8..6ffa6265 100644 --- a/.github/.codespellignore +++ b/.github/.codespellignore @@ -11,3 +11,4 @@ mmapped dnsmasq iif prefered +padd diff --git a/src/api/CMakeLists.txt b/src/api/CMakeLists.txt index e42a2505..a96a0f30 100644 --- a/src/api/CMakeLists.txt +++ b/src/api/CMakeLists.txt @@ -20,6 +20,7 @@ set(sources dhcp.c dns.c network.c + padd.c history.c info.c list.c diff --git a/src/api/api.c b/src/api/api.c index 8d21de2f..32acbb61 100644 --- a/src/api/api.c +++ b/src/api/api.c @@ -102,6 +102,7 @@ static struct { { "/api/action/restartdns", "", api_action_restartDNS, { API_PARSE_JSON, 0 }, true, HTTP_POST }, { "/api/action/flush/logs", "", api_action_flush_logs, { API_PARSE_JSON, 0 }, true, HTTP_POST }, { "/api/action/flush/arp", "", api_action_flush_arp, { API_PARSE_JSON, 0 }, true, HTTP_POST }, + { "/api/padd", "", api_padd, { API_PARSE_JSON, 0 }, true, HTTP_GET }, { "/api/docs", "", api_docs, { API_PARSE_JSON, 0 }, false, HTTP_GET }, }; diff --git a/src/api/api.h b/src/api/api.h index 793da3c6..b72a1e92 100644 --- a/src/api/api.h +++ b/src/api/api.h @@ -17,6 +17,8 @@ #include "webserver/http-common.h" // regex_t #include "regex_r.h" +// enum conf_type +#include "config/config.h" // Common definitions #define LOCALHOSTv4 "127.0.0.1" @@ -27,6 +29,7 @@ int api_handler(struct mg_connection *conn, void *ignored); // Statistic methods int __attribute__((pure)) cmpdesc(const void *a, const void *b); +unsigned int get_active_clients(void); 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); @@ -37,7 +40,7 @@ cJSON *get_top_domains(struct ftl_conn *api, const int count, const bool blocked, const bool domains_only); cJSON *get_top_clients(struct ftl_conn *api, const int count, const bool blocked, const bool clients_only, - const bool names_only); + const bool names_only, const bool ip_if_no_name); cJSON *get_top_upstreams(struct ftl_conn *api, const bool upstreams_only); // History methods @@ -71,9 +74,15 @@ int api_info_messages_count(struct ftl_conn *api); int api_info_messages(struct ftl_conn *api); int api_info_metrics(struct ftl_conn *api); int api_info_login(struct ftl_conn *api); +cJSON *read_sys_property(const char *path); +int get_system_obj(struct ftl_conn *api, cJSON *system); +int get_sensors_obj(struct ftl_conn *api, cJSON *sensors, const bool add_list); +int get_version_obj(struct ftl_conn *api, cJSON *version); // Config methods int api_config(struct ftl_conn *api); +int get_json_config(struct ftl_conn *api, cJSON *json, const bool detailed); +cJSON *addJSONConfValue(const enum conf_type conf_type, union conf_value *val); // Log methods int api_logs(struct ftl_conn *api); @@ -84,6 +93,7 @@ int api_network_routes(struct ftl_conn *api); int api_network_interfaces(struct ftl_conn *api); int api_network_devices(struct ftl_conn *api); int api_client_suggestions(struct ftl_conn *api); +int get_gateway(struct ftl_conn *api, cJSON * json, const bool detailed); // DNS methods int api_dns_blocking(struct ftl_conn *api); @@ -132,4 +142,7 @@ int api_search(struct ftl_conn *api); int api_dhcp_leases_GET(struct ftl_conn *api); int api_dhcp_leases_DELETE(struct ftl_conn *api); +// PADD methods +int api_padd(struct ftl_conn *api); + #endif // ROUTES_H diff --git a/src/api/config.c b/src/api/config.c index 91ad30c3..c5d005bb 100644 --- a/src/api/config.c +++ b/src/api/config.c @@ -92,7 +92,7 @@ static cJSON *get_or_create_object(cJSON *parent, const char *path_element) // This function is used to add a property to the JSON output using the // appropriate type of the config item to add. -static cJSON *addJSONvalue(const enum conf_type conf_type, union conf_value *val) +cJSON *addJSONConfValue(const enum conf_type conf_type, union conf_value *val) { switch(conf_type) { @@ -464,17 +464,9 @@ static const char *getJSONvalue(struct conf_item *conf_item, cJSON *elem, struct return NULL; } -static int api_config_get(struct ftl_conn *api) +int get_json_config(struct ftl_conn *api, cJSON *json, const bool detailed) { - // Parse query string parameters - bool detailed = false; - if(api->request->query_string != NULL) - { - // Check if we should return detailed config information - get_bool_var(api->request->query_string, "detailed", &detailed); - } - - // Create root JSON object + // Create root config object cJSON *config_j = JSON_NEW_OBJECT(); // Does the user request only a subset of /config? @@ -552,7 +544,7 @@ static int api_config_get(struct ftl_conn *api) else { // Add current value - cJSON *val = addJSONvalue(conf_item->t, &conf_item->v); + cJSON *val = addJSONConfValue(conf_item->t, &conf_item->v); if(val == NULL) { log_warn("Cannot format config item type %s of type %i", @@ -563,7 +555,7 @@ static int api_config_get(struct ftl_conn *api) } // Add default value - cJSON *dval = addJSONvalue(conf_item->t, &conf_item->d); + cJSON *dval = addJSONConfValue(conf_item->t, &conf_item->d); if(dval == NULL) { log_warn("Cannot format config item type %s of type %i", @@ -592,7 +584,7 @@ static int api_config_get(struct ftl_conn *api) else { // Create the config item leaf object - cJSON *leaf = addJSONvalue(conf_item->t, &conf_item->v); + cJSON *leaf = addJSONConfValue(conf_item->t, &conf_item->v); if(leaf == NULL) { log_warn("Cannot format config item type %s of type %i", @@ -607,8 +599,6 @@ static int api_config_get(struct ftl_conn *api) // Release allocated memory free_config_path(requested_path); - cJSON *json = JSON_NEW_OBJECT(); - // Add topics and DNS server suggestions if in detailed mode if(detailed) { @@ -650,6 +640,24 @@ static int api_config_get(struct ftl_conn *api) // Build and return JSON response JSON_ADD_ITEM_TO_OBJECT(json, "config", config_j); + + return 0; +} + +static int api_config_get(struct ftl_conn *api) +{ + // Parse query string parameters + bool detailed = false; + if(api->request->query_string != NULL) + { + // Check if we should return detailed config information + get_bool_var(api->request->query_string, "detailed", &detailed); + } + + cJSON *json = JSON_NEW_OBJECT(); + get_json_config(api, json, detailed); + + // Build and return JSON response JSON_SEND_OBJECT(json); } diff --git a/src/api/dns.c b/src/api/dns.c index 489ed7df..a7747868 100644 --- a/src/api/dns.c +++ b/src/api/dns.c @@ -31,21 +31,8 @@ static int get_blocking(struct ftl_conn *api) // Return current status cJSON *json = JSON_NEW_OBJECT(); const enum blocking_status blocking = get_blockingstatus(); - switch(blocking) - { - case BLOCKING_ENABLED: - JSON_REF_STR_IN_OBJECT(json, "blocking", "enabled"); - break; - case BLOCKING_DISABLED: - JSON_REF_STR_IN_OBJECT(json, "blocking", "disabled"); - break; - case DNS_FAILED: - JSON_REF_STR_IN_OBJECT(json, "blocking", "failure"); - break; - case BLOCKING_UNKNOWN: - JSON_REF_STR_IN_OBJECT(json, "blocking", "unknown"); - break; - } + const char *status = get_blocking_status_str(blocking); + JSON_REF_STR_IN_OBJECT(json, "blocking", status); // Get timer information (if applicable) double delay; diff --git a/src/api/docs/CMakeLists.txt b/src/api/docs/CMakeLists.txt index 8df3045a..8e22c45d 100644 --- a/src/api/docs/CMakeLists.txt +++ b/src/api/docs/CMakeLists.txt @@ -34,6 +34,7 @@ set(sources hex/specs/logs.yaml hex/specs/main.yaml hex/specs/network.yaml + hex/specs/padd.yaml hex/specs/queries.yaml hex/specs/search.yaml hex/specs/stats.yaml diff --git a/src/api/docs/content/specs/main.yaml b/src/api/docs/content/specs/main.yaml index 6aa7dc79..5f1e35ea 100644 --- a/src/api/docs/content/specs/main.yaml +++ b/src/api/docs/content/specs/main.yaml @@ -63,6 +63,8 @@ tags: description: Methods used to gather advanced information about your network - name: "Actions" description: Methods used to trigger certain actions on your Pi-hole + - name: "PADD" + description: Methods used to query Pi-hole from PADD @@ -274,6 +276,9 @@ paths: /docs: $ref: 'docs.yaml#/components/paths/docs' + /padd: + $ref: 'padd.yaml#/components/paths/padd' + components: securitySchemes: query_sid: diff --git a/src/api/docs/content/specs/padd.yaml b/src/api/docs/content/specs/padd.yaml new file mode 100644 index 00000000..ea7ead7b --- /dev/null +++ b/src/api/docs/content/specs/padd.yaml @@ -0,0 +1,248 @@ +openapi: 3.0.2 +components: + paths: + padd: + get: + summary: Get summarized data for PADD + tags: + - "PADD" + operationId: "get_padd" + parameters: + - in: query + description: (Optional) Return full data + name: full + schema: + type: boolean + required: false + example: true + responses: + '200': + description: OK + content: + application/json: + schema: + allOf: + - $ref: 'padd.yaml#/components/schemas/padd' + - $ref: 'info.yaml#/components/schemas/system' + - $ref: 'info.yaml#/components/schemas/version' + - $ref: 'common.yaml#/components/schemas/took' + '401': + description: Unauthorized + content: + application/json: + schema: + allOf: + - $ref: 'common.yaml#/components/errors/unauthorized' + - $ref: 'common.yaml#/components/schemas/took' + + schemas: + padd: + type: object + properties: + recent_blocked: + type: string + description: "Most recent blocked domain" + nullable: true + example: "bad.example.com" + top_domain: + type: string + description: "Most requested domain" + nullable: true + example: "good.example.com" + top_blocked: + type: string + description: "Most blocked domain" + nullable: true + example: "bad.example.com" + top_client: + type: string + description: "Most active client" + nullable: true + example: "localhost" + active_clients: + type: integer + description: "Number of active clients" + example: 22 + gravity_size: + type: integer + description: "Gravity list size" + example: 225382 + blocking: + type: string + description: "Blocking status" + example: "enabled" + queries: + type: object + properties: + total: + type: integer + description: "Total number of queries within the last 24 hours" + example: 92258 + blocked: + type: integer + description: "Number of blocked queries" + example: 4784 + percent_blocked: + type: number + description: "Percentage of blocked queries" + example: 5.18 + cache: + type: object + properties: + size: + type: integer + description: "Total cache size" + example: 10000 + inserted: + type: integer + description: "Number of inserted cache entries" + example: 233 + evicted: + type: integer + description: "Number of evicted cache entries" + example: 0 + iface: + type: object + description: "Default interfaces" + properties: + v4: + type: object + description: "IPv4 interface" + properties: + addr: + type: string + description: "Primary address" + nullable: true # there may be no IPv4 address + example: "192.168.2.11" + rx_bytes: + type: object + description: "Received bytes" + properties: + value: + type: number + example: 76.46 + unit: + type: string + example: "G" + tx_bytes: + type: object + description: "Transmitted bytes" + properties: + value: + type: number + example: 68.58 + unit: + type: string + example: "G" + num_addrs: + type: integer + description: "Number of addresses on the interface" + example: 1 + name: + type: string + description: "Interface name" + example: "eth0" + gw_addr: + type: string + description: "Gateway address" + nullable: true # there may be no IPv4 gateway + example: "192.168.2.1" + v6: + type: object + description: "IPv6 interface" + properties: + addr: + type: string + description: "Primary address" + nullable: true # there may be no IPv6 address + example: "fe80::b0e4:1b1e:7b7d:5855" + num_addrs: + type: integer + description: "Number of addresses on the interface" + example: 3 + name: + type: string + description: "Interface name" + example: "eth0" + gw_addr: + type: string + description: "Gateway address" + nullable: true # there may be no IPv6 gateway + example: "fe80::b0e4:1b1e:7b7d:1b1e" + node_name: + type: string + description: "Pi-hole host's name" + example: "pihole" + host_model: + type: string + description: "Host model" + example: "Raspberry Pi 3 Model B Plus Rev 1.3" + nullable: true + config: + type: object + description: "Pi-hole configuration (excerpt)" + properties: + dhcp_active: + type: boolean + description: "DHCP server status" + example: true + dhcp_start: + type: string + description: "DHCP start address" + example: "192.168.0.1" + dhcp_end: + type: string + description: "DHCP end address" + example: "192.168.0.254" + dhcp_ipv6: + type: boolean + description: "DHCPv6 server status" + example: false + dns_domain: + type: string + description: "DNS domain" + example: "lan" + dns_port: + type: integer + description: "DNS port" + example: 53 + dns_num_upstreams: + type: integer + description: "Number of upstream DNS servers" + example: 1 + dns_dnssec: + type: boolean + description: "DNSSEC status" + example: true + dns_revServer_active: + type: boolean + description: "Reverse DNS server status" + example: false + "%cpu": + type: number + description: "CPU usage" + example: 0.0 + "%mem": + type: number + description: "Memory usage" + example: 1.5 + pid: + type: integer + description: "FTL's process ID" + example: 1639 + sensors: + type: object + properties: + cpu_temp: + type: number + description: "CPU temperature" + nullable: true + example: 45.0 + hot_limit: + type: number + description: "Temperature limit" + example: 80.0 + unit: + type: string + description: "Temperature unit" + example: "C" diff --git a/src/api/docs/docs.h b/src/api/docs/docs.h index 2884c88d..66c3ad70 100644 --- a/src/api/docs/docs.h +++ b/src/api/docs/docs.h @@ -132,6 +132,10 @@ static const unsigned char specs_action_yaml[] = { #include "hex/specs/action.yaml" }; +static const unsigned char specs_padd_yaml[] = { +#include "hex/specs/padd.yaml" +}; + struct { const char *path; const char *mime_type; @@ -168,6 +172,7 @@ struct { {"specs/stats.yaml", "text/plain", (const char*)specs_stats_yaml, sizeof(specs_stats_yaml)}, {"specs/teleporter.yaml", "text/plain", (const char*)specs_teleporter_yaml, sizeof(specs_teleporter_yaml)}, {"specs/action.yaml", "text/plain", (const char*)specs_action_yaml, sizeof(specs_action_yaml)}, + {"specs/padd.yaml", "text/plain", (const char*)specs_padd_yaml, sizeof(specs_padd_yaml)}, }; #endif // API_DOCS_H diff --git a/src/api/info.c b/src/api/info.c index 581fc0b7..6256d6a0 100644 --- a/src/api/info.c +++ b/src/api/info.c @@ -157,7 +157,7 @@ int api_info_database(struct ftl_conn *api) JSON_SEND_OBJECT(json); } -static int get_system_obj(struct ftl_conn *api, cJSON *system) +int get_system_obj(struct ftl_conn *api, cJSON *system) { const int nprocs = get_nprocs(); struct sysinfo info; @@ -465,7 +465,7 @@ static int get_hwmon_sensors(struct ftl_conn *api, cJSON *sensors) return 0; } -static cJSON *read_sys_property(const char *path) +cJSON *read_sys_property(const char *path) { if(!file_exists(path)) return cJSON_CreateNull(); @@ -641,16 +641,15 @@ int api_info_host(struct ftl_conn *api) JSON_SEND_OBJECT(json); } -int api_info_sensors(struct ftl_conn *api) +int get_sensors_obj(struct ftl_conn *api, cJSON *sensors, const bool add_list) { - cJSON *sensors = JSON_NEW_OBJECT(); - // Get sensors array cJSON *list = JSON_NEW_ARRAY(); int ret = get_hwmon_sensors(api, list); if (ret != 0) return ret; - JSON_ADD_ITEM_TO_OBJECT(sensors, "list", list); + if(add_list) + JSON_ADD_ITEM_TO_OBJECT(sensors, "list", list); // Loop over available sensors and try to identify the most suitable CPU temperature sensor int cpu_temp_sensor = -1; @@ -708,12 +707,25 @@ int api_info_sensors(struct ftl_conn *api) unit = "K"; JSON_REF_STR_IN_OBJECT(sensors, "unit", unit); + if(!add_list) + cJSON_Delete(list); + + return 0; +} + +int api_info_sensors(struct ftl_conn *api) +{ + cJSON *sensors = JSON_NEW_OBJECT(); + int ret = get_sensors_obj(api, sensors, true); + if (ret != 0) + return ret; + cJSON *json = JSON_NEW_OBJECT(); JSON_ADD_ITEM_TO_OBJECT(json, "sensors", sensors); JSON_SEND_OBJECT(json); } -int api_info_version(struct ftl_conn *api) +int get_version_obj(struct ftl_conn *api, cJSON *version) { char *line = NULL; size_t len = 0; @@ -802,8 +814,6 @@ int api_info_version(struct ftl_conn *api) JSON_REF_STR_IN_OBJECT(ftl_local, "version", get_FTL_version()); JSON_REF_STR_IN_OBJECT(ftl_local, "date", GIT_DATE); - cJSON *version = JSON_NEW_OBJECT(); - cJSON *core = JSON_NEW_OBJECT(); JSON_ADD_NULL_IF_NOT_EXISTS(core_local, "branch"); JSON_ADD_NULL_IF_NOT_EXISTS(core_local, "version"); @@ -839,7 +849,14 @@ int api_info_version(struct ftl_conn *api) JSON_ADD_NULL_IF_NOT_EXISTS(docker, "remote"); JSON_ADD_ITEM_TO_OBJECT(version, "docker", docker); + return 0; +} + +int api_info_version(struct ftl_conn *api) +{ // Send reply + cJSON *version = JSON_NEW_OBJECT(); + get_version_obj(api, version); cJSON *json = JSON_NEW_OBJECT(); JSON_ADD_ITEM_TO_OBJECT(json, "version", version); JSON_SEND_OBJECT(json); diff --git a/src/api/network.c b/src/api/network.c index 6ff36a62..67d80c57 100644 --- a/src/api/network.c +++ b/src/api/network.c @@ -32,11 +32,8 @@ // nlroutes(), nladdrs(), nllinks() #include "tools/netlink.h" -int api_network_gateway(struct ftl_conn *api) +int get_gateway(struct ftl_conn *api, cJSON * json, const bool detailed) { - // Get ?detailed parameter - bool detailed = false; - get_bool_var(api->request->query_string, "detailed", &detailed); // Get routing information cJSON *routes = JSON_NEW_ARRAY(); @@ -106,7 +103,6 @@ int api_network_gateway(struct ftl_conn *api) } // Send gateway information - cJSON *json = JSON_NEW_OBJECT(); JSON_ADD_ITEM_TO_OBJECT(json, "gateway", gateway); if(detailed) @@ -121,6 +117,18 @@ int api_network_gateway(struct ftl_conn *api) cJSON_Delete(interfaces); } + return 0; +} + +int api_network_gateway(struct ftl_conn *api) +{ + // Get ?detailed parameter + bool detailed = false; + get_bool_var(api->request->query_string, "detailed", &detailed); + + cJSON *json = JSON_NEW_OBJECT(); + get_gateway(api, json, detailed); + JSON_SEND_OBJECT(json); } diff --git a/src/api/padd.c b/src/api/padd.c new file mode 100644 index 00000000..a3db08b9 --- /dev/null +++ b/src/api/padd.c @@ -0,0 +1,308 @@ +/* Pi-hole: A black hole for Internet advertisements +* (c) 2019 Pi-hole, LLC (https://pi-hole.net) +* Network-wide ad blocking via your own hardware. +* +* FTL Engine +* API Implementation /api/dns +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ + +#include "FTL.h" +#include "webserver/http-common.h" +#include "webserver/json_macros.h" +#include "api.h" +// lock_shm() and unlock_shm() +#include "shmem.h" +// counters +#include "datastructure.h" +// get_dnsmasq_metrics(&metrics) +#include "metrics.h" +// get_blockingstatus() +#include "config/config.h" +// uname() +#include +// nlroutes(), nladdrs(), nllinks() +#include "tools/netlink.h" +// struct proc_mem, getProcessMemory() +#include "procps.h" +// getcpu_percentage() +#include "daemon.h" + +int api_padd(struct ftl_conn *api) +{ + // Parse parameters + bool full = true; + if(api->request->query_string != NULL) + get_bool_var(api->request->query_string, "full", &full); + + cJSON *json = JSON_NEW_OBJECT(); + // Lock shared memory + lock_shm(); + + const int total = counters->queries; + const int blocked = get_blocked_count(); + const unsigned int active_clients = get_active_clients(); + const int num_gravity = counters->database.gravity; + + // If privacy level is set to hide domains, do not return the most + // recent blocked domain + if(config.misc.privacylevel.v.privacy_level < PRIVACY_HIDE_DOMAINS) + { + // Find most recently blocked query + for(int queryID = counters->queries - 1; queryID > 0 ; queryID--) + { + const queriesData* query = getQuery(queryID, true); + if(query == NULL) + continue; + + if(query->flags.blocked) + { + // Ask subroutine for domain. It may return "hidden" depending on + // the privacy settings at the time the query was made + const char *domain = getDomainString(query); + if(domain == NULL) + continue; + + JSON_COPY_STR_TO_OBJECT(json, "recent_blocked", domain); + break; + } + } + } + + // Unlock shared memory + unlock_shm(); + + // Add the number of active clients, the size of the gravity list + JSON_ADD_NUMBER_TO_OBJECT(json, "active_clients", active_clients); + JSON_ADD_NUMBER_TO_OBJECT(json, "gravity_size", num_gravity); + + cJSON *top_domains = get_top_domains(api, 1, false, true); + if(cJSON_GetArraySize(top_domains) == 0) + { + JSON_ADD_NULL_TO_OBJECT(json, "top_domain"); + } + else + { + cJSON *top_domain = cJSON_GetArrayItem(top_domains, 0); + const char *domain = cJSON_GetStringValue(top_domain); + JSON_COPY_STR_TO_OBJECT(json, "top_domain", domain); + } + cJSON_Delete(top_domains); + cJSON *top_blocked = get_top_domains(api, 1, true, true); + if(cJSON_GetArraySize(top_blocked) == 0) + { + JSON_ADD_NULL_TO_OBJECT(json, "top_blocked"); + } + else + { + cJSON *top_block = cJSON_GetArrayItem(top_blocked, 0); + const char *domain = cJSON_GetStringValue(top_block); + JSON_COPY_STR_TO_OBJECT(json, "top_blocked", domain); + } + cJSON *top_clients = get_top_clients(api, 1, false, true, false, true); + if(cJSON_GetArraySize(top_clients) == 0) + { + JSON_ADD_NULL_TO_OBJECT(json, "top_client"); + } + else + { + cJSON *top_client = cJSON_GetArrayItem(top_clients, 0); + const char *client = cJSON_GetStringValue(top_client); + JSON_COPY_STR_TO_OBJECT(json, "top_client", client); + } + + // Add a null entry if the domain is hidden or there is no recent + // blocked domain (e.g. when blocking is disabled) + JSON_ADD_NULL_IF_NOT_EXISTS(json, "recent_blocked"); + + // Calculate percentage of blocked queries + float percent_blocked = 0.0f; + // Avoid 1/0 condition + if(total > 0) + percent_blocked = 1e2f*blocked/total; + + // Add the blocking status + const char *blocking = get_blocking_status_str(get_blockingstatus()); + JSON_REF_STR_IN_OBJECT(json, "blocking", blocking); + + // Add query statistics + cJSON *queries = JSON_NEW_OBJECT(); + JSON_ADD_NUMBER_TO_OBJECT(queries, "total", total); + JSON_ADD_NUMBER_TO_OBJECT(queries, "blocked", blocked); + JSON_ADD_NUMBER_TO_OBJECT(queries, "percent_blocked", percent_blocked); + JSON_ADD_ITEM_TO_OBJECT(json, "queries", queries); + + // Add cache statistics + cJSON *cache = JSON_NEW_OBJECT(); + struct metrics metrics = { 0 }; + get_dnsmasq_metrics(&metrics); + JSON_ADD_NUMBER_TO_OBJECT(cache, "size", metrics.dns.cache.size); + JSON_ADD_NUMBER_TO_OBJECT(cache, "inserted", metrics.dns.cache.inserted); + JSON_ADD_NUMBER_TO_OBJECT(cache, "evicted", metrics.dns.cache.live_freed); + JSON_ADD_ITEM_TO_OBJECT(json, "cache", cache); + + // info/system + cJSON *system = JSON_NEW_OBJECT(); + get_system_obj(api, system); + JSON_ADD_ITEM_TO_OBJECT(json, "system", system); + + // info/host + struct utsname un = { 0 }; + uname(&un); + JSON_COPY_STR_TO_OBJECT(json, "node_name", un.nodename); + JSON_ADD_ITEM_TO_OBJECT(json, "host_model", read_sys_property("/sys/firmware/devicetree/base/model")); + + // Expensive calls, do only if full is requested + if(full) + { + // network/gateway + cJSON *gateway_ = JSON_NEW_OBJECT(); + get_gateway(api, gateway_, true); + + cJSON *gateway = cJSON_GetObjectItemCaseSensitive(gateway_, "gateway"); + cJSON *interfaces = cJSON_GetObjectItemCaseSensitive(gateway_, "interfaces"); + + // Loop over gateway and find first entry with family == "inet" + cJSON *entry = NULL; + const char *gw_v4_name = NULL, *gw_v6_name = NULL; + const char *gw_v4_addr = NULL, *gw_v6_addr = NULL; + cJSON_ArrayForEach(entry, gateway) + { + cJSON *family = cJSON_GetObjectItemCaseSensitive(entry, "family"); + if(gw_v4_name == NULL && strcmp(cJSON_GetStringValue(family), "inet") == 0) + { + gw_v4_name = cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(entry, "interface")); + gw_v4_addr = cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(entry, "address")); + } + if(gw_v6_name == NULL && strcmp(cJSON_GetStringValue(family), "inet6") == 0) + { + gw_v6_name = cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(entry, "interface")); + gw_v6_addr = cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(entry, "address")); + } + + // Break if both addresses are found + if(gw_v4_name && gw_v6_name) + break; + } + + // If no IPv6 gateway is found, use the IPv4 gateway + if(gw_v6_name == NULL) + gw_v6_name = gw_v4_name; + + // Iterate over all interfaces until we find the one associated + // with the IPv4 gateway + cJSON *iface_v4 = JSON_NEW_OBJECT(); + cJSON *iface_v6 = JSON_NEW_OBJECT(); + unsigned int v4_addrs = 0, v6_addrs = 0; + cJSON_ArrayForEach(entry, interfaces) + { + if(strcmp(cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(entry, "name")), gw_v4_name) == 0) + { + // Add first interface address with family == inet + cJSON *addr = NULL; + cJSON *addrs = cJSON_GetObjectItemCaseSensitive(entry, "addresses"); + cJSON_ArrayForEach(addr, addrs) + { + cJSON *family = cJSON_GetObjectItemCaseSensitive(addr, "family"); + if(strcmp(cJSON_GetStringValue(family), "inet") == 0) + { + if(v4_addrs == 0) + { + cJSON *_addr = cJSON_GetObjectItemCaseSensitive(addr, "address"); + JSON_COPY_STR_TO_OBJECT(iface_v4, "addr", cJSON_GetStringValue(_addr)); + } + v4_addrs++; + } + } + + // Add NULL if no IPv4 address is found + if(v4_addrs == 0) + JSON_ADD_NULL_TO_OBJECT(iface_v4, "addr"); + + // Also add IPv4 interface statistics + cJSON *stats = cJSON_GetObjectItemCaseSensitive(entry, "stats"); + cJSON *rx_bytes = cJSON_GetObjectItemCaseSensitive(stats, "rx_bytes"); + JSON_ADD_ITEM_TO_OBJECT(iface_v4, "rx_bytes", cJSON_Duplicate(rx_bytes, true)); + cJSON *tx_bytes = cJSON_GetObjectItemCaseSensitive(stats, "tx_bytes"); + JSON_ADD_ITEM_TO_OBJECT(iface_v4, "tx_bytes", cJSON_Duplicate(tx_bytes, true)); + } + if(strcmp(cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(entry, "name")), gw_v6_name) == 0) + { + // Add first interface address with family == inet + cJSON *addr = NULL; + cJSON *addrs = cJSON_GetObjectItemCaseSensitive(entry, "addresses"); + cJSON_ArrayForEach(addr, addrs) + { + cJSON *family = cJSON_GetObjectItemCaseSensitive(addr, "family"); + if(strcmp(cJSON_GetStringValue(family), "inet6") == 0) + { + if(v6_addrs == 0) + { + cJSON *_addr = cJSON_GetObjectItemCaseSensitive(addr, "address"); + JSON_COPY_STR_TO_OBJECT(iface_v6, "addr", cJSON_GetStringValue(_addr)); + } + v6_addrs++; + } + } + + // Add NULL if no IPv6 address is found + if(v6_addrs == 0) + JSON_ADD_NULL_TO_OBJECT(iface_v6, "addr"); + } + } + + // Add the number of addresses found + JSON_ADD_NUMBER_TO_OBJECT(iface_v4, "num_addrs", v4_addrs); + JSON_ADD_NUMBER_TO_OBJECT(iface_v6, "num_addrs", v6_addrs); + + // Add the interfaces to the gateway object + JSON_COPY_STR_TO_OBJECT(iface_v4, "name", gw_v4_name); + JSON_COPY_STR_TO_OBJECT(iface_v4, "gw_addr", gw_v4_addr); + JSON_COPY_STR_TO_OBJECT(iface_v6, "name", gw_v6_name); + JSON_COPY_STR_TO_OBJECT(iface_v6, "gw_addr", gw_v6_addr); + + // Create interface object + cJSON *iface = JSON_NEW_OBJECT(); + JSON_ADD_ITEM_TO_OBJECT(iface, "v4", iface_v4); + JSON_ADD_ITEM_TO_OBJECT(iface, "v6", iface_v6); + JSON_ADD_ITEM_TO_OBJECT(json, "iface", iface); + + // Free memory + cJSON_Delete(gateway_); + + // info/version + cJSON *version = JSON_NEW_OBJECT(); + get_version_obj(api, version); + JSON_ADD_ITEM_TO_OBJECT(json, "version", version); + } + + // subset of config + cJSON *jconfig = JSON_NEW_OBJECT(); + JSON_ADD_BOOL_TO_OBJECT(jconfig, "dhcp_active", config.dhcp.active.v.b); + JSON_ADD_ITEM_TO_OBJECT(jconfig, "dhcp_start", addJSONConfValue(config.dhcp.start.t, &config.dhcp.start.v)); + JSON_ADD_ITEM_TO_OBJECT(jconfig, "dhcp_end", addJSONConfValue(config.dhcp.end.t, &config.dhcp.end.v)); + JSON_ADD_BOOL_TO_OBJECT(jconfig, "dhcp_ipv6", config.dhcp.ipv6.v.b); + JSON_COPY_STR_TO_OBJECT(jconfig, "dns_domain", config.dns.domain.v.s); + JSON_ADD_NUMBER_TO_OBJECT(jconfig, "dns_port", config.dns.port.v.u16); + JSON_ADD_NUMBER_TO_OBJECT(jconfig, "dns_num_upstreams", cJSON_GetArraySize(config.dns.upstreams.v.json)); + JSON_ADD_BOOL_TO_OBJECT(jconfig, "dns_dnssec", config.dns.dnssec.v.b); + JSON_ADD_BOOL_TO_OBJECT(jconfig, "dns_revServer_active", cJSON_GetArraySize(config.dns.revServers.v.json) > 0); + JSON_ADD_ITEM_TO_OBJECT(json, "config", jconfig); + + // subset of info/ftl + struct proc_mem pmem = { 0 }; + struct proc_meminfo mem = { 0 }; + parse_proc_meminfo(&mem); + getProcessMemory(&pmem, mem.total); + JSON_ADD_NUMBER_TO_OBJECT(json, "%mem", pmem.VmRSS_percent); + JSON_ADD_NUMBER_TO_OBJECT(json, "%cpu", get_cpu_percentage()); + JSON_ADD_NUMBER_TO_OBJECT(json, "pid", getpid()); + + // info/sensors -> CPU temp sensor + cJSON *sensors = JSON_NEW_OBJECT(); + get_sensors_obj(api, sensors, false); + JSON_ADD_ITEM_TO_OBJECT(json, "sensors", sensors); + + JSON_SEND_OBJECT(json); +} diff --git a/src/api/queries.c b/src/api/queries.c index bc8dac1d..5b94d940 100644 --- a/src/api/queries.c +++ b/src/api/queries.c @@ -114,8 +114,8 @@ int api_queries_suggestions(struct ftl_conn *api) cJSON_Delete(blocked); // Get clients, both by IP and names - cJSON *client_ip = get_top_clients(api, count, false, true, false); - cJSON *client_name = get_top_clients(api, count, false, true, true); + cJSON *client_ip = get_top_clients(api, count, false, true, false, false); + cJSON *client_name = get_top_clients(api, count, false, true, true, false); // Delete duplicate entries from client_name cJSON_unique_array(client_name); diff --git a/src/api/stats.c b/src/api/stats.c index fcfa02a6..12024bfc 100644 --- a/src/api/stats.c +++ b/src/api/stats.c @@ -92,26 +92,54 @@ static int get_query_types_obj(struct ftl_conn *api, cJSON *types) return 0; } +// shmem needs to be locked while calling this function +unsigned int get_active_clients(void) +{ + unsigned int activeclients = 0; + for(int clientID=0; clientID < counters->clients; clientID++) + { + // Get client pointer + const clientsData* client = getClient(clientID, true); + if(client == NULL) + continue; + + if(client->count > 0) + activeclients++; + } + + return activeclients; +} + int api_stats_summary(struct ftl_conn *api) { - const int blocked = get_blocked_count(); - const int forwarded = get_forwarded_count(); - const int cached = get_cached_count(); - const int total = counters->queries; - float percent_blocked = 0.0f; + // Lock shared memory + lock_shm(); + const int blocked = get_blocked_count(); + const int forwarded = get_forwarded_count(); + const int cached = get_cached_count(); + const int total = counters->queries; + const int num_gravity = counters->database.gravity; + const int num_clients = counters->clients; + const int num_domains = counters->domains; + + // Count clients that have been active within the most recent 24 hours + unsigned int activeclients = get_active_clients(); + + // Unlock shared memory + unlock_shm(); + + // Calculate percentage of blocked queries + float percent_blocked = 0.0f; // Avoid 1/0 condition if(total > 0) percent_blocked = 1e2f*blocked/total; - // Lock shared memory - lock_shm(); - cJSON *queries = JSON_NEW_OBJECT(); JSON_ADD_NUMBER_TO_OBJECT(queries, "total", total); JSON_ADD_NUMBER_TO_OBJECT(queries, "blocked", blocked); JSON_ADD_NUMBER_TO_OBJECT(queries, "percent_blocked", percent_blocked); - JSON_ADD_NUMBER_TO_OBJECT(queries, "unique_domains", counters->domains); + JSON_ADD_NUMBER_TO_OBJECT(queries, "unique_domains", num_domains); JSON_ADD_NUMBER_TO_OBJECT(queries, "forwarded", forwarded); JSON_ADD_NUMBER_TO_OBJECT(queries, "cached", cached); @@ -131,28 +159,12 @@ int api_stats_summary(struct ftl_conn *api) JSON_ADD_NUMBER_TO_OBJECT(replies, get_query_reply_str(reply), counters->reply[reply]); JSON_ADD_ITEM_TO_OBJECT(queries, "replies", replies); - // Count clients that have been active within the most recent 24 hours - unsigned int activeclients = 0; - for(int clientID=0; clientID < counters->clients; clientID++) - { - // Get client pointer - const clientsData* client = getClient(clientID, true); - if(client == NULL) - continue; - - if(client->count > 0) - activeclients++; - } - cJSON *clients = JSON_NEW_OBJECT(); JSON_ADD_NUMBER_TO_OBJECT(clients, "active", activeclients); - JSON_ADD_NUMBER_TO_OBJECT(clients, "total", counters->clients); + JSON_ADD_NUMBER_TO_OBJECT(clients, "total", num_clients); cJSON *gravity = JSON_NEW_OBJECT(); - JSON_ADD_NUMBER_TO_OBJECT(gravity, "domains_being_blocked", counters->database.gravity); - - // Unlock shared memory - unlock_shm(); + JSON_ADD_NUMBER_TO_OBJECT(gravity, "domains_being_blocked", num_gravity); cJSON *json = JSON_NEW_OBJECT(); JSON_ADD_ITEM_TO_OBJECT(json, "queries", queries); @@ -335,7 +347,7 @@ int api_stats_top_domains(struct ftl_conn *api) cJSON *get_top_clients(struct ftl_conn *api, const int count, const bool blocked, const bool clients_only, - const bool names_only) + const bool names_only, const bool ip_if_no_name) { // Exit before processing any data if requested via config setting if(config.misc.privacylevel.v.privacy_level >= PRIVACY_HIDE_DOMAINS_CLIENTS) @@ -449,7 +461,14 @@ cJSON *get_top_clients(struct ftl_conn *api, const int count, if(clients_only) { - if(names_only) + if(ip_if_no_name) + { + if(strlen(client_name) > 0) + cJSON_AddStringToArray(jtop_clients, client_name); + else + cJSON_AddStringToArray(jtop_clients, client_ip); + } + else if(names_only) { if(strlen(client_name) > 0) cJSON_AddStringToArray(jtop_clients, client_name); @@ -516,7 +535,7 @@ int api_stats_top_clients(struct ftl_conn *api) get_int_var(api->request->query_string, "count", &count); } - cJSON *json = get_top_clients(api, count, blocked, false, false); + cJSON *json = get_top_clients(api, count, blocked, false, false, false); JSON_SEND_OBJECT(json); } diff --git a/src/datastructure.c b/src/datastructure.c index 72376a9f..7ac9a1e2 100644 --- a/src/datastructure.c +++ b/src/datastructure.c @@ -835,6 +835,22 @@ int __attribute__ ((pure)) get_blocking_mode_val(const char *blocking_mode) return -1; } +const char * __attribute__ ((const)) get_blocking_status_str(const enum blocking_status blocking) +{ + switch(blocking) + { + case BLOCKING_ENABLED: + return "enabled"; + case BLOCKING_DISABLED: + return "disabled"; + case DNS_FAILED: + return "failure"; + case BLOCKING_UNKNOWN: + default: + return "unknown"; + } +} + bool __attribute__ ((const)) is_blocked(const enum query_status status) { switch (status) diff --git a/src/datastructure.h b/src/datastructure.h index b2b66772..48450a94 100644 --- a/src/datastructure.h +++ b/src/datastructure.h @@ -159,6 +159,7 @@ const char *get_refresh_hostnames_str(const enum refresh_hostnames refresh) __at int get_refresh_hostnames_val(const char *refresh_hostnames) __attribute__ ((pure)); const char *get_blocking_mode_str(const enum blocking_mode mode) __attribute__ ((const)); int get_blocking_mode_val(const char *blocking_mode) __attribute__ ((pure)); +const char * __attribute__ ((const)) get_blocking_status_str(const enum blocking_status blocking); const char *get_ptr_type_str(const enum ptr_type piholePTR) __attribute__ ((const)); int get_ptr_type_val(const char *piholePTR) __attribute__ ((pure)); const char *get_busy_reply_str(const enum busy_reply replyWhenBusy) __attribute__ ((const));