diff --git a/.github/.codespellignore b/.github/.codespellignore index 9d8b4f3ff..cdccd1cda 100644 --- a/.github/.codespellignore +++ b/.github/.codespellignore @@ -6,3 +6,4 @@ doubleclick requestor requestors punycode +bitap diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7a10b0b3f..3f255961b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -87,7 +87,7 @@ jobs: run: ls -l - name: Build and test FTL in ftl-build container (QEMU) - uses: Wandalen/wretry.action@v1.3.0 + uses: Wandalen/wretry.action@v1.4.4 with: attempt_limit: 3 action: docker/build-push-action@v5.0.0 @@ -119,7 +119,7 @@ jobs: - name: Store binary artifacts for later deployoment if: github.event_name != 'pull_request' - uses: actions/upload-artifact@v4.3.0 + uses: actions/upload-artifact@v4.3.1 with: name: ${{ matrix.bin_name }}-binary path: '${{ matrix.bin_name }}*' @@ -131,7 +131,7 @@ jobs: - name: Upload documentation artifacts for deployoment if: github.event_name != 'pull_request' && matrix.platform == 'linux/amd64' - uses: actions/upload-artifact@v4.3.0 + uses: actions/upload-artifact@v4.3.1 with: name: pihole-api-docs path: 'api-docs.tar.gz' @@ -146,7 +146,7 @@ jobs: uses: actions/checkout@v4.1.1 - name: Get Binaries and documentation built in previous jobs - uses: actions/download-artifact@v4.1.1 + uses: actions/download-artifact@v4.1.2 id: download with: path: ftl_builds/ diff --git a/src/config/CMakeLists.txt b/src/config/CMakeLists.txt index 5f847a214..92824c42f 100644 --- a/src/config/CMakeLists.txt +++ b/src/config/CMakeLists.txt @@ -15,12 +15,16 @@ set(sources config.h dnsmasq_config.c dnsmasq_config.h + env.c + env.h inotify.c inotify.h legacy_reader.c legacy_reader.h password.c password.h + suggest.c + suggest.h setupVars.c setupVars.h toml_writer.c diff --git a/src/config/cli.c b/src/config/cli.c index 372896416..d012d0d8f 100644 --- a/src/config/cli.c +++ b/src/config/cli.c @@ -22,6 +22,17 @@ #include "config/password.h" // check_capability() #include "capabilities.h" +// suggest_closest_conf_key() +#include "config/suggest.h" + +enum exit_codes { + OKAY = 0, + FAIL = 1, + VALUE_INVALID = 2, + DNSMASQ_TEST_FAILED = 3, + KEY_UNKNOWN = 4, + ENV_VAR_FORCED = 5, +} __attribute__((packed)); // Read a TOML value from a table depending on its type static bool readStringValue(struct conf_item *conf_item, const char *value, struct config *newconf) @@ -402,7 +413,7 @@ int set_config_from_CLI(const char *key, const char *value) { log_err("Config option %s is read-only (set via environmental variable)", key); free_config(&newconf); - return 5; + return ENV_VAR_FORCED; } // This is the config option we are looking for @@ -418,16 +429,22 @@ int set_config_from_CLI(const char *key, const char *value) // Check if we found the config option if(new_item == NULL) { - log_err("Unknown config option: %s", key); + unsigned int N = 0; + char **matches = suggest_closest_conf_key(false, key, &N); + log_err("Unknown config option %s, did you mean:", key); + for(unsigned int i = 0; i < N; i++) + log_err(" - %s", matches[i]); + free(matches); + free_config(&newconf); - return 4; + return KEY_UNKNOWN; } // Parse value if(!readStringValue(new_item, value, &newconf)) { free_config(&newconf); - return 2; + return VALUE_INVALID; } // Check if value changed compared to current value @@ -459,7 +476,7 @@ int set_config_from_CLI(const char *key, const char *value) // Test failed log_debug(DEBUG_CONFIG, "Config item %s: dnsmasq config test failed", conf_item->k); free_config(&newconf); - return 3; + return DNSMASQ_TEST_FAILED; } } else if(conf_item == &config.dns.hosts) @@ -487,7 +504,7 @@ int set_config_from_CLI(const char *key, const char *value) putchar('\n'); writeFTLtoml(false); - return EXIT_SUCCESS; + return OKAY; } int get_config_from_CLI(const char *key, const bool quiet) @@ -546,14 +563,20 @@ int get_config_from_CLI(const char *key, const bool quiet) // Check if we found the config option if(conf_item == NULL) { - log_err("Unknown config option: %s", key); - return 2; + unsigned int N = 0; + char **matches = suggest_closest_conf_key(false, key, &N); + log_err("Unknown config option %s, did you mean:", key); + for(unsigned int i = 0; i < N; i++) + log_err(" - %s", matches[i]); + free(matches); + + return KEY_UNKNOWN; } // Use return status if this is a boolean value // and we are in quiet mode if(quiet && conf_item != NULL && conf_item->t == CONF_BOOL) - return conf_item->v.b ? EXIT_SUCCESS : EXIT_FAILURE; + return conf_item->v.b ? OKAY : FAIL; - return EXIT_SUCCESS; + return OKAY; } diff --git a/src/config/config.c b/src/config/config.c index a933e2a2a..14d2496f0 100644 --- a/src/config/config.c +++ b/src/config/config.c @@ -31,6 +31,8 @@ #include "signals.h" // validation functions #include "config/validator.h" +// getEnvVars() +#include "config/env.h" // sha256sum() #include "files.h" @@ -1313,7 +1315,7 @@ void initConfig(struct config *conf) conf->debug.regex.c = validate_stub; // Only type-based checking conf->debug.api.k = "debug.api"; - conf->debug.api.h = "Print extra debugging information during telnet API calls. Currently only used to send extra information when getting all queries."; + conf->debug.api.h = "Print extra debugging information concerning API calls. This includes the request, the request parameters, and the internal details about how the algorithms decide which data to present and in what form. This very verbose output should only be used when debugging specific API issues and can be helpful, e.g., when a client cannot connect due to an obscure API error. Furthermore, this setting enables logging of all API requests (auth log) and details about user authentication attempts."; conf->debug.api.t = CONF_BOOL; conf->debug.api.f = FLAG_ADVANCED_SETTING; conf->debug.api.d.b = false; @@ -1459,17 +1461,24 @@ void initConfig(struct config *conf) // Parse and split paths conf_item->p = gen_config_path(conf_item->k, '.'); - // Verify all config options are defined above - if(!conf_item->p || !conf_item->k || !conf_item->h) - { - log_err("Config option %u/%u is not set!", i, (unsigned int)CONFIG_ELEMENTS); - continue; - } + // Initialize environment variable name + // Allocate memory for config key + prefix (sizeof includes the trailing '\0') + const size_t envkey_size = strlen(conf_item->k) + sizeof(FTLCONF_PREFIX); + conf_item->e = calloc(envkey_size, sizeof(char)); - // Verify that all config options have a type - if(conf_item->t == 0) + // Build env key to look for + strcpy(conf_item->e, FTLCONF_PREFIX); + strcat(conf_item->e, conf_item->k); + + // Replace all "." by "_" as this is the convention used in v5.x and earlier + for(unsigned int j = 0; j < envkey_size - 1; j++) + if(conf_item->e[j] == '.') + conf_item->e[j] = '_'; + + // Verify all config options are defined above + if(!conf_item->p || !conf_item->k || !conf_item->h || !conf_item->e || conf_item->t == 0) { - log_err("Config option %s has no type!", conf_item->k); + log_err("Config option %u/%u is not fully configured!", i, (unsigned int)CONFIG_ELEMENTS); continue; } @@ -1529,7 +1538,10 @@ bool readFTLconf(struct config *conf, const bool rewrite) // Initialize config with default values initConfig(conf); - // First try to read TOML config file + // First, read the environment + getEnvVars(); + + // Try to read TOML config file // If we cannot parse /etc/pihole.toml (due to missing or invalid syntax), // we try to read the rotated files in /etc/pihole/config_backup starting at // the most recent one and going back in time until we find a valid config diff --git a/src/config/config.h b/src/config/config.h index 3f4a0e19f..e1d686151 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -105,6 +105,7 @@ enum conf_type { struct conf_item { const char *k; // item Key char **p; // item Path + char *e; // item Environment variable const char *h; // Help text / description cJSON *a; // JSON array or object of Allowed values (where applicable) enum conf_type t; // variable Type diff --git a/src/config/env.c b/src/config/env.c new file mode 100644 index 000000000..d332a3483 --- /dev/null +++ b/src/config/env.c @@ -0,0 +1,548 @@ +/* Pi-hole: A black hole for Internet advertisements +* (c) 2023 Pi-hole, LLC (https://pi-hole.net) +* Network-wide ad blocking via your own hardware. +* +* FTL Engine +* Environment-related routines +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ + +#include "env.h" +#include "log.h" +#include "config/config.h" +// get_refresh_hostnames_str() +#include "datastructure.h" +//set_and_check_password() +#include "config/password.h" +// cli_tick() +#include "args.h" +// suggest_closest() +#include "config/suggest.h" +struct env_item +{ + bool used; + bool valid; + char *key; + char *value; + const char *error; + const char *allowed; + struct env_item *next; +}; + +static struct env_item *env_list = NULL; + +void getEnvVars(void) +{ + // Read environment variables only once + if(env_list != NULL) + return; + + // Get all environment variables + for(char **env = environ; *env != NULL; env++) + { + // Check if this is a FTLCONF_ variable + if(strncmp(*env, FTLCONF_PREFIX, sizeof(FTLCONF_PREFIX) - 1) == 0) + { + // Split key and value + char *key = strtok(*env, "="); + char *value = strtok(NULL, "="); + + // Add to list + struct env_item *new_item = calloc(1, sizeof(struct env_item)); + new_item->used = false; + new_item->key = strdup(key); + new_item->value = strdup(value); + new_item->error = NULL; + new_item->allowed = NULL; + new_item->next = env_list; + env_list = new_item; + } + } + +} + +void printFTLenv(void) +{ + // Nothing to print if no env vars are used + if(env_list == NULL) + return; + + // Count number of used and ignored env vars + unsigned int used = 0, invalid = 0, ignored = 0; + for(struct env_item *item = env_list; item != NULL; item = item->next) + { + if(item->used) + if(item->valid) + used++; + else + invalid++; + else + ignored++; + } + + const unsigned int sum = used + invalid + ignored; + log_info("%u FTLCONF environment variable%s found (%u used, %u invalid, %u ignored)", + sum, sum == 1 ? "" : "s", used, invalid, ignored); + + // Iterate over all known FTLCONF environment variables + for(struct env_item *item = env_list; item != NULL; item = item->next) + { + if(item->used) + { + if(item->valid) + log_info(" %s %s is used", cli_tick(), item->key); + else + { + if(item->error != NULL && item->allowed == NULL) + log_err(" %s %s is invalid (%s)", + cli_cross(), item->key, item->error); + else if(item->error != NULL && item->allowed != NULL) + log_err(" %s %s is invalid (%s, allowed options are: %s)", + cli_cross(), item->key, item->error, item->allowed); + else + log_err(" %s %s is invalid", + cli_cross(), item->key); + } + + continue; + } + // else: print warning + unsigned int N = 0; + char **matches = suggest_closest_conf_key(true, item->key, &N); + + // Print the closest matches + log_warn("%s %s is unknown, did you mean any of these?", cli_qst(), item->key); + for(size_t i = 0; i < N; ++i) + log_warn(" - %s", matches[i]); + free(matches); + } +} + +static struct env_item *__attribute__((pure)) getFTLenv(const char *key) +{ + // Iterate over all known FTLCONF environment variables + for(struct env_item *item = env_list; item != NULL; item = item->next) + { + // Check if this is the requested key + if(strcmp(item->key, key) == 0) + return item; + } + + // Return NULL if the key was not found + return NULL; +} + +void freeEnvVars(void) +{ + // Free all environment variables + while(env_list != NULL) + { + struct env_item *next = env_list->next; + free(env_list->key); + free(env_list->value); + free(env_list); + env_list = next; + } +} + +bool readEnvValue(struct conf_item *conf_item, struct config *newconf) +{ + // First check if a environmental variable with the given key exists by + // iterating over the list of FTLCONF_ variables + struct env_item *item = getFTLenv(conf_item->e); + + // Return early if this environment variable does not exist + if(item == NULL) + return false; + + + // Mark this environment variable as used + item->used = true; + + // else: We found an environment variable with the given key + const char *envvar = item != NULL ? item->value : NULL; + + log_debug(DEBUG_CONFIG, "ENV %s = %s", conf_item->e, envvar); + + switch(conf_item->t) + { + case CONF_BOOL: + { + if(strcasecmp(envvar, "true") == 0 || strcasecmp(envvar, "yes") == 0) + { + conf_item->v.b = true; + item->valid = true; + } + else if(strcasecmp(envvar, "false") == 0 || strcasecmp(envvar, "no") == 0) + { + conf_item->v.b = false; + item->valid = true; + } + else + { + item->error = "not of type bool"; + log_warn("ENV %s is %s", conf_item->e, item->error); + item->valid = false; + } + break; + } + case CONF_ALL_DEBUG_BOOL: + { + if(strcasecmp(envvar, "true") == 0 || strcasecmp(envvar, "yes") == 0) + { + set_all_debug(newconf, true); + item->valid = true; + } + else if(strcasecmp(envvar, "false") == 0 || strcasecmp(envvar, "no") == 0) + { + set_all_debug(newconf, false); + item->valid = true; + } + else + { + item->error = "not of type bool"; + log_warn("ENV %s is %s", conf_item->e, item->error); + item->valid = false; + } + break; + } + case CONF_INT: + { + int val = 0; + if(sscanf(envvar, "%i", &val) == 1) + { + conf_item->v.i = val; + item->valid = true; + } + else + { + item->error = "not of type integer"; + log_warn("ENV %s is %s", conf_item->e, item->error); + item->valid = false; + } + break; + } + case CONF_UINT: + { + unsigned int val = 0; + if(sscanf(envvar, "%u", &val) == 1) + { + conf_item->v.ui = val; + item->valid = true; + } + else + { + item->error = "not of type unsigned integer"; + log_warn("ENV %s is %s", conf_item->e, item->error); + item->valid = false; + } + break; + } + case CONF_UINT16: + { + unsigned int val = 0; + if(sscanf(envvar, "%u", &val) == 1 && val <= UINT16_MAX) + { + conf_item->v.ui = val; + item->valid = true; + } + else + { + item->error = "not of type unsigned integer (16 bit"; + log_warn("ENV %s is %s)", conf_item->e, item->error); + item->valid = false; + } + break; + } + case CONF_LONG: + { + long val = 0; + if(sscanf(envvar, "%li", &val) == 1) + { + conf_item->v.l = val; + item->valid = true; + } + else + { + item->error = "not of type long"; + log_warn("ENV %s is %s", conf_item->e, item->error); + item->valid = false; + } + break; + } + case CONF_ULONG: + { + unsigned long val = 0; + if(sscanf(envvar, "%lu", &val) == 1) + { + conf_item->v.ul = val; + item->valid = true; + } + else + { + item->error = "not of type unsigned long"; + log_warn("ENV %s is %s", conf_item->e, item->error); + item->valid = false; + } + break; + } + case CONF_DOUBLE: + { + double val = 0; + if(sscanf(envvar, "%lf", &val) == 1) + { + conf_item->v.d = val; + item->valid = true; + } + else + { + item->error = "not of type double"; + log_warn("ENV %s is %s", conf_item->e, item->error); + item->valid = false; + } + break; + } + case CONF_STRING: + case CONF_STRING_ALLOCATED: + { + if(conf_item->t == CONF_STRING_ALLOCATED) + free(conf_item->v.s); + conf_item->v.s = strdup(envvar); + conf_item->t = CONF_STRING_ALLOCATED; + item->valid = true; + break; + } + case CONF_ENUM_PTR_TYPE: + { + const int ptr_type = get_ptr_type_val(envvar); + if(ptr_type != -1) + { + conf_item->v.ptr_type = ptr_type; + item->valid = true; + } + else + { + item->error = "not an allowed option"; + item->allowed = conf_item->h; + log_warn("ENV %s is %s, allowed options are: %s", + conf_item->e, item->error, item->allowed); + item->valid = false; + } + break; + } + case CONF_ENUM_BUSY_TYPE: + { + const int busy_reply = get_busy_reply_val(envvar); + if(busy_reply != -1) + { + conf_item->v.busy_reply = busy_reply; + item->valid = true; + } + else + { + + item->error = "not an allowed option"; + item->allowed = conf_item->h; + log_warn("ENV %s is %s, allowed options are: %s", + conf_item->e, item->error, item->allowed); + item->valid = false; + } + break; + } + case CONF_ENUM_BLOCKING_MODE: + { + const int blocking_mode = get_blocking_mode_val(envvar); + if(blocking_mode != -1) + { + conf_item->v.blocking_mode = blocking_mode; + item->valid = true; + } + else + { + + item->error = "not an allowed option"; + item->allowed = conf_item->h; + log_warn("ENV %s is %s, allowed options are: %s", + conf_item->e, item->error, item->allowed); + item->valid = false; + } + break; + } + case CONF_ENUM_REFRESH_HOSTNAMES: + { + const int refresh_hostnames = get_refresh_hostnames_val(envvar); + if(refresh_hostnames != -1) + { + conf_item->v.refresh_hostnames = refresh_hostnames; + item->valid = true; + } + else + { + + item->error = "not an allowed option"; + item->allowed = conf_item->h; + log_warn("ENV %s is %s, allowed options are: %s", + conf_item->e, item->error, item->allowed); + item->valid = false; + } + break; + } + case CONF_ENUM_LISTENING_MODE: + { + const int listeningMode = get_listeningMode_val(envvar); + if(listeningMode != -1) + { + conf_item->v.listeningMode = listeningMode; + item->valid = true; + } + else + { + + item->error = "not an allowed option"; + item->allowed = conf_item->h; + log_warn("ENV %s is %s, allowed options are: %s", + conf_item->e, item->error, item->allowed); + item->valid = false; + } + break; + } + case CONF_ENUM_WEB_THEME: + { + const int web_theme = get_web_theme_val(envvar); + if(web_theme != -1) + { + conf_item->v.web_theme = web_theme; + item->valid = true; + } + else + { + + item->error = "not an allowed option"; + item->allowed = conf_item->h; + log_warn("ENV %s is %s, allowed options are: %s", + conf_item->e, item->error, item->allowed); + item->valid = false; + } + break; + } + case CONF_ENUM_TEMP_UNIT: + { + const int temp_unit = get_temp_unit_val(envvar); + if(temp_unit != -1) + { + conf_item->v.temp_unit = temp_unit; + item->valid = true; + } + else + { + + item->error = "not an allowed option"; + item->allowed = conf_item->h; + log_warn("ENV %s is %s, allowed options are: %s", + conf_item->e, item->error, item->allowed); + item->valid = false; + } + break; + } + case CONF_ENUM_PRIVACY_LEVEL: + { + int val = 0; + if(sscanf(envvar, "%i", &val) == 1 && val >= PRIVACY_SHOW_ALL && val <= PRIVACY_MAXIMUM) + { + conf_item->v.i = val; + item->valid = true; + } + else + { + item->error = "not of type integer or outside allowed bounds"; + log_warn("ENV %s is %s", conf_item->e, item->error); + item->valid = false; + } + break; + } + case CONF_STRUCT_IN_ADDR: + { + struct in_addr addr4 = { 0 }; + if(strlen(envvar) == 0) + { + // Special case: empty string -> 0.0.0.0 + conf_item->v.in_addr.s_addr = INADDR_ANY; + } + else if(inet_pton(AF_INET, envvar, &addr4)) + { + memcpy(&conf_item->v.in_addr, &addr4, sizeof(addr4)); + item->valid = true; + } + else + { + item->error = "not of type IPv4 address"; + log_warn("ENV %s is %s", conf_item->e, item->error); + item->valid = false; + } + break; + } + case CONF_STRUCT_IN6_ADDR: + { + struct in6_addr addr6 = { 0 }; + if(strlen(envvar) == 0) + { + // Special case: empty string -> :: + memcpy(&conf_item->v.in6_addr, &in6addr_any, sizeof(in6addr_any)); + } + else if(inet_pton(AF_INET6, envvar, &addr6)) + { + memcpy(&conf_item->v.in6_addr, &addr6, sizeof(addr6)); + item->valid = true; + } + else + { + item->error = "not of type IPv6 address"; + log_warn("ENV %s is %s", conf_item->e, item->error); + item->valid = false; + } + break; + } + case CONF_JSON_STRING_ARRAY: + { + // Make a copy of envvar as strtok modified the input string + char *envvar_copy = strdup(envvar); + // Free previously allocated JSON array + cJSON_Delete(conf_item->v.json); + conf_item->v.json = cJSON_CreateArray(); + // Parse envvar array and generate a JSON array (env var + // arrays are ;-delimited) + const char delim[] =";"; + const char *elem = strtok(envvar_copy, delim); + while(elem != NULL) + { + // Only import non-empty entries + if(strlen(elem) > 0) + { + // Add string to our JSON array + cJSON *citem = cJSON_CreateString(elem); + cJSON_AddItemToArray(conf_item->v.json, citem); + } + + // Search for the next element + elem = strtok(NULL, delim); + } + free(envvar_copy); + item->valid = true; + break; + } + case CONF_PASSWORD: + { + if(!set_and_check_password(conf_item, envvar)) + { + log_warn("ENV %s is invalid", conf_item->e); + item->valid = false; + break; + } + item->valid = true; + break; + } + } + + return true; +} diff --git a/src/config/env.h b/src/config/env.h new file mode 100644 index 000000000..d26c7e751 --- /dev/null +++ b/src/config/env.h @@ -0,0 +1,28 @@ +/* Pi-hole: A black hole for Internet advertisements +* (c) 2023 Pi-hole, LLC (https://pi-hole.net) +* Network-wide ad blocking via your own hardware. +* +* FTL Engine +* Environment-related prototypes +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ +#ifndef CONFIG_ENV_H +#define CONFIG_ENV_H + +#include "FTL.h" +// union conf_value +#include "config.h" +// type toml_table_t +#include "tomlc99/toml.h" + +#define FTLCONF_PREFIX "FTLCONF_" + +int dist(const char *str); + +void getEnvVars(void); +void freeEnvVars(void); +void printFTLenv(void); +bool readEnvValue(struct conf_item *conf_item, struct config *newconf); + +#endif //CONFIG_ENV_H diff --git a/src/config/suggest.c b/src/config/suggest.c new file mode 100644 index 000000000..ed121e44f --- /dev/null +++ b/src/config/suggest.c @@ -0,0 +1,320 @@ + +/* Pi-hole: A black hole for Internet advertisements +* (c) 2023 Pi-hole, LLC (https://pi-hole.net) +* Network-wide ad blocking via your own hardware. +* +* FTL Engine +* String suggestion routines +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ + +#include "config/suggest.h" + +// Returns the minimum of three size_t values +static size_t min3(size_t x, size_t y, size_t z) +{ + const size_t tmp = x < y ? x : y; + return tmp < z ? tmp : z; +} + +// Simply swaps two size_t pointers in memory +static void swap(size_t **a, size_t **b) +{ + size_t *tmp = *a; + *a = *b; + *b = tmp; +} + +// The Levenshtein distance is a string metric for measuring the difference +// between two sequences. Informally, the Levenshtein distance between two words +// is the minimum number of single-character edits (insertions, deletions or +// substitutions) required to change one word into the other. It is named after +// the Soviet mathematician Vladimir Levenshtein, who considered this distance +// in 1965. (Wikipedia) +// +// For example, the Levenshtein distance between "kitten" and "sitting" is 3, +// since the following 3 edits change one into the other, and there is no way to +// do it with fewer than 3 edits: +// kitten -> sitten (substitution of "s" for "k"), +// sitten -> sittin (substitution of "i" for "e"), +// sittin -> sitting (insertion of "g" at the end). +// +// Our implementation follows the algorithm described in Wikipedia but was +// inspired by https://stackoverflow.com/a/71810739/2087442 +static size_t levenshtein_distance(const char *s1, const size_t len1, const char *s2, const size_t len2) +{ + // Allocate two vectors of size len2 + 1 + size_t *v0 = calloc(len2 + 1, sizeof(size_t)); + size_t *v1 = calloc(len2 + 1, sizeof(size_t)); + + // Initialize v0 + // v0[i] = the Levenshtein distance between s1[0..i] and the empty string + // v0[i] = i + for (size_t j = 0; j <= len2; ++j) + v0[j] = j; + + // Calculate v1 + // v1[i] = the Levenshtein distance between s1[0..i] and s2[0..j] + // v1[i] = min(v0[j] + 1, v1[j - 1] + 1, v0[j - 1] + (s1[i] == s2[j] ? 0 : 1)) + for (size_t i = 0; i < len1; ++i) + { + // Initialize v1 + v1[0] = i + 1; + + // Loop over remaining columns + for (size_t j = 0; j < len2; ++j) + { + // Calculate deletion, insertion and substitution costs + const size_t delcost = v0[j + 1] + 1; + const size_t inscost = v1[j] + 1; + const size_t subcost = s1[i] == s2[j] ? v0[j] : v0[j] + 1; + + // Take the minimum of the three costs (see above) + v1[j + 1] = min3(delcost, inscost, subcost); + } + + // Swap addresses to avoid copying data around + swap(&v0, &v1); + } + + // Return the Levenshtein distance between s1 and s2 + size_t dist = v0[len2]; + free(v0); + free(v1); + return dist; +} + +// The Bitap algorithm (also known as the shift-or, shift-and or Baeza-Yates- +// Gonnet algorithm) is an approximate string matching algorithm. The algorithm +// tells whether a given text contains a substring which is "approximately equal" +// to a given pattern, where approximate equality is defined in terms of Levenshtein +// distance — if the substring and pattern are within a given distance k of each +// other, then the algorithm considers them equal. (Wikipedia) +// +// Bitap distinguishes itself from other well-known string searching algorithms in +// its natural mapping onto simple bitwise operations +// +// Notice that in this implementation, counterintuitively, each bit with value +// zero indicates a match, and each bit with value 1 indicates a non-match. The +// same algorithm can be written with the intuitive semantics for 0 and 1, but +// in that case we must introduce another instruction into the inner loop to set +// R |= 1. In this implementation, we take advantage of the fact that +// left-shifting a value shifts in zeros on the right, which is precisely the +// behavior we need. +// +// This implementation is based on https://en.wikipedia.org/wiki/Bitap_algorithm +static const char *__attribute__((pure)) bitap_bitwise_search(const char *text, const char *pattern, + const size_t pattern_len, unsigned int k) +{ + // The bit array R is used to keep track of the current state of the + // search. + unsigned long R = ~1; + + // The pattern bitmask pattern_mask is used to represent the pattern + // string in a bitwise format. We use a size of 256 because our alphabet + // is all values of an unsigned char (0-255). + unsigned long pattern_mask[256]; + + // Sanity checks + if (pattern[0] == '\0') + return text; + + if (pattern_len > 31) + return NULL; + + // Initialize the pattern bitmasks + // First sets all bits in the bitmask to 1, ... + for (unsigned int i = 0; i < sizeof(pattern_mask) / sizeof(*pattern_mask); ++i) + pattern_mask[i] = ~0; + // ... and then set the corresponding bit in the bitmask to 0 for each + // character in the pattern + for (unsigned int i = 0; i < pattern_len; ++i) + pattern_mask[(unsigned char)pattern[i]] &= ~(1UL << i); + + // Loop over all characters in the text + for (unsigned int i = 0; text[i] != '\0'; ++i) { + // Update the bit array R based on the pattern bitmask + R |= pattern_mask[(unsigned char)text[i]]; + // Shift R one bit to the left + R <<= 1; + + // If the bit at the position corresponding to the pattern + // length in `R` is 0, an approximate match of the pattern has + // been found. Return the pointer to the start of this match + if ((R & (1UL << pattern_len)) == 0) + return (text + i - pattern_len) + 1; + } + + // No match was found with the given allowed number of errors (k) + return NULL; +} + +// Returns the the closest matching string using the Levenshtein distance +static const char *__attribute__((pure)) suggest_levenshtein(const char *strings[], size_t nstrings, + const char *string, const size_t string_len) +{ + size_t mindist = 0; + ssize_t minidx = -1; + + // The Levenshtein distance is at most the length of the longer string + for(size_t i = 0; i < nstrings; ++i) + { + const size_t len = strlen(strings[i]); + if(len > mindist) + mindist = len; + } + + // Loop over all strings and find the closest match + for (size_t i = 0; i < nstrings; ++i) + { + // Calculate the Levenshtein distance between the current string + // (out of nstrings) and the string we are checking against + const char *current = strings[i]; + size_t dist = levenshtein_distance(current, strlen(current), string, string_len); + + // If the distance is smaller than the smallest minimum we found + // so far, update the minimum and the index of the closest match + if (mindist >= dist) + { + mindist = dist; + minidx = i; + } + } + + // Return NULL if no match was found (this can only happen if no + // strings were given) + if(minidx == -1) + return NULL; + + // else: Return the closest match + return strings[minidx]; +} + +// Returns the the closest matching string using fuzzy searching +static unsigned int __attribute__((pure)) suggest_bitap(const char *strings[], size_t nstrings, + const char *string, const size_t string_len, + char **results, unsigned int num_results) +{ + unsigned int found = 0; + + // Try to find a match with at most j errors + for(unsigned int j = 0; j < string_len; j++) + { + // Iterate over all strings and try to find a match + for(unsigned int i = 0; i < nstrings; ++i) + { + // Get the current string + const char *current = strings[i]; + + // Use the Bitap algorithm to find a match + const char *result = bitap_bitwise_search(current, string, string_len, j); + + // If we found a match, add it to the list of results + if(result != NULL) + results[found++] = (char*)result; + + // If we found enough matches, stop searching + if(found >= num_results) + break; + } + + // If we found enough matches, stop searching + if(found >= num_results) + break; + } + + // Return the number of matches we found + return found; +} + +// Find string from list that starts with the given string +static const char *__attribute__((pure)) startswith(const char *strings[], size_t nstrings, + const char *string, const size_t string_len) +{ + // Loop over all strings + for (size_t i = 0; i < nstrings; ++i) + { + // Get the current string + const char *current = strings[i]; + + // If the current string starts with the given string, return it + if(strncasecmp(current, string, string_len) == 0) + return current; + } + + // Return NULL if no match was found + return NULL; +} + +// Try to find up to two matches using the Bitap algorithm and one using the +// Levenshtein distance +#define MAX_MATCHES 6 +static char **__attribute__((pure)) suggest_closest(const char *strings[], size_t nstrings, + const char *string, const size_t string_len, + unsigned int *N) +{ + // Allocate memory for MAX_MATCHES matches + char** matches = calloc(MAX_MATCHES, sizeof(char*)); + + // Try to find (MAX_MATCHES - 2) matches using the Bitap algorithm + *N = suggest_bitap(strings, nstrings, string, string_len, matches, MAX_MATCHES - 2); + + // Try to find a match that starts with the given string + matches[(*N)++] = (char*)startswith(strings, nstrings, string, string_len); + + // Try to find a last match using the Levenshtein distance + matches[(*N)++] = (char*)suggest_levenshtein(strings, nstrings, string, string_len); + + // Loop over matches and remove duplicates + for(unsigned int i = 0; i < *N; ++i) + { + // Skip if there is no match here + if(matches[i] == NULL) + continue; + + // Loop over all matches after the current one + for(unsigned int j = i + 1; j < *N; ++j) + { + // Set all duplicates to NULL + if(matches[j] != NULL && strcmp(matches[i], matches[j]) == 0) + { + matches[j] = NULL; + } + } + } + + // Remove NULL entries from the list of matches + unsigned int j = 0; + for(unsigned int i = 0; i < *N; ++i) + { + // If the i-th element is not NULL, the i-th element is assigned + // to the j-th position in the array, and j is incremented by 1. + // This effectively moves non-NULL elements towards the front of + // the array. + if(matches[i] != NULL) + matches[j++] = matches[i]; + } + // Update the number of matches to the number of non-NULL elements + *N = j; + + // Return the list of matches + return matches; +} + +char **suggest_closest_conf_key(const bool env, const char *string, unsigned int *N) +{ + // Collect all config item keys in a static list + const char *conf_keys[CONFIG_ELEMENTS] = { NULL }; + for(unsigned int i = 0; i < CONFIG_ELEMENTS; i++) + { + struct conf_item *conf_item = get_conf_item(&config, i); + if(!conf_item) + continue; + // Use either the environment key or the config key + conf_keys[i] = env ? conf_item->e : conf_item->k; + } + + // Return the list of closest matches + return suggest_closest(conf_keys, CONFIG_ELEMENTS, string, strlen(string), N); +} diff --git a/src/config/suggest.h b/src/config/suggest.h new file mode 100644 index 000000000..f0b543e14 --- /dev/null +++ b/src/config/suggest.h @@ -0,0 +1,19 @@ +/* Pi-hole: A black hole for Internet advertisements +* (c) 2023 Pi-hole, LLC (https://pi-hole.net) +* Network-wide ad blocking via your own hardware. +* +* FTL Engine +* String suggestion prototypes +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ +#ifndef LEVENSHTEIN_H +#define LEVENSHTEIN_H + +#include "FTL.h" +// union conf_value +#include "config.h" + +char **suggest_closest_conf_key(const bool env, const char *string, unsigned int *N); + +#endif //LEVENSHTEIN_H diff --git a/src/config/toml_helper.c b/src/config/toml_helper.c index 49c1da520..ab866f487 100644 --- a/src/config/toml_helper.c +++ b/src/config/toml_helper.c @@ -8,7 +8,6 @@ * 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 "toml_helper.h" #include "log.h" #include "config/config.h" @@ -809,259 +808,3 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t } } } - -#define FTLCONF_PREFIX "FTLCONF_" -bool readEnvValue(struct conf_item *conf_item, struct config *newconf) -{ - // Allocate memory for config key + prefix (sizeof includes the trailing '\0') - const size_t envkey_size = strlen(conf_item->k) + sizeof(FTLCONF_PREFIX); - char *envkey = calloc(envkey_size, sizeof(char)); - - // Build env key to look for - strcpy(envkey, FTLCONF_PREFIX); - strcat(envkey, conf_item->k); - - // Replace all "." by "_" as this is the convention used in v5.x and earlier - for(unsigned int i = 0; i < envkey_size - 1; i++) - if(envkey[i] == '.') - envkey[i] = '_'; - - // First check if a environmental variable with the given key exists - const char *envvar = getenv(envkey); - - // Return early if this environment variable does not exist - if(envvar == NULL) - { - log_debug(DEBUG_CONFIG, "ENV %s is not set", envkey); - free(envkey); - return false; - } - - log_debug(DEBUG_CONFIG, "ENV %s = \"%s\"", envkey, envvar); - - switch(conf_item->t) - { - case CONF_BOOL: - { - if(strcasecmp(envvar, "true") == 0 || strcasecmp(envvar, "yes") == 0) - conf_item->v.b = true; - else if(strcasecmp(envvar, "false") == 0 || strcasecmp(envvar, "no") == 0) - conf_item->v.b = false; - else - log_warn("ENV %s is not of type bool", envkey); - break; - } - case CONF_ALL_DEBUG_BOOL: - { - if(strcasecmp(envvar, "true") == 0 || strcasecmp(envvar, "yes") == 0) - set_all_debug(newconf, true); - else if(strcasecmp(envvar, "false") == 0 || strcasecmp(envvar, "no") == 0) - set_all_debug(newconf, false); - else - log_warn("ENV %s is not of type bool", envkey); - break; - } - case CONF_INT: - { - int val = 0; - if(sscanf(envvar, "%i", &val) == 1) - conf_item->v.i = val; - else - log_warn("ENV %s is not of type integer", envkey); - break; - } - case CONF_UINT: - { - unsigned int val = 0; - if(sscanf(envvar, "%u", &val) == 1) - conf_item->v.ui = val; - else - log_warn("ENV %s is not of type unsigned integer", envkey); - break; - } - case CONF_UINT16: - { - unsigned int val = 0; - if(sscanf(envvar, "%u", &val) == 1 && val <= UINT16_MAX) - conf_item->v.ui = val; - else - log_warn("ENV %s is not of type unsigned integer (16 bit)", envkey); - break; - } - case CONF_LONG: - { - long val = 0; - if(sscanf(envvar, "%li", &val) == 1) - conf_item->v.l = val; - else - log_warn("ENV %s is not of type long", envkey); - break; - } - case CONF_ULONG: - { - unsigned long val = 0; - if(sscanf(envvar, "%lu", &val) == 1) - conf_item->v.ul = val; - else - log_warn("ENV %s is not of type unsigned long", envkey); - break; - } - case CONF_DOUBLE: - { - double val = 0; - if(sscanf(envvar, "%lf", &val) == 1) - conf_item->v.d = val; - else - log_warn("ENV %s is not of type double", envkey); - break; - } - case CONF_STRING: - case CONF_STRING_ALLOCATED: - { - if(conf_item->t == CONF_STRING_ALLOCATED) - free(conf_item->v.s); - conf_item->v.s = strdup(envvar); - conf_item->t = CONF_STRING_ALLOCATED; - break; - } - case CONF_ENUM_PTR_TYPE: - { - const int ptr_type = get_ptr_type_val(envvar); - if(ptr_type != -1) - conf_item->v.ptr_type = ptr_type; - else - log_warn("ENV %s is invalid, allowed options are: %s", envkey, conf_item->h); - break; - } - case CONF_ENUM_BUSY_TYPE: - { - const int busy_reply = get_busy_reply_val(envvar); - if(busy_reply != -1) - conf_item->v.busy_reply = busy_reply; - else - log_warn("ENV %s is invalid, allowed options are: %s", envkey, conf_item->h); - break; - } - case CONF_ENUM_BLOCKING_MODE: - { - const int blocking_mode = get_blocking_mode_val(envvar); - if(blocking_mode != -1) - conf_item->v.blocking_mode = blocking_mode; - else - log_warn("ENV %s is invalid, allowed options are: %s", envkey, conf_item->h); - break; - } - case CONF_ENUM_REFRESH_HOSTNAMES: - { - const int refresh_hostnames = get_refresh_hostnames_val(envvar); - if(refresh_hostnames != -1) - conf_item->v.refresh_hostnames = refresh_hostnames; - else - log_warn("ENV %s is invalid, allowed options are: %s", envkey, conf_item->h); - break; - } - case CONF_ENUM_LISTENING_MODE: - { - const int listeningMode = get_listeningMode_val(envvar); - if(listeningMode != -1) - conf_item->v.listeningMode = listeningMode; - else - log_warn("ENV %s is invalid, allowed options are: %s", envkey, conf_item->h); - break; - } - case CONF_ENUM_WEB_THEME: - { - const int web_theme = get_web_theme_val(envvar); - if(web_theme != -1) - conf_item->v.web_theme = web_theme; - else - log_warn("ENV %s is invalid, allowed options are: %s", envkey, conf_item->h); - break; - } - case CONF_ENUM_TEMP_UNIT: - { - const int temp_unit = get_temp_unit_val(envvar); - if(temp_unit != -1) - conf_item->v.temp_unit = temp_unit; - else - log_warn("ENV %s is invalid, allowed options are: %s", envkey, conf_item->h); - break; - } - case CONF_ENUM_PRIVACY_LEVEL: - { - int val = 0; - if(sscanf(envvar, "%i", &val) == 1 && val >= PRIVACY_SHOW_ALL && val <= PRIVACY_MAXIMUM) - conf_item->v.i = val; - else - log_warn("ENV %s is invalid (not of type integer or outside allowed bounds)", envkey); - break; - } - case CONF_STRUCT_IN_ADDR: - { - struct in_addr addr4 = { 0 }; - if(strlen(envvar) == 0) - { - // Special case: empty string -> 0.0.0.0 - conf_item->v.in_addr.s_addr = INADDR_ANY; - } - else if(inet_pton(AF_INET, envvar, &addr4)) - memcpy(&conf_item->v.in_addr, &addr4, sizeof(addr4)); - else - log_warn("ENV %s is invalid (not of type IPv4 address)", envkey); - break; - } - case CONF_STRUCT_IN6_ADDR: - { - struct in6_addr addr6 = { 0 }; - if(strlen(envvar) == 0) - { - // Special case: empty string -> :: - memcpy(&conf_item->v.in6_addr, &in6addr_any, sizeof(in6addr_any)); - } - else if(inet_pton(AF_INET6, envvar, &addr6)) - memcpy(&conf_item->v.in6_addr, &addr6, sizeof(addr6)); - else - log_warn("ENV %s is invalid (not of type IPv6 address)", envkey); - break; - } - case CONF_JSON_STRING_ARRAY: - { - // Make a copy of envvar as strtok modified the input string - char *envvar_copy = strdup(envvar); - // Free previously allocated JSON array - cJSON_Delete(conf_item->v.json); - conf_item->v.json = cJSON_CreateArray(); - // Parse envvar array and generate a JSON array (env var - // arrays are ;-delimited) - const char delim[] =";"; - const char *elem = strtok(envvar_copy, delim); - while(elem != NULL) - { - // Only import non-empty entries - if(strlen(elem) > 0) - { - // Add string to our JSON array - cJSON *item = cJSON_CreateString(elem); - cJSON_AddItemToArray(conf_item->v.json, item); - } - - // Search for the next element - elem = strtok(NULL, delim); - } - free(envvar_copy); - break; - } - case CONF_PASSWORD: - { - if(!set_and_check_password(conf_item, envvar)) - { - log_warn("ENV %s is invalid", envkey); - break; - } - } - } - - // Free allocated env var name - free(envkey); - return true; -} diff --git a/src/config/toml_helper.h b/src/config/toml_helper.h index db0d259a0..70f4844c6 100644 --- a/src/config/toml_helper.h +++ b/src/config/toml_helper.h @@ -23,6 +23,5 @@ void print_comment(FILE *fp, const char *str, const char *intro, const unsigned void print_toml_allowed_values(cJSON *allowed_values, FILE *fp, const unsigned int width, const unsigned int indent); void writeTOMLvalue(FILE * fp, const int indent, const enum conf_type t, union conf_value *v); void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *toml, struct config *newconf); -bool readEnvValue(struct conf_item *conf_item, struct config *newconf); #endif //CONFIG_WRITER_H diff --git a/src/config/toml_reader.c b/src/config/toml_reader.c index 91fd4e8d2..4e7b9e07a 100644 --- a/src/config/toml_reader.c +++ b/src/config/toml_reader.c @@ -23,6 +23,8 @@ #include "config/toml_helper.h" // delete_all_sessions() #include "api/api.h" +// readEnvValue() +#include "config/env.h" // Private prototypes static toml_table_t *parseTOML(const unsigned int version); @@ -129,6 +131,9 @@ bool readFTLtoml(struct config *oldconf, struct config *newconf, if(verbose) reportDebugFlags(); + // Print FTL environment variables (if used) + printFTLenv(); + // Free memory allocated by the TOML parser and return success toml_free(toml); return true; diff --git a/src/config/toml_writer.c b/src/config/toml_writer.c index 340900963..31978b464 100644 --- a/src/config/toml_writer.c +++ b/src/config/toml_writer.c @@ -51,6 +51,7 @@ bool writeFTLtoml(const bool verbose) // Iterate over configuration and store it into the file char *last_path = (char*)""; + unsigned int modified = 0, env_vars = 0; for(unsigned int i = 0; i < CONFIG_ELEMENTS; i++) { // Get pointer to memory location of this conf_item @@ -87,7 +88,10 @@ bool writeFTLtoml(const bool verbose) // Print info if this value is overwritten by an env var if(conf_item->f & FLAG_ENV_VAR) + { print_comment(fp, ">>> This config is overwritten by an environmental variable <<<", "", 85, level-1); + env_vars++; + } // Write value indentTOML(fp, level-1); @@ -107,12 +111,26 @@ bool writeFTLtoml(const bool verbose) { fprintf(fp, " ### CHANGED, default = "); writeTOMLvalue(fp, -1, conf_item->t, &conf_item->d); + modified++; } // Add newlines after each entry fputs("\n\n", fp); } + // Log some statistics in verbose mode + if(verbose || config.debug.config.v.b) + { + log_info("Wrote config file:"); + log_info(" - %zu total entries", CONFIG_ELEMENTS); + log_info(" - %zu %s default", CONFIG_ELEMENTS - modified, + CONFIG_ELEMENTS - modified == 1 ? "entry is" : "entries are"); + log_info(" - %u %s modified", modified, + modified == 1 ? "entry is" : "entries are"); + log_info(" - %u %s forced through environment", env_vars, + env_vars == 1 ? "entry is" : "entries are"); + } + // Close file and release exclusive lock closeFTLtoml(fp); diff --git a/src/daemon.c b/src/daemon.c index 510c06af2..0345e0469 100644 --- a/src/daemon.c +++ b/src/daemon.c @@ -37,6 +37,8 @@ #include "api/api.h" // setlocale() #include +// freeEnvVars() +#include "config/env.h" pthread_t threads[THREADS_MAX] = { 0 }; bool resolver_ready = false; @@ -370,6 +372,9 @@ void cleanup(const int ret) // This should be the last action when c destroy_shmem(); + // Free environment variables + freeEnvVars(); + char buffer[42] = { 0 }; format_time(buffer, 0, timer_elapsed_msec(EXIT_TIMER)); log_info("########## FTL terminated after%s (code %i)! ##########", buffer, ret); diff --git a/src/log.c b/src/log.c index bc0ce1c12..8eac8c708 100644 --- a/src/log.c +++ b/src/log.c @@ -54,7 +54,7 @@ void init_FTL_log(const char *name) if((logfile = fopen(config.files.log.ftl.v.s, "a+")) == NULL) { syslog(LOG_ERR, "Opening of FTL\'s log file failed, using syslog instead!"); - printf("ERR: Opening of FTL log (%s) failed!\n",config.files.log.ftl.v.s); + printf("ERROR: Opening of FTL log (%s) failed!\n",config.files.log.ftl.v.s); config.files.log.ftl.v.s = NULL; } @@ -140,7 +140,7 @@ static const char * __attribute__((const)) priostr(const int priority, const enu return "CRIT"; // error conditions case LOG_ERR: - return "ERR"; + return "ERROR"; // warning conditions case LOG_WARNING: return "WARNING"; diff --git a/src/webserver/webserver.c b/src/webserver/webserver.c index f9844f4f2..5485046f9 100644 --- a/src/webserver/webserver.c +++ b/src/webserver/webserver.c @@ -80,7 +80,6 @@ static int redirect_root_handler(struct mg_connection *conn, void *input) if(config.debug.api.v.b) { log_debug(DEBUG_API, "Host header: \"%s\", extracted host: \"%.*s\"", host, (int)host_len, host); - log_debug(DEBUG_API, "URI: %s", uri); } diff --git a/test/pihole.toml b/test/pihole.toml index 9a3939b16..538730f85 100644 --- a/test/pihole.toml +++ b/test/pihole.toml @@ -915,8 +915,12 @@ # Controls if FTLDNS should print extended details about regex matching into FTL.log. regex = true ### CHANGED, default = false - # Print extra debugging information during telnet API calls. Currently only used to - # send extra information when getting all queries. + # Print extra debugging information concerning API calls. This includes the request, + # the request parameters, and the internal details about how the algorithms decide + # which data to present and in what form. This very verbose output should only be used + # when debugging specific API issues and can be helpful, e.g., when a client cannot + # connect due to an obscure API error. Furthermore, this setting enables logging of + # all API requests (auth log) and details about user authentication attempts. api = true ### CHANGED, default = false # Print extra debugging information about TLS connections. This includes the TLS diff --git a/test/run.sh b/test/run.sh index 88e8f7f2b..5f34e9d60 100755 --- a/test/run.sh +++ b/test/run.sh @@ -70,6 +70,8 @@ umask 0022 # Set exemplary config value by environment variable export FTLCONF_misc_nice="-11" +export FTLCONF_dns_upstrrr="-11" +export FTLCONF_debug_api="not_a_bool" # Start FTL if ! su pihole -s /bin/sh -c /home/pihole/pihole-FTL; then diff --git a/test/test_suite.bats b/test/test_suite.bats index dd33955cd..5a8fd9a09 100644 --- a/test/test_suite.bats +++ b/test/test_suite.bats @@ -501,8 +501,8 @@ [[ ${lines[0]} == "The Pi-hole FTL engine - "* ]] } -@test "No WARNING messages in FTL.log (besides known capability issues)" { - run bash -c 'grep "WARNING:" /var/log/pihole/FTL.log | grep -v -E "CAP_NET_ADMIN|CAP_NET_RAW|CAP_SYS_NICE|CAP_IPC_LOCK|CAP_CHOWN|CAP_NET_BIND_SERVICE|(Cannot set process priority)"' +@test "No WARNING messages in FTL.log (besides known warnings)" { + run bash -c 'grep "WARNING:" /var/log/pihole/FTL.log | grep -v -E "CAP_NET_ADMIN|CAP_NET_RAW|CAP_SYS_NICE|CAP_IPC_LOCK|CAP_CHOWN|CAP_NET_BIND_SERVICE|(Cannot set process priority)|FTLCONF_"' printf "%s\n" "${lines[@]}" [[ "${lines[@]}" == "" ]] } @@ -1207,10 +1207,10 @@ [[ "${lines[@]}" != *"ERROR"* ]] } -@test "No ERROR messages in FTL.log (besides known index.html error)" { - run bash -c 'grep "ERR: " /var/log/pihole/FTL.log' +@test "No ERROR messages in FTL.log (besides known/intended error)" { + run bash -c 'grep "ERROR: " /var/log/pihole/FTL.log' printf "%s\n" "${lines[@]}" - run bash -c 'grep "ERR: " /var/log/pihole/FTL.log | grep -c -v -E "(index\.html)|(Failed to create shared memory object)"' + run bash -c 'grep "ERROR: " /var/log/pihole/FTL.log | grep -c -v -E "(index\.html)|(Failed to create shared memory object)|(FTLCONF_debug_api is invalid)"' printf "count: %s\n" "${lines[@]}" [[ ${lines[0]} == "0" ]] } @@ -1341,6 +1341,42 @@ [[ ${lines[1]} == " nice = -11 ### CHANGED, default = -10" ]] } +@test "Correct number of environmental variables is logged" { + run bash -c 'grep -q "3 FTLCONF environment variables found (1 used, 1 invalid, 1 ignored)" /var/log/pihole/FTL.log' + printf "%s\n" "${lines[@]}" + [[ $status == 0 ]] +} + +@test "Correct environmental variable is logged" { + run bash -c 'grep -q "FTLCONF_misc_nice is used" /var/log/pihole/FTL.log' + printf "%s\n" "${lines[@]}" + [[ $status == 0 ]] +} + +@test "Invalid environmental variable is logged" { + run bash -c 'grep -q "FTLCONF_debug_api is invalid" /var/log/pihole/FTL.log' + printf "%s\n" "${lines[@]}" + [[ $status == 0 ]] +} + +@test "Unknown environmental variable is logged, a useful alternative is suggested" { + run bash -c 'grep -A1 "FTLCONF_dns_upstrrr is unknown" /var/log/pihole/FTL.log' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == *"WARNING: [?] FTLCONF_dns_upstrrr is unknown, did you mean any of these?" ]] + [[ ${lines[1]} == *"WARNING: - FTLCONF_dns_upstreams" ]] +} + +@test "CLI complains about unknown config key and offers a suggestion" { + run bash -c './pihole-FTL --config dbg.all' + [[ ${lines[0]} == "Unknown config option dbg.all, did you mean:" ]] + [[ ${lines[1]} == " - debug.all" ]] + [[ $status == 4 ]] + run bash -c './pihole-FTL --config misc.privacyLLL' + [[ ${lines[0]} == "Unknown config option misc.privacyLLL, did you mean:" ]] + [[ ${lines[1]} == " - misc.privacylevel" ]] + [[ $status == 4 ]] +} + @test "Changing a config option set forced by ENVVAR is not possible via the CLI" { run bash -c './pihole-FTL --config misc.nice -12' printf "%s\n" "${lines[@]}"