diff --git a/patch/civetweb.sh b/patch/civetweb.sh index 3fcc732a0..8896d0bb7 100644 --- a/patch/civetweb.sh +++ b/patch/civetweb.sh @@ -20,4 +20,7 @@ patch -p1 < patch/civetweb/0001-Register-CSRF-token-in-conn-request_info.patch echo "Applying patch 0001-Log-debug-messages-to-webserver.log-when-debug.webse.patch" patch -p1 < patch/civetweb/0001-Log-debug-messages-to-webserver.log-when-debug.webse.patch +echo "Applying patch 0001-Expose-bound-to-addresses-from-CivetWeb-to-the-front.patch" +patch -p1 < patch/civetweb/0001-Expose-bound-to-addresses-from-CivetWeb-to-the-front.patch + echo "ALL PATCHES APPLIED OKAY" diff --git a/patch/civetweb/0001-Expose-bound-to-addresses-from-CivetWeb-to-the-front.patch b/patch/civetweb/0001-Expose-bound-to-addresses-from-CivetWeb-to-the-front.patch new file mode 100644 index 000000000..d5b10dd89 --- /dev/null +++ b/patch/civetweb/0001-Expose-bound-to-addresses-from-CivetWeb-to-the-front.patch @@ -0,0 +1,50 @@ +From 0bee9e9f7942c5b73a715eaeadf5ab2d09a8c74d Mon Sep 17 00:00:00 2001 +From: DL6ER +Date: Sat, 22 Feb 2025 18:33:25 +0100 +Subject: [PATCH] Expose bound-to addresses from CivetWeb to the frontend + +Signed-off-by: DL6ER +--- + src/webserver/civetweb/civetweb.c | 1 + + src/webserver/civetweb/civetweb.h | 6 ++++++ + 2 files changed, 7 insertions(+) + +diff --git a/src/webserver/civetweb/civetweb.c b/src/webserver/civetweb/civetweb.c +index fa908e54..66fcab01 100644 +--- a/src/webserver/civetweb/civetweb.c ++++ b/src/webserver/civetweb/civetweb.c +@@ -3339,6 +3339,7 @@ mg_get_server_ports(const struct mg_context *ctx, + ports[cnt].is_ssl = ctx->listening_sockets[i].is_ssl; + ports[cnt].is_redirect = ctx->listening_sockets[i].ssl_redir; + ports[cnt].is_optional = ctx->listening_sockets[i].is_optional; ++ memcpy(&ports[cnt].addr, &ctx->listening_sockets[i].lsa, sizeof(ports[cnt].addr)); + + if (ctx->listening_sockets[i].lsa.sa.sa_family == AF_INET) { + /* IPv4 */ +diff --git a/src/webserver/civetweb/civetweb.h b/src/webserver/civetweb/civetweb.h +index eee958b4..6dcfa457 100644 +--- a/src/webserver/civetweb/civetweb.h ++++ b/src/webserver/civetweb/civetweb.h +@@ -23,6 +23,8 @@ + #ifndef CIVETWEB_HEADER_INCLUDED + #define CIVETWEB_HEADER_INCLUDED + ++#include /* Pi-hole extension */ ++ + #define CIVETWEB_VERSION "1.17" + #define CIVETWEB_VERSION_MAJOR (1) + #define CIVETWEB_VERSION_MINOR (17) +@@ -721,6 +723,10 @@ struct mg_server_port { + int _reserved2; + int _reserved3; + int _reserved4; ++ union { ++ struct sockaddr_in sa4; /* Pi-hole extension */ ++ struct sockaddr_in6 sa6; /* Pi-hole extension */ ++ } addr; + }; + + /* Legacy name */ +-- +2.43.0 + diff --git a/src/config/config.c b/src/config/config.c index 09a0c5939..a2295a286 100644 --- a/src/config/config.c +++ b/src/config/config.c @@ -430,7 +430,7 @@ static void initConfig(struct config *conf) 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.h = "Should FTL analyze and show internally generated DNSSEC queries?"; conf->dns.showDNSSEC.t = CONF_BOOL; conf->dns.showDNSSEC.d.b = true; conf->dns.showDNSSEC.c = validate_stub; // Only type-based checking diff --git a/src/config/setupVars.c b/src/config/setupVars.c index 547236384..0eaa71f91 100644 --- a/src/config/setupVars.c +++ b/src/config/setupVars.c @@ -177,8 +177,13 @@ static void get_revServer_from_setupVars(void) char *domain_str = read_setupVarsconf("REV_SERVER_DOMAIN"); if(domain_str != NULL) { - domain = strdup(domain_str); - trim_whitespace(domain); + if(strlen(domain_str) == 0) + log_info("setupVars.conf:REV_SERVER_DOMAIN -> Empty string, ignoring"); + else + { + domain = strdup(domain_str); + trim_whitespace(domain); + } } else log_info("setupVars.conf:REV_SERVER_DOMAIN -> Not set"); @@ -187,16 +192,19 @@ static void get_revServer_from_setupVars(void) clearSetupVarsArray(); // Only add the entry if all values are present and active - if(cidr != NULL && target != NULL && domain != NULL) + if(cidr != NULL && target != NULL) { // Build comma-separated string of all values // 9 = 3 commas, "true/false", and null terminator - char *old = calloc(strlen(cidr) + strlen(target) + strlen(domain) + 9, sizeof(char)); + char *old = calloc(strlen(cidr) + strlen(target) + (domain != NULL ? strlen(domain) : 0) + 9, sizeof(char)); if(old != NULL) { // Add to new config // active is always true as we only add active entries - sprintf(old, "%s,%s,%s,%s", active ? "true" : "false", cidr, target, domain); + if(domain != NULL && strlen(domain) > 0) + sprintf(old, "%s,%s,%s,%s", active ? "true" : "false", cidr, target, domain); + else + sprintf(old, "%s,%s,%s", active ? "true" : "false", cidr, target); cJSON_AddItemToArray(config.dns.revServers.v.json, cJSON_CreateString(old)); // Parameter present in setupVars.conf diff --git a/src/config/validator.c b/src/config/validator.c index 428f8ec5a..d6a8983e6 100644 --- a/src/config/validator.c +++ b/src/config/validator.c @@ -45,14 +45,14 @@ bool validate_dns_hosts(union conf_value *val, const char *key, char err[VALIDAT return false; } - // Check if it's in the form "IP HOSTNAME" + // Check if it's in the form "IP[ \t]HOSTNAME" char *str = strdup(item->valuestring); char *tmp = str; - char *ip = strsep(&tmp, " "); + char *ip = strsep(&tmp, " \t"); if(!ip || !*ip) { - snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not an IP address (\"%s\")", + snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: found no first element (\"%s\")", key, i, item->valuestring); free(str); return false; @@ -74,8 +74,23 @@ bool validate_dns_hosts(union conf_value *val, const char *key, char err[VALIDAT // hostnames to come after the IP address unsigned int hosts = 0; char *host = NULL; - while((host = strsep(&tmp, " ")) != NULL) + while((host = strsep(&tmp, " \t")) != NULL) { + // Skip extra whitespace/tabs + while(isspace((unsigned char)*host)) + host++; + + // Skip this entry if it's empty after trimming + // the whitespaces/tabs at the end of the line + if(strlen(host) == 0) + break; + + // If this hostname is actually the start of a comment + // (first letter is '#'), skip parsing the rest of the + // entire line + if(host[0] == '#') + break; + if(!valid_domain(host, strlen(host), false)) { snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: invalid hostname (\"%s\")", diff --git a/src/webserver/civetweb/civetweb.c b/src/webserver/civetweb/civetweb.c index fa908e54f..66fcab01b 100644 --- a/src/webserver/civetweb/civetweb.c +++ b/src/webserver/civetweb/civetweb.c @@ -3339,6 +3339,7 @@ mg_get_server_ports(const struct mg_context *ctx, ports[cnt].is_ssl = ctx->listening_sockets[i].is_ssl; ports[cnt].is_redirect = ctx->listening_sockets[i].ssl_redir; ports[cnt].is_optional = ctx->listening_sockets[i].is_optional; + memcpy(&ports[cnt].addr, &ctx->listening_sockets[i].lsa, sizeof(ports[cnt].addr)); if (ctx->listening_sockets[i].lsa.sa.sa_family == AF_INET) { /* IPv4 */ diff --git a/src/webserver/civetweb/civetweb.h b/src/webserver/civetweb/civetweb.h index eee958b44..6dcfa4573 100644 --- a/src/webserver/civetweb/civetweb.h +++ b/src/webserver/civetweb/civetweb.h @@ -23,6 +23,8 @@ #ifndef CIVETWEB_HEADER_INCLUDED #define CIVETWEB_HEADER_INCLUDED +#include /* Pi-hole extension */ + #define CIVETWEB_VERSION "1.17" #define CIVETWEB_VERSION_MAJOR (1) #define CIVETWEB_VERSION_MINOR (17) @@ -721,6 +723,10 @@ struct mg_server_port { int _reserved2; int _reserved3; int _reserved4; + union { + struct sockaddr_in sa4; /* Pi-hole extension */ + struct sockaddr_in6 sa6; /* Pi-hole extension */ + } addr; }; /* Legacy name */ diff --git a/src/webserver/webserver.c b/src/webserver/webserver.c index 5d176b247..4cdcf33ad 100644 --- a/src/webserver/webserver.c +++ b/src/webserver/webserver.c @@ -207,8 +207,9 @@ static struct serverports bool is_secure :1; bool is_redirect :1; bool is_optional :1; - unsigned char protocol; // 1 = IPv4, 2 = IPv4+IPv6, 3 = IPv6 - in_port_t port; + char addr[INET6_ADDRSTRLEN + 2]; // +2 for square brackets around IPv6 address + int port; + int protocol; // 1 = IPv4, 3 = IPv6 } server_ports[MAXPORTS] = { 0 }; static in_port_t https_port = 0; /** @@ -252,8 +253,23 @@ static void get_server_ports(void) server_ports[i].is_secure = mgports[i].is_ssl; server_ports[i].is_redirect = mgports[i].is_redirect; server_ports[i].is_optional = mgports[i].is_optional; + // 1 = IPv4, 3 = IPv6 (can also be a combo-socker serving both), + // the documentation in civetweb.h is wrong server_ports[i].protocol = mgports[i].protocol; + // Convert listening address to string + if(server_ports[i].protocol == 1) + inet_ntop(AF_INET, &mgports[i].addr.sa4.sin_addr, server_ports[i].addr, INET_ADDRSTRLEN); + else if(server_ports[i].protocol == 3) + { + char tmp[INET6_ADDRSTRLEN] = { 0 }; + inet_ntop(AF_INET6, &mgports[i].addr.sa6.sin6_addr, tmp, INET6_ADDRSTRLEN); + // Enclose IPv6 address in square brackets + snprintf(server_ports[i].addr, sizeof(server_ports[i].addr), "[%s]", tmp); + } + else + log_warn("Unsupported protocol for port %d", mgports[i].port); + // Store (first) HTTPS port if not already set if(mgports[i].is_ssl && https_port == 0) https_port = mgports[i].port; @@ -261,11 +277,13 @@ static void get_server_ports(void) // Print port information if(i == 0) log_info("Web server ports:"); - log_info(" - %d (HTTP%s, IPv%s%s%s)", - mgports[i].port, mgports[i].is_ssl ? "S" : "", - mgports[i].protocol == 1 ? "4" : (mgports[i].protocol == 3 ? "6" : "4+6"), - mgports[i].is_redirect ? ", redirecting" : "", - mgports[i].is_optional ? ", optional" : ""); + log_info(" - %s:%d (HTTP%s, IPv%s%s%s)", + server_ports[i].addr, + server_ports[i].port, + server_ports[i].is_secure ? "S" : "", + server_ports[i].protocol == 1 ? "4" : "6", + server_ports[i].is_redirect ? ", redirecting" : "", + server_ports[i].is_optional ? ", optional" : ""); } } @@ -275,21 +293,13 @@ in_port_t __attribute__((pure)) get_https_port(void) return https_port; } +#define MAX_URL_LEN 255 unsigned short get_api_string(char **buf, const bool domain) { // Initialize buffer to empty string size_t len = 0; // First byte has the length of the first string **buf = 0; - const char *domain_str = domain ? config.webserver.domain.v.s : "pi.hole"; - size_t api_str_size = strlen(domain_str) + 20; - - // Check if the string is too long for the TXT record - if(api_str_size > 255) - { - log_err("API URL too long for TXT record!"); - return 0; - } // TXT record format: // @@ -311,20 +321,30 @@ unsigned short get_api_string(char **buf, const bool domain) continue; // Reallocate additional memory for every port - if((*buf = realloc(*buf, (i+1)*api_str_size)) == NULL) + const size_t bufsz = (i + 1) * MAX_URL_LEN; + if((*buf = realloc(*buf, bufsz)) == NULL) { log_err("Failed to reallocate API URL buffer!"); return 0; } - const size_t bufsz = (i+1)*api_str_size; + + // Use appropriate domain + const char *addr = domain ? config.webserver.domain.v.s : server_ports[i].addr; + + // If we bound to the wildcard address, substitute it with + // 127.0.0.1 + if(strcmp(addr, "0.0.0.0") == 0) + addr = "127.0.0.1"; + else if(strcasecmp(addr, "[::]") == 0) + addr = "[::1]"; // Append API URL to buffer // We add this at buffer + 1 because the first byte is the // length of the string, which we don't know yet - char *api_str = calloc(api_str_size, sizeof(char)); - const ssize_t this_len = snprintf(api_str, bufsz - len - 1, "http%s://%s:%d/api/", + char *api_str = calloc(MAX_URL_LEN, sizeof(char)); + const ssize_t this_len = snprintf(api_str, MAX_URL_LEN, "http%s://%s:%d/api/", server_ports[i].is_secure ? "s" : "", - domain_str, server_ports[i].port); + addr, server_ports[i].port); // Check if snprintf() failed if(this_len < 0) { diff --git a/test/pihole.toml b/test/pihole.toml index efeee600e..7d2c6eb75 100644 --- a/test/pihole.toml +++ b/test/pihole.toml @@ -40,7 +40,7 @@ # Should FTL hide queries made by localhost? ignoreLocalhost = false - # Should FTL should analyze and show internally generated DNSSEC queries? + # Should FTL analyze and show internally generated DNSSEC queries? showDNSSEC = true # Should FTL analyze *only* A and AAAA queries? diff --git a/test/test_suite.bats b/test/test_suite.bats index 1891abde8..32ce60476 100644 --- a/test/test_suite.bats +++ b/test/test_suite.bats @@ -1179,7 +1179,7 @@ @test "API addresses reported correctly by CHAOS TXT local.api.ftl" { run bash -c 'dig CHAOS TXT local.api.ftl +short @127.0.0.1' printf "dig (full): %s\n" "${lines[@]}" - [[ ${lines[0]} == '"http://pi.hole:80/api/" "https://pi.hole:443/api/"' ]] + [[ ${lines[0]} == '"http://127.0.0.1:80/api/" "https://127.0.0.1:443/api/" "http://[::1]:80/api/" "https://[::1]:443/api/"' ]] } @test "API addresses reported by CHAOS TXT api.ftl identical to domain.api.ftl" {