From f199ac2f089ed91cc20b43aacfa2b6dcda01331b Mon Sep 17 00:00:00 2001 From: DL6ER Date: Sun, 8 Jan 2023 21:21:38 +0100 Subject: [PATCH] Automatically migrate the API password hash and the lists of clients and domains to be excluded from setupVars.conf Signed-off-by: DL6ER --- src/api/auth.c | 36 ++--- src/api/config.c | 20 +++ src/api/docs/content/specs/config.yaml | 34 ++++- src/api/history.c | 34 +++-- src/api/queries.c | 22 ++- src/api/stats.c | 56 ++++---- src/config/config.c | 105 +++++++++++---- src/config/config.h | 21 ++- src/config/legacy_reader.c | 18 +-- src/config/toml_helper.c | 43 ++++++ src/config/toml_reader.c | 4 +- src/config/toml_writer.c | 11 +- src/database/database-thread.c | 37 ++--- src/dnsmasq_interface.c | 19 +-- src/enums.h | 3 - src/events.c | 6 - src/main.c | 1 - src/setupVars.c | 180 +++++++++---------------- src/setupVars.h | 13 +- src/signals.c | 9 -- src/timers.c | 2 +- src/webserver/http-common.c | 2 +- test/api/checkAPI.py | 2 +- test/api/libs/responseVerifyer.py | 3 + 24 files changed, 376 insertions(+), 305 deletions(-) diff --git a/src/api/auth.c b/src/api/auth.c index 4775e302..1b96568d 100644 --- a/src/api/auth.c +++ b/src/api/auth.c @@ -14,7 +14,7 @@ #include "api.h" #include "../log.h" #include "../config/config.h" -// read_setupVarsconf() +// get_password_hash() #include "../setupVars.h" // (un)lock_shm() #include "../shmem.h" @@ -87,15 +87,12 @@ int check_client_auth(struct ftl_conn *api) { // Is the user requesting from localhost? // This may be allowed without authentication depending on the configuration - if(!config.http.localAPIauth.v.b && (strcmp(api->request->remote_addr, LOCALHOSTv4) == 0 || + if(!config.api.localAPIauth.v.b && (strcmp(api->request->remote_addr, LOCALHOSTv4) == 0 || strcmp(api->request->remote_addr, LOCALHOSTv6) == 0)) return API_AUTH_LOCALHOST; // Check if there is a password hash - char *password_hash = get_password_hash(); - const bool empty_password = (strlen(password_hash) == 0u); - free(password_hash); - if(empty_password) + if(strlen(config.api.pwhash.v.s) == 0u) return API_AUTH_EMPTYPASS; // Does the client provide a session cookie? @@ -189,12 +186,12 @@ int check_client_auth(struct ftl_conn *api) // Update timestamp of this client to extend // the validity of their API authentication - auth_data[user_id].valid_until = now + config.http.sessionTimeout.v.ui; + auth_data[user_id].valid_until = now + config.api.sessionTimeout.v.ui; // Update user cookie if(snprintf(pi_hole_extra_headers, sizeof(pi_hole_extra_headers), FTL_SET_COOKIE, - auth_data[user_id].sid, config.http.sessionTimeout.v.ui) < 0) + auth_data[user_id].sid, config.api.sessionTimeout.v.ui) < 0) { return send_json_error(api, 500, "internal_error", "Internal server error", NULL); } @@ -360,7 +357,7 @@ static void generateChallenge(const unsigned int idx, const time_t now) challenges[idx].valid_until = now + API_CHALLENGE_TIMEOUT; } -static void generateResponse(const unsigned int idx, const char *password_hash) +static void generateResponse(const unsigned int idx) { uint8_t raw_response[SHA256_DIGEST_SIZE]; struct sha256_ctx ctx; @@ -376,8 +373,8 @@ static void generateResponse(const unsigned int idx, const char *password_hash) // Get and add password hash from setupVars.conf sha256_update(&ctx, - strlen(password_hash), - (uint8_t*)password_hash); + strlen(config.api.pwhash.v.s), + (uint8_t*)config.api.pwhash.v.s); sha256_digest(&ctx, SHA256_DIGEST_SIZE, raw_response); sha256_hex(raw_response, challenges[idx].response); @@ -407,10 +404,7 @@ int api_auth(struct ftl_conn *api) // Check HTTP method const time_t now = time(NULL); - lock_shm(); - char *password_hash = get_password_hash(); - unlock_shm(); - const bool empty_password = (strlen(password_hash) == 0u); + const bool empty_password = strlen(config.api.pwhash.v.s) == 0u; int user_id = API_AUTH_UNAUTHORIZED; @@ -498,7 +492,7 @@ int api_auth(struct ftl_conn *api) if(!auth_data[i].used) { auth_data[i].used = true; - auth_data[i].valid_until = now + config.http.sessionTimeout.v.ui; + auth_data[i].valid_until = now + config.api.sessionTimeout.v.ui; strncpy(auth_data[i].remote_addr, api->request->remote_addr, sizeof(auth_data[i].remote_addr)); auth_data[i].remote_addr[sizeof(auth_data[i].remote_addr)-1] = '\0'; generateSID(auth_data[i].sid); @@ -524,12 +518,10 @@ int api_auth(struct ftl_conn *api) } else { - log_debug(DEBUG_API, "API: Response incorrect. Response=%s, setupVars=%s", response, password_hash); + log_debug(DEBUG_API, "API: Response incorrect. Response=%s, FTL=%s", response, config.api.pwhash.v.s); } // Free allocated memory - free(password_hash); - password_hash = NULL; return send_api_auth_status(api, user_id, now); } else @@ -564,11 +556,7 @@ int api_auth(struct ftl_conn *api) generateChallenge(i, now); // Compute and store expected response for this challenge (SHA-256) - generateResponse(i, password_hash); - - // Free allocated memory - free(password_hash); - password_hash = NULL; + generateResponse(i); log_debug(DEBUG_API, "API: Sending challenge=%s", challenges[i].challenge); diff --git a/src/api/config.c b/src/api/config.c index 78eab450..2f63c48a 100644 --- a/src/api/config.c +++ b/src/api/config.c @@ -102,6 +102,12 @@ static cJSON *addJSONvalue(const enum conf_type conf_type, union conf_value *val inet_ntop(AF_INET6, &val->in6_addr, addr6, INET6_ADDRSTRLEN); return cJSON_CreateString(addr6); // Performs a copy } + case CONF_JSON_STRING_ARRAY: + { + // Return a duplicate to ensure our instance isn't getting freed + // after returning the reply + return cJSON_Duplicate(val->json, true); + } default: return NULL; } @@ -278,6 +284,20 @@ static const char *getJSONvalue(struct conf_item *conf_item, cJSON *elem) log_debug(DEBUG_CONFIG, "Set %s to %s", conf_item->k, elem->valuestring); break; } + case CONF_JSON_STRING_ARRAY: + { + if(!cJSON_IsArray(elem)) + return "not of type array"; + const unsigned int elems = cJSON_GetArraySize(elem); + for(unsigned int i = 0; i < elems; i++) + { + const cJSON *item = cJSON_GetArrayItem(elem, i); + if(!cJSON_IsString(item)) + return "array has invalid elements"; + } + // If we reach this point, all elements are valid + conf_item->v.json = cJSON_Duplicate(elem, true); + } } return NULL; } diff --git a/src/api/docs/content/specs/config.yaml b/src/api/docs/content/specs/config.yaml index c23a92af..ab3d7c9f 100644 --- a/src/api/docs/content/specs/config.yaml +++ b/src/api/docs/content/specs/config.yaml @@ -77,10 +77,15 @@ components: type: string blockTTL: type: integer - blockingmode: - type: string port: type: integer + blocking: + type: object + properties: + active: + type: boolean + mode: + type: string specialDomains: type: object properties: @@ -151,7 +156,7 @@ components: type: boolean expire: type: integer - http: + api: type: object properties: localAPIauth: @@ -160,6 +165,19 @@ components: type: boolean sessionTimeout: type: integer + pwhash: + type: string + exclude_clients: + type: array + items: + type: string + exclude_domains: + type: array + items: + type: string + http: + type: object + properties: domain: type: string acl: @@ -279,7 +297,9 @@ components: piholePTR: PI.HOLE replyWhenBusy: ALLOW blockTTL: 2 - blockingmode: 'NULL' + blocking: + active: true + mode: 'NULL' specialDomains: mozillaCanary: true iCloudPrivateRelay: true @@ -312,10 +332,14 @@ components: network: parseARPcache: true expire: 365 - http: + api: localAPIauth: false prettyJSON: false sessionTimeout: 300 + pwhash: '' + exclude_clients: [ '1.2.3.4', 'localhost', 'fe80::345' ] + exclude_domains: [ 'google.de', 'pi-hole.net' ] + http: domain: pi.hole acl: "+0.0.0.0/0" port: 8080,[::]:8080 diff --git a/src/api/history.c b/src/api/history.c index 623a9631..56434f3c 100644 --- a/src/api/history.c +++ b/src/api/history.c @@ -126,31 +126,42 @@ int api_history_clients(struct ftl_conn *api) } // Get clients which the user doesn't want to see - char * excludeclients = read_setupVarsconf("API_EXCLUDE_CLIENTS"); - // Array of clients to be skipped in the output // if skipclient[i] == true then this client should be hidden from // returned data. We initialize it with false bool skipclient[counters->clients]; memset(skipclient, false, counters->clients*sizeof(bool)); - if(excludeclients != NULL) + unsigned int exclude_clients = cJSON_GetArraySize(config.api.exclude_clients.v.json); + if(exclude_clients > 0) { - getSetupVarsArray(excludeclients); - - for(int clientID=0; clientID < counters->clients; clientID++) + 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(insetupVarsArray(getstr(client->ippos)) || - insetupVarsArray(getstr(client->namepos)) || - (!client->flags.aliasclient && client->aliasclient_id > -1)) - skipclient[clientID] = true; + for(unsigned int i = 0; i < exclude_clients; i++) + { + cJSON *item = cJSON_GetArrayItem(config.api.exclude_clients.v.json, i); + if(strcmp(getstr(client->ippos), item->valuestring) == 0 || + strcmp(getstr(client->namepos), item->valuestring) == 0) + skipclient[clientID] = true; + } } } + // Also skip alias-clients + for(int clientID=0; clientID < counters->clients; clientID++) + { + // Get client pointer + const clientsData* client = getClient(clientID, true); + if(client == NULL) + continue; + if(!client->flags.aliasclient && client->aliasclient_id > -1) + skipclient[clientID] = true; + } + cJSON *history = JSON_NEW_ARRAY(); // Main return loop for(int slot = sendit; slot < until; slot++) @@ -204,8 +215,5 @@ int api_history_clients(struct ftl_conn *api) } JSON_ADD_ITEM_TO_OBJECT(json, "clients", clients); - if(excludeclients != NULL) - clearSetupVarsArray(); - JSON_SEND_OBJECT_UNLOCK(json); } diff --git a/src/api/queries.c b/src/api/queries.c index 8f4c2c51..5869c536 100644 --- a/src/api/queries.c +++ b/src/api/queries.c @@ -8,23 +8,21 @@ * 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" -#include "../shmem.h" -#include "../datastructure.h" +#include "FTL.h" +#include "webserver/http-common.h" +#include "webserver/json_macros.h" +#include "api/api.h" +#include "shmem.h" +#include "datastructure.h" // config struct -#include "../config/config.h" -// read_setupVarsconf() -#include "../setupVars.h" +#include "config/config.h" // get_aliasclient_list() -#include "../database/aliasclients.h" +#include "database/aliasclients.h" // get_memdb() -#include "../database/query-table.h" +#include "database/query-table.h" // dbopen(), dbclose() -#include "../database/common.h" +#include "database/common.h" static int add_strings_to_array(struct ftl_conn *api, cJSON *array, const char *querystr, const int max_count) { diff --git a/src/api/stats.c b/src/api/stats.c index 19ba856a..88dbccbb 100644 --- a/src/api/stats.c +++ b/src/api/stats.c @@ -190,17 +190,11 @@ int api_stats_top_domains(struct ftl_conn *api) clearSetupVarsArray(); // Get domains which the user doesn't want to see - char *excludedomains = NULL; - if(!audit) - { - excludedomains = read_setupVarsconf("API_EXCLUDE_DOMAINS"); - if(excludedomains != NULL) - getSetupVarsArray(excludedomains); - } + unsigned int exclude_domains = cJSON_GetArraySize(config.api.exclude_domains.v.json); int n = 0; cJSON *top_domains = JSON_NEW_ARRAY(); - for(int i=0; i < counters->domains; i++) + for(int i = 0; i < counters->domains; i++) { // Get sorted index const int domainID = temparray[i][0]; @@ -209,9 +203,23 @@ int api_stats_top_domains(struct ftl_conn *api) if(domain == NULL) continue; - // Skip this domain if there is a filter on it - if(excludedomains != NULL && insetupVarsArray(getstr(domain->domainpos))) - continue; + // Skip this domain if there is a filter on it (but only if not in audit mode) + if(!audit) + { + // Check if this client should be skipped + bool skip_domain = false; + for(unsigned int j = 0; j < exclude_domains; j++) + { + cJSON *item = cJSON_GetArrayItem(config.api.exclude_domains.v.json, j); + if(strcmp(getstr(domain->domainpos), item->valuestring) == 0) + { + skip_domain = true; + break; + } + } + if(skip_domain) + continue; + } // Skip this domain if already audited if(audit && in_auditlist(getstr(domain->domainpos)) > 0) @@ -248,9 +256,6 @@ int api_stats_top_domains(struct ftl_conn *api) break; } - if(excludedomains != NULL) - clearSetupVarsArray(); - cJSON *json = JSON_NEW_OBJECT(); JSON_ADD_ITEM_TO_OBJECT(json, "domains", top_domains); @@ -322,11 +327,7 @@ int api_stats_top_clients(struct ftl_conn *api) qsort(temparray, counters->clients, sizeof(int[2]), cmpdesc); // Get clients which the user doesn't want to see - const char* excludeclients = read_setupVarsconf("API_EXCLUDE_CLIENTS"); - if(excludeclients != NULL) - { - getSetupVarsArray(excludeclients); - } + unsigned int exclude_clients = cJSON_GetArraySize(config.api.exclude_clients.v.json); int n = 0; cJSON *top_clients = JSON_NEW_ARRAY(); @@ -341,8 +342,18 @@ int api_stats_top_clients(struct ftl_conn *api) continue; // Skip this client if there is a filter on it - if(excludeclients != NULL && - (insetupVarsArray(getstr(client->ippos)) || insetupVarsArray(getstr(client->namepos)))) + bool skip_domain = false; + for(unsigned int j = 0; j < exclude_clients; j++) + { + cJSON *item = cJSON_GetArrayItem(config.api.exclude_clients.v.json, j); + if(strcmp(getstr(client->ippos), item->valuestring) == 0 || + strcmp(getstr(client->namepos), item->valuestring) == 0) + { + skip_domain = true; + break; + } + } + if(skip_domain) continue; // Hidden client, probably due to privacy level. Skip this in the top lists @@ -370,9 +381,6 @@ int api_stats_top_clients(struct ftl_conn *api) break; } - if(excludeclients != NULL) - clearSetupVarsArray(); - cJSON *json = JSON_NEW_OBJECT(); JSON_ADD_ITEM_TO_OBJECT(json, "clients", top_clients); diff --git a/src/config/config.c b/src/config/config.c index 7f800156..24da5d37 100644 --- a/src/config/config.c +++ b/src/config/config.c @@ -185,11 +185,17 @@ void initConfig(void) config.dns.blockTTL.t = CONF_UINT; config.dns.blockTTL.d.ui = 2; - config.dns.blockingmode.k = "dns.blockingmode"; - config.dns.blockingmode.h = "How should FTL reply to blocked queries?"; - config.dns.blockingmode.a = "[ \"NULL\", \"IP-NODATA-AAAA\", \"IP\", \"NXDOMAIN\", \"NODATA\" ]"; - config.dns.blockingmode.t = CONF_ENUM_BLOCKING_MODE; - config.dns.blockingmode.d.blocking_mode = MODE_NULL; + // sub-struct dns.blocking + config.dns.blocking.active.k = "dns.blocking.active"; + config.dns.blocking.active.h = "Should FTL block queries?"; + config.dns.blocking.active.t = CONF_BOOL; + config.dns.blocking.active.d.b = true; + + config.dns.blocking.mode.k = "dns.blocking.mode"; + config.dns.blocking.mode.h = "How should FTL reply to blocked queries?"; + config.dns.blocking.mode.a = "[ \"NULL\", \"IP-NODATA-AAAA\", \"IP\", \"NXDOMAIN\", \"NODATA\" ]"; + config.dns.blocking.mode.t = CONF_ENUM_BLOCKING_MODE; + config.dns.blocking.mode.d.blocking_mode = MODE_NULL; // sub-struct dns.rate_limit config.dns.rateLimit.count.k = "dns.rateLimit.count"; @@ -320,22 +326,42 @@ void initConfig(void) config.database.network.expire.d.ui = config.database.maxDBdays.d.ui; + // struct api + config.api.localAPIauth.k = "api.localAPIauth"; + config.api.localAPIauth.h = "Does local clients need to authenticate to access the API?"; + config.api.localAPIauth.t = CONF_BOOL; + config.api.localAPIauth.d.b = true; + + config.api.prettyJSON.k = "api.prettyJSON"; + config.api.prettyJSON.h = "Should FTL prettify the API output?"; + config.api.prettyJSON.t = CONF_BOOL; + config.api.prettyJSON.d.b = false; + + config.api.sessionTimeout.k = "api.sessionTimeout"; + config.api.sessionTimeout.h = "How long should a session be considered valid after login [seconds]?"; + config.api.sessionTimeout.t = CONF_UINT; + config.api.sessionTimeout.d.ui = 300; + + config.api.pwhash.k = "api.pwhash"; + config.api.pwhash.h = "API password hash"; + config.api.pwhash.a = ""; + config.api.pwhash.t = CONF_STRING; + config.api.pwhash.d.s = NULL; + + config.api.exclude_clients.k = "api.exclude_clients"; + config.api.exclude_clients.h = "Array of clients to be excluded from certain API responses"; + config.api.exclude_clients.a = "array of IP addresses and/or hostnames, e.g. [ \"192.168.2.56\", \"fe80::341\", \"localhost\" ]"; + config.api.exclude_clients.t = CONF_JSON_STRING_ARRAY; + config.api.exclude_clients.d.json = cJSON_CreateArray(); + + config.api.exclude_domains.k = "api.exclude_domains"; + config.api.exclude_domains.h = "Array of domains to be excluded from certain API responses"; + config.api.exclude_domains.a = "array of IP addresses and/or hostnames, e.g. [ \"google.de\", \"pi-hole.net\" ]"; + config.api.exclude_domains.t = CONF_JSON_STRING_ARRAY; + config.api.exclude_domains.d.json = cJSON_CreateArray(); + + // struct http - config.http.localAPIauth.k = "http.localAPIauth"; - config.http.localAPIauth.h = "Does local clients need to authenticate to access the API?"; - config.http.localAPIauth.t = CONF_BOOL; - config.http.localAPIauth.d.b = true; - - config.http.prettyJSON.k = "http.prettyJSON"; - config.http.prettyJSON.h = "Should FTL prettify the API output?"; - config.http.prettyJSON.t = CONF_BOOL; - config.http.prettyJSON.d.b = false; - - config.http.sessionTimeout.k = "http.sessionTimeout"; - config.http.sessionTimeout.h = "How long should a session be considered valid after login [seconds]?"; - config.http.sessionTimeout.t = CONF_UINT; - config.http.sessionTimeout.d.ui = 300; - config.http.domain.k = "http.domain"; config.http.domain.h = "On which domain is the web interface served?"; config.http.domain.a = ""; @@ -599,7 +625,15 @@ void initConfig(void) // Initialize config value with default one for all *except* the log file path if(conf_item != &config.files.log) - memcpy(&conf_item->v, &conf_item->d, sizeof(conf_item->d)); + { + if(conf_item->t == CONF_JSON_STRING_ARRAY) + // JSON objects really need to be duplicated as the config + // structure stores only a pointer to memory somewhere else + conf_item->v.json = cJSON_Duplicate(conf_item->d.json, true); + else + // Ordinary value: Simply copy the union over + memcpy(&conf_item->v, &conf_item->d, sizeof(conf_item->d)); + } // Parse and split paths conf_item->p = gen_config_path(conf_item->k); @@ -643,10 +677,19 @@ void readFTLconf(const bool rewrite) log_warn("Unable to move %s to %s: %s", path, target, strerror(errno)); } - // We initialize the TOML config file (every user gets one) only if none is already - // present (may be containing errors) - if(!file_exists(GLOBALTOMLPATH)) - writeFTLtoml(); + importsetupVarsConf(); + + // When we reach this point but the FTL TOML config file exists, it may + // contain errors such as syntax errors, etc. We move it into a ".bck" location + // so it can be revisited later + if(file_exists(GLOBALTOMLPATH)) + { + const char new_name[] = GLOBALTOMLPATH ".bck"; + rename(GLOBALTOMLPATH, new_name); + } + + // Initialize the TOML config file + writeFTLtoml(); } bool getLogFilePath(void) @@ -668,3 +711,15 @@ bool getLogFilePath(void) return true; } + +bool __attribute__((pure)) get_blockingstatus(void) +{ + return config.dns.blocking.active.v.b; +} + +void set_blockingstatus(bool enabled) +{ + config.dns.blocking.active.v.b = enabled; + writeFTLtoml(); + raise(SIGHUP); +} diff --git a/src/config/config.h b/src/config/config.h index ddd4f858..cf131f1b 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -23,6 +23,8 @@ #include // type FILE #include +// type cJSON +#include "cJSON/cJSON.h" #define GLOBALTOMLPATH "/etc/pihole/pihole-FTL.toml" @@ -40,6 +42,8 @@ bool getPrivacyLevel(void); bool getBlockingMode(void); bool readDebugSettings(void); void init_config_mutex(void); +bool get_blockingstatus(void) __attribute__((pure)); +void set_blockingstatus(bool enabled); union conf_value { bool b; @@ -56,6 +60,7 @@ union conf_value { enum debug_flag debug_flag; struct in_addr in_addr; struct in6_addr in6_addr; + cJSON *json; }; enum conf_type { @@ -72,7 +77,10 @@ enum conf_type { CONF_ENUM_REFRESH_HOSTNAMES, CONF_ENUM_PRIVACY_LEVEL, CONF_STRUCT_IN_ADDR, - CONF_STRUCT_IN6_ADDR + CONF_STRUCT_IN6_ADDR, + // We could theoretically use a more generic type, however, we want this + // here for strict input checking + CONF_JSON_STRING_ARRAY } __attribute__ ((packed)); #define MAX_CONFIG_PATH_DEPTH 4 @@ -99,7 +107,10 @@ struct config { struct conf_item piholePTR; struct conf_item replyWhenBusy; struct conf_item blockTTL; - struct conf_item blockingmode; + struct { + struct conf_item active; + struct conf_item mode; + } blocking; struct { struct conf_item mozillaCanary; struct conf_item iCloudPrivateRelay; @@ -147,6 +158,12 @@ struct config { struct conf_item localAPIauth; struct conf_item prettyJSON; struct conf_item sessionTimeout; + struct conf_item pwhash; + struct conf_item exclude_clients; + struct conf_item exclude_domains; + } api; + + struct { struct conf_item domain; struct conf_item acl; struct conf_item port; diff --git a/src/config/legacy_reader.c b/src/config/legacy_reader.c index 859229bf..9ce1ec33 100644 --- a/src/config/legacy_reader.c +++ b/src/config/legacy_reader.c @@ -310,7 +310,7 @@ const char *readFTLlegacy(void) // API_AUTH_FOR_LOCALHOST // defaults to: true buffer = parseFTLconf(fp, "API_AUTH_FOR_LOCALHOST"); - parseBool(buffer, &config.http.localAPIauth.v.b); + parseBool(buffer, &config.api.localAPIauth.v.b); // API_SESSION_TIMEOUT // How long should a session be considered valid after login? @@ -319,12 +319,12 @@ const char *readFTLlegacy(void) value = 0; if(buffer != NULL && sscanf(buffer, "%i", &value) && value > 0) - config.http.sessionTimeout.v.ui = value; + config.api.sessionTimeout.v.ui = value; // API_PRETTY_JSON // defaults to: false buffer = parseFTLconf(fp, "API_PRETTY_JSON"); - parseBool(buffer, &config.http.prettyJSON.v.b); + parseBool(buffer, &config.api.prettyJSON.v.b); // API_ERROR_LOG config.files.ph7_error.v.s = getPath(fp, "API_ERROR_LOG", config.files.ph7_error.v.s); @@ -751,7 +751,7 @@ static void getPrivacyLevelLegacy(FILE *fp) static void getBlockingModeLegacy(FILE *fp) { // (Re-)set default value - config.dns.blockingmode.v.blocking_mode = config.dns.blockingmode.d.blocking_mode; + config.dns.blocking.mode.v.blocking_mode = config.dns.blocking.mode.d.blocking_mode; // See if we got a file handle, if not we have to open // the config file ourselves @@ -770,15 +770,15 @@ static void getBlockingModeLegacy(FILE *fp) if(buffer != NULL) { if(strcasecmp(buffer, "NXDOMAIN") == 0) - config.dns.blockingmode.v.blocking_mode = MODE_NX; + config.dns.blocking.mode.v.blocking_mode = MODE_NX; else if(strcasecmp(buffer, "NULL") == 0) - config.dns.blockingmode.v.blocking_mode = MODE_NULL; + config.dns.blocking.mode.v.blocking_mode = MODE_NULL; else if(strcasecmp(buffer, "IP-NODATA-AAAA") == 0) - config.dns.blockingmode.v.blocking_mode = MODE_IP_NODATA_AAAA; + config.dns.blocking.mode.v.blocking_mode = MODE_IP_NODATA_AAAA; else if(strcasecmp(buffer, "IP") == 0) - config.dns.blockingmode.v.blocking_mode = MODE_IP; + config.dns.blocking.mode.v.blocking_mode = MODE_IP; else if(strcasecmp(buffer, "NODATA") == 0) - config.dns.blockingmode.v.blocking_mode = MODE_NODATA; + config.dns.blocking.mode.v.blocking_mode = MODE_NODATA; else log_warn("Unknown blocking mode, using NULL as fallback"); } diff --git a/src/config/toml_helper.c b/src/config/toml_helper.c index 9b02cb92..91e7541a 100644 --- a/src/config/toml_helper.c +++ b/src/config/toml_helper.c @@ -162,6 +162,21 @@ void writeTOMLvalue(FILE * fp, const enum conf_type t, union conf_value *v) printTOMLstring(fp, addr6); break; } + case CONF_JSON_STRING_ARRAY: + { + fputs("[ ", fp); + const unsigned int elems = cJSON_GetArraySize(v->json); + for(unsigned int i = 0; i < elems; i++) + { + cJSON *item = cJSON_GetArrayItem(v->json, i); + printTOMLstring(fp, item->valuestring); + // Add a comma if there is one more element to come + if(item->next) + fputs(", ", fp); + } + fputs(" ]", fp); + break; + } } } @@ -321,5 +336,33 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t memcpy(&conf_item->v.in6_addr, &addr6, sizeof(addr6)); break; } + case CONF_JSON_STRING_ARRAY: + { + // Free previously allocated JSON array + cJSON_free(conf_item->v.json); + conf_item->v.json = cJSON_CreateArray(); + // Parse TOML array and generate a JSON array + const toml_array_t *array = toml_array_in(toml, key); + if(array != NULL) + { + const unsigned int nelem = toml_array_nelem(array); + for(unsigned int i = 0; i < nelem; i++) + { + // Get string from TOML + const toml_datum_t d = toml_string_at(array, i); + if(!d.ok) + { + log_debug(DEBUG_CONFIG, "%s is an invalid array (found at index %d)", conf_item->k, i); + break; + } + // Add string to our JSON array + cJSON *item = cJSON_CreateString(d.u.s); + cJSON_AddItemToArray(conf_item->v.json, item); + } + } + else + log_debug(DEBUG_CONFIG, "%s does not exist", conf_item->k); + break; + } } } diff --git a/src/config/toml_reader.c b/src/config/toml_reader.c index 1b37e513..f48233d2 100644 --- a/src/config/toml_reader.c +++ b/src/config/toml_reader.c @@ -181,10 +181,10 @@ bool getBlockingMode(void) // Iterate over possible blocking modes and check if it applies const int blocking_mode = get_blocking_mode_val(blockingmode.u.s); if(blocking_mode != -1) - config.dns.blockingmode.v.blocking_mode = blocking_mode; + config.dns.blocking.mode.v.blocking_mode = blocking_mode; else log_warn("Config setting %s is invalid, allowed options are: %s", - config.dns.blockingmode.k, config.dns.blockingmode.h); + config.dns.blocking.mode.k, config.dns.blocking.mode.h); free(blockingmode.u.s); // Free memory and return success diff --git a/src/config/toml_writer.c b/src/config/toml_writer.c index c61e6cc3..bab35a53 100644 --- a/src/config/toml_writer.c +++ b/src/config/toml_writer.c @@ -79,8 +79,15 @@ bool writeFTLtoml(void) writeTOMLvalue(fp, conf_item->t, &conf_item->v); // Compare with default value and add a comment on difference - if((conf_item->t == CONF_STRING && strcmp(conf_item->v.s, conf_item->d.s) != 0) || - (conf_item->t != CONF_STRING && memcmp(&conf_item->v, &conf_item->d, sizeof(conf_item->v)) != 0)) + bool changed = false; + if(conf_item->t == CONF_STRING) + changed = strcmp(conf_item->v.s, conf_item->d.s) != 0; + else if(conf_item->t == CONF_JSON_STRING_ARRAY) + changed = !cJSON_Compare(conf_item->v.json, conf_item->d.json, true); + else + changed = memcmp(&conf_item->v, &conf_item->d, sizeof(conf_item->v)) != 0; + + if(changed) { fprintf(fp, " ### CHANGED, default = "); writeTOMLvalue(fp, conf_item->t, &conf_item->d); diff --git a/src/database/database-thread.c b/src/database/database-thread.c index a78606c0..13cc8b81 100644 --- a/src/database/database-thread.c +++ b/src/database/database-thread.c @@ -8,28 +8,26 @@ * 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 "FTL.h" #include "database-thread.h" #include "common.h" // [un]lock_shm(); -#include "../shmem.h" +#include "shmem.h" // parse_neighbor_cache() #include "network-table.h" // export_queries_to_disk() #include "query-table.h" -#include "../config/config.h" -#include "../log.h" -#include "../timers.h" +#include "config/config.h" +#include "log.h" +#include "timers.h" // global variable killed -#include "../signals.h" +#include "signals.h" // reimport_aliasclients() #include "aliasclients.h" // Eventqueue routines -#include "../events.h" +#include "events.h" // get_FTL_db_filesize() -#include "../files.h" -// read_blocking_status() -#include "../setupVars.h" +#include "files.h" #define TIME_T "%li" @@ -166,13 +164,6 @@ void *DB_thread(void *val) // Intermediate cancellation-point BREAK_IF_KILLED(); - // Reload privacy level from pihole-FTL config - if(get_and_clear_event(RELOAD_PRIVACY_LEVEL)) - getPrivacyLevel(); - - // Intermediate cancellation-point - BREAK_IF_KILLED(); - // Import alias-clients if(get_and_clear_event(REIMPORT_ALIASCLIENTS)) { @@ -183,18 +174,6 @@ void *DB_thread(void *val) BREAK_IF_KILLED(); - // Inspect setupVars.conf to see if Pi-hole blocking is enabled - if(get_and_clear_event(RELOAD_BLOCKINGSTATUS)) - read_blocking_status(); - - BREAK_IF_KILLED(); - - // Read blocking mode from pihole-FTL.toml - if(get_and_clear_event(RELOAD_BLOCKINGMODE)) - getBlockingMode(); - - BREAK_IF_KILLED(); - // Sleep 0.1 sec thread_sleepms(DB, 100); } diff --git a/src/dnsmasq_interface.c b/src/dnsmasq_interface.c index 26355207..35bc9492 100644 --- a/src/dnsmasq_interface.c +++ b/src/dnsmasq_interface.c @@ -266,21 +266,21 @@ size_t _FTL_make_answer(struct dns_header *header, char *limit, const size_t len else { // Overwrite flags only if not replying with a forced reply - if(config.dns.blockingmode.v.blocking_mode == MODE_NX) + if(config.dns.blocking.mode.v.blocking_mode == MODE_NX) { // If we block in NXDOMAIN mode, we set flags to NXDOMAIN // (NEG will be added after setup_reply() below) flags = F_NXDOMAIN; log_debug(DEBUG_FLAGS, "Configured blocking mode is NXDOMAIN"); } - else if(config.dns.blockingmode.v.blocking_mode == MODE_NODATA || - (config.dns.blockingmode.v.blocking_mode == MODE_IP_NODATA_AAAA && (flags & F_IPV6))) + else if(config.dns.blocking.mode.v.blocking_mode == MODE_NODATA || + (config.dns.blocking.mode.v.blocking_mode == MODE_IP_NODATA_AAAA && (flags & F_IPV6))) { // If we block in NODATA mode or NODATA for AAAA queries, we apply // the NOERROR response flag. This ensures we're sending an empty response flags = F_NOERR; log_debug(DEBUG_FLAGS, "Configured blocking mode is NODATA%s", - config.dns.blockingmode.v.blocking_mode == MODE_IP_NODATA_AAAA ? "-IPv6" : ""); + config.dns.blocking.mode.v.blocking_mode == MODE_IP_NODATA_AAAA ? "-IPv6" : ""); } } @@ -334,8 +334,8 @@ size_t _FTL_make_answer(struct dns_header *header, char *limit, const size_t len // Overwrite with IP address if requested if(redirecting) memcpy(&addr, &redirect_addr4, sizeof(addr)); - else if(config.dns.blockingmode.v.blocking_mode == MODE_IP || - config.dns.blockingmode.v.blocking_mode == MODE_IP_NODATA_AAAA || + else if(config.dns.blocking.mode.v.blocking_mode == MODE_IP || + config.dns.blocking.mode.v.blocking_mode == MODE_IP_NODATA_AAAA || forced_ip) { if(hostname && config.dns.reply.host.overwrite_v4.v.b) @@ -370,7 +370,7 @@ size_t _FTL_make_answer(struct dns_header *header, char *limit, const size_t len // Overwrite with IP address if requested if(redirecting) memcpy(&addr, &redirect_addr6, sizeof(addr)); - else if(config.dns.blockingmode.v.blocking_mode == MODE_IP || + else if(config.dns.blocking.mode.v.blocking_mode == MODE_IP || forced_ip) { if(hostname && config.dns.reply.host.overwrite_v6.v.b) @@ -1736,11 +1736,6 @@ void FTL_dnsmasq_reload(void) // This function is called by the dnsmasq code on receive of SIGHUP // *before* clearing the cache and re-reading the lists - // Request reload the privacy level and blocking status + mode - set_event(RELOAD_PRIVACY_LEVEL); - set_event(RELOAD_BLOCKINGSTATUS); - set_event(RELOAD_BLOCKINGMODE); - // Gravity database updates // - (Re-)open gravity database connection // - Get number of blocked domains diff --git a/src/enums.h b/src/enums.h index 01e8db57..cbbc54b9 100644 --- a/src/enums.h +++ b/src/enums.h @@ -161,14 +161,11 @@ enum debug_flag { enum events { RELOAD_GRAVITY, - RELOAD_PRIVACY_LEVEL, RESOLVE_NEW_HOSTNAMES, RERESOLVE_HOSTNAMES, RERESOLVE_HOSTNAMES_FORCE, REIMPORT_ALIASCLIENTS, PARSE_NEIGHBOR_CACHE, - RELOAD_BLOCKINGSTATUS, - RELOAD_BLOCKINGMODE, EVENTS_MAX } __attribute__ ((packed)); diff --git a/src/events.c b/src/events.c index b4f32fb1..ac0c778a 100644 --- a/src/events.c +++ b/src/events.c @@ -88,8 +88,6 @@ static const char *eventtext(const enum events event) { case RELOAD_GRAVITY: return "RELOAD_GRAVITY"; - case RELOAD_PRIVACY_LEVEL: - return "RELOAD_PRIVACY_LEVEL"; case RERESOLVE_HOSTNAMES: return "RERESOLVE_HOSTNAMES"; case RERESOLVE_HOSTNAMES_FORCE: @@ -100,10 +98,6 @@ static const char *eventtext(const enum events event) return "PARSE_NEIGHBOR_CACHE"; case RESOLVE_NEW_HOSTNAMES: return "RESOLVE_NEW_HOSTNAMES"; - case RELOAD_BLOCKINGSTATUS: - return "RELOAD_BLOCKINGSTATUS"; - case RELOAD_BLOCKINGMODE: - return "RELOAD_BLOCKINGMODE"; case EVENTS_MAX: // fall through default: return "UNKNOWN"; diff --git a/src/main.c b/src/main.c index 534a97d3..729b9f49 100644 --- a/src/main.c +++ b/src/main.c @@ -122,7 +122,6 @@ int main (int argc, char *argv[]) } log_counter_info(); - check_setupVarsconf(); // Check for availability of capabilities in debug mode if(config.debug.caps.v.b) diff --git a/src/setupVars.c b/src/setupVars.c index b3b8161a..6267ac55 100644 --- a/src/setupVars.c +++ b/src/setupVars.c @@ -13,21 +13,78 @@ #include "config/config.h" #include "setupVars.h" -int setupVarsElements = 0; +unsigned int setupVarsElements = 0; char ** setupVarsArray = NULL; -void check_setupVarsconf(void) +void importsetupVarsConf(void) { - FILE *setupVarsfp; - if((setupVarsfp = fopen(config.files.setupVars.v.s, "r")) == NULL) + // Try to obtain password hash from setupVars.conf + const char* pwhash = read_setupVarsconf("WEBPASSWORD"); + if(pwhash == NULL) + pwhash = ""; + + // Free previously allocated memory (if applicable) + if(config.api.pwhash.t == CONF_STRING_ALLOCATED) + free(config.api.pwhash.v.s); + config.api.pwhash.v.s = strdup(pwhash); + config.api.pwhash.t = CONF_STRING_ALLOCATED; + + // Free memory, harmless to call if read_setupVarsconf() didn't return a result + clearSetupVarsArray(); + + // Try to obtain blocking active boolean + const char* blocking = read_setupVarsconf("BLOCKING_ENABLED"); + + if(blocking == NULL || getSetupVarsBool(blocking)) { - log_warn("Opening of setupVars.conf failed: %s Make sure it exists and is readable", - strerror(errno)); + // Parameter either not present in setupVars.conf + // or explicitly set to true + config.dns.blocking.active.v.b = true; } else { - fclose(setupVarsfp); + // Disabled + config.dns.blocking.active.v.b = false; } + + // Free memory, harmless to call if read_setupVarsconf() didn't return a result + clearSetupVarsArray(); + + // Get clients which the user doesn't want to see + char *excludeclients = read_setupVarsconf("API_EXCLUDE_CLIENTS"); + + if(excludeclients != NULL) + { + getSetupVarsArray(excludeclients); + for (unsigned int i = 0; i < setupVarsElements; ++i) + { + log_debug(DEBUG_CONFIG, "API_EXCLUDE_CLIENTS: [%d] = %s\n", i, setupVarsArray[i]); + // Add string to our JSON array + cJSON *item = cJSON_CreateString(setupVarsArray[i]); + cJSON_AddItemToArray(config.api.exclude_clients.v.json, item); + } + } + + // Free memory, harmless to call if read_setupVarsconf() didn't return a result + clearSetupVarsArray(); + + // Get domains which the user doesn't want to see + char *excludedomains = read_setupVarsconf("API_EXCLUDE_DOMAINS"); + + if(excludedomains != NULL) + { + getSetupVarsArray(excludedomains); + for (unsigned int i = 0; i < setupVarsElements; ++i) + { + log_debug(DEBUG_CONFIG, "API_EXCLUDE_DOMAINS: [%d] = %s\n", i, setupVarsArray[i]); + // Add string to our JSON array + cJSON *item = cJSON_CreateString(setupVarsArray[i]); + cJSON_AddItemToArray(config.api.exclude_domains.v.json, item); + } + } + + // Free memory, harmless to call if read_setupVarsconf() didn't return a result + clearSetupVarsArray(); } char* __attribute__((pure)) find_equals(const char* s) @@ -67,7 +124,7 @@ void trim_whitespace(char *string) char * linebuffer = NULL; size_t linebuffersize = 0; -char * read_setupVarsconf(const char *key) +char *read_setupVarsconf(const char *key) { FILE *setupVarsfp; if((setupVarsfp = fopen(config.files.setupVars.v.s, "r")) == NULL) @@ -167,55 +224,6 @@ void clearSetupVarsArray(void) } } -/* Example - char * iface = read_setupVarsconf("API_EXCLUDE_DOMAINS"); - if(iface != NULL) - logg_str("Interface: ",iface); - getSetupVarsArray(iface); - int i; - for (i = 0; i <= setupVarsElements; ++i) - printf ("[%d] = %s\n", i, setupVarsArray[i]); - clearSetupVarsArray(); -*/ - -bool insetupVarsArray(const char * str) -{ - // Check for possible NULL pointer - // (this is valid input, e.g. if clients[i].name is unspecified) - if(str == NULL) - return false; - - // Loop over all entries in setupVarsArray - for (int i = 0; i < setupVarsElements; ++i) - if(setupVarsArray[i][0] == '*') - { - // Copying strlen-1 chars into buffer of size strlen: OK - size_t length = strlen(setupVarsArray[i]); - char * domain = calloc(length, sizeof(char)); - if(domain == NULL) return false; - // strncat() NULL-terminates the copied string (strncpy() doesn't!) - strncat(domain, setupVarsArray[i]+1, length-1); - - if(strstr(str, domain) != NULL) - { - free(domain); - return true; - } - else - { - free(domain); - } - } - else - { - if(strcmp(setupVarsArray[i], str) == 0) - return true; - } - - // If not found - return false; -} - bool __attribute__((pure)) getSetupVarsBool(const char * input) { if((strcmp(input, "true")) == 0) @@ -223,61 +231,3 @@ bool __attribute__((pure)) getSetupVarsBool(const char * input) else return false; } - -// Global variable showing current blocking status -enum blocking_status blockingstatus = BLOCKING_UNKNOWN; - -void read_blocking_status(void) -{ - const char* blocking = read_setupVarsconf("BLOCKING_ENABLED"); - const char* message; - - if(blocking == NULL || getSetupVarsBool(blocking)) - { - // Parameter either not present in setupVars.conf - // or explicitly set to true - blockingstatus = BLOCKING_ENABLED; - message = "enabled"; - clearSetupVarsArray(); - } - else - { - // Disabled - blockingstatus = BLOCKING_DISABLED; - message = "disabled"; - } - - log_info("Blocking status is %s", message); -} - -bool __attribute__((pure)) get_blockingstatus(void) -{ - return blockingstatus; -} - -void set_blockingstatus(bool enabled) -{ - blockingstatus = enabled; - raise(SIGHUP); -} - -// Source password hash from setupVars.conf -__attribute__((malloc)) char *get_password_hash(void) -{ - // Try to obtain password from setupVars.conf - const char* password = read_setupVarsconf("WEBPASSWORD"); - - // If the value was not set (or we couldn't open the file for reading), - // we hand an empty string back to the caller - if(password == NULL || (password != NULL && strlen(password) == 0u)) - { - password = ""; - } - - char *hash = strdup(password); - - // Free memory, harmless to call if read_setupVarsconf() didn't return a result - clearSetupVarsArray(); - - return hash; -} diff --git a/src/setupVars.h b/src/setupVars.h index 96896c8f..07c47826 100644 --- a/src/setupVars.h +++ b/src/setupVars.h @@ -3,24 +3,19 @@ * Network-wide ad blocking via your own hardware. * * FTL Engine -* pihole-FTL.conf processing prototypes +* setupVars.conf processing prototypes * * This file is copyright under the latest version of the EUPL. * Please see LICENSE file for your rights under this license. */ #ifndef SETUPVARS_H #define SETUPVARS_H -void check_setupVarsconf(void); -char * read_setupVarsconf(const char * key); +void importsetupVarsConf(void); +char *read_setupVarsconf(const char * key); void getSetupVarsArray(const char * input); void clearSetupVarsArray(void); -bool insetupVarsArray(const char * str); bool getSetupVarsBool(const char * input) __attribute__((pure)); -char* find_equals(const char* s) __attribute__((pure)); +char *find_equals(const char* s) __attribute__((pure)); void trim_whitespace(char *string); -void read_blocking_status(void); -bool get_blockingstatus(void) __attribute__((pure)); -void set_blockingstatus(bool enabled); -char *get_password_hash(void) __attribute__((malloc)); #endif //SETUPVARS_H diff --git a/src/signals.c b/src/signals.c index bc15e3fe..af689740 100644 --- a/src/signals.c +++ b/src/signals.c @@ -286,15 +286,6 @@ static void SIGRT_handler(int signum, siginfo_t *si, void *unused) // - denied domains and regex // WITHOUT wiping the DNS cache itself set_event(RELOAD_GRAVITY); - - // Reload the privacy level in case the user changed it - set_event(RELOAD_PRIVACY_LEVEL); - - // Reload blocking status - set_event(RELOAD_BLOCKINGSTATUS); - - // Reload blocking mode - set_event(RELOAD_BLOCKINGMODE); } else if(rtsig == 2) { diff --git a/src/timers.c b/src/timers.c index 998d87a0..1c79590d 100644 --- a/src/timers.c +++ b/src/timers.c @@ -14,7 +14,7 @@ // killed #include "signals.h" // set_blockingmode() -#include "setupVars.h" +#include "config/config.h" struct timespec t0[NUMTIMERS]; diff --git a/src/webserver/http-common.c b/src/webserver/http-common.c index 917fcea7..5e24f05d 100644 --- a/src/webserver/http-common.c +++ b/src/webserver/http-common.c @@ -25,7 +25,7 @@ char pi_hole_extra_headers[PIHOLE_HEADERS_MAXLEN] = { 0 }; // tyoically contain a JSON explorer const char* json_formatter(const cJSON *object) { - if(config.http.prettyJSON.v.b) + if(config.api.prettyJSON.v.b) { /* Examplary output: { diff --git a/test/api/checkAPI.py b/test/api/checkAPI.py index 0a10c88f..810a8a70 100644 --- a/test/api/checkAPI.py +++ b/test/api/checkAPI.py @@ -67,7 +67,7 @@ if __name__ == "__main__": if errs[1] > 0: print("Found " + str(errs[1]) + " undocumented endpoints") if errs[2] > 0: - print("Found " + str(errs[2]) + " endpoints not matching specs") + print("Found " + str(errs[2]) + " specs mismatches") # Exit with an error if there are missing endpoints if sum(errs) > 0: diff --git a/test/api/libs/responseVerifyer.py b/test/api/libs/responseVerifyer.py index 9b3eacb8..1181a519 100644 --- a/test/api/libs/responseVerifyer.py +++ b/test/api/libs/responseVerifyer.py @@ -121,6 +121,9 @@ class ResponseVerifyer(): if prop_type is type(None) and yaml_nullable: return True # Check if the type is correct using the YAML_TYPES translation table + if yaml_type not in self.YAML_TYPES: + self.errors.append("Property type \"" + yaml_type + "\" is not valid in OpenAPI specs") + return False return prop_type in self.YAML_TYPES[yaml_type]