diff --git a/src/config/toml_helper.c b/src/config/toml_helper.c index 91f27c31..74f00047 100644 --- a/src/config/toml_helper.c +++ b/src/config/toml_helper.c @@ -444,7 +444,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t if(val.ok) conf_item->v.b = val.u.b; else - log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not of type bool", conf_item->k); + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not a valid bool", conf_item->k); break; } case CONF_ALL_DEBUG_BOOL: @@ -453,7 +453,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t if(val.ok) set_all_debug(newconf, val.u.b); else - log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not of type bool", conf_item->k); + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not a valid bool", conf_item->k); break; } case CONF_INT: @@ -462,7 +462,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t if(val.ok) conf_item->v.i = val.u.i; else - log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not of type integer", conf_item->k); + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not a valid integer", conf_item->k); break; } case CONF_UINT: @@ -471,7 +471,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t if(val.ok && val.u.i >= 0) conf_item->v.ui = val.u.i; else - log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not of type unsigned integer", conf_item->k); + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not a valid unsigned integer", conf_item->k); break; } case CONF_UINT16: @@ -480,7 +480,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t if(val.ok && val.u.i >= 0 && val.u.i <= UINT16_MAX) conf_item->v.ui = val.u.i; else - log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not of type unsigned integer (16 bit)", conf_item->k); + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not a valid unsigned integer (16 bit)", conf_item->k); break; } case CONF_LONG: @@ -489,7 +489,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t if(val.ok) conf_item->v.l = val.u.i; else - log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not of type long", conf_item->k); + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not a valid long integer", conf_item->k); break; } case CONF_ULONG: @@ -498,7 +498,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t if(val.ok && val.u.i >= 0) conf_item->v.ul = val.u.i; else - log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not of type unsigned long", conf_item->k); + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not a valid unsigned long integer", conf_item->k); break; } case CONF_DOUBLE: @@ -507,7 +507,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t if(val.ok) conf_item->v.d = val.u.d; else - log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not of type double", conf_item->k); + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not a valid double", conf_item->k); break; } case CONF_STRING: @@ -522,7 +522,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t conf_item->t = CONF_STRING_ALLOCATED; } else - log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not of type string", conf_item->k); + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not a valid string", conf_item->k); break; } case CONF_ENUM_PTR_TYPE: @@ -538,7 +538,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t log_warn("Config setting %s is invalid, allowed options are: %s", conf_item->k, conf_item->h); } else - log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not of type string", conf_item->k); + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not a valid string", conf_item->k); break; } case CONF_ENUM_BUSY_TYPE: @@ -554,7 +554,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t log_warn("Config setting %s is invalid, allowed options are: %s", conf_item->k, conf_item->h); } else - log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not of type string", conf_item->k); + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not a valid string", conf_item->k); break; } case CONF_ENUM_BLOCKING_MODE: @@ -570,7 +570,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t log_warn("Config setting %s is invalid, allowed options are: %s", conf_item->k, conf_item->h); } else - log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not of type string", conf_item->k); + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not a validstring", conf_item->k); break; } case CONF_ENUM_REFRESH_HOSTNAMES: @@ -586,7 +586,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t log_warn("Config setting %s is invalid, allowed options are: %s", conf_item->k, conf_item->h); } else - log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not of type string", conf_item->k); + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not a valid string", conf_item->k); break; } case CONF_ENUM_LISTENING_MODE: @@ -602,7 +602,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t log_warn("Config setting %s is invalid, allowed options are: %s", conf_item->k, conf_item->h); } else - log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not of type string", conf_item->k); + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not a valid string", conf_item->k); break; } case CONF_ENUM_WEB_THEME: @@ -618,7 +618,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t log_warn("Config setting %s is invalid, allowed options are: %s", conf_item->k, conf_item->h); } else - log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not of type string", conf_item->k); + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not a valid string", conf_item->k); break; } case CONF_ENUM_TEMP_UNIT: @@ -634,7 +634,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t log_warn("Config setting %s is invalid, allowed options are: %s", conf_item->k, conf_item->h); } else - log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not of type string", conf_item->k); + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is not a valid string", conf_item->k); break; } case CONF_ENUM_PRIVACY_LEVEL: @@ -643,7 +643,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t if(val.ok && val.u.i >= PRIVACY_SHOW_ALL && val.u.i <= PRIVACY_MAXIMUM) conf_item->v.i = val.u.i; else - log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is invalid (not of type integer or outside allowed bounds)", conf_item->k); + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is invalid (not an integer or outside allowed bounds)", conf_item->k); break; } case CONF_STRUCT_IN_ADDR: @@ -654,8 +654,12 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t { if(inet_pton(AF_INET, val.u.s, &addr4)) memcpy(&conf_item->v.in_addr, &addr4, sizeof(addr4)); + else + log_warn("Config %s is invalid (not of type IPv4 address)", conf_item->k); free(val.u.s); } + else + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is invalid (not a valid string of type IPv4 address)", conf_item->k); break; } case CONF_STRUCT_IN6_ADDR: @@ -666,8 +670,12 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t { if(inet_pton(AF_INET6, val.u.s, &addr6)) memcpy(&conf_item->v.in6_addr, &addr6, sizeof(addr6)); + else + log_warn("Config %s is invalid (not of type IPv6 address)", conf_item->k); free(val.u.s); } + else + log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST or is invalid (not a valid string of type IPv6 address)", conf_item->k); break; } case CONF_JSON_STRING_ARRAY: @@ -686,7 +694,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t 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 %u)", conf_item->k, i); + log_warn("Config %s is an invalid array (found at index %u)", conf_item->k, i); break; } // Only import non-empty entries @@ -709,3 +717,244 @@ 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(envkey, "true") == 0 || strcasecmp(envkey, "yes") == 0) + conf_item->v.b = true; + else if(strcasecmp(envkey, "false") == 0 || strcasecmp(envkey, "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(envkey, "true") == 0 || strcasecmp(envkey, "yes") == 0) + set_all_debug(newconf, true); + else if(strcasecmp(envkey, "false") == 0 || strcasecmp(envkey, "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(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(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 + 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: + { + // This is ignored, it is only a pseudo-element with no real content + } + } + + // Free allocated env var name + free(envkey); + return true; +} diff --git a/src/config/toml_helper.h b/src/config/toml_helper.h index 2b0249fd..4a972f44 100644 --- a/src/config/toml_helper.h +++ b/src/config/toml_helper.h @@ -23,5 +23,6 @@ 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 b6aadfea..f076022e 100644 --- a/src/config/toml_reader.c +++ b/src/config/toml_reader.c @@ -30,11 +30,10 @@ static void reportDebugFlags(void); bool readFTLtoml(struct config *conf, toml_table_t *toml, const bool verbose) { // Parse lines in the config file if we did not receive a pointer to a TOML - // table (e.g. from an imported Teleporter file) - bool external = true; - if(toml == NULL) + // table from an imported Teleporter file + bool teleporter = (toml != NULL); + if(!teleporter) { - external = false; toml = parseTOML(); if(!toml) return false; @@ -42,13 +41,17 @@ bool readFTLtoml(struct config *conf, toml_table_t *toml, const bool verbose) // Try to read debug config. This is done before the full config // parsing to allow for debug output further down - toml_table_t *conf_debug = toml_table_in(toml, "debug"); - if(conf_debug) - readTOMLvalue(&conf->debug.config, "config", conf_debug, conf); + // First try to read env variable, if this fails, read TOML + if(teleporter || !readEnvValue(&conf->debug.config, conf)) + { + toml_table_t *conf_debug = toml_table_in(toml, "debug"); + if(conf_debug) + readTOMLvalue(&conf->debug.config, "config", conf_debug, conf); + } set_debug_flags(conf); log_debug(DEBUG_CONFIG, "Reading %s TOML config file: full config", - external ? "external" : "default"); + teleporter ? "teleporter" : "default"); // Read all known config items for(unsigned int i = 0; i < CONFIG_ELEMENTS; i++) @@ -56,6 +59,12 @@ bool readFTLtoml(struct config *conf, toml_table_t *toml, const bool verbose) // Get pointer to memory location of this conf_item struct conf_item *conf_item = get_conf_item(conf, i); + // First try to read this config option from an environment variable + // Skip reading environment variables when importing from Teleporter + // If this succeeds, skip searching the TOML file for this config item + if(!teleporter && readEnvValue(conf_item, conf)) + continue; + // Get config path depth unsigned int level = config_path_depth(conf_item->p); diff --git a/test/run.sh b/test/run.sh index e2563e82..e237b3fd 100755 --- a/test/run.sh +++ b/test/run.sh @@ -69,6 +69,7 @@ OLDUMASK=$(umask) umask 0022 # Start FTL +export FTLCONF_misc_nice="-11" if ! su pihole -s /bin/sh -c /home/pihole/pihole-FTL; then echo "pihole-FTL failed to start" exit 1 diff --git a/test/test_suite.bats b/test/test_suite.bats index 6401306e..6dfbab63 100644 --- a/test/test_suite.bats +++ b/test/test_suite.bats @@ -1185,7 +1185,7 @@ [[ ${lines[0]} == "0" ]] } -@test "No config errors in pihole.toml" { +@test "No missing config items in pihole.toml" { run bash -c 'grep "DEBUG_CONFIG: " /var/log/pihole/FTL.log' printf "%s\n" "${lines[@]}" run bash -c 'grep "DEBUG_CONFIG: " /var/log/pihole/FTL.log | grep -c "DOES NOT EXIST"' @@ -1251,6 +1251,13 @@ [[ "${lines[0]}" == "192.168.1.7" ]] } +@test "Environmental variable is favored over config file" { + # The config file has -10 but we set FTLCONF_misc_nice="-11" + run bash -c 'grep -c "nice = -11" /etc/pihole/pihole.toml' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == "1" ]] +} + @test "API domain search: Non-existing domain returns expected JSON" { run bash -c 'curl -s 127.0.0.1/api/search/non.existent' printf "%s\n" "${lines[@]}"