From 88e57ef9e337b0afca367bdfa5a6c14ceb0fac31 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Fri, 20 Jan 2023 18:32:21 +0100 Subject: [PATCH] Manage custom.list through the universal /config/dns/hosts instead of its own interface. This reduces code duplication. Signed-off-by: DL6ER --- src/api/api.c | 2 - src/api/api.h | 1 - src/api/config.c | 47 +++- src/api/dns.c | 338 ------------------------- src/api/docs/content/specs/config.yaml | 6 + src/api/docs/content/specs/dns.yaml | 110 +------- src/api/docs/content/specs/main.yaml | 6 - src/config/cli.c | 8 + src/config/config.c | 24 +- src/config/config.h | 1 + src/config/dnsmasq_config.c | 133 +++++++++- src/config/dnsmasq_config.h | 2 + src/dnsmasq_interface.c | 2 +- src/log.c | 8 + src/log.h | 1 + src/webserver/webserver.c | 3 +- test/pihole-FTL.toml | 6 + 17 files changed, 215 insertions(+), 483 deletions(-) diff --git a/src/api/api.c b/src/api/api.c index f5d04fdf..cd24e2a7 100644 --- a/src/api/api.c +++ b/src/api/api.c @@ -36,8 +36,6 @@ static struct { // appear *before* less specific URIs: 1. "/a/b/c", 2. "/a/b", 3. "/a" { "/api/dns/blocking", "", api_dns_blocking, { false, 0 }, true, HTTP_GET | HTTP_POST }, { "/api/dns/cache", "", api_dns_cache, { false, 0 }, true, HTTP_GET }, - { "/api/dns/entries", "", api_dns_entries, { false, 0 }, true, HTTP_GET }, - { "/api/dns/entries", "/{ip}/{host}", api_dns_entries, { false, 0 }, true, HTTP_PUT | HTTP_DELETE }, { "/api/clients", "/{client}", api_list, { false, 0 }, true, HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_DELETE }, { "/api/domains", "/{type}/{kind}/{domain}", api_list, { false, 0 }, true, HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_DELETE }, { "/api/groups", "/{name}", api_list, { false, 0 }, true, HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_DELETE }, diff --git a/src/api/api.h b/src/api/api.h index 2820eddb..0eb619d6 100644 --- a/src/api/api.h +++ b/src/api/api.h @@ -64,7 +64,6 @@ int api_network_devices(struct ftl_conn *api); // DNS methods int api_dns_blocking(struct ftl_conn *api); int api_dns_cache(struct ftl_conn *api); -int api_dns_entries(struct ftl_conn *api); // List methods int api_list(struct ftl_conn *api); diff --git a/src/api/config.c b/src/api/config.c index e5d8d95c..e39aa65c 100644 --- a/src/api/config.c +++ b/src/api/config.c @@ -457,6 +457,7 @@ static int api_config_patch(struct ftl_conn *api) // Read all known config items bool dnsmasq_changed = false; + bool rewrite_custom_list = false; struct config conf_copy; duplicate_config(&conf_copy); for(unsigned int i = 0; i < CONFIG_ELEMENTS; i++) @@ -491,6 +492,10 @@ static int api_config_patch(struct ftl_conn *api) // Check if this item requires a config-rewrite + restart of dnsmasq if(conf_item->restart_dnsmasq) dnsmasq_changed = true; + + // Check if this item requires a rewrite of the custom.list file + if(conf_item == &conf_copy.dns.hosts) + rewrite_custom_list = true; } // Request restart of FTL @@ -507,6 +512,14 @@ static int api_config_patch(struct ftl_conn *api) errbuf); } } + else if(rewrite_custom_list) + { + // We need to rewrite the custom.list file but do not need to + // restart dnsmasq. If dnsmasq is going to be restarted anyway, + // this is not necessary as the file will be rewritten during + // the restart + write_custom_list(); + } // Install new configuration lock_shm(); @@ -570,6 +583,7 @@ static int api_config_put_delete(struct ftl_conn *api) // Read all known config items bool dnsmasq_changed = false; + bool rewrite_custom_list = false; bool found = false; struct config conf_copy; duplicate_config(&conf_copy); @@ -615,37 +629,42 @@ static int api_config_put_delete(struct ftl_conn *api) { if(found) { + // Item already present message = "Item already present"; hint = "Uniqueness of items is enforced"; + break; } else { + // Add new item to array JSON_COPY_STR_TO_ARRAY(conf_item->v.json, new_item); found = true; - - // If we reach this point, a valid setting was found and changed - // Check if this item requires a config-rewrite + restart of dnsmasq - if(conf_item->restart_dnsmasq) - dnsmasq_changed = true; } } else { if(found) { + // Remove item from array cJSON_DeleteItemFromArray(conf_item->v.json, idx); - - // If we reach this point, a valid setting was found and changed - // Check if this item requires a config-rewrite + restart of dnsmasq - if(conf_item->restart_dnsmasq) - dnsmasq_changed = true; } else { + // Item not found message = "Item not found"; hint = "Can only delete existing items"; + break; } } + + // If we reach this point, a valid setting was found and changed + // Check if this item requires a config-rewrite + restart of dnsmasq + if(conf_item->restart_dnsmasq) + dnsmasq_changed = true; + + // Check if this item requires a rewrite of the custom.list file + if(conf_item == &conf_copy.dns.hosts) + rewrite_custom_list = true; break; } @@ -681,6 +700,14 @@ static int api_config_put_delete(struct ftl_conn *api) errbuf); } } + else if(rewrite_custom_list) + { + // We need to rewrite the custom.list file but do not need to + // restart dnsmasq. If dnsmasq is going to be restarted anyway, + // this is not necessary as the file will be rewritten during + // the restart + write_custom_list(); + } // Install new configuration lock_shm(); diff --git a/src/api/dns.c b/src/api/dns.c index 1eee13d8..4f1640fc 100644 --- a/src/api/dns.c +++ b/src/api/dns.c @@ -21,12 +21,6 @@ #include "cache_info.h" // config struct #include "config/config.h" -// regex functions for domain validation -#include "regex_r.h" -// file_exists() -#include "files.h" -// flock(), LOCK_SH -#include // Location of custom.list #include "config/dnsmasq_config.h" @@ -149,335 +143,3 @@ int api_dns_cache(struct ftl_conn *api) JSON_ADD_ITEM_TO_OBJECT(json, "cache", cache); JSON_SEND_OBJECT(json); } - -static regex_t domain_validation_regex = { 0 }; -static regex_t label_validation_regex = { 0 }; -static const char *check_domain(const char *domain) -{ - // Compiled regular expressions - if(domain_validation_regex.value == NULL) - { - if(regcomp(&domain_validation_regex, DOMAIN_VALIDATION_REGEX, REG_EXTENDED)) - { - log_err("Could not compile domain validation regex: "DOMAIN_VALIDATION_REGEX"\n"); - return "Internal error: Cannot compile regex (1)"; - } - } - if(label_validation_regex.value == NULL) - { - if(regcomp(&label_validation_regex, LABEL_VALIDATION_REGEX, REG_EXTENDED)) - { - log_err("Could not compile label validation regex: "LABEL_VALIDATION_REGEX"\n"); - return "Internal error: Cannot compile regex (2)"; - } - } - - // Execute compiled regular expression - if(regexec(&domain_validation_regex, domain, 0, NULL, 0) != REG_OK) - { - return "Domain validation failed"; - } - if(regexec(&label_validation_regex, domain, 0, NULL, 0) != REG_OK) - { - return "Domain label validation failed"; - } - - return NULL; -} - -static int read_custom_list(struct ftl_conn *api, cJSON *entries) -{ - if(file_exists(DNSMASQ_CUSTOM_LIST)) - { - FILE *fp = fopen(DNSMASQ_CUSTOM_LIST, "r"); - if(!fp) - { - cJSON_Delete(entries); - return send_json_error(api, 500, - "file_error", - "Cannot open custom DNS records file for reading", - DNSMASQ_CUSTOM_LIST); - } - char *linebuffer = NULL; - size_t size = 0u; - while(getline(&linebuffer, &size, fp) != -1) - { - // Check if memory allocation failed - if(linebuffer == NULL) - break; - - // Parse lines in the file - // Skip lines which are not in the format - // IPADDRESS HOSTNAME - char *save_ptr = NULL; - char *file_ip = strtok_r(linebuffer, " \t\n", &save_ptr); - if(file_ip == NULL) - continue; - char *file_host = strtok_r(NULL, " \t\n", &save_ptr); - if(file_host == NULL) - continue; - - cJSON *entry = JSON_NEW_OBJECT(); - JSON_COPY_STR_TO_OBJECT(entry, "ip", file_ip); - JSON_COPY_STR_TO_OBJECT(entry, "host", file_host); - JSON_ADD_ITEM_TO_ARRAY(entries, entry); - } - - // Free allocated memory - free(linebuffer); - fclose(fp); - } - - return 200; -} - -static int write_custom_list(struct ftl_conn *api, cJSON *entries) -{ - // Write list of entries to the file - FILE *fp = fopen(DNSMASQ_CUSTOM_LIST, "w"); - if(!fp) - { - const int err = errno; - const char *error = "Cannot open "DNSMASQ_CUSTOM_LIST" for writing"; - log_err("%s: %s", error, strerror(err)); - cJSON_Delete(entries); - return send_json_error(api, 500, - "internal_error", - error, - strerror(err)); - } - - // Lock file, may block if the file is currently opened - if(flock(fileno(fp), LOCK_EX) != 0) - { - const int err = errno; - const char *error = "Cannot lock "DNSMASQ_CUSTOM_LIST" for writing"; - log_err("%s: %s", error, strerror(err)); - cJSON_Delete(entries); - return send_json_error(api, 500, - "internal_error", - error, - strerror(err)); - } - - // Write lines into the file - for(int i = 0; i < cJSON_GetArraySize(entries); i++) - { - cJSON *entry = cJSON_GetArrayItem(entries, i); - cJSON *list_ip = cJSON_GetObjectItem(entry, "ip"); - cJSON *list_host = cJSON_GetObjectItem(entry, "host"); - - // Add "IP HOSTNAME" line - fputs(list_ip->valuestring, fp); - fputc(' ', fp); - fputs(list_host->valuestring, fp); - fputc('\n', fp); - } - - // Unlock file - if(flock(fileno(fp), LOCK_UN) != 0) - { - const int err = errno; - const char *error = "Cannot unlock file "DNSMASQ_CUSTOM_LIST" after writing"; - log_err("%s: %s", error, strerror(err)); - cJSON_Delete(entries); - return send_json_error(api, 500, - "internal_error", - error, - strerror(err)); - } - - // Close file - fclose(fp); - - return 200; -} - -static int add_to_custom_list(struct ftl_conn *api, cJSON *entries) -{ - // Split URI into IP and host - char *ip = NULL; - char *host = NULL; - if(sscanf(api->item, "%m[^/]/%m[^/]", &ip, &host) != 2) - { - cJSON_Delete(entries); - if(ip != NULL) - free(ip); - if(host != NULL) - free(host); - return send_json_error(api, 400, - "validation_error", - "Invalid URI", - "URI must be in the format /api/dns/entries/{ip}/{host}"); - } - - // Convert domain to lowercase - strtolower(host); - - // Validate domain - const char *error = check_domain(host); - if(error != NULL) - { - cJSON_Delete(entries); - free(ip); - free(host); - return send_json_error(api, 400, - "validation_error", - "Specified host is not valid", - error); - return false; - } - - // Validate address - if(!isValidIPv4(ip) && !isValidIPv6(ip)) - { - cJSON_Delete(entries); - free(ip); - free(host); - return send_json_error(api, 400, - "validation_error", - "Specified IP address is neither a valid IPv4 nor IPv6 address", - ip); - } - - // If we reach this point, validation succeeded - - // Read list from disk - int ret = read_custom_list(api, entries); - if(ret != 200) - return ret; - - // Check if this entry does already exist in the list - for(int i = 0; i < cJSON_GetArraySize(entries); i++) - { - cJSON *entry = cJSON_GetArrayItem(entries, i); - cJSON *list_ip = cJSON_GetObjectItem(entry, "ip"); - cJSON *list_host = cJSON_GetObjectItem(entry, "host"); - - if(list_ip->valuestring != NULL && - list_host->valuestring != NULL && - strcmp(ip, list_ip->valuestring) == 0 && - strcmp(host, list_host->valuestring) == 0) - { - // Entry already present in list, no need to add it - free(ip); - free(host); - return 200; - } - } - - // If we reach this point, the combination is unique - cJSON *entry = JSON_NEW_OBJECT(); - JSON_COPY_STR_TO_OBJECT(entry, "ip", ip); - JSON_COPY_STR_TO_OBJECT(entry, "host", host); - JSON_ADD_ITEM_TO_ARRAY(entries, entry); - - // Write the file - free(ip); - free(host); - return write_custom_list(api, entries); -} - -static int remove_from_custom_list(struct ftl_conn *api, cJSON *entries) -{ - // Split URI into IP and host - char *ip = NULL; - char *host = NULL; - if(sscanf(api->item, "%m[^/]/%m[^/]", &ip, &host) != 2) - { - cJSON_Delete(entries); - if(ip != NULL) - free(ip); - if(host != NULL) - free(host); - return send_json_error(api, 400, - "validation_error", - "Invalid URI", - "URI must be in the format /api/dns/entries/{ip}/{host}"); - } - - // Convert domain to lowercase - strtolower(host); - - // Read list from disk - int ret = read_custom_list(api, entries); - if(ret != 200) - { - free(ip); - free(host); - return ret; - } - - // Check if this entry exists in the list - for(int i = 0; i < cJSON_GetArraySize(entries); i++) - { - cJSON *entry = cJSON_GetArrayItem(entries, i); - cJSON *list_ip = cJSON_GetObjectItem(entry, "ip"); - cJSON *list_host = cJSON_GetObjectItem(entry, "host"); - - if(list_ip->valuestring != NULL && - list_host->valuestring != NULL && - strcmp(ip, list_ip->valuestring) == 0 && - strcmp(host, list_host->valuestring) == 0) - { - // Entry exists in the array at index i, remove this item. - // We rewrite the file afterwards - cJSON_DeleteItemFromArray(entries, i); - } - } - - // Write the file - free(ip); - free(host); - return write_custom_list(api, entries); -} - -int api_dns_entries(struct ftl_conn *api) -{ - if(api->method == HTTP_GET) - { - // Read list item identified by URI (or read them all) - // We would not actually need the SHM lock here, however, we do - // this for simplicity to ensure nobody else is editing the - // lists while we're doing this here - cJSON *entries = JSON_NEW_ARRAY(); - int ret = read_custom_list(api, entries); - if(ret == 200) - { - cJSON *json = JSON_NEW_OBJECT(); - JSON_ADD_ITEM_TO_OBJECT(json, "entries", entries); - JSON_SEND_OBJECT(json); - } - } - else if(api->method == HTTP_PUT) - { - // Add item to list identified by payload - cJSON *entries = JSON_NEW_ARRAY(); - int ret = add_to_custom_list(api, entries); - if(ret == 200) - { - cJSON *json = JSON_NEW_OBJECT(); - JSON_ADD_ITEM_TO_OBJECT(json, "entries", entries); - JSON_SEND_OBJECT(json); - } - else - return ret; - } - else if(api->method == HTTP_DELETE) - { - // Delete item from list - cJSON *entries = JSON_NEW_ARRAY(); - int ret = remove_from_custom_list(api, entries); - if(ret == 200) - { - cJSON *json = JSON_NEW_OBJECT(); - JSON_ADD_ITEM_TO_OBJECT(json, "entries", entries); - JSON_SEND_OBJECT(json); - } - else - return ret; - } - - // This results in error 404 - return 0; -} \ No newline at end of file diff --git a/src/api/docs/content/specs/config.yaml b/src/api/docs/content/specs/config.yaml index 8ad3c7d6..67ed6dfa 100644 --- a/src/api/docs/content/specs/config.yaml +++ b/src/api/docs/content/specs/config.yaml @@ -175,6 +175,10 @@ components: type: string blockTTL: type: integer + hosts: + type: array + items: + type: string blocking: type: object properties: @@ -477,6 +481,8 @@ components: piholePTR: PI.HOLE replyWhenBusy: ALLOW blockTTL: 2 + hosts: + - "192.168.2.123 mymusicbox" blocking: active: true mode: 'NULL' diff --git a/src/api/docs/content/specs/dns.yaml b/src/api/docs/content/specs/dns.yaml index b8586463..17fed198 100644 --- a/src/api/docs/content/specs/dns.yaml +++ b/src/api/docs/content/specs/dns.yaml @@ -89,81 +89,7 @@ components: application/json: schema: $ref: 'common.yaml#/components/errors/unauthorized' - entries: - get: - summary: Read list of additional DNS records - get: - summary: Get current list of additional DNS records - tags: - - DNS control - operationId: "get_dns_entries" - description: Returns a list of currently configured additional DNS records (if any) - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: 'dns.yaml#/components/schemas/host_records' - put_delete: - summary: Modify list of additional DNS records - parameters: - - $ref: 'dns.yaml#/components/parameters/ip' - - $ref: 'dns.yaml#/components/parameters/host' - put: - summary: Add entry to list of additional DNS records - tags: - - DNS control - operationId: "add_dns_entries" - description: | - Adds an additional DNS records to the list of known DNS records - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: 'dns.yaml#/components/schemas/host_records' - '400': - description: Bad request - content: - application/json: - schema: - oneOf: - - $ref: 'dns.yaml#/components/schemas/errors/no_payload' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: 'common.yaml#/components/errors/unauthorized' - delete: - summary: Remove entry to list of additional DNS records - tags: - - DNS control - operationId: "remove_dns_entries" - description: | - Removes an additional DNS records from the list of known DNS records - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: 'dns.yaml#/components/schemas/host_records' - '400': - description: Bad request - content: - application/json: - schema: - oneOf: - - $ref: 'dns.yaml#/components/schemas/errors/no_payload' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: 'common.yaml#/components/errors/unauthorized' + schemas: blocking: type: object @@ -180,22 +106,7 @@ components: description: Remaining seconds until blocking mode is automatically changed nullable: true example: 15 - host_record: - type: object - properties: - ip: - type: string - example: "129.168.1.13" - host: - type: string - example: "mymusicbox" - host_records: - type: object - properties: - entries: - type: array - items: - $ref: 'dns.yaml#/components/schemas/host_record' + cache: type: object properties: @@ -302,20 +213,3 @@ components: ds: 60 dnskey: 35 other: 14 - parameters: - ip: - in: path - name: ip - description: IP address (Pv4 or IPv6) - schema: - type: string - required: true - example: "192.168.2.10" - host: - in: path - name: host - description: Hostname - schema: - type: string - required: true - example: "mymusicbox" diff --git a/src/api/docs/content/specs/main.yaml b/src/api/docs/content/specs/main.yaml index 37d0e7f1..1ae198da 100644 --- a/src/api/docs/content/specs/main.yaml +++ b/src/api/docs/content/specs/main.yaml @@ -110,12 +110,6 @@ paths: /dns/cache: $ref: 'dns.yaml#/components/paths/cache' - /dns/entries: - $ref: 'dns.yaml#/components/paths/entries/get' - - /dns/entries/{ip}/{host}: - $ref: 'dns.yaml#/components/paths/entries/put_delete' - /domains/{type}/{kind}/{domain}: $ref: 'domains.yaml#/components/paths/type_kind_domain' diff --git a/src/config/cli.c b/src/config/cli.c index 694ada83..57dfe60e 100644 --- a/src/config/cli.c +++ b/src/config/cli.c @@ -296,6 +296,14 @@ bool set_config_from_CLI(const char *key, const char *value) return false; } } + else if(conf_item == &conf_copy.dns.hosts) + { + // We need to rewrite the custom.list file but do not need to + // restart dnsmasq. If dnsmasq is going to be restarted anyway, + // this is not necessary as the file will be rewritten during + // the restart + write_custom_list(); + } // Install new configuration // Backup old config struct (so we can free it) diff --git a/src/config/config.c b/src/config/config.c index f55e2d46..add54f7e 100644 --- a/src/config/config.c +++ b/src/config/config.c @@ -311,6 +311,12 @@ void initConfig(void) config.dns.blockTTL.t = CONF_UINT; config.dns.blockTTL.d.ui = 2; + config.dns.hosts.k = "dns.hosts"; + config.dns.hosts.h = "Custom DNS records\n Example: hosts = [ \"127.0.0.1 mylocal\", \"192.168.0.1 therouter\" ]"; + config.dns.hosts.a = "Array of custom DNS records each one in HOSTS form: \"IP HOSTNAME\""; + config.dns.hosts.t = CONF_JSON_STRING_ARRAY; + config.dns.hosts.d.json = cJSON_CreateArray(); + // sub-struct dns.blocking config.dns.blocking.active.k = "dns.blocking.active"; config.dns.blocking.active.h = "Should FTL block queries?"; @@ -393,8 +399,8 @@ void initConfig(void) // struct dnsmasq config.dnsmasq.upstreams.k = "dnsmasq.upstreams"; - config.dnsmasq.upstreams.h = "Array of upstream DNS servers used by Pi-hole"; - config.dnsmasq.upstreams.a = "array of IP addresses and/or hostnames, optionally with a port, e.g. [ \"8.8.8.8\", \"127.0.0.1#5353\", \"docker-resolver\" ]"; + config.dnsmasq.upstreams.h = "Array of upstream DNS servers used by Pi-hole\n Example: [ \"8.8.8.8\", \"127.0.0.1#5353\", \"docker-resolver\" ]"; + config.dnsmasq.upstreams.a = "array of IP addresses and/or hostnames, optionally with a port"; config.dnsmasq.upstreams.t = CONF_JSON_STRING_ARRAY; config.dnsmasq.upstreams.d.json = cJSON_CreateArray(); config.dnsmasq.upstreams.restart_dnsmasq = true; @@ -627,7 +633,7 @@ void initConfig(void) config.api.localAPIauth.d.b = true; config.api.prettyJSON.k = "api.prettyJSON"; - config.api.prettyJSON.h = "Should FTL prettify the API output?"; + config.api.prettyJSON.h = "Should FTL prettify the API output (add extra spaces, newlines and indentation)?"; config.api.prettyJSON.t = CONF_BOOL; config.api.prettyJSON.d.b = false; @@ -643,14 +649,14 @@ void initConfig(void) config.api.pwhash.d.s = (char*)""; config.api.exclude_clients.k = "api.exclude_clients"; - config.api.exclude_clients.h = "Array of clients to be excluded from certain API responses"; - config.api.exclude_clients.a = "array of IP addresses and/or hostnames, e.g. [ \"192.168.2.56\", \"fe80::341\", \"localhost\" ]"; + config.api.exclude_clients.h = "Array of clients to be excluded from certain API responses\n Example: [ \"192.168.2.56\", \"fe80::341\", \"localhost\" ]"; + config.api.exclude_clients.a = "array of IP addresses and/or hostnames"; config.api.exclude_clients.t = CONF_JSON_STRING_ARRAY; config.api.exclude_clients.d.json = cJSON_CreateArray(); config.api.exclude_domains.k = "api.exclude_domains"; - config.api.exclude_domains.h = "Array of domains to be excluded from certain API responses"; - config.api.exclude_domains.a = "array of IP addresses and/or hostnames, e.g. [ \"google.de\", \"pi-hole.net\" ]"; + config.api.exclude_domains.h = "Array of domains to be excluded from certain API responses\n Example: [ \"google.de\", \"pi-hole.net\" ]"; + config.api.exclude_domains.a = "array of IP addresses and/or hostnames"; config.api.exclude_domains.t = CONF_JSON_STRING_ARRAY; config.api.exclude_domains.d.json = cJSON_CreateArray(); @@ -978,6 +984,7 @@ void readFTLconf(const bool rewrite) { writeFTLtoml(true); write_dnsmasq_config(&config, false, NULL); + write_custom_list(); } return; } @@ -999,6 +1006,8 @@ void readFTLconf(const bool rewrite) read_legacy_dhcp_static_config(); // 05-pihole-custom-cname.conf read_legacy_cnames_config(); + // custom.list + read_legacy_custom_hosts_config(); // When we reach this point but the FTL TOML config file exists, it may // contain errors such as syntax errors, etc. We move it into a @@ -1013,6 +1022,7 @@ void readFTLconf(const bool rewrite) // Initialize the TOML config file writeFTLtoml(true); write_dnsmasq_config(&config, false, NULL); + write_custom_list(); } bool getLogFilePath(void) diff --git a/src/config/config.h b/src/config/config.h index 590d5899..aa9bdd0c 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -101,6 +101,7 @@ struct config { struct conf_item piholePTR; struct conf_item replyWhenBusy; struct conf_item blockTTL; + struct conf_item hosts; struct { struct conf_item active; struct conf_item mode; diff --git a/src/config/dnsmasq_config.c b/src/config/dnsmasq_config.c index 54488fda..c2a9e566 100644 --- a/src/config/dnsmasq_config.c +++ b/src/config/dnsmasq_config.c @@ -165,30 +165,31 @@ char *get_dnsmasq_line(const unsigned int lineno) return NULL; } -static void write_config_header(FILE *fp) +static void write_config_header(FILE *fp, const char *description) { + const time_t now = time(NULL); fputs("# Pi-hole: A black hole for Internet advertisements\n", fp); - fputs("# (c) 2023 Pi-hole, LLC (https://pi-hole.net)\n", fp); + fprintf(fp, "# (c) %u Pi-hole, LLC (https://pi-hole.net)\n", get_year(now)); fputs("# Network-wide ad blocking via your own hardware.\n", fp); fputs("#\n", fp); - fputs("# Dnsmasq config for Pi-hole's FTLDNS\n", fp); + fputs("# ", fp); + fputs(description, fp); + fputs("\n", fp); fputs("#\n", fp); - fputs("# This file is copyright under the latest version of the EUPL.\n", fp); - fputs("# Please see LICENSE file for your rights under this license.\n\n", fp); + fputs("# This file is copyright under the latest version of the EUPL.\n#\n", fp); fputs("###############################################################################\n", fp); fputs("# FILE AUTOMATICALLY POPULATED BY PI-HOLE #\n", fp); fputs("# ANY CHANGES MADE TO THIS FILE WILL BE LOST WHEN THE CONFIGURATION CHANGES #\n", fp); fputs("# #\n", fp); - fputs("# IF YOU WISH TO CHANGE THE UPSTREAM SERVERS, CHANGE THEM IN: #\n", fp); + fputs("# IF YOU WISH TO CHANGE ANY OF THESE VALUES, CHANGE THEM IN: #\n", fp); fputs("# /etc/pihole/pihole-FTL.toml #\n", fp); fputs("# and restart pihole-FTL #\n", fp); fputs("# #\n", fp); - fputs("# #\n", fp); fputs("# ANY OTHER CHANGES SHOULD BE MADE IN A SEPARATE CONFIG FILE #\n", fp); fputs("# WITHIN /etc/dnsmasq.d/yourname.conf #\n", fp); fputs("# #\n", fp); char timestring[84] = ""; - get_timestr(timestring, time(NULL), false); + get_timestr(timestring, now, false); fputs("# Last update: ", fp); fputs(timestring, fp); fputs(" #\n", fp); @@ -214,7 +215,7 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_ return false; } - write_config_header(pihole_conf); + write_config_header(pihole_conf, "Dnsmasq config for Pi-hole's FTLDNS"); fputs("addn-hosts=/etc/pihole/local.list\n", pihole_conf); fputs("addn-hosts="DNSMASQ_CUSTOM_LIST"\n", pihole_conf); fputs("\n", pihole_conf); @@ -624,3 +625,117 @@ bool read_legacy_cnames_config(void) return true; } + +bool read_legacy_custom_hosts_config(void) +{ + // Check if file exists, if not, there is nothing to do + const char *path = DNSMASQ_CUSTOM_LIST; + const char *target = DNSMASQ_CUSTOM_LIST".bck"; + if(!file_exists(path)) + return true; + + FILE *fp = fopen(path, "r"); + if(!fp) + { + log_err("Cannot read %s for reading, unable to import list of custom cnames: %s", + path, strerror(errno)); + return false; + } + + char *linebuffer = NULL; + size_t size = 0u; + errno = 0; + while(getline(&linebuffer, &size, fp) != -1) + { + // Check if memory allocation failed + if(linebuffer == NULL) + break; + + // Import lines in the file + // Trim whitespace at beginning and end, this function + // modifies the string inplace + trim_whitespace(linebuffer); + + // Skip empty lines + if(strlen(linebuffer) == 0) + continue; + + // Skip comments + if(linebuffer[0] == '#') + continue; + + // Add entry to config.dns.hosts + cJSON *item = cJSON_CreateString(linebuffer); + cJSON_AddItemToArray(config.dns.hosts.v.json, item); + } + + // Free allocated memory + free(linebuffer); + + // Close file + if(fclose(fp) != 0) + { + log_err("Cannot close %s: %s", path, strerror(errno)); + return false; + } + + // Move file to backup location + log_info("Moving %s to %s", path, target); + if(rename(path, target) != 0) + log_warn("Unable to move %s to %s: %s", path, target, strerror(errno)); + + return true; +} + +bool write_custom_list(void) +{ + // Rotate old hosts files + rotate_files(DNSMASQ_CUSTOM_LIST, MAX_ROTATION); + + log_debug(DEBUG_CONFIG, "Opening "DNSMASQ_CUSTOM_LIST" for writing"); + FILE *pihole_conf = fopen(DNSMASQ_CUSTOM_LIST, "w"); + // Return early if opening failed + if(!pihole_conf) + { + log_err("Cannot open "DNSMASQ_CUSTOM_LIST" for writing, unable to update custom.list: %s", strerror(errno)); + return false; + } + + // Lock file, may block if the file is currently opened + if(flock(fileno(pihole_conf), LOCK_EX) != 0) + { + log_err("Cannot open "DNSMASQ_CUSTOM_LIST" in exclusive mode: %s", strerror(errno)); + fclose(pihole_conf); + return false; + } + + write_config_header(pihole_conf, "Custom DNS entries (HOSTS file)"); + + if(cJSON_GetArraySize(config.dns.hosts.v.json) > 0) + { + const int n = cJSON_GetArraySize(config.dns.hosts.v.json); + for(int i = 0; i < n; i++) + { + cJSON *entry = cJSON_GetArrayItem(config.dns.hosts.v.json, i); + if(entry != NULL && cJSON_IsString(entry)) + fprintf(pihole_conf, "%s\n", entry->valuestring); + } + fputs("\n", pihole_conf); + } + + // Unlock file + if(flock(fileno(pihole_conf), LOCK_UN) != 0) + { + log_err("Cannot release lock on custom.list: %s", strerror(errno)); + fclose(pihole_conf); + return false; + } + + // Close file + if(fclose(pihole_conf) != 0) + { + log_err("Cannot close custom.list: %s", strerror(errno)); + return false; + } + return true; +} \ No newline at end of file diff --git a/src/config/dnsmasq_config.h b/src/config/dnsmasq_config.h index f9b4415c..33e435dc 100644 --- a/src/config/dnsmasq_config.h +++ b/src/config/dnsmasq_config.h @@ -19,6 +19,8 @@ int get_lineno_from_string(const char *string); char *get_dnsmasq_line(const unsigned int lineno); bool read_legacy_dhcp_static_config(void); bool read_legacy_cnames_config(void); +bool read_legacy_custom_hosts_config(void); +bool write_custom_list(void); #define DNSMASQ_PH_CONFIG "/etc/pihole/dnsmasq.conf" #define DNSMASQ_TEMP_CONF "/etc/pihole/dnsmasq.conf.temp" diff --git a/src/dnsmasq_interface.c b/src/dnsmasq_interface.c index 85bc278f..0b201e9f 100644 --- a/src/dnsmasq_interface.c +++ b/src/dnsmasq_interface.c @@ -3306,7 +3306,7 @@ int check_struct_sizes(void) int result = 0; // sizeof(struct conf_item) is 72 on x86_64 and 52 on x86_32 // number of config elements: CONFIG_ELEMENTS - result += check_one_struct("struct config", sizeof(struct config), 8280, 5980); + result += check_one_struct("struct config", sizeof(struct config), 8352, 6032); result += check_one_struct("queriesData", sizeof(queriesData), 72, 64); result += check_one_struct("upstreamsData", sizeof(upstreamsData), 640, 628); result += check_one_struct("clientsData", sizeof(clientsData), 672, 652); diff --git a/src/log.c b/src/log.c index f40e622c..a69484a5 100644 --- a/src/log.c +++ b/src/log.c @@ -123,6 +123,14 @@ void get_timestr(char * const timestring, const time_t timein, const bool millis } } +// Return the current year +unsigned int get_year(const time_t timein) +{ + struct tm tm; + localtime_r(&timein, &tm); + return tm.tm_year + 1900; +} + static const char *priostr(const int priority, const enum debug_flag flag) { const char *name; diff --git a/src/log.h b/src/log.h index 3bfa3e19..e0b9a10c 100644 --- a/src/log.h +++ b/src/log.h @@ -27,6 +27,7 @@ void log_counter_info(void); void format_memory_size(char prefix[2], unsigned long long int bytes, double * const formatted); void format_time(char buffer[42], unsigned long seconds, double milliseconds); +unsigned int get_year(const time_t timein); const char *get_FTL_version(void); void log_FTL_version(bool crashreport); double double_time(void); diff --git a/src/webserver/webserver.c b/src/webserver/webserver.c index 8691c5df..99a478bd 100644 --- a/src/webserver/webserver.c +++ b/src/webserver/webserver.c @@ -138,6 +138,7 @@ void http_init(void) char num_threads[3] = { 0 }; sprintf(num_threads, "%d", get_nprocs() > 8 ? 16 : 2*get_nprocs()); const char *options[] = { + // All passed strings are duplicated internally. See also comment below. "document_root", config.http.paths.webroot.v.s, "listening_ports", config.http.port.v.s, "decode_url", "yes", @@ -151,7 +152,7 @@ void http_init(void) // "cgi_interpreter", config.http.php_location, // "cgi_pattern", "**.php$", // ** allows the files to by anywhere inside the web root "index_files", "index.html,index.htm,index.php", - NULL, NULL, // Leave two slots for access control list (ACL) if configured + NULL, NULL, // Leave two slots for access control list (ACL) at the end NULL }; diff --git a/test/pihole-FTL.toml b/test/pihole-FTL.toml index f7096e6b..170e4b89 100644 --- a/test/pihole-FTL.toml +++ b/test/pihole-FTL.toml @@ -38,6 +38,12 @@ # TTL for blocked queries [seconds] blockTTL = 2 + # Custom DNS records + # Example: hosts = [ "127.0.0.1 mylocal", "192.168.0.1 therouter" ] + # Possible values are: Array of custom DNS records each one in HOSTS form: "IP + # HOSTNAME" + hosts = [ ] + [dns.blocking] # Should FTL block queries? active = true