diff --git a/src/api/config.c b/src/api/config.c index ade2896f4..3d3563e94 100644 --- a/src/api/config.c +++ b/src/api/config.c @@ -766,6 +766,17 @@ static int api_config_patch(struct ftl_conn *api) // If we reach this point, a valid setting was found and changed + // Validate new value (if validation function is defined) + char errbuf[VALIDATOR_ERRBUF_LEN] = { 0 }; + if(!conf_item->c(&new_item->v, new_item->k, errbuf)) + { + free_config(&newconf); + return send_json_error(api, 400, + "bad_request", + "Config item validation failed", + errbuf); + } + // Check if this item requires a config-rewrite + restart of dnsmasq if(conf_item->f & FLAG_RESTART_FTL) dnsmasq_changed = true; @@ -949,6 +960,21 @@ static int api_config_put_delete(struct ftl_conn *api) } // If we reach this point, a valid setting was found and changed + + // Validate new value on PUT (if validation function is defined) + if(api->method == HTTP_PUT) + { + char errbuf[VALIDATOR_ERRBUF_LEN] = { 0 }; + if(!new_item->c(&new_item->v, new_item->k, errbuf)) + { + free_config(&newconf); + return send_json_error(api, 400, + "bad_request", + "Invalid value", + errbuf); + } + } + // Check if this item requires a config-rewrite + restart of dnsmasq if(new_item->f & FLAG_RESTART_FTL) dnsmasq_changed = true; diff --git a/src/config/CMakeLists.txt b/src/config/CMakeLists.txt index 11eed7b94..92824c42f 100644 --- a/src/config/CMakeLists.txt +++ b/src/config/CMakeLists.txt @@ -33,6 +33,8 @@ set(sources toml_reader.h toml_helper.c toml_helper.h + validator.c + validator.h ) add_library(config OBJECT ${sources}) diff --git a/src/config/cli.c b/src/config/cli.c index 3ffbd261a..d012d0d8f 100644 --- a/src/config/cli.c +++ b/src/config/cli.c @@ -455,6 +455,18 @@ int set_config_from_CLI(const char *key, const char *value) { // Config item changed + // Validate new value(if validation function is defined) + if(new_item->c != NULL) + { + char errbuf[VALIDATOR_ERRBUF_LEN] = { 0 }; + if(!new_item->c(&new_item->v, new_item->k, errbuf)) + { + free_config(&newconf); + log_err("Invalid value: %s", errbuf); + return 3; + } + } + // Is this a dnsmasq option we need to check? if(conf_item->f & FLAG_RESTART_FTL) { diff --git a/src/config/config.c b/src/config/config.c index b1be060ae..723ee7b52 100644 --- a/src/config/config.c +++ b/src/config/config.c @@ -29,6 +29,8 @@ #include "api/api.h" // exit_code #include "signals.h" +// validation functions +#include "config/validator.h" // getEnvVars() #include "config/env.h" // sha256sum() @@ -388,42 +390,49 @@ void initConfig(struct config *conf) conf->dns.upstreams.t = CONF_JSON_STRING_ARRAY; conf->dns.upstreams.d.json = cJSON_CreateArray(); conf->dns.upstreams.f = FLAG_RESTART_FTL; + conf->dns.upstreams.c = validate_stub; // Type-based checking + dnsmasq syntax checking conf->dns.CNAMEdeepInspect.k = "dns.CNAMEdeepInspect"; conf->dns.CNAMEdeepInspect.h = "Use this option to control deep CNAME inspection. Disabling it might be beneficial for very low-end devices"; conf->dns.CNAMEdeepInspect.t = CONF_BOOL; conf->dns.CNAMEdeepInspect.f = FLAG_ADVANCED_SETTING; conf->dns.CNAMEdeepInspect.d.b = true; + conf->dns.CNAMEdeepInspect.c = validate_stub; // Only type-based checking conf->dns.blockESNI.k = "dns.blockESNI"; conf->dns.blockESNI.h = "Should _esni. subdomains be blocked by default? Encrypted Server Name Indication (ESNI) is certainly a good step into the right direction to enhance privacy on the web. It prevents on-path observers, including ISPs, coffee shop owners and firewalls, from intercepting the TLS Server Name Indication (SNI) extension by encrypting it. This prevents the SNI from being used to determine which websites users are visiting.\n ESNI will obviously cause issues for pixelserv-tls which will be unable to generate matching certificates on-the-fly when it cannot read the SNI. Cloudflare and Firefox are already enabling ESNI. According to the IEFT draft (link above), we can easily restore piselserv-tls's operation by replying NXDOMAIN to _esni. subdomains of blocked domains as this mimics a \"not configured for this domain\" behavior."; conf->dns.blockESNI.t = CONF_BOOL; conf->dns.blockESNI.f = FLAG_ADVANCED_SETTING; conf->dns.blockESNI.d.b = true; + conf->dns.blockESNI.c = validate_stub; // Only type-based checking conf->dns.EDNS0ECS.k = "dns.EDNS0ECS"; conf->dns.EDNS0ECS.h = "Should we overwrite the query source when client information is provided through EDNS0 client subnet (ECS) information? This allows Pi-hole to obtain client IPs even if they are hidden behind the NAT of a router. This feature has been requested and discussed on Discourse where further information how to use it can be found: https://discourse.pi-hole.net/t/support-for-add-subnet-option-from-dnsmasq-ecs-edns0-client-subnet/35940"; conf->dns.EDNS0ECS.t = CONF_BOOL; conf->dns.EDNS0ECS.f = FLAG_ADVANCED_SETTING; conf->dns.EDNS0ECS.d.b = true; + conf->dns.EDNS0ECS.c = validate_stub; // Only type-based checking conf->dns.ignoreLocalhost.k = "dns.ignoreLocalhost"; conf->dns.ignoreLocalhost.h = "Should FTL hide queries made by localhost?"; conf->dns.ignoreLocalhost.t = CONF_BOOL; conf->dns.ignoreLocalhost.f = FLAG_ADVANCED_SETTING; conf->dns.ignoreLocalhost.d.b = false; + conf->dns.ignoreLocalhost.c = validate_stub; // Only type-based checking conf->dns.showDNSSEC.k = "dns.showDNSSEC"; conf->dns.showDNSSEC.h = "Should FTL should analyze and show internally generated DNSSEC queries?"; conf->dns.showDNSSEC.t = CONF_BOOL; conf->dns.showDNSSEC.f = FLAG_ADVANCED_SETTING; conf->dns.showDNSSEC.d.b = true; + conf->dns.showDNSSEC.c = validate_stub; // Only type-based checking conf->dns.analyzeOnlyAandAAAA.k = "dns.analyzeOnlyAandAAAA"; conf->dns.analyzeOnlyAandAAAA.h = "Should FTL analyze *only* A and AAAA queries?"; conf->dns.analyzeOnlyAandAAAA.t = CONF_BOOL; conf->dns.analyzeOnlyAandAAAA.f = FLAG_ADVANCED_SETTING; conf->dns.analyzeOnlyAandAAAA.d.b = false; + conf->dns.analyzeOnlyAandAAAA.c = validate_stub; // Only type-based checking conf->dns.piholePTR.k = "dns.piholePTR"; conf->dns.piholePTR.h = "Controls whether and how FTL will reply with for address for which a local interface exists."; @@ -440,6 +449,7 @@ void initConfig(struct config *conf) conf->dns.piholePTR.t = CONF_ENUM_PTR_TYPE; conf->dns.piholePTR.f = FLAG_ADVANCED_SETTING; conf->dns.piholePTR.d.ptr_type = PTR_PIHOLE; + conf->dns.piholePTR.c = validate_stub; // Only type-based checking conf->dns.replyWhenBusy.k = "dns.replyWhenBusy"; conf->dns.replyWhenBusy.h = "How should FTL handle queries when the gravity database is not available?"; @@ -456,12 +466,14 @@ void initConfig(struct config *conf) conf->dns.replyWhenBusy.t = CONF_ENUM_BUSY_TYPE; conf->dns.replyWhenBusy.f = FLAG_ADVANCED_SETTING; conf->dns.replyWhenBusy.d.busy_reply = BUSY_ALLOW; + conf->dns.replyWhenBusy.c = validate_stub; // Only type-based checking conf->dns.blockTTL.k = "dns.blockTTL"; conf->dns.blockTTL.h = "FTL's internal TTL to be handed out for blocked queries in seconds. This settings allows users to select a value different from the dnsmasq config option local-ttl. This is useful in context of locally used hostnames that are known to stay constant over long times (printers, etc.).\n Note that large values may render whitelisting ineffective due to client-side caching of blocked queries."; conf->dns.blockTTL.t = CONF_UINT; conf->dns.blockTTL.f = FLAG_ADVANCED_SETTING; conf->dns.blockTTL.d.ui = 2; + conf->dns.blockTTL.c = validate_stub; // Only type-based checking conf->dns.hosts.k = "dns.hosts"; conf->dns.hosts.h = "Array of custom DNS records\n Example: hosts = [ \"127.0.0.1 mylocal\", \"192.168.0.1 therouter\" ]"; @@ -469,18 +481,21 @@ void initConfig(struct config *conf) conf->dns.hosts.t = CONF_JSON_STRING_ARRAY; conf->dns.hosts.f = FLAG_ADVANCED_SETTING; conf->dns.hosts.d.json = cJSON_CreateArray(); + conf->dns.hosts.c = validate_dns_hosts; conf->dns.domainNeeded.k = "dns.domainNeeded"; conf->dns.domainNeeded.h = "If set, A and AAAA queries for plain names, without dots or domain parts, are never forwarded to upstream nameservers"; conf->dns.domainNeeded.t = CONF_BOOL; conf->dns.domainNeeded.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING; conf->dns.domainNeeded.d.b = false; + conf->dns.domainNeeded.c = validate_stub; // Only type-based checking conf->dns.expandHosts.k = "dns.expandHosts"; conf->dns.expandHosts.h = "If set, the domain is added to simple names (without a period) in /etc/hosts in the same way as for DHCP-derived names"; conf->dns.expandHosts.t = CONF_BOOL; conf->dns.expandHosts.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING; conf->dns.expandHosts.d.b = false; + conf->dns.expandHosts.c = validate_stub; // Only type-based checking conf->dns.domain.k = "dns.domain"; conf->dns.domain.h = "The DNS domain used by your Pi-hole to expand hosts and for DHCP.\n\n Only if DHCP is enabled below: For DHCP, this has two effects; firstly it causes the DHCP server to return the domain to any hosts which request it, and secondly it sets the domain which it is legal for DHCP-configured hosts to claim. The intention is to constrain hostnames so that an untrusted host on the LAN cannot advertise its name via DHCP as e.g. \"google.com\" and capture traffic not meant for it. If no domain suffix is specified, then any DHCP hostname with a domain part (ie with a period) will be disallowed and logged. If a domain is specified, then hostnames with a domain part are allowed, provided the domain part matches the suffix. In addition, when a suffix is set then hostnames without a domain part have the suffix added as an optional domain part. For instance, we can set domain=mylab.com and have a machine whose DHCP hostname is \"laptop\". The IP address for that machine is available both as \"laptop\" and \"laptop.mylab.com\".\n\n You can disable setting a domain by setting this option to an empty string."; @@ -488,17 +503,20 @@ void initConfig(struct config *conf) conf->dns.domain.t = CONF_STRING; conf->dns.domain.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING; conf->dns.domain.d.s = (char*)"lan"; + conf->dns.domain.c = validate_domain; conf->dns.bogusPriv.k = "dns.bogusPriv"; conf->dns.bogusPriv.h = "Should all reverse lookups for private IP ranges (i.e., 192.168.x.y, etc) which are not found in /etc/hosts or the DHCP leases file be answered with \"no such domain\" rather than being forwarded upstream?"; conf->dns.bogusPriv.t = CONF_BOOL; conf->dns.bogusPriv.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING; conf->dns.bogusPriv.d.b = true; + conf->dns.bogusPriv.c = validate_stub; // Only type-based checking conf->dns.dnssec.k = "dns.dnssec"; conf->dns.dnssec.h = "Validate DNS replies using DNSSEC?"; conf->dns.dnssec.t = CONF_BOOL; conf->dns.dnssec.f = FLAG_RESTART_FTL; + conf->dns.dnssec.c = validate_stub; // Only type-based checking conf->dns.dnssec.d.b = false; conf->dns.interface.k = "dns.interface"; @@ -507,6 +525,7 @@ void initConfig(struct config *conf) conf->dns.interface.t = CONF_STRING; conf->dns.interface.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING; conf->dns.interface.d.s = (char*)""; + conf->dns.interface.c = validate_stub; // Type-based checking + dnsmasq syntax checking conf->dns.hostRecord.k = "dns.hostRecord"; conf->dns.hostRecord.h = "Add A, AAAA and PTR records to the DNS. This adds one or more names to the DNS with associated IPv4 (A) and IPv6 (AAAA) records"; @@ -514,6 +533,7 @@ void initConfig(struct config *conf) conf->dns.hostRecord.t = CONF_STRING; conf->dns.hostRecord.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING; conf->dns.hostRecord.d.s = (char*)""; + conf->dns.hostRecord.c = validate_stub; // Type-based checking + dnsmasq syntax checking conf->dns.listeningMode.k = "dns.listeningMode"; conf->dns.listeningMode.h = "Pi-hole interface listening modes"; @@ -531,12 +551,14 @@ void initConfig(struct config *conf) conf->dns.listeningMode.t = CONF_ENUM_LISTENING_MODE; conf->dns.listeningMode.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING; conf->dns.listeningMode.d.listeningMode = LISTEN_LOCAL; + conf->dns.listeningMode.c = validate_stub; // Only type-based checking conf->dns.queryLogging.k = "dns.queryLogging"; conf->dns.queryLogging.h = "Log DNS queries and replies to pihole.log"; conf->dns.queryLogging.t = CONF_BOOL; conf->dns.queryLogging.f = FLAG_RESTART_FTL; conf->dns.queryLogging.d.b = true; + conf->dns.queryLogging.c = validate_stub; // Only type-based checking conf->dns.cnameRecords.k = "dns.cnameRecords"; conf->dns.cnameRecords.h = "List of CNAME records which indicate that is really . If the is given, it overwrites the value of local-ttl"; @@ -544,12 +566,14 @@ void initConfig(struct config *conf) conf->dns.cnameRecords.t = CONF_JSON_STRING_ARRAY; conf->dns.cnameRecords.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING; conf->dns.cnameRecords.d.json = cJSON_CreateArray(); + conf->dns.cnameRecords.c = validate_dns_cnames; conf->dns.port.k = "dns.port"; conf->dns.port.h = "Port used by the DNS server"; conf->dns.port.t = CONF_UINT16; conf->dns.port.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING; conf->dns.port.d.ui = 53u; + conf->dns.port.c = validate_stub; // Only type-based checking // sub-struct dns.cache conf->dns.cache.size.k = "dns.cache.size"; @@ -557,18 +581,21 @@ void initConfig(struct config *conf) conf->dns.cache.size.t = CONF_UINT; conf->dns.cache.size.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING; conf->dns.cache.size.d.ui = 10000u; + conf->dns.cache.size.c = validate_stub; // Only type-based checking conf->dns.cache.optimizer.k = "dns.cache.optimizer"; conf->dns.cache.optimizer.h = "Query cache optimizer: If a DNS name exists in the cache, but its time-to-live has expired only recently, the data will be used anyway (a refreshing from upstream is triggered). This can improve DNS query delays especially over unreliable Internet connections. This feature comes at the expense of possibly sometimes returning out-of-date data and less efficient cache utilization, since old data cannot be flushed when its TTL expires, so the cache becomes mostly least-recently-used. To mitigate issues caused by massively outdated DNS replies, the maximum overaging of cached records is limited. We strongly recommend staying below 86400 (1 day) with this option.\n Setting the TTL excess time to zero will serve stale cache data regardless how long it has expired. This is not recommended as it may lead to stale data being served for a long time. Setting this option to any negative value will disable this feature altogether."; conf->dns.cache.optimizer.t = CONF_INT; conf->dns.cache.optimizer.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING; conf->dns.cache.optimizer.d.i = 3600u; + conf->dns.cache.optimizer.c = validate_stub; // Only type-based checking // sub-struct dns.blocking conf->dns.blocking.active.k = "dns.blocking.active"; conf->dns.blocking.active.h = "Should FTL block queries?"; conf->dns.blocking.active.t = CONF_BOOL; conf->dns.blocking.active.d.b = true; + conf->dns.blocking.active.c = validate_stub; // Only type-based checking conf->dns.blocking.mode.k = "dns.blocking.mode"; conf->dns.blocking.mode.h = "How should FTL reply to blocked queries?"; @@ -585,12 +612,14 @@ void initConfig(struct config *conf) } conf->dns.blocking.mode.t = CONF_ENUM_BLOCKING_MODE; conf->dns.blocking.mode.d.blocking_mode = MODE_NULL; + conf->dns.blocking.mode.c = validate_stub; // Only type-based checking conf->dns.revServers.k = "dns.revServers"; conf->dns.revServers.h = "Reverse server (former also called \"conditional forwarding\") feature\n Array of reverse servers each one in one of the following forms: \",[/],[#],\"\n\n Individual components:\n\n : either \"true\" or \"false\"\n\n [/]: Address range for the reverse server feature in CIDR notation. If the prefix length is omitted, either 32 (IPv4) or 128 (IPv6) are substituted (exact address match). This is almost certainly not what you want here.\n Example: \"192.168.0.0/24\" for the range 192.168.0.1 - 192.168.0.255\n\n [#]: Target server to be used for the reverse server feature\n Example: \"192.168.0.1#53\"\n\n : Domain used for the reverse server feature (e.g., \"fritz.box\")\n Example: \"fritz.box\""; conf->dns.revServers.a = cJSON_CreateStringReference("array of reverse servers each one in one of the following forms: \",[/],[#],\", e.g., \"true,192.168.0.0/24,192.168.0.1,fritz.box\""); conf->dns.revServers.t = CONF_JSON_STRING_ARRAY; conf->dns.revServers.d.json = cJSON_CreateArray(); + conf->dns.revServers.c = validate_dns_revServers; conf->dns.revServers.f = FLAG_RESTART_FTL; // sub-struct dns.rate_limit @@ -598,22 +627,26 @@ void initConfig(struct config *conf) conf->dns.rateLimit.count.h = "Rate-limited queries are answered with a REFUSED reply and not further processed by FTL.\n The default settings for FTL's rate-limiting are to permit no more than 1000 queries in 60 seconds. Both numbers can be customized independently. It is important to note that rate-limiting is happening on a per-client basis. Other clients can continue to use FTL while rate-limited clients are short-circuited at the same time.\n For this setting, both numbers, the maximum number of queries within a given time, and the length of the time interval (seconds) have to be specified. For instance, if you want to set a rate limit of 1 query per hour, the option should look like RATE_LIMIT=1/3600. The time interval is relative to when FTL has finished starting (start of the daemon + possible delay by DELAY_STARTUP) then it will advance in steps of the rate-limiting interval. If a client reaches the maximum number of queries it will be blocked until the end of the current interval. This will be logged to /var/log/pihole/FTL.log, e.g. Rate-limiting 10.0.1.39 for at least 44 seconds. If the client continues to send queries while being blocked already and this number of queries during the blocking exceeds the limit the client will continue to be blocked until the end of the next interval (FTL.log will contain lines like Still rate-limiting 10.0.1.39 as it made additional 5007 queries). As soon as the client requests less than the set limit, it will be unblocked (Ending rate-limitation of 10.0.1.39).\n Rate-limiting may be disabled altogether by setting both values to zero (this results in the same behavior as before FTL v5.7).\n How many queries are permitted..."; conf->dns.rateLimit.count.t = CONF_UINT; conf->dns.rateLimit.count.d.ui = 1000; + conf->dns.rateLimit.count.c = validate_stub; // Only type-based checking conf->dns.rateLimit.interval.k = "dns.rateLimit.interval"; conf->dns.rateLimit.interval.h = "... in the set interval before rate-limiting?"; conf->dns.rateLimit.interval.t = CONF_UINT; conf->dns.rateLimit.interval.d.ui = 60; + conf->dns.rateLimit.interval.c = validate_stub; // Only type-based checking // sub-struct dns.special_domains conf->dns.specialDomains.mozillaCanary.k = "dns.specialDomains.mozillaCanary"; conf->dns.specialDomains.mozillaCanary.h = "Should Pi-hole always replies with NXDOMAIN to A and AAAA queries of use-application-dns.net to disable Firefox automatic DNS-over-HTTP? This is following the recommendation on https://support.mozilla.org/en-US/kb/configuring-networks-disable-dns-over-https"; conf->dns.specialDomains.mozillaCanary.t = CONF_BOOL; conf->dns.specialDomains.mozillaCanary.d.b = true; + conf->dns.specialDomains.mozillaCanary.c = validate_stub; // Only type-based checking conf->dns.specialDomains.iCloudPrivateRelay.k = "dns.specialDomains.iCloudPrivateRelay"; conf->dns.specialDomains.iCloudPrivateRelay.h = "Should Pi-hole always replies with NXDOMAIN to A and AAAA queries of mask.icloud.com and mask-h2.icloud.com to disable Apple's iCloud Private Relay to prevent Apple devices from bypassing Pi-hole? This is following the recommendation on https://developer.apple.com/support/prepare-your-network-for-icloud-private-relay"; conf->dns.specialDomains.iCloudPrivateRelay.t = CONF_BOOL; conf->dns.specialDomains.iCloudPrivateRelay.d.b = true; + conf->dns.specialDomains.iCloudPrivateRelay.c = validate_stub; // Only type-based checking // sub-struct dns.reply_addr conf->dns.reply.host.force4.k = "dns.reply.host.force4"; @@ -621,6 +654,7 @@ void initConfig(struct config *conf) conf->dns.reply.host.force4.t = CONF_BOOL; conf->dns.reply.host.force4.f = FLAG_ADVANCED_SETTING; conf->dns.reply.host.force4.d.b = false; + conf->dns.reply.host.force4.c = validate_stub; // Only type-based checking conf->dns.reply.host.v4.k = "dns.reply.host.IPv4"; conf->dns.reply.host.v4.h = "Custom IPv4 address for the Pi-hole host"; @@ -628,12 +662,14 @@ void initConfig(struct config *conf) conf->dns.reply.host.v4.t = CONF_STRUCT_IN_ADDR; conf->dns.reply.host.v4.f = FLAG_ADVANCED_SETTING; memset(&conf->dns.reply.host.v4.d.in_addr, 0, sizeof(struct in_addr)); + conf->dns.reply.host.v4.c = validate_stub; // Only type-based checking conf->dns.reply.host.force6.k = "dns.reply.host.force6"; conf->dns.reply.host.force6.h = "Use a specific IPv6 address for the Pi-hole host? See description for the IPv4 variant above for further details."; conf->dns.reply.host.force6.t = CONF_BOOL; conf->dns.reply.host.force6.f = FLAG_ADVANCED_SETTING; conf->dns.reply.host.force6.d.b = false; + conf->dns.reply.host.force6.c = validate_stub; // Only type-based checking conf->dns.reply.host.v6.k = "dns.reply.host.IPv6"; conf->dns.reply.host.v6.h = "Custom IPv6 address for the Pi-hole host"; @@ -641,12 +677,14 @@ void initConfig(struct config *conf) conf->dns.reply.host.v6.t = CONF_STRUCT_IN6_ADDR; conf->dns.reply.host.v6.f = FLAG_ADVANCED_SETTING; memset(&conf->dns.reply.host.v6.d.in6_addr, 0, sizeof(struct in6_addr)); + conf->dns.reply.host.v6.c = validate_stub; // Only type-based checking conf->dns.reply.blocking.force4.k = "dns.reply.blocking.force4"; conf->dns.reply.blocking.force4.h = "Use a specific IPv4 address in IP blocking mode? By default, FTL determines the address of the interface a query arrived on and uses this address for replying to A queries with the most suitable address for the requesting client. This setting can be used to use a fixed, rather than the dynamically obtained, address when Pi-hole responds in the following cases: IP blocking mode is used and this query is to be blocked, regular expressions with the ;reply=IP regex extension."; conf->dns.reply.blocking.force4.t = CONF_BOOL; conf->dns.reply.blocking.force4.f = FLAG_ADVANCED_SETTING; conf->dns.reply.blocking.force4.d.b = false; + conf->dns.reply.blocking.force4.c = validate_stub; // Only type-based checking conf->dns.reply.blocking.v4.k = "dns.reply.blocking.IPv4"; conf->dns.reply.blocking.v4.h = "Custom IPv4 address for IP blocking mode"; @@ -654,12 +692,14 @@ void initConfig(struct config *conf) conf->dns.reply.blocking.v4.t = CONF_STRUCT_IN_ADDR; conf->dns.reply.blocking.v4.f = FLAG_ADVANCED_SETTING; memset(&conf->dns.reply.blocking.v4.d.in_addr, 0, sizeof(struct in_addr)); + conf->dns.reply.blocking.v4.c = validate_stub; // Only type-based checking conf->dns.reply.blocking.force6.k = "dns.reply.blocking.force6"; conf->dns.reply.blocking.force6.h = "Use a specific IPv6 address in IP blocking mode? See description for the IPv4 variant above for further details."; conf->dns.reply.blocking.force6.t = CONF_BOOL; conf->dns.reply.blocking.force6.f = FLAG_ADVANCED_SETTING; conf->dns.reply.blocking.force6.d.b = false; + conf->dns.reply.blocking.force6.c = validate_stub; // Only type-based checking conf->dns.reply.blocking.v6.k = "dns.reply.blocking.IPv6"; conf->dns.reply.blocking.v6.h = "Custom IPv6 address for IP blocking mode"; @@ -667,6 +707,7 @@ void initConfig(struct config *conf) conf->dns.reply.blocking.v6.t = CONF_STRUCT_IN6_ADDR; conf->dns.reply.blocking.v6.f = FLAG_ADVANCED_SETTING; memset(&conf->dns.reply.blocking.v6.d.in6_addr, 0, sizeof(struct in6_addr)); + conf->dns.reply.blocking.v6.c = validate_stub; // Only type-based checking // sub-struct dhcp conf->dhcp.active.k = "dhcp.active"; @@ -674,6 +715,7 @@ void initConfig(struct config *conf) conf->dhcp.active.t = CONF_BOOL; conf->dhcp.active.f = FLAG_RESTART_FTL; conf->dhcp.active.d.b = false; + conf->dhcp.active.c = validate_stub; // Only type-based checking conf->dhcp.start.k = "dhcp.start"; conf->dhcp.start.h = "Start address of the DHCP address pool"; @@ -681,6 +723,7 @@ void initConfig(struct config *conf) conf->dhcp.start.t = CONF_STRUCT_IN_ADDR; conf->dhcp.start.f = FLAG_RESTART_FTL; memset(&conf->dhcp.start.d.in_addr, 0, sizeof(struct in_addr)); + conf->dhcp.start.c = validate_stub; // Only type-based checking conf->dhcp.end.k = "dhcp.end"; conf->dhcp.end.h = "End address of the DHCP address pool"; @@ -688,6 +731,7 @@ void initConfig(struct config *conf) conf->dhcp.end.t = CONF_STRUCT_IN_ADDR; conf->dhcp.end.f = FLAG_RESTART_FTL; memset(&conf->dhcp.end.d.in_addr, 0, sizeof(struct in_addr)); + conf->dhcp.end.c = validate_stub; // Only type-based checking conf->dhcp.router.k = "dhcp.router"; conf->dhcp.router.h = "Address of the gateway to be used (typically the address of your router in a home installation)"; @@ -695,6 +739,7 @@ void initConfig(struct config *conf) conf->dhcp.router.t = CONF_STRUCT_IN_ADDR; conf->dhcp.router.f = FLAG_RESTART_FTL; memset(&conf->dhcp.router.d.in_addr, 0, sizeof(struct in_addr)); + conf->dhcp.router.c = validate_stub; // Only type-based checking conf->dhcp.netmask.k = "dhcp.netmask"; conf->dhcp.netmask.h = "The netmask used by your Pi-hole. For directly connected networks (i.e., networks on which the machine running Pi-hole has an interface) the netmask is optional and may be set to an empty string (\"\"): it will then be determined from the interface configuration itself. For networks which receive DHCP service via a relay agent, we cannot determine the netmask itself, so it should explicitly be specified, otherwise Pi-hole guesses based on the class (A, B or C) of the network address."; @@ -702,6 +747,7 @@ void initConfig(struct config *conf) conf->dhcp.netmask.t = CONF_STRUCT_IN_ADDR; conf->dhcp.netmask.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING; memset(&conf->dhcp.netmask.d.in_addr, 0, sizeof(struct in_addr)); + conf->dhcp.netmask.c = validate_stub; // Only type-based checking conf->dhcp.leaseTime.k = "dhcp.leaseTime"; conf->dhcp.leaseTime.h = "If the lease time is given, then leases will be given for that length of time. If not given, the default lease time is one hour for IPv4 and one day for IPv6."; @@ -709,24 +755,28 @@ void initConfig(struct config *conf) conf->dhcp.leaseTime.t = CONF_STRING; conf->dhcp.leaseTime.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING; conf->dhcp.leaseTime.d.s = (char*)""; + conf->dhcp.leaseTime.c = validate_stub; // Type-based checking + dnsmasq syntax checking conf->dhcp.ipv6.k = "dhcp.ipv6"; conf->dhcp.ipv6.h = "Should Pi-hole make an attempt to also satisfy IPv6 address requests (be aware that IPv6 works a whole lot different than IPv4)"; conf->dhcp.ipv6.t = CONF_BOOL; conf->dhcp.ipv6.f = FLAG_RESTART_FTL; conf->dhcp.ipv6.d.b = false; + conf->dhcp.ipv6.c = validate_stub; // Only type-based checking conf->dhcp.multiDNS.k = "dhcp.multiDNS"; conf->dhcp.multiDNS.h = "Advertise DNS server multiple times to clients. Some devices will add their own proprietary DNS servers to the list of DNS servers, which can cause issues with Pi-hole. This option will advertise the Pi-hole DNS server multiple times to clients, which should prevent this from happening."; conf->dhcp.multiDNS.t = CONF_BOOL; conf->dhcp.multiDNS.f = FLAG_RESTART_FTL; conf->dhcp.multiDNS.d.b = false; + conf->dhcp.multiDNS.c = validate_stub; // Only type-based checking conf->dhcp.rapidCommit.k = "dhcp.rapidCommit"; conf->dhcp.rapidCommit.h = "Enable DHCPv4 Rapid Commit Option specified in RFC 4039. Should only be enabled if either the server is the only server for the subnet to avoid conflicts"; conf->dhcp.rapidCommit.t = CONF_BOOL; conf->dhcp.rapidCommit.f = FLAG_RESTART_FTL; conf->dhcp.rapidCommit.d.b = false; + conf->dhcp.rapidCommit.c = validate_stub; // Only type-based checking conf->dhcp.hosts.k = "dhcp.hosts"; conf->dhcp.hosts.h = "Per host parameters for the DHCP server. This allows a machine with a particular hardware address to be always allocated the same hostname, IP address and lease time or to specify static DHCP leases"; @@ -734,6 +784,7 @@ void initConfig(struct config *conf) conf->dhcp.hosts.t = CONF_JSON_STRING_ARRAY; conf->dhcp.hosts.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING; conf->dhcp.hosts.d.json = cJSON_CreateArray(); + conf->dhcp.hosts.c = validate_stub; // Type-based checking + dnsmasq syntax checking // struct resolver @@ -741,17 +792,20 @@ void initConfig(struct config *conf) conf->resolver.resolveIPv6.h = "Should FTL try to resolve IPv6 addresses to hostnames?"; conf->resolver.resolveIPv6.t = CONF_BOOL; conf->resolver.resolveIPv6.d.b = true; + conf->resolver.resolveIPv6.c = validate_stub; // Only type-based checking conf->resolver.resolveIPv4.k = "resolver.resolveIPv4"; conf->resolver.resolveIPv4.h = "Should FTL try to resolve IPv4 addresses to hostnames?"; conf->resolver.resolveIPv4.t = CONF_BOOL; conf->resolver.resolveIPv4.d.b = true; + conf->resolver.resolveIPv4.c = validate_stub; // Only type-based checking conf->resolver.networkNames.k = "resolver.networkNames"; conf->resolver.networkNames.h = "Control whether FTL should use the fallback option to try to obtain client names from checking the network table. This behavior can be disabled with this option.\n Assume an IPv6 client without a host names. However, the network table knows - though the client's MAC address - that this is the same device where we have a host name for another IP address (e.g., a DHCP server managed IPv4 address). In this case, we use the host name associated to the other address as this is the same device."; conf->resolver.networkNames.t = CONF_BOOL; conf->resolver.networkNames.f = FLAG_ADVANCED_SETTING; conf->resolver.networkNames.d.b = true; + conf->resolver.networkNames.c = validate_stub; // Only type-based checking conf->resolver.refreshNames.k = "resolver.refreshNames"; conf->resolver.refreshNames.h = "With this option, you can change how (and if) hourly PTR requests are made to check for changes in client and upstream server hostnames."; @@ -768,6 +822,7 @@ void initConfig(struct config *conf) conf->resolver.refreshNames.t = CONF_ENUM_REFRESH_HOSTNAMES; conf->resolver.refreshNames.f = FLAG_ADVANCED_SETTING; conf->resolver.refreshNames.d.refresh_hostnames = REFRESH_IPV4_ONLY; + conf->resolver.refreshNames.c = validate_stub; // Only type-based checking // struct database @@ -775,21 +830,25 @@ void initConfig(struct config *conf) conf->database.DBimport.h = "Should FTL load information from the database on startup to be aware of the most recent history?"; conf->database.DBimport.t = CONF_BOOL; conf->database.DBimport.d.b = true; + conf->database.DBimport.c = validate_stub; // Only type-based checking conf->database.DBexport.k = "database.DBexport"; conf->database.DBexport.h = "Should FTL store queries in the long-term database?"; conf->database.DBexport.t = CONF_BOOL; conf->database.DBexport.d.b = true; + conf->database.DBexport.c = validate_stub; // Only type-based checking conf->database.maxDBdays.k = "database.maxDBdays"; conf->database.maxDBdays.h = "How long should queries be stored in the database [days]?"; conf->database.maxDBdays.t = CONF_INT; conf->database.maxDBdays.d.i = (365/4); + conf->database.maxDBdays.c = validate_stub; // Only type-based checking conf->database.DBinterval.k = "database.DBinterval"; conf->database.DBinterval.h = "How often do we store queries in FTL's database [seconds]?"; conf->database.DBinterval.t = CONF_UINT; conf->database.DBinterval.d.ui = 60; + conf->database.DBinterval.c = validate_stub; // Only type-based checking // sub-struct database.network conf->database.network.parseARPcache.k = "database.network.parseARPcache"; @@ -797,12 +856,14 @@ void initConfig(struct config *conf) conf->database.network.parseARPcache.t = CONF_BOOL; conf->database.network.parseARPcache.f = FLAG_ADVANCED_SETTING; conf->database.network.parseARPcache.d.b = true; + conf->database.network.parseARPcache.c = validate_stub; // Only type-based checking conf->database.network.expire.k = "database.network.expire"; conf->database.network.expire.h = "How long should IP addresses be kept in the network_addresses table [days]? IP addresses (and associated host names) older than the specified number of days are removed to avoid dead entries in the network overview table."; conf->database.network.expire.t = CONF_UINT; conf->database.network.expire.f = FLAG_ADVANCED_SETTING; conf->database.network.expire.d.ui = conf->database.maxDBdays.d.ui; + conf->database.network.expire.c = validate_stub; // Only type-based checking // struct http @@ -812,6 +873,7 @@ void initConfig(struct config *conf) conf->webserver.domain.t = CONF_STRING; conf->webserver.domain.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL; conf->webserver.domain.d.s = (char*)"pi.hole"; + conf->webserver.domain.c = validate_domain; conf->webserver.acl.k = "webserver.acl"; conf->webserver.acl.h = "Webserver access control list (ACL) allowing for restrictions to be put on the list of IP addresses which have access to the web server. The ACL is a comma separated list of IP subnets, where each subnet is prepended by either a - or a + sign. A plus sign means allow, where a minus sign means deny. If a subnet mask is omitted, such as -1.2.3.4, this means to deny only that single IP address. If this value is not set (empty string), all accesses are allowed. Otherwise, the default setting is to deny all accesses. On each request the full list is traversed, and the last (!) match wins. IPv6 addresses may be specified in CIDR-form [a:b::c]/64.\n\n Example 1: acl = \"+127.0.0.1,+[::1]\"\n ---> deny all access, except from 127.0.0.1 and ::1,\n Example 2: acl = \"+192.168.0.0/16\"\n ---> deny all accesses, except from the 192.168.0.0/16 subnet,\n Example 3: acl = \"+[::]/0\" ---> allow only IPv6 access."; @@ -819,6 +881,7 @@ void initConfig(struct config *conf) conf->webserver.acl.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL; conf->webserver.acl.t = CONF_STRING; conf->webserver.acl.d.s = (char*)""; + conf->webserver.acl.c = validate_stub; // Type-based checking + civetweb syntax checking conf->webserver.port.k = "webserver.port"; conf->webserver.port.h = "Ports to be used by the webserver.\n Comma-separated list of ports to listen on. It is possible to specify an IP address to bind to. In this case, an IP address and a colon must be prepended to the port number. For example, to bind to the loopback interface on port 80 (IPv4) and to all interfaces port 8080 (IPv4), use \"127.0.0.1:80,8080\". \"[::]:80\" can be used to listen to IPv6 connections to port 80. IPv6 addresses of network interfaces can be specified as well, e.g. \"[::1]:80\" for the IPv6 loopback interface. [::]:80 will bind to port 80 IPv6 only.\n In order to use port 80 for all interfaces, both IPv4 and IPv6, use either the configuration \"80,[::]:80\" (create one socket for IPv4 and one for IPv6 only), or \"+80\" (create one socket for both, IPv4 and IPv6). The + notation to use IPv4 and IPv6 will only work if no network interface is specified. Depending on your operating system version and IPv6 network environment, some configurations might not work as expected, so you have to test to find the configuration most suitable for your needs. In case \"+80\" does not work for your environment, you need to use \"80,[::]:80\".\n If the port is TLS/SSL, a letter 's' must be appended, for example, \"80,443s\" will open port 80 and port 443, and connections on port 443 will be encrypted. For non-encrypted ports, it is allowed to append letter 'r' (as in redirect). Redirected ports will redirect all their traffic to the first configured SSL port. For example, if webserver.port is \"80r,443s\", then all HTTP traffic coming at port 80 will be redirected to HTTPS port 443. If this value is not set (empty string), the web server will not be started and, hence, the API will not be available."; @@ -826,12 +889,14 @@ void initConfig(struct config *conf) conf->webserver.port.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL; conf->webserver.port.t = CONF_STRING; conf->webserver.port.d.s = (char*)"80,[::]:80,443s,[::]:443s"; + conf->webserver.port.c = validate_stub; // Type-based checking + civetweb syntax checking conf->webserver.tls.rev_proxy.k = "webserver.tls.rev_proxy"; conf->webserver.tls.rev_proxy.h = "Is Pi-hole running behind a reverse proxy? If yes, Pi-hole will not consider HTTP-only connections being insecure. This is useful if you are running Pi-hole in a trusted environment, for example, in a local network, and you are using a reverse proxy to provide TLS encryption, e.g., by using Traefik (docker). If you are using a reverse proxy, you can alternatively set webserver.tls.cert to the path of the TLS certificate file and let Pi-hole handle true end-to-end encryption."; conf->webserver.tls.rev_proxy.f = FLAG_ADVANCED_SETTING; conf->webserver.tls.rev_proxy.t = CONF_BOOL; conf->webserver.tls.rev_proxy.d.b = false; + conf->webserver.tls.rev_proxy.c = validate_stub; // Only type-based checking conf->webserver.tls.cert.k = "webserver.tls.cert"; conf->webserver.tls.cert.h = "Path to the TLS (SSL) certificate file. This option is only required when at least one of webserver.port is TLS. The file must be in PEM format, and it must have both, private key and certificate (the *.pem file created must contain a 'CERTIFICATE' section as well as a 'RSA PRIVATE KEY' section).\n The *.pem file can be created using\n cp server.crt server.pem\n cat server.key >> server.pem\n if you have these files instead"; @@ -839,16 +904,19 @@ void initConfig(struct config *conf) conf->webserver.tls.cert.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL; conf->webserver.tls.cert.t = CONF_STRING; conf->webserver.tls.cert.d.s = (char*)"/etc/pihole/tls.pem"; + conf->webserver.tls.cert.c = validate_filepath; conf->webserver.session.timeout.k = "webserver.session.timeout"; conf->webserver.session.timeout.h = "Session timeout in seconds. If a session is inactive for more than this time, it will be terminated. Sessions are continuously refreshed by the web interface, preventing sessions from timing out while the web interface is open.\n This option may also be used to make logins persistent for long times, e.g. 86400 seconds (24 hours), 604800 seconds (7 days) or 2592000 seconds (30 days). Note that the total number of concurrent sessions is limited so setting this value too high may result in users being rejected and unable to log in if there are already too many sessions active."; conf->webserver.session.timeout.t = CONF_UINT; conf->webserver.session.timeout.d.ui = 1800u; + conf->webserver.session.timeout.c = validate_stub; // Only type-based checking conf->webserver.session.restore.k = "webserver.session.restore"; conf->webserver.session.restore.h = "Should Pi-hole backup and restore sessions from the database? This is useful if you want to keep your sessions after a restart of the web interface."; conf->webserver.session.restore.t = CONF_BOOL; conf->webserver.session.restore.d.b = true; + conf->webserver.session.restore.c = validate_stub; // Only type-based checking // sub-struct paths conf->webserver.paths.webroot.k = "webserver.paths.webroot"; @@ -857,6 +925,7 @@ void initConfig(struct config *conf) conf->webserver.paths.webroot.t = CONF_STRING; conf->webserver.paths.webroot.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL; conf->webserver.paths.webroot.d.s = (char*)"/var/www/html"; + conf->webserver.paths.webroot.c = validate_filepath; conf->webserver.paths.webhome.k = "webserver.paths.webhome"; conf->webserver.paths.webhome.h = "Sub-directory of the root containing the web interface"; @@ -864,12 +933,14 @@ void initConfig(struct config *conf) conf->webserver.paths.webhome.t = CONF_STRING; conf->webserver.paths.webhome.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL; conf->webserver.paths.webhome.d.s = (char*)"/admin/"; + conf->webserver.paths.webhome.c = validate_filepath; // sub-struct interface conf->webserver.interface.boxed.k = "webserver.interface.boxed"; conf->webserver.interface.boxed.h = "Should the web interface use the boxed layout?"; conf->webserver.interface.boxed.t = CONF_BOOL; conf->webserver.interface.boxed.d.b = true; + conf->webserver.interface.boxed.c = validate_stub; // Only type-based checking conf->webserver.interface.theme.k = "webserver.interface.theme"; conf->webserver.interface.theme.h = "Theme used by the Pi-hole web interface"; @@ -884,29 +955,34 @@ void initConfig(struct config *conf) } conf->webserver.interface.theme.t = CONF_ENUM_WEB_THEME; conf->webserver.interface.theme.d.web_theme = THEME_DEFAULT_AUTO; + conf->webserver.interface.theme.c = validate_stub; // Only type-based checking // sub-struct api conf->webserver.api.searchAPIauth.k = "webserver.api.searchAPIauth"; conf->webserver.api.searchAPIauth.h = "Do local clients need to authenticate to access the search API? This settings allows local clients to use pihole -q ... without authentication. Note that \"local\" in the sense of the option means only 127.0.0.1 and [::1]"; conf->webserver.api.searchAPIauth.t = CONF_BOOL; conf->webserver.api.searchAPIauth.d.b = false; + conf->webserver.api.searchAPIauth.c = validate_stub; // Only type-based checking conf->webserver.api.localAPIauth.k = "webserver.api.localAPIauth"; conf->webserver.api.localAPIauth.h = "Do local clients need to authenticate to access the API? This settings allows local clients to use the API without authentication."; conf->webserver.api.localAPIauth.t = CONF_BOOL; conf->webserver.api.localAPIauth.d.b = true; + conf->webserver.api.localAPIauth.c = validate_stub; // Only type-based checking conf->webserver.api.max_sessions.k = "webserver.api.max_sessions"; conf->webserver.api.max_sessions.h = "Number of concurrent sessions allowed for the API. If the number of sessions exceeds this value, no new sessions will be allowed until the number of sessions drops due to session expiration or logout. Note that the number of concurrent sessions is irrelevant if authentication is disabled as no sessions are used in this case."; conf->webserver.api.max_sessions.t = CONF_UINT16; conf->webserver.api.max_sessions.d.u16 = 16; conf->webserver.api.max_sessions.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL; + conf->webserver.api.max_sessions.c = validate_stub; // Only type-based checking conf->webserver.api.prettyJSON.k = "webserver.api.prettyJSON"; conf->webserver.api.prettyJSON.h = "Should FTL prettify the API output (add extra spaces, newlines and indentation)?"; conf->webserver.api.prettyJSON.t = CONF_BOOL; conf->webserver.api.prettyJSON.f = FLAG_ADVANCED_SETTING; conf->webserver.api.prettyJSON.d.b = false; + conf->webserver.api.prettyJSON.c = validate_stub; // Only type-based checking conf->webserver.api.pwhash.k = "webserver.api.pwhash"; conf->webserver.api.pwhash.h = "API password hash"; @@ -914,6 +990,7 @@ void initConfig(struct config *conf) conf->webserver.api.pwhash.t = CONF_STRING; conf->webserver.api.pwhash.f = FLAG_INVALIDATE_SESSIONS; conf->webserver.api.pwhash.d.s = (char*)""; + conf->webserver.api.pwhash.c = validate_stub; // Only type-based checking conf->webserver.api.password.k = "webserver.api.password"; conf->webserver.api.password.h = "Pi-hole web interface and API password. When set to something different than \""PASSWORD_VALUE"\", this property will compute the corresponding password hash to set webserver.api.pwhash"; @@ -921,6 +998,7 @@ void initConfig(struct config *conf) conf->webserver.api.password.t = CONF_PASSWORD; conf->webserver.api.password.f = FLAG_PSEUDO_ITEM | FLAG_INVALIDATE_SESSIONS; conf->webserver.api.password.d.s = (char*)""; + conf->webserver.api.password.c = validate_stub; // Only type-based checking conf->webserver.api.totp_secret.k = "webserver.api.totp_secret"; conf->webserver.api.totp_secret.h = "Pi-hole 2FA TOTP secret. When set to something different than \"""\", 2FA authentication will be enforced for the API and the web interface. This setting is write-only, you can not read the secret back."; @@ -928,6 +1006,7 @@ void initConfig(struct config *conf) conf->webserver.api.totp_secret.t = CONF_STRING; conf->webserver.api.totp_secret.f = FLAG_WRITE_ONLY | FLAG_INVALIDATE_SESSIONS; conf->webserver.api.totp_secret.d.s = (char*)""; + conf->webserver.api.totp_secret.c = validate_stub; // Only type-based checking conf->webserver.api.app_pwhash.k = "webserver.api.app_pwhash"; conf->webserver.api.app_pwhash.h = "Pi-hole application password.\n After you turn on two-factor (2FA) verification and set up an Authenticator app, you may run into issues if you use apps or other services that don't support two-step verification. In this case, you can create and use an app password to sign in. An app password is a long, randomly generated password that can be used instead of your regular password + TOTP token when signing in to the API. The app password can be generated through the API and will be shown only once. You can revoke the app password at any time. If you revoke the app password, be sure to generate a new one and update your app with the new password."; @@ -935,39 +1014,46 @@ void initConfig(struct config *conf) conf->webserver.api.app_pwhash.t = CONF_STRING; conf->webserver.api.app_pwhash.f = FLAG_INVALIDATE_SESSIONS; conf->webserver.api.app_pwhash.d.s = (char*)""; + conf->webserver.api.app_pwhash.c = validate_stub; // Only type-based checking conf->webserver.api.excludeClients.k = "webserver.api.excludeClients"; conf->webserver.api.excludeClients.h = "Array of clients to be excluded from certain API responses (regex):\n - Query Log (/api/queries)\n - Top Clients (/api/stats/top_clients)\n This setting accepts both IP addresses (IPv4 and IPv6) as well as hostnames.\n Note that backslashes \"\\\" need to be escaped, i.e. \"\\\\\" in this setting\n\n Example: [ \"^192\\\\.168\\\\.2\\\\.56$\", \"^fe80::341:[0-9a-f]*$\", \"^localhost$\" ]"; conf->webserver.api.excludeClients.a = cJSON_CreateStringReference("array of regular expressions describing clients"); conf->webserver.api.excludeClients.t = CONF_JSON_STRING_ARRAY; conf->webserver.api.excludeClients.d.json = cJSON_CreateArray(); + conf->webserver.api.excludeClients.c = validate_regex_array; conf->webserver.api.excludeDomains.k = "webserver.api.excludeDomains"; conf->webserver.api.excludeDomains.h = "Array of domains to be excluded from certain API responses (regex):\n - Query Log (/api/queries)\n - Top Clients (/api/stats/top_domains)\n Note that backslashes \"\\\" need to be escaped, i.e. \"\\\\\" in this setting\n\n Example: [ \"(^|\\\\.)\\\\.google\\\\.de$\", \"\\\\.pi-hole\\\\.net$\" ]"; conf->webserver.api.excludeDomains.a = cJSON_CreateStringReference("array of regular expressions describing domains"); conf->webserver.api.excludeDomains.t = CONF_JSON_STRING_ARRAY; conf->webserver.api.excludeDomains.d.json = cJSON_CreateArray(); + conf->webserver.api.excludeDomains.c = validate_regex_array; conf->webserver.api.maxHistory.k = "webserver.api.maxHistory"; conf->webserver.api.maxHistory.h = "How much history should be imported from the database and returned by the API [seconds]? (max 24*60*60 = 86400)"; conf->webserver.api.maxHistory.t = CONF_UINT; conf->webserver.api.maxHistory.d.ui = MAXLOGAGE*3600; + conf->webserver.api.maxHistory.c = validate_stub; // Only type-based checking conf->webserver.api.maxClients.k = "webserver.api.maxClients"; conf->webserver.api.maxClients.h = "Up to how many clients should be returned in the activity graph endpoint (/api/history/clients)?\n This setting can be overwritten at run-time using the parameter N"; conf->webserver.api.maxClients.t = CONF_UINT16; conf->webserver.api.maxClients.d.u16 = 10; + conf->webserver.api.maxClients.c = validate_stub; // Only type-based checking conf->webserver.api.allow_destructive.k = "webserver.api.allow_destructive"; conf->webserver.api.allow_destructive.h = "Allow destructive API calls (e.g. deleting all queries, powering off the system, ...)"; conf->webserver.api.allow_destructive.t = CONF_BOOL; conf->webserver.api.allow_destructive.d.b = true; + conf->webserver.api.allow_destructive.c = validate_stub; // Only type-based checking // sub-struct webserver.api.temp conf->webserver.api.temp.limit.k = "webserver.api.temp.limit"; conf->webserver.api.temp.limit.h = "Which upper temperature limit should be used by Pi-hole? Temperatures above this limit will be shown as \"hot\". The number specified here is in the unit defined below"; conf->webserver.api.temp.limit.t = CONF_DOUBLE; conf->webserver.api.temp.limit.d.d = 60.0; // °C + conf->webserver.api.temp.limit.c = validate_stub; // Only type-based checking conf->webserver.api.temp.unit.k = "webserver.api.temp.unit"; conf->webserver.api.temp.unit.h = "Which temperature unit should be used for temperatures processed by FTL?"; @@ -982,7 +1068,7 @@ void initConfig(struct config *conf) } conf->webserver.api.temp.unit.t = CONF_ENUM_TEMP_UNIT; conf->webserver.api.temp.unit.d.temp_unit = TEMP_UNIT_C; - + conf->webserver.api.temp.unit.c = validate_stub; // Only type-based checking // struct files conf->files.pid.k = "files.pid"; @@ -991,6 +1077,7 @@ void initConfig(struct config *conf) conf->files.pid.t = CONF_STRING; conf->files.pid.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL; conf->files.pid.d.s = (char*)"/run/pihole-FTL.pid"; + conf->files.pid.c = validate_filepath; conf->files.database.k = "files.database"; conf->files.database.h = "The location of FTL's long-term database"; @@ -998,6 +1085,7 @@ void initConfig(struct config *conf) conf->files.database.t = CONF_STRING; conf->files.database.f = FLAG_ADVANCED_SETTING; conf->files.database.d.s = (char*)"/etc/pihole/pihole-FTL.db"; + conf->files.database.c = validate_filepath; conf->files.gravity.k = "files.gravity"; conf->files.gravity.h = "The location of Pi-hole's gravity database"; @@ -1005,6 +1093,7 @@ void initConfig(struct config *conf) conf->files.gravity.t = CONF_STRING; conf->files.gravity.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL; conf->files.gravity.d.s = (char*)"/etc/pihole/gravity.db"; + conf->files.gravity.c = validate_filepath; conf->files.gravity_tmp.k = "files.gravity_tmp"; conf->files.gravity_tmp.h = "A temporary directory where Pi-hole can store files during gravity updates. This directory must be writable by the user running gravity (typically pihole)."; @@ -1012,6 +1101,7 @@ void initConfig(struct config *conf) conf->files.gravity_tmp.t = CONF_STRING; conf->files.gravity_tmp.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL; conf->files.gravity_tmp.d.s = (char*)"/tmp"; + conf->files.gravity_tmp.c = validate_stub; // Only type-based checking conf->files.macvendor.k = "files.macvendor"; conf->files.macvendor.h = "The database containing MAC -> Vendor information for the network table"; @@ -1019,6 +1109,7 @@ void initConfig(struct config *conf) conf->files.macvendor.t = CONF_STRING; conf->files.macvendor.f = FLAG_ADVANCED_SETTING; conf->files.macvendor.d.s = (char*)"/etc/pihole/macvendor.db"; + conf->files.macvendor.c = validate_filepath; conf->files.setupVars.k = "files.setupVars"; conf->files.setupVars.h = "The old config file of Pi-hole used before v6.0"; @@ -1026,6 +1117,7 @@ void initConfig(struct config *conf) conf->files.setupVars.t = CONF_STRING; conf->files.setupVars.f = FLAG_ADVANCED_SETTING; conf->files.setupVars.d.s = (char*)"/etc/pihole/setupVars.conf"; + conf->files.setupVars.c = validate_filepath; conf->files.pcap.k = "files.pcap"; conf->files.pcap.h = "An optional file containing a pcap capture of the network traffic. This file is used for debugging purposes only. If you don't know what this is, you don't need it.\n Setting this to an empty string disables pcap recording. The file must be writable by the user running FTL (typically pihole). Failure to write to this file will prevent the DNS resolver from starting. The file is appended to if it already exists."; @@ -1033,6 +1125,10 @@ void initConfig(struct config *conf) conf->files.pcap.t = CONF_STRING; conf->files.pcap.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL; conf->files.pcap.d.s = (char*)""; + conf->files.pcap.c = validate_filepath_empty; + + // sub-struct files.log + // conf->files.log.ftl is set in a separate function conf->files.log.webserver.k = "files.log.webserver"; conf->files.log.webserver.h = "The log file used by the webserver"; @@ -1040,9 +1136,7 @@ void initConfig(struct config *conf) conf->files.log.webserver.t = CONF_STRING; conf->files.log.webserver.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL; conf->files.log.webserver.d.s = (char*)"/var/log/pihole/webserver.log"; - - // sub-struct files.log - // conf->files.log.ftl is set in a separate function + conf->files.log.webserver.c = validate_filepath; conf->files.log.dnsmasq.k = "files.log.dnsmasq"; conf->files.log.dnsmasq.h = "The log file used by the embedded dnsmasq DNS server"; @@ -1050,6 +1144,7 @@ void initConfig(struct config *conf) conf->files.log.dnsmasq.t = CONF_STRING; conf->files.log.dnsmasq.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL; conf->files.log.dnsmasq.d.s = (char*)"/var/log/pihole/pihole.log"; + conf->files.log.dnsmasq.c = validate_filepath_dash; // struct misc @@ -1067,29 +1162,34 @@ void initConfig(struct config *conf) } conf->misc.privacylevel.t = CONF_ENUM_PRIVACY_LEVEL; conf->misc.privacylevel.d.privacy_level = PRIVACY_SHOW_ALL; + conf->misc.privacylevel.c = validate_stub; // Only type-based checking conf->misc.delay_startup.k = "misc.delay_startup"; conf->misc.delay_startup.h = "During startup, in some configurations, network interfaces appear only late during system startup and are not ready when FTL tries to bind to them. Therefore, you may want FTL to wait a given amount of time before trying to start the DNS revolver. This setting takes any integer value between 0 and 300 seconds. To prevent delayed startup while the system is already running and FTL is restarted, the delay only takes place within the first 180 seconds (hard-coded) after booting."; conf->misc.delay_startup.t = CONF_UINT; conf->misc.delay_startup.d.ui = 0; + conf->misc.delay_startup.c = validate_stub; // Only type-based checking conf->misc.nice.k = "misc.nice"; conf->misc.nice.h = "Set niceness of pihole-FTL. Defaults to -10 and can be disabled altogether by setting a value of -999. The nice value is an attribute that can be used to influence the CPU scheduler to favor or disfavor a process in scheduling decisions. The range of the nice value varies across UNIX systems. On modern Linux, the range is -20 (high priority = not very nice to other processes) to +19 (low priority)."; conf->misc.nice.t = CONF_INT; conf->misc.nice.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL; conf->misc.nice.d.i = -10; + conf->misc.nice.c = validate_stub; // Only type-based checking conf->misc.addr2line.k = "misc.addr2line"; conf->misc.addr2line.h = "Should FTL translate its own stack addresses into code lines during the bug backtrace? This improves the analysis of crashed significantly. It is recommended to leave the option enabled. This option should only be disabled when addr2line is known to not be working correctly on the machine because, in this case, the malfunctioning addr2line can prevent from generating any backtrace at all."; conf->misc.addr2line.t = CONF_BOOL; conf->misc.addr2line.f = FLAG_ADVANCED_SETTING; conf->misc.addr2line.d.b = true; + conf->misc.addr2line.c = validate_stub; // Only type-based checking conf->misc.etc_dnsmasq_d.k = "misc.etc_dnsmasq_d"; conf->misc.etc_dnsmasq_d.h = "Should FTL load additional dnsmasq configuration files from /etc/dnsmasq.d/?"; conf->misc.etc_dnsmasq_d.t = CONF_BOOL; conf->misc.etc_dnsmasq_d.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING; conf->misc.etc_dnsmasq_d.d.b = false; + conf->misc.etc_dnsmasq_d.c = validate_stub; // Only type-based checking conf->misc.dnsmasq_lines.k = "misc.dnsmasq_lines"; conf->misc.dnsmasq_lines.h = "Additional lines to inject into the generated dnsmasq configuration.\n Warning: This is an advanced setting and should only be used with care. Incorrectly formatted or duplicated lines as well as lines conflicting with the automatic configuration of Pi-hole can break the embedded dnsmasq and will stop DNS resolution from working.\n Use this option with extra care."; @@ -1097,28 +1197,33 @@ void initConfig(struct config *conf) conf->misc.dnsmasq_lines.t = CONF_JSON_STRING_ARRAY; conf->misc.dnsmasq_lines.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL; conf->misc.dnsmasq_lines.d.json = cJSON_CreateArray(); + conf->misc.dnsmasq_lines.c = validate_stub; // Type-based checking + dnsmasq syntax checking conf->misc.extraLogging.k = "misc.extraLogging"; conf->misc.extraLogging.h = "Log additional information about queries and replies to pihole.log\n When this setting is enabled, the log has extra information at the start of each line. This consists of a serial number which ties together the log lines associated with an individual query, and the IP address of the requestor. This setting is only effective if dns.queryLogging is enabled, too. This option is only useful for debugging and is not recommended for normal use."; conf->misc.extraLogging.t = CONF_BOOL; conf->misc.extraLogging.f = FLAG_RESTART_FTL; conf->misc.extraLogging.d.b = false; + conf->misc.extraLogging.c = validate_stub; // Only type-based checking // sub-struct misc.check conf->misc.check.load.k = "misc.check.load"; conf->misc.check.load.h = "Pi-hole is very lightweight on resources. Nevertheless, this does not mean that you should run Pi-hole on a server that is otherwise extremely busy as queuing on the system can lead to unnecessary delays in DNS operation as the system becomes less and less usable as the system load increases because all resources are permanently in use. To account for this, FTL regularly checks the system load. To bring this to your attention, FTL warns about excessive load when the 15 minute system load average exceeds the number of cores.\n This check can be disabled with this setting."; conf->misc.check.load.t = CONF_BOOL; conf->misc.check.load.d.b = true; + conf->misc.check.load.c = validate_stub; // Only type-based checking conf->misc.check.disk.k = "misc.check.disk"; conf->misc.check.disk.h = "FTL stores its long-term history in a database file on disk. Furthermore, FTL stores log files. By default, FTL warns if usage of the disk holding any crucial file exceeds 90%. You can set any integer limit between 0 to 100 (interpreted as percentages) where 0 means that checking of disk usage is disabled."; conf->misc.check.disk.t = CONF_UINT; conf->misc.check.disk.d.ui = 90; + conf->misc.check.disk.c = validate_stub; // Only type-based checking conf->misc.check.shmem.k = "misc.check.shmem"; conf->misc.check.shmem.h = "FTL stores history in shared memory to allow inter-process communication with forked dedicated TCP workers. If FTL runs out of memory, it cannot continue to work as queries cannot be analyzed any further. Hence, FTL checks if enough shared memory is available on your system and warns you if this is not the case.\n By default, FTL warns if the shared-memory usage exceeds 90%. You can set any integer limit between 0 to 100 (interpreted as percentages) where 0 means that checking of shared-memory usage is disabled."; conf->misc.check.shmem.t = CONF_UINT; conf->misc.check.shmem.d.ui = 90; + conf->misc.check.shmem.c = validate_stub; // Only type-based checking // struct debug @@ -1127,168 +1232,196 @@ void initConfig(struct config *conf) conf->debug.database.t = CONF_BOOL; conf->debug.database.f = FLAG_ADVANCED_SETTING; conf->debug.database.d.b = false; + conf->debug.database.c = validate_stub; // Only type-based checking conf->debug.networking.k = "debug.networking"; conf->debug.networking.h = "Prints a list of the detected interfaces on the startup of pihole-FTL. Also, prints whether these interfaces are IPv4 or IPv6 interfaces."; conf->debug.networking.t = CONF_BOOL; conf->debug.networking.f = FLAG_ADVANCED_SETTING; conf->debug.networking.d.b = false; + conf->debug.networking.c = validate_stub; // Only type-based checking conf->debug.locks.k = "debug.locks"; conf->debug.locks.h = "Print information about shared memory locks. Messages will be generated when waiting, obtaining, and releasing a lock."; conf->debug.locks.t = CONF_BOOL; conf->debug.locks.f = FLAG_ADVANCED_SETTING; conf->debug.locks.d.b = false; + conf->debug.locks.c = validate_stub; // Only type-based checking conf->debug.queries.k = "debug.queries"; conf->debug.queries.h = "Print extensive query information (domains, types, replies, etc.). This has always been part of the legacy debug mode of pihole-FTL."; conf->debug.queries.t = CONF_BOOL; conf->debug.queries.f = FLAG_ADVANCED_SETTING; conf->debug.queries.d.b = false; + conf->debug.queries.c = validate_stub; // Only type-based checking conf->debug.flags.k = "debug.flags"; conf->debug.flags.h = "Print flags of queries received by the DNS hooks. Only effective when DEBUG_QUERIES is enabled as well."; conf->debug.flags.t = CONF_BOOL; conf->debug.flags.f = FLAG_ADVANCED_SETTING; conf->debug.flags.d.b = false; + conf->debug.flags.c = validate_stub; // Only type-based checking conf->debug.shmem.k = "debug.shmem"; conf->debug.shmem.h = "Print information about shared memory buffers. Messages are either about creating or enlarging shmem objects or string injections."; conf->debug.shmem.t = CONF_BOOL; conf->debug.shmem.f = FLAG_ADVANCED_SETTING; conf->debug.shmem.d.b = false; + conf->debug.shmem.c = validate_stub; // Only type-based checking conf->debug.gc.k = "debug.gc"; conf->debug.gc.h = "Print information about garbage collection (GC): What is to be removed, how many have been removed and how long did GC take."; conf->debug.gc.t = CONF_BOOL; conf->debug.gc.f = FLAG_ADVANCED_SETTING; conf->debug.gc.d.b = false; + conf->debug.gc.c = validate_stub; // Only type-based checking conf->debug.arp.k = "debug.arp"; conf->debug.arp.h = "Print information about ARP table processing: How long did parsing take, whether read MAC addresses are valid, and if the macvendor.db file exists."; conf->debug.arp.t = CONF_BOOL; conf->debug.arp.f = FLAG_ADVANCED_SETTING; conf->debug.arp.d.b = false; + conf->debug.arp.c = validate_stub; // Only type-based checking conf->debug.regex.k = "debug.regex"; conf->debug.regex.h = "Controls if FTLDNS should print extended details about regex matching into FTL.log."; conf->debug.regex.t = CONF_BOOL; conf->debug.regex.f = FLAG_ADVANCED_SETTING; conf->debug.regex.d.b = false; + conf->debug.regex.c = validate_stub; // Only type-based checking conf->debug.api.k = "debug.api"; 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; + conf->debug.api.c = validate_stub; // Only type-based checking conf->debug.tls.k = "debug.tls"; conf->debug.tls.h = "Print extra debugging information about TLS connections. This includes the TLS version, the cipher suite, the certificate chain and much more. This very verbose output should only be used when debugging specific TLS issues and can be helpful, e.g., when a client cannot connect due to an obscure TLS error as modern browsers do not provide much information about the underlying TLS connection and most often give only very generic error messages without much/any underlying technical information."; conf->debug.tls.t = CONF_BOOL; conf->debug.tls.f = FLAG_ADVANCED_SETTING; conf->debug.tls.d.b = false; + conf->debug.tls.c = validate_stub; // Only type-based checking conf->debug.overtime.k = "debug.overtime"; conf->debug.overtime.h = "Print information about overTime memory operations, such as initializing or moving overTime slots."; conf->debug.overtime.t = CONF_BOOL; conf->debug.overtime.f = FLAG_ADVANCED_SETTING; conf->debug.overtime.d.b = false; + conf->debug.overtime.c = validate_stub; // Only type-based checking conf->debug.status.k = "debug.status"; conf->debug.status.h = "Print information about status changes for individual queries. This can be useful to identify unexpected unknown queries."; conf->debug.status.t = CONF_BOOL; conf->debug.status.f = FLAG_ADVANCED_SETTING; conf->debug.status.d.b = false; + conf->debug.status.c = validate_stub; // Only type-based checking conf->debug.caps.k = "debug.caps"; conf->debug.caps.h = "Print information about capabilities granted to the pihole-FTL process. The current capabilities are printed on receipt of SIGHUP, i.e., the current set of capabilities can be queried without restarting pihole-FTL (by setting DEBUG_CAPS=true and thereafter sending killall -HUP pihole-FTL)."; conf->debug.caps.t = CONF_BOOL; conf->debug.caps.f = FLAG_ADVANCED_SETTING; conf->debug.caps.d.b = false; + conf->debug.caps.c = validate_stub; // Only type-based checking conf->debug.dnssec.k = "debug.dnssec"; conf->debug.dnssec.h = "Print information about DNSSEC activity"; conf->debug.dnssec.t = CONF_BOOL; conf->debug.dnssec.f = FLAG_ADVANCED_SETTING; conf->debug.dnssec.d.b = false; + conf->debug.dnssec.c = validate_stub; // Only type-based checking conf->debug.vectors.k = "debug.vectors"; conf->debug.vectors.h = "FTL uses dynamically allocated vectors for various tasks. This config option enables extensive debugging information such as information about allocation, referencing, deletion, and appending."; conf->debug.vectors.t = CONF_BOOL; conf->debug.vectors.f = FLAG_ADVANCED_SETTING; conf->debug.vectors.d.b = false; + conf->debug.vectors.c = validate_stub; // Only type-based checking conf->debug.resolver.k = "debug.resolver"; conf->debug.resolver.h = "Extensive information about hostname resolution like which DNS servers are used in the first and second hostname resolving tries (only affecting internally generated PTR queries)."; conf->debug.resolver.t = CONF_BOOL; conf->debug.resolver.f = FLAG_ADVANCED_SETTING; conf->debug.resolver.d.b = false; + conf->debug.resolver.c = validate_stub; // Only type-based checking conf->debug.edns0.k = "debug.edns0"; conf->debug.edns0.h = "Print debugging information about received EDNS(0) data."; conf->debug.edns0.t = CONF_BOOL; conf->debug.edns0.f = FLAG_ADVANCED_SETTING; conf->debug.edns0.d.b = false; + conf->debug.edns0.c = validate_stub; // Only type-based checking conf->debug.clients.k = "debug.clients"; conf->debug.clients.h = "Log various important client events such as change of interface (e.g., client switching from WiFi to wired or VPN connection), as well as extensive reporting about how clients were assigned to its groups."; conf->debug.clients.t = CONF_BOOL; conf->debug.clients.f = FLAG_ADVANCED_SETTING; conf->debug.clients.d.b = false; + conf->debug.clients.c = validate_stub; // Only type-based checking conf->debug.aliasclients.k = "debug.aliasclients"; conf->debug.aliasclients.h = "Log information related to alias-client processing."; conf->debug.aliasclients.t = CONF_BOOL; conf->debug.aliasclients.f = FLAG_ADVANCED_SETTING; conf->debug.aliasclients.d.b = false; + conf->debug.aliasclients.c = validate_stub; // Only type-based checking conf->debug.events.k = "debug.events"; conf->debug.events.h = "Log information regarding FTL's embedded event handling queue."; conf->debug.events.t = CONF_BOOL; conf->debug.events.f = FLAG_ADVANCED_SETTING; conf->debug.events.d.b = false; + conf->debug.events.c = validate_stub; // Only type-based checking conf->debug.helper.k = "debug.helper"; conf->debug.helper.h = "Log information about script helpers, e.g., due to dhcp-script."; conf->debug.helper.t = CONF_BOOL; conf->debug.helper.f = FLAG_ADVANCED_SETTING; conf->debug.helper.d.b = false; + conf->debug.helper.c = validate_stub; // Only type-based checking conf->debug.config.k = "debug.config"; conf->debug.config.h = "Print config parsing details"; conf->debug.config.t = CONF_BOOL; conf->debug.config.f = FLAG_ADVANCED_SETTING; conf->debug.config.d.b = false; + conf->debug.config.c = validate_stub; // Only type-based checking conf->debug.inotify.k = "debug.inotify"; conf->debug.inotify.h = "Debug monitoring of /etc/pihole filesystem events"; conf->debug.inotify.t = CONF_BOOL; conf->debug.inotify.f = FLAG_ADVANCED_SETTING; conf->debug.inotify.d.b = false; + conf->debug.inotify.c = validate_stub; // Only type-based checking conf->debug.webserver.k = "debug.webserver"; conf->debug.webserver.h = "Debug monitoring of the webserver (CivetWeb) events"; conf->debug.webserver.t = CONF_BOOL; conf->debug.webserver.f = FLAG_ADVANCED_SETTING; conf->debug.webserver.d.b = false; + conf->debug.webserver.c = validate_stub; // Only type-based checking conf->debug.extra.k = "debug.extra"; conf->debug.extra.h = "Temporary flag that may print additional information. This debug flag is meant to be used whenever needed for temporary investigations. The logged content may change without further notice at any time."; conf->debug.extra.t = CONF_BOOL; conf->debug.extra.f = FLAG_ADVANCED_SETTING; conf->debug.extra.d.b = false; + conf->debug.extra.c = validate_stub; // Only type-based checking conf->debug.reserved.k = "debug.reserved"; conf->debug.reserved.h = "Reserved debug flag"; conf->debug.reserved.t = CONF_BOOL; conf->debug.reserved.f = FLAG_ADVANCED_SETTING; conf->debug.reserved.d.b = false; + conf->debug.reserved.c = validate_stub; // Only type-based checking conf->debug.all.k = "debug.all"; conf->debug.all.h = "Set all debug flags at once. This is a convenience option to enable all debug flags at once. Note that this option is not persistent, setting it to true will enable all *remaining* debug flags but unsetting it will disable *all* debug flags."; conf->debug.all.t = CONF_ALL_DEBUG_BOOL; conf->debug.all.f = FLAG_ADVANCED_SETTING; conf->debug.all.d.b = false; + conf->debug.all.c = validate_stub; // Only type-based checking // Post-processing: // Initialize and verify config data @@ -1338,6 +1471,13 @@ void initConfig(struct config *conf) log_err("Config option %s has NULL default JSON array!", conf_item->k); continue; } + + // Verify that all config options have a validator function + if(conf_item->c == NULL) + { + log_err("Config option %s has no validator function!", conf_item->k); + continue; + } } } @@ -1486,6 +1626,7 @@ bool getLogFilePath(void) config.files.log.ftl.f = FLAG_ADVANCED_SETTING; config.files.log.ftl.d.s = (char*)"/var/log/pihole/FTL.log"; config.files.log.ftl.v.s = config.files.log.ftl.d.s; + config.files.log.ftl.c = validate_filepath; // Check if the config file contains a different path if(!getLogFilePathTOML()) diff --git a/src/config/config.h b/src/config/config.h index 1a3397517..9d0cbfb6c 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -38,6 +38,9 @@ // characters will be replaced by their UTF-8 escape sequences (UCS-2) #define TOML_UTF8 +// Size of the buffer used to report possible errors during config validation +#define VALIDATOR_ERRBUF_LEN 256 + // Location of the legacy (pre-v6.0) config file #define GLOBALCONFFILE_LEGACY "/etc/pihole/pihole-FTL.conf" @@ -109,6 +112,7 @@ struct conf_item { uint8_t f; // additional Flags union conf_value v; // current Value union conf_value d; // Default value + bool (*c)(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]); // Function pointer to validate the value }; struct enum_options { diff --git a/src/config/dnsmasq_config.c b/src/config/dnsmasq_config.c index e622d964d..efe9d1bde 100644 --- a/src/config/dnsmasq_config.c +++ b/src/config/dnsmasq_config.c @@ -473,7 +473,7 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_ if(active == NULL || cidr == NULL || target == NULL || domain == NULL) { - log_err("Invalid reverse server string: %s", revServer->valuestring); + log_err("Skipped invalid dns.revServers[%u]: %s", i, revServer->valuestring); free(copy); continue; } @@ -697,6 +697,14 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_ if(test_config && !test_dnsmasq_config(errbuf)) { log_warn("New dnsmasq configuration is not valid (%s), config remains unchanged", errbuf); + + // Remove temporary config file + if(remove(DNSMASQ_TEMP_CONF) != 0) + { + log_err("Cannot remove temporary dnsmasq config file: %s", strerror(errno)); + return false; + } + return false; } @@ -707,8 +715,14 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_ if(rename(DNSMASQ_TEMP_CONF, DNSMASQ_PH_CONFIG) != 0) { log_err("Cannot install dnsmasq config file: %s", strerror(errno)); + + // Remove temporary config file + if(remove(DNSMASQ_TEMP_CONF) != 0) + log_err("Cannot remove temporary dnsmasq config file: %s", strerror(errno)); + return false; } + log_debug(DEBUG_CONFIG, "Config file written to "DNSMASQ_PH_CONFIG); } else diff --git a/src/config/env.c b/src/config/env.c index d332a3483..5ca440d61 100644 --- a/src/config/env.c +++ b/src/config/env.c @@ -48,6 +48,13 @@ void getEnvVars(void) char *key = strtok(*env, "="); char *value = strtok(NULL, "="); + // Log warning if value is missing + if(value == NULL) + { + log_warn("Environment variable %s has no value, substituting with empty string", key); + value = (char*)""; + } + // Add to list struct env_item *new_item = calloc(1, sizeof(struct env_item)); new_item->used = false; diff --git a/src/config/validator.c b/src/config/validator.c new file mode 100644 index 000000000..543057d76 --- /dev/null +++ b/src/config/validator.c @@ -0,0 +1,556 @@ +/* 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 +* Config validation routines +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ + +#include "validator.h" +#include "log.h" +// valid_domain() +#include "tools/gravity-parseList.h" +// regex +#include "regex_r.h" + +// Stub validator for config types that need to dedicated validation as they can +// be tested by their type only (e.g., integers, strings, booleans, enums, etc.) +bool __attribute__((const)) validate_stub(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]) +{ + return true; +} + +// Validate the dns.hosts array +// Each entry needs to be a string in form "IP HOSTNAME" +bool validate_dns_hosts(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]) +{ + if(!cJSON_IsArray(val->json)) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not an array", key); + return false; + } + + for(int i = 0; i < cJSON_GetArraySize(val->json); i++) + { + // Get array item + cJSON *item = cJSON_GetArrayItem(val->json, i); + + // Check if it's a string + if(!cJSON_IsString(item)) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not a string", + key, i); + return false; + } + + // Check if it's in the form "IP HOSTNAME" + char *str = strdup(item->valuestring); + char *tmp = str; + char *ip = strsep(&tmp, " "); + + if(!ip || !*ip) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not an IP address (\"%s\")", + key, i, item->valuestring); + free(str); + return false; + } + + // Check if IP is valid + struct in_addr addr; + struct in6_addr addr6; + if(inet_pton(AF_INET, ip, &addr) != 1 && inet_pton(AF_INET6, ip, &addr6) != 1) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: neither a valid IPv4 nor IPv6 address (\"%s\")", + key, i, ip); + free(str); + return false; + } + + // Check if all hostnames are valid + // The HOSTS format allows any number of space-separated + // hostnames to come after the IP address + unsigned int hosts = 0; + char *host = NULL; + while((host = strsep(&tmp, " ")) != NULL) + { + if(!valid_domain(host, strlen(host), false)) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: invalid hostname (\"%s\")", + key, i, host); + free(str); + return false; + } + hosts++; + } + + // Check if there is at least one hostname in this record + if(hosts < 1) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: entry does not have at least one hostname (\"%s\")", + key, i, item->valuestring); + free(str); + return false; + } + + free(str); + } + + return true; +} + +// Validate the dns.cnames array +// Each entry needs to be a string in form ",[,][,]" +bool validate_dns_cnames(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]) +{ + if(!cJSON_IsArray(val->json)) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not an array", key); + return false; + } + + for(int i = 0; i < cJSON_GetArraySize(val->json); i++) + { + // Get array item + cJSON *item = cJSON_GetArrayItem(val->json, i); + + // Check if it's a string + if(!cJSON_IsString(item)) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not a string", key, i); + return false; + } + + // Count the number of elements in the string + unsigned int elements = 1; + for(unsigned int j = 0; j < strlen(item->valuestring); j++) + if(item->valuestring[j] == ',') + elements++; + + // Check if it's in the form ",[,][,]" + // is optional and may be repeated + char *str = strdup(item->valuestring); + char *tmp = str, *s = NULL; + unsigned int j = 0; + + while((s = strsep(&tmp, ",")) != NULL) + { + // Check if it's a valid cname + if(strlen(s) == 0) + { + // Contains an empty string + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: contains an empty string at position %u", key, i, j); + free(str); + return false; + } + + j++; + } + free(str); + + // Check if there are at least one cname and a target + if(j < 2) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not a valid CNAME definition (too few elements)", key, i); + return false; + } + } + + return true; +} + +// Validate IPs in CIDR notation +bool validate_cidr(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]) +{ + // Check if it's a valid CIDR + char *str = strdup(val->s); + char *tmp = str; + char *ip = strsep(&tmp, "/"); + char *cidr = strsep(&tmp, "/"); + char *tail = strsep(&tmp, "/"); + + // Check if there is an IP and no tail + if(!ip || !*ip || tail) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid IP in CIDR notation (\"%s\")", key, val->s); + free(str); + return false; + } + + // Check if IP is valid + struct in_addr addr; + struct in6_addr addr6; + int ip4 = 0, ip6 = 0; + if((ip4 = inet_pton(AF_INET, ip, &addr) != 1) && (ip6 = inet_pton(AF_INET6, ip, &addr6)) != 1) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid IPv4 nor IPv6 address (\"%s\")", key, ip); + free(str); + return false; + } + + // Check if CIDR is valid + if(cidr) + { + if(strlen(cidr) == 0) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: empty CIDR value", key); + free(str); + return false; + } + int cidr_int = atoi(cidr); + if(ip4 && (cidr_int < 0 || cidr_int > 32)) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid IPv4 CIDR (\"%s\")", key, cidr); + free(str); + return false; + } + else if(ip6 && (cidr_int < 0 || cidr_int > 128)) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid IPv6 CIDR (\"%s\")", key, cidr); + free(str); + return false; + } + } + + free(str); + return true; +} + +// Validate IP address optionally followed by a port (separator is "#") +bool validate_ip_port(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]) +{ + // Check if it's a valid IP + char *str = strdup(val->s); + char *tmp = str; + char *ip = strsep(&tmp, "#"); + char *port = strsep(&tmp, "#"); + char *tail = strsep(&tmp, "#"); + + // Check if there is an IP and no tail + if(!ip || !*ip || tail) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid IP (\"%s\")", key, val->s); + free(str); + return false; + } + + // Check if IP is valid + struct in_addr addr; + struct in6_addr addr6; + int ip4 = 0, ip6 = 0; + if((ip4 = inet_pton(AF_INET, ip, &addr) != 1) && (ip6 = inet_pton(AF_INET6, ip, &addr6)) != 1) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid IPv4 nor IPv6 address (\"%s\")", key, ip); + free(str); + return false; + } + + // Check if port is valid + if(port) + { + if(strlen(port) == 0) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: empty port value", key); + free(str); + return false; + } + int port_int = atoi(port); + if(port_int < 0 || port_int > 65535) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid port (\"%s\")", key, port); + free(str); + return false; + } + } + + free(str); + return true; +} + +// Validate domain +bool validate_domain(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]) +{ + // Check if domain is valid + if(!valid_domain(val->s, strlen(val->s), false)) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid domain (\"%s\")", key, val->s); + return false; + } + + return true; +} + +// Validate file path +bool validate_filepath(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]) +{ + // Check if the path contains only valid characters + for(unsigned int i = 0; i < strlen(val->s); i++) + { + if(!isalnum(val->s[i]) && val->s[i] != '/' && val->s[i] != '.' && val->s[i] != '-' && val->s[i] != '_' && val->s[i] != ' ') + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid file path (\"%s\")", key, val->s); + return false; + } + } + + return true; +} + +// Validate file path (empty allowed) +bool validate_filepath_empty(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]) +{ + // Empty paths are allowed, e.g., to disable a feature like PCAP + if(strlen(val->s) == 0) + return true; + + // else: + return validate_filepath(val, key, err); +} + +// Validate file path (dash allowed), used by files.log.dnsmasq +bool validate_filepath_dash(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]) +{ + // Dash is allowed, this enabled printing to stderr + if(strlen(val->s) == 1 && val->s[0] == '-') + return true; + + // else: + return validate_filepath(val, key, err); +} + +// Validate a single regular expression +static bool validate_regex(const char *regex, char err[VALIDATOR_ERRBUF_LEN]) +{ + // Compile regex + regex_t preg = { 0 }; + const int ret = regcomp(&preg, regex, REG_EXTENDED); + if(ret != 0) + { + regerror(ret, &preg, err, VALIDATOR_ERRBUF_LEN); + regfree(&preg); + return false; + } + + // Free regex + regfree(&preg); + + return true; +} + +// Validate array of regexes +bool validate_regex_array(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]) +{ + if(val == NULL || !cJSON_IsArray(val->json)) + { + strncat(err, "%s: not an array", VALIDATOR_ERRBUF_LEN); + return false; + } + + for(int i = 0; i < cJSON_GetArraySize(val->json); i++) + { + // Get array item + cJSON *item = cJSON_GetArrayItem(val->json, i); + + // Check if it's a string + if(!cJSON_IsString(item)) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not a string", + key, i); + return false; + } + + // Check if it's a valid regex + char errbuf[VALIDATOR_ERRBUF_LEN] = { 0 }; + if(!validate_regex(item->valuestring, errbuf)) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not a valid regex (\"%s\"): %s", + key, i, item->valuestring, errbuf); + return false; + } + } + + return true; +} + +// Validate dns.revServers array +// Each entry has to be of form ",[/],[#]," +bool validate_dns_revServers(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]) +{ + // Check if it's an array + if(!cJSON_IsArray(val->json)) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not an array", key); + return false; + } + + // Iterate over all array items + for(int i = 0; i < cJSON_GetArraySize(val->json); i++) + { + // Get array item + cJSON *item = cJSON_GetArrayItem(val->json, i); + + // Check if it's a string + if(!cJSON_IsString(item)) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not a string", key, i); + return false; + } + + // Count the number of elements in the string + unsigned int elements = 1; + for(unsigned int j = 0; j < strlen(item->valuestring); j++) + if(item->valuestring[j] == ',') + elements++; + + // Check if it's in the form ",[/],[#]," + // Mandatory elements are: , , , and + // Optional elements are: [/] and [#] + char *str = strdup(item->valuestring); + char *tmp = str, *s = NULL; + unsigned int e = 0; + + while((s = strsep(&tmp, ",")) != NULL) + { + // Check if it's a valid element + if(strlen(s) == 0) + { + // Contains an empty string + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: contains two commas following each other immediately", key, i); + free(str); + return false; + } + // Check if the zeroth element is a boolean + if(e == 0) + { + if(strcmp(s, "true") != 0 && strcmp(s, "false") != 0) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not a boolean (\"%s\")", key, i, s); + free(str); + return false; + } + } + // Check if the first element is an IP address + else if(e == 1) + { + // Extract IP and prefix length (if present) + char *ip = strsep(&s, "/"); + char *prefix = strsep(&s, "/"); + if(strlen(ip) == 0) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: empty", key, i); + free(str); + return false; + } + + // Check if IP is valid + struct in_addr addr = { 0 }; + struct in6_addr addr6 = { 0 }; + const bool ipv4 = inet_pton(AF_INET, ip, &addr) == 1; + const bool ipv6 = inet_pton(AF_INET6, ip, &addr6) == 1; + if(!ipv4 && !ipv6) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: neither a valid IPv4 nor IPv6 address (\"%s\")", key, i, ip); + free(str); + return false; + } + + // Check if prefix length is valid (if present) + if(prefix != NULL) + { + if(strlen(prefix) == 0) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: empty", key, i); + free(str); + return false; + } + const int prefix_int = atoi(prefix); + if(prefix_int < 0 || (ipv4 && prefix_int > 32) || (ipv6 && prefix_int > 128)) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not a valid %sprefix length (\"%s\")", + key, i, ipv4 ? "IPv4 " : ipv6 ? "IPv6 " : "", prefix); + free(str); + return false; + } + } + } + // Check if the second element is a valid server (either an IP address or a domain, optionally with a port) + else if(e == 2) + { + // Extract server and port (if present) + char *server = strsep(&s, "#"); + char *port = strsep(&s, "#"); + if(strlen(server) == 0) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: empty", key, i); + free(str); + return false; + } + + struct in_addr addr = { 0 }; + struct in6_addr addr6 = { 0 }; + const bool server_ipv4 = inet_pton(AF_INET, server, &addr) == 1; + const bool server_ipv6 = inet_pton(AF_INET6, server, &addr6) == 1; + const bool server_domain = valid_domain(server, strlen(server), false); + + // Check if server is valid + if(!server_ipv4 && !server_ipv6 && !server_domain) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: neither a valid domain nor an IPv4 or IPv6 address (\"%s\")", key, i, server); + free(str); + return false; + } + + // Check if port is valid (if present) + if(port != NULL) + { + if(strlen(port) == 0) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: specified server empty", key, i); + free(str); + return false; + } + const int port_int = atoi(port); + if(port_int < 0 || port_int > 65535) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: server not a valid port (\"%s\")", key, i, port); + free(str); + return false; + } + } + } + // Check if the third element is a valid domain + else if(e == 3) + { + if(!valid_domain(s, strlen(s), false)) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not a valid domain (\"%s\")", key, i, s); + free(str); + return false; + } + } + // Check if there are too many elements + else + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: too many elements", key, i); + free(str); + return false; + } + + // Increment element counter + e++; + } + + // Check if there are all required elements + if(e < 4) + { + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: entry does not have all required elements (,[/],[#],)", key, i); + free(str); + return false; + } + } + + // Return success + return true; +} diff --git a/src/config/validator.h b/src/config/validator.h new file mode 100644 index 000000000..f8c7541d1 --- /dev/null +++ b/src/config/validator.h @@ -0,0 +1,29 @@ +/* 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 +* Config validation routines +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ + +#ifndef CONFIG_VALIDATOR_H +#define CONFIG_VALIDATOR_H + +#include "FTL.h" +#include "config/config.h" + +bool validate_stub(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]) __attribute__((const)); +bool validate_dns_hosts(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]); +bool validate_dns_cnames(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]); +bool validate_cidr(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]); +bool validate_ip_port(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]); +bool validate_domain(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]); +bool validate_filepath(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]); +bool validate_filepath_empty(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]); +bool validate_filepath_dash(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]); +bool validate_regex_array(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]); +bool validate_dns_revServers(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]); + +#endif // CONFIG_VALIDATOR_H diff --git a/src/files.c b/src/files.c index 65e932f6c..d34bea093 100644 --- a/src/files.c +++ b/src/files.c @@ -221,7 +221,7 @@ void ls_dir(const char* path) format_memory_size(prefix, (unsigned long long)st.st_size, &formatted); // Log output for this file - log_info("%s %-15s %3.0f%s %s", permissions, usergroup, formatted, prefix, filename); + log_info("%s %-15s %3.0f%s %s", permissions, usergroup, formatted, strlen(prefix) > 0 ? prefix : " ", filename); } log_info("---------------------------------------------------"); diff --git a/test/test_suite.bats b/test/test_suite.bats index 5a8fd9a09..eba5a59c3 100644 --- a/test/test_suite.bats +++ b/test/test_suite.bats @@ -1426,6 +1426,93 @@ [[ ${lines[0]} == '{"session":{"valid":true,"totp":false,"sid":null,"validity":-1},"took":'*'}' ]] } +@test "Config validation working on the CLI (type-based checking)" { + run bash -c './pihole-FTL --config dns.port true' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == 'Config setting dns.port is invalid, allowed options are: unsigned integer (16 bit)' ]] + [[ $status == 2 ]] + + run bash -c './pihole-FTL --config dns.revServers "abc"' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == 'Config setting dns.revServers is invalid: not valid JSON, error before: abc' ]] + [[ $status == 2 ]] +} + +@test "Config validation working on the API (type-based checking)" { + run bash -c 'curl -s -X PATCH http://127.0.0.1/api/config -d "{\"config\":{\"dns\":{\"blockESNI\":15.5}}}"' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == "{\"error\":{\"key\":\"bad_request\",\"message\":\"Config item is invalid\",\"hint\":\"dns.blockESNI: not of type bool\"},\"took\":"*"}" ]] + + run bash -c 'curl -s -X PATCH http://127.0.0.1/api/config -d "{\"config\":{\"dns\":{\"piholePTR\":\"something_else\"}}}"' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == "{\"error\":{\"key\":\"bad_request\",\"message\":\"Config item is invalid\",\"hint\":\"dns.piholePTR: invalid option\"},\"took\":"*"}" ]] +} + +@test "Config validation working on the CLI (validator-based checking)" { + run bash -c './pihole-FTL --config dns.hosts "[\"111.222.333.444 abc\"]"' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == 'Invalid value: dns.hosts[0]: neither a valid IPv4 nor IPv6 address ("111.222.333.444")' ]] + [[ $status == 3 ]] + + run bash -c './pihole-FTL --config dns.hosts "[\"1.1.1.1 cf\",\"8.8.8.8 google\",\"1.2.3.4\"]"' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == 'Invalid value: dns.hosts[2]: entry does not have at least one hostname ("1.2.3.4")' ]] + [[ $status == 3 ]] + + run bash -c './pihole-FTL --config dns.revServers "[\"abc,def,ghi\"]"' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == 'Invalid value: dns.revServers[0]: not a boolean ("abc")' ]] + [[ $status == 3 ]] + + run bash -c './pihole-FTL --config dns.revServers "[\"true,abc,def,ghi\"]"' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == 'Invalid value: dns.revServers[0]: neither a valid IPv4 nor IPv6 address ("abc")' ]] + [[ $status == 3 ]] + + run bash -c './pihole-FTL --config dns.revServers "[\"true,1.2.3.4/55,def,ghi\"]"' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == 'Invalid value: dns.revServers[0]: not a valid IPv4 prefix length ("55")' ]] + [[ $status == 3 ]] + + run bash -c './pihole-FTL --config dns.revServers "[\"true,::1/255,def,ghi\"]"' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == 'Invalid value: dns.revServers[0]: not a valid IPv6 prefix length ("255")' ]] + [[ $status == 3 ]] + + run bash -c './pihole-FTL --config dns.revServers "[\"true,1.1.1.1,def\"]"' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == 'Invalid value: dns.revServers[0]: entry does not have all required elements (,[/],[#],)' ]] + [[ $status == 3 ]] + + run bash -c './pihole-FTL --config dns.revServers "[\"true,1.1.1.1,def,ghi\"]"' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == 'New dnsmasq configuration is not valid ('*'Name does not resolve at line '*' of /etc/pihole/dnsmasq.conf.temp: "rev-server=1.1.1.1,def"), config remains unchanged' ]] + [[ $status == 3 ]] + + run bash -c './pihole-FTL --config webserver.api.excludeClients "[\".*\",\"$$$\",\"[[[\"]"' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == 'Invalid value: webserver.api.excludeClients[2]: not a valid regex ("[[["): Missing '\'']'\' ]] + [[ $status == 3 ]] +} + +@test "Config validation working on the API (validator-based checking)" { + run bash -c 'curl -s -X PATCH http://127.0.0.1/api/config -d "{\"config\":{\"files\":{\"pcap\":\"%gh4b\"}}}"' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == "{\"error\":{\"key\":\"bad_request\",\"message\":\"Config item validation failed\",\"hint\":\"files.pcap: not a valid file path (\\\"%gh4b\\\")\"},\"took\":"*"}" ]] + + run bash -c 'curl -s -X PATCH http://127.0.0.1/api/config -d "{\"config\":{\"dns\":{\"cnameRecords\":[\"a\"]}}}"' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == "{\"error\":{\"key\":\"bad_request\",\"message\":\"Config item validation failed\",\"hint\":\"dns.cnameRecords[0]: not a valid CNAME definition (too few elements)\"},\"took\":"*"}" ]] + + run bash -c 'curl -s -X PATCH http://127.0.0.1/api/config -d "{\"config\":{\"dns\":{\"cnameRecords\":[\"a,b,c\",\"a,b,c,,c\"]}}}"' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == "{\"error\":{\"key\":\"bad_request\",\"message\":\"Config item validation failed\",\"hint\":\"dns.cnameRecords[1]: contains an empty string at position 3\"},\"took\":"*"}" ]] + + run bash -c 'curl -s -X PATCH http://127.0.0.1/api/config -d "{\"config\":{\"dns\":{\"cnameRecords\":[\"a,b,c\",\"a,b,c\",5]}}}"' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == "{\"error\":{\"key\":\"bad_request\",\"message\":\"Config item is invalid\",\"hint\":\"dns.cnameRecords: array has invalid elements\"},\"took\":"*"}" ]] +} + @test "Create, set, and use application password" { run bash -c 'curl -s 127.0.0.1/api/auth/app' printf "%s\n" "${lines[@]}"