Add POST /api/teleporter to upload and install backed up configuration

Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
DL6ER
2023-01-25 21:51:12 +01:00
parent d51aa378a3
commit 48fc06d46b
48 changed files with 2128 additions and 1315 deletions
+2
View File
@@ -180,6 +180,7 @@ add_executable(pihole-FTL
$<TARGET_OBJECTS:civetweb>
$<TARGET_OBJECTS:cJSON>
$<TARGET_OBJECTS:miniz>
$<TARGET_OBJECTS:compression>
$<TARGET_OBJECTS:ph7>
$<TARGET_OBJECTS:database>
$<TARGET_OBJECTS:dnsmasq>
@@ -258,6 +259,7 @@ install(CODE "execute_process(COMMAND ${SETCAP} CAP_NET_BIND_SERVICE,CAP_NET_RAW
add_subdirectory(api)
add_subdirectory(webserver)
add_subdirectory(civetweb)
add_subdirectory(compression)
add_subdirectory(cJSON)
add_subdirectory(miniz)
add_subdirectory(ph7)
+76 -69
View File
@@ -31,58 +31,59 @@ static struct {
bool require_auth;
enum http_method methods;
} api_request[] = {
// URI ARGUMENTS FUNCTION OPTIONS AUTH ALLOWED METHODS
// URI ARGUMENTS FUNCTION OPTIONS AUTH ALLOWED METHODS
// domains json fifo
// Note: The order of appearance matters here, more specific URIs have to
// 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/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 },
{ "/api/lists", "/{list}", api_list, { false, 0 }, true, HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_DELETE },
{ "/api/info/client", "", api_info_client, { false, 0 }, false, HTTP_GET },
{ "/api/info/system", "", api_info_system, { false, 0 }, true, HTTP_GET },
{ "/api/info/database", "", api_info_database, { false, 0 }, true, HTTP_GET },
{ "/api/info/sensors", "", api_info_sensors, { false, 0 }, true, HTTP_GET },
{ "/api/info/host", "", api_info_host, { false, 0 }, true, HTTP_GET },
{ "/api/info/ftl", "", api_info_ftl, { false, 0 }, true, HTTP_GET },
{ "/api/info/version", "", api_info_version, { false, 0 }, true, HTTP_GET },
{ "/api/logs/dnsmasq", "", api_logs, { false, FIFO_DNSMASQ }, true, HTTP_GET },
{ "/api/logs/ftl", "", api_logs, { false, FIFO_FTL }, true, HTTP_GET },
{ "/api/logs/http", "", api_logs, { false, FIFO_CIVETWEB }, true, HTTP_GET },
{ "/api/logs/ph7", "", api_logs, { false, FIFO_PH7 }, true, HTTP_GET },
{ "/api/history/clients", "", api_history_clients, { false, 0 }, true, HTTP_GET },
{ "/api/history/database/clients", "", api_history_database_clients, { false, 0 }, true, HTTP_GET },
{ "/api/history/database", "", api_history_database, { false, 0 }, true, HTTP_GET },
{ "/api/history", "", api_history, { false, 0 }, true, HTTP_GET },
{ "/api/queries/suggestions", "", api_queries_suggestions, { false, 0 }, true, HTTP_GET },
{ "/api/queries", "", api_queries, { false, 0 }, true, HTTP_GET },
{ "/api/stats/summary", "", api_stats_summary, { false, 0 }, true, HTTP_GET },
{ "/api/stats/query_types", "", api_stats_query_types, { false, 0 }, true, HTTP_GET },
{ "/api/stats/upstreams", "", api_stats_upstreams, { false, 0 }, true, HTTP_GET },
{ "/api/stats/top_domains", "", api_stats_top_domains, { false, 0 }, true, HTTP_GET },
{ "/api/stats/top_clients", "", api_stats_top_clients, { false, 0 }, true, HTTP_GET },
{ "/api/stats/recent_blocked", "", api_stats_recentblocked, { false, 0 }, true, HTTP_GET },
{ "/api/stats/database/top_domains", "", api_stats_database_top_items, { true, 0 }, true, HTTP_GET },
{ "/api/stats/database/top_clients", "", api_stats_database_top_items, { false, 0 }, true, HTTP_GET },
{ "/api/stats/database/summary", "", api_stats_database_summary, { false, 0 }, true, HTTP_GET },
{ "/api/stats/database/query_types", "", api_stats_database_query_types, { false, 0 }, true, HTTP_GET },
{ "/api/stats/database/upstreams", "", api_stats_database_upstreams, { false, 0 }, true, HTTP_GET },
{ "/api/auth", "", api_auth, { false, 0 }, false, HTTP_GET | HTTP_POST | HTTP_DELETE },
{ "/api/config/_topics", "", api_config_topics, { false, 0 }, true, HTTP_GET },
{ "/api/config/_server", "", api_config_server, { false, 0 }, true, HTTP_GET },
{ "/api/config", "", api_config, { false, 0 }, true, HTTP_GET | HTTP_PATCH },
{ "/api/config", "/{element}", api_config, { false, 0 }, true, HTTP_GET | HTTP_PATCH },
{ "/api/config", "/{element}/{value}", api_config, { false, 0 }, true, HTTP_DELETE | HTTP_PUT },
{ "/api/network/gateway", "", api_network_gateway, { false, 0 }, true, HTTP_GET },
{ "/api/network/interfaces", "", api_network_interfaces, { false, 0 }, true, HTTP_GET },
{ "/api/network/devices", "", api_network_devices, { false, 0 }, true, HTTP_GET },
{ "/api/endpoints", "", api_endpoints, { false, 0 }, true, HTTP_GET },
{ "/api/teleporter", "", api_teleporter, { false, 0 }, true, HTTP_GET | HTTP_POST },
{ "/api/action/gravity", "", api_action_gravity, { false, 0 }, true, HTTP_POST },
{ "/api/action/reboot", "", api_action_reboot, { false, 0 }, true, HTTP_POST },
{ "/api/action/poweroff", "", api_action_poweroff, { false, 0 }, true, HTTP_POST },
{ "/api/docs", "", api_docs, { false, 0 }, false, HTTP_GET },
{ "/api/dns/blocking", "", api_dns_blocking, { false, true, 0 }, true, HTTP_GET | HTTP_POST },
{ "/api/dns/cache", "", api_dns_cache, { false, true, 0 }, true, HTTP_GET },
{ "/api/clients", "/{client}", api_list, { false, true, 0 }, true, HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_DELETE },
{ "/api/domains", "/{type}/{kind}/{domain}", api_list, { false, true, 0 }, true, HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_DELETE },
{ "/api/groups", "/{name}", api_list, { false, true, 0 }, true, HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_DELETE },
{ "/api/lists", "/{list}", api_list, { false, true, 0 }, true, HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_DELETE },
{ "/api/info/client", "", api_info_client, { false, true, 0 }, false, HTTP_GET },
{ "/api/info/system", "", api_info_system, { false, true, 0 }, true, HTTP_GET },
{ "/api/info/database", "", api_info_database, { false, true, 0 }, true, HTTP_GET },
{ "/api/info/sensors", "", api_info_sensors, { false, true, 0 }, true, HTTP_GET },
{ "/api/info/host", "", api_info_host, { false, true, 0 }, true, HTTP_GET },
{ "/api/info/ftl", "", api_info_ftl, { false, true, 0 }, true, HTTP_GET },
{ "/api/info/version", "", api_info_version, { false, true, 0 }, true, HTTP_GET },
{ "/api/logs/dnsmasq", "", api_logs, { false, true, FIFO_DNSMASQ }, true, HTTP_GET },
{ "/api/logs/ftl", "", api_logs, { false, true, FIFO_FTL }, true, HTTP_GET },
{ "/api/logs/http", "", api_logs, { false, true, FIFO_CIVETWEB }, true, HTTP_GET },
{ "/api/logs/ph7", "", api_logs, { false, true, FIFO_PH7 }, true, HTTP_GET },
{ "/api/history/clients", "", api_history_clients, { false, true, 0 }, true, HTTP_GET },
{ "/api/history/database/clients", "", api_history_database_clients, { false, true, 0 }, true, HTTP_GET },
{ "/api/history/database", "", api_history_database, { false, true, 0 }, true, HTTP_GET },
{ "/api/history", "", api_history, { false, true, 0 }, true, HTTP_GET },
{ "/api/queries/suggestions", "", api_queries_suggestions, { false, true, 0 }, true, HTTP_GET },
{ "/api/queries", "", api_queries, { false, true, 0 }, true, HTTP_GET },
{ "/api/stats/summary", "", api_stats_summary, { false, true, 0 }, true, HTTP_GET },
{ "/api/stats/query_types", "", api_stats_query_types, { false, true, 0 }, true, HTTP_GET },
{ "/api/stats/upstreams", "", api_stats_upstreams, { false, true, 0 }, true, HTTP_GET },
{ "/api/stats/top_domains", "", api_stats_top_domains, { false, true, 0 }, true, HTTP_GET },
{ "/api/stats/top_clients", "", api_stats_top_clients, { false, true, 0 }, true, HTTP_GET },
{ "/api/stats/recent_blocked", "", api_stats_recentblocked, { false, true, 0 }, true, HTTP_GET },
{ "/api/stats/database/top_domains", "", api_stats_database_top_items, { true, true, 0 }, true, HTTP_GET },
{ "/api/stats/database/top_clients", "", api_stats_database_top_items, { false, true, 0 }, true, HTTP_GET },
{ "/api/stats/database/summary", "", api_stats_database_summary, { false, true, 0 }, true, HTTP_GET },
{ "/api/stats/database/query_types", "", api_stats_database_query_types, { false, true, 0 }, true, HTTP_GET },
{ "/api/stats/database/upstreams", "", api_stats_database_upstreams, { false, true, 0 }, true, HTTP_GET },
{ "/api/auth", "", api_auth, { false, true, 0 }, false, HTTP_GET | HTTP_POST | HTTP_DELETE },
{ "/api/config/_topics", "", api_config_topics, { false, true, 0 }, true, HTTP_GET },
{ "/api/config/_server", "", api_config_server, { false, true, 0 }, true, HTTP_GET },
{ "/api/config", "", api_config, { false, true, 0 }, true, HTTP_GET | HTTP_PATCH },
{ "/api/config", "/{element}", api_config, { false, true, 0 }, true, HTTP_GET | HTTP_PATCH },
{ "/api/config", "/{element}/{value}", api_config, { false, true, 0 }, true, HTTP_DELETE | HTTP_PUT },
{ "/api/network/gateway", "", api_network_gateway, { false, true, 0 }, true, HTTP_GET },
{ "/api/network/interfaces", "", api_network_interfaces, { false, true, 0 }, true, HTTP_GET },
{ "/api/network/devices", "", api_network_devices, { false, true, 0 }, true, HTTP_GET },
{ "/api/endpoints", "", api_endpoints, { false, true, 0 }, true, HTTP_GET },
{ "/api/teleporter", "", api_teleporter, { false, false, 0 }, true, HTTP_GET | HTTP_POST },
{ "/api/action/gravity", "", api_action_gravity, { false, true, 0 }, true, HTTP_POST },
{ "/api/action/reboot", "", api_action_reboot, { false, true, 0 }, true, HTTP_POST },
{ "/api/action/poweroff", "", api_action_poweroff, { false, true, 0 }, true, HTTP_POST },
{ "/api/docs", "", api_docs, { false, true, 0 }, false, HTTP_GET },
};
int api_handler(struct mg_connection *conn, void *ignored)
@@ -96,22 +97,9 @@ int api_handler(struct mg_connection *conn, void *ignored)
NULL,
{ false, NULL, NULL, NULL, 0u },
{ false },
{ false, 0 }
{ false, false, 0 }
};
// Allocate memory for the payload
api.payload.raw = calloc(MAX_PAYLOAD_BYTES, sizeof(char));
if(!api.payload.raw)
{
log_crit("Cannot handle API request %s %s: %s",
api.request->request_method,
api.request->local_uri_raw,
strerror(ENOMEM));
}
// Read and try to parse payload
read_and_parse_payload(&api);
log_debug(DEBUG_API, "Requested API URI: %s %s ? %s",
api.request->request_method,
api.request->local_uri_raw,
@@ -122,7 +110,7 @@ int api_handler(struct mg_connection *conn, void *ignored)
// Loop over all API endpoints and check if the requested URI matches
bool unauthorized = false;
enum http_method allowed_methods = 0;
for(unsigned int i = 0; i < sizeof(api_request)/sizeof(api_request[0]); i++)
for(unsigned int i = 0; i < ArraySize(api_request); i++)
{
// Check if the requested method is allowed
if(!(api_request[i].methods & api.method) && api.method != HTTP_OPTIONS)
@@ -143,6 +131,22 @@ int api_handler(struct mg_connection *conn, void *ignored)
continue;
}
if(api_request[i].opts.parse_json)
{
// Allocate memory for the payload
api.payload.raw = calloc(MAX_PAYLOAD_BYTES, sizeof(char));
if(!api.payload.raw)
{
log_crit("Cannot handle API request %s %s: %s",
api.request->request_method,
api.request->local_uri_raw,
strerror(ENOMEM));
}
// Read and try to parse payload
read_and_parse_payload(&api);
}
// Verify requesting client is allowed to see this ressource
if(api_request[i].require_auth && check_client_auth(&api) == API_AUTH_UNAUTHORIZED)
{
@@ -172,9 +176,12 @@ int api_handler(struct mg_connection *conn, void *ignored)
api.payload.json = NULL;
}
// Free raw payload bytes (always allocated)
free(api.payload.raw);
api.payload.raw = NULL;
// Free raw payload bytes
if(api.payload.raw != NULL)
{
free(api.payload.raw);
api.payload.raw = NULL;
}
// Check if we need to return with unauthorized payload
if(unauthorized)
@@ -240,7 +247,7 @@ static int api_endpoints(struct ftl_conn *api)
cJSON *delete = JSON_NEW_ARRAY();
// Add endpoints to JSON array
for(unsigned int i = 0; i < sizeof(api_request)/sizeof(api_request[0]); i++)
for(unsigned int i = 0; i < ArraySize(api_request); i++)
{
for(enum http_method method = HTTP_GET; method <= HTTP_DELETE; method <<= 1)
{
+2 -1
View File
@@ -142,7 +142,8 @@ int check_client_auth(struct ftl_conn *api)
{
const char *sid_header = NULL;
// Try to extract SID from header
if((sid_header = mg_get_header(api->conn, "sid")) != NULL)
if((sid_header = mg_get_header(api->conn, "sid")) != NULL ||
(sid_header = mg_get_header(api->conn, "X-FTL-SID")) != NULL)
{
// Copy SID string
strncpy(sid, sid_header, SID_SIZE - 1u);
+31 -47
View File
@@ -525,33 +525,33 @@ static int api_config_patch(struct ftl_conn *api)
bool config_changed = false;
bool dnsmasq_changed = false;
bool rewrite_custom_list = false;
struct config conf_copy;
duplicate_config(&conf_copy);
struct config newconf;
duplicate_config(&newconf, &config);
for(unsigned int i = 0; i < CONFIG_ELEMENTS; i++)
{
// Get pointer to memory location of this conf_item (copy)
struct conf_item *copy_item = get_conf_item(&conf_copy, i);
struct conf_item *new_item = get_conf_item(&newconf, i);
// Get path depth
unsigned int level = config_path_depth(copy_item->p);
unsigned int level = config_path_depth(new_item->p);
cJSON *elem = conf;
// Parse tree of properties and get the individual JSON elements
for(unsigned int j = 0; j < level; j++)
elem = cJSON_GetObjectItem(elem, copy_item->p[j]);
elem = cJSON_GetObjectItem(elem, new_item->p[j]);
// Check if this element is present - it doesn't have to be!
if(elem == NULL)
{
log_debug(DEBUG_CONFIG, "%s not in JSON payload", copy_item->k);
log_debug(DEBUG_CONFIG, "%s not in JSON payload", new_item->k);
continue;
}
// Try to set value and report error on failure
const char *response = getJSONvalue(copy_item, elem);
const char *response = getJSONvalue(new_item, elem);
if(response != NULL)
{
log_err("/api/config: %s invalid: %s", copy_item->k, response);
log_err("/api/config: %s invalid: %s", new_item->k, response);
continue;
}
@@ -559,7 +559,7 @@ static int api_config_patch(struct ftl_conn *api)
struct conf_item *conf_item = get_conf_item(&config, i);
// Skip processing if value didn't change compared to current value
if(compare_config_item(copy_item, conf_item))
if(compare_config_item(new_item, conf_item))
{
log_debug(DEBUG_CONFIG, "Config item %s: Unchanged", conf_item->k);
continue;
@@ -574,7 +574,7 @@ static int api_config_patch(struct ftl_conn *api)
dnsmasq_changed = true;
// Check if this item requires a rewrite of the custom.list file
if(conf_item == &conf_copy.dns.hosts)
if(conf_item == &newconf.dns.hosts)
rewrite_custom_list = true;
}
@@ -585,7 +585,7 @@ static int api_config_patch(struct ftl_conn *api)
if(dnsmasq_changed)
{
char errbuf[ERRBUF_SIZE] = { 0 };
if(write_dnsmasq_config(&conf_copy, true, errbuf))
if(write_dnsmasq_config(&newconf, true, errbuf))
api->ftl.restart = true;
else
{
@@ -605,15 +605,7 @@ static int api_config_patch(struct ftl_conn *api)
}
// Install new configuration
lock_shm();
// Backup old config struct (so we can free it)
struct config old_conf;
memcpy(&old_conf, &config, sizeof(struct config));
// Replace old config struct by changed one
memcpy(&config, &conf_copy, sizeof(struct config));
// Free old backup struct
free_config(&old_conf);
unlock_shm();
replace_config(&newconf);
// Store changed configuration to disk
writeFTLtoml(true);
@@ -624,7 +616,7 @@ static int api_config_patch(struct ftl_conn *api)
else
{
// Nothing changed, merely release copied config memory
free_config(&conf_copy);
free_config(&newconf);
}
// Return full config after possible changes above
@@ -663,7 +655,7 @@ static int api_config_put_delete(struct ftl_conn *api)
hint);
}
char *new_item = requested_path[min_level - 1];
char *new_item_str = requested_path[min_level - 1];
// Convert path to items, e.g.,
// dnsmasq/dhcp/active -> dhcp.active
@@ -674,26 +666,26 @@ static int api_config_put_delete(struct ftl_conn *api)
bool dnsmasq_changed = false;
bool rewrite_custom_list = false;
bool found = false;
struct config conf_copy;
duplicate_config(&conf_copy);
struct config newconf;
duplicate_config(&newconf, &config);
for(unsigned int i = 0; i < CONFIG_ELEMENTS; i++)
{
// Get pointer to memory location of this conf_item
struct conf_item *conf_item = get_conf_item(&conf_copy, i);
struct conf_item *new_item = get_conf_item(&newconf, i);
// We support PUT only for adding to string arrays
if(conf_item->t != CONF_JSON_STRING_ARRAY)
if(new_item->t != CONF_JSON_STRING_ARRAY)
continue;
// Get path depth
const unsigned int level = config_path_depth(conf_item->p);
const unsigned int level = config_path_depth(new_item->p);
// Check equality of paths up to the requested level (if any)
// Examples:
// requested was /config/dnsmasq -> skip all entries that do not start in dnsmasq.
// requested was /config/dnsmasq/dhcp -> skip all entries that do not start in dhcp
// etc.
if(!check_paths_equal(conf_item->p, requested_path, max(min_level - 2, level - 1)))
if(!check_paths_equal(new_item->p, requested_path, max(min_level - 2, level - 1)))
continue;
// Check if this is a property where we want to add an item
@@ -702,11 +694,11 @@ static int api_config_put_delete(struct ftl_conn *api)
// Check if this entry does already exist in the array
int idx = 0;
for(; idx < cJSON_GetArraySize(conf_item->v.json); idx++)
for(; idx < cJSON_GetArraySize(new_item->v.json); idx++)
{
cJSON *elem = cJSON_GetArrayItem(conf_item->v.json, idx);
cJSON *elem = cJSON_GetArrayItem(new_item->v.json, idx);
if(elem != NULL && elem->valuestring != NULL &&
strcmp(elem->valuestring, new_item) == 0)
strcmp(elem->valuestring, new_item_str) == 0)
{
found = true;
break;
@@ -725,7 +717,7 @@ static int api_config_put_delete(struct ftl_conn *api)
else
{
// Add new item to array
JSON_COPY_STR_TO_ARRAY(conf_item->v.json, new_item);
JSON_COPY_STR_TO_ARRAY(new_item->v.json, new_item);
found = true;
}
}
@@ -734,7 +726,7 @@ static int api_config_put_delete(struct ftl_conn *api)
if(found)
{
// Remove item from array
cJSON_DeleteItemFromArray(conf_item->v.json, idx);
cJSON_DeleteItemFromArray(new_item->v.json, idx);
}
else
{
@@ -747,11 +739,11 @@ static int api_config_put_delete(struct ftl_conn *api)
// 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->f & FLAG_RESTART_DNSMASQ)
if(new_item->f & FLAG_RESTART_DNSMASQ)
dnsmasq_changed = true;
// Check if this item requires a rewrite of the custom.list file
if(conf_item == &conf_copy.dns.hosts)
if(new_item == &newconf.dns.hosts)
rewrite_custom_list = true;
break;
}
@@ -779,7 +771,7 @@ static int api_config_put_delete(struct ftl_conn *api)
{
char errbuf[ERRBUF_SIZE] = { 0 };
// Request restart of FTL
if(write_dnsmasq_config(&conf_copy, true, errbuf))
if(write_dnsmasq_config(&newconf, true, errbuf))
api->ftl.restart = true;
else
{
@@ -800,15 +792,7 @@ static int api_config_put_delete(struct ftl_conn *api)
}
// Install new configuration
lock_shm();
// Backup old config struct (so we can free it)
struct config old_conf;
memcpy(&old_conf, &config, sizeof(struct config));
// Replace old config struct by changed one
memcpy(&config, &conf_copy, sizeof(struct config));
// Free old backup struct
free_config(&old_conf);
unlock_shm();
replace_config(&newconf);
// Store changed configuration to disk
writeFTLtoml(true);
@@ -840,7 +824,7 @@ int api_config(struct ftl_conn *api)
int api_config_topics(struct ftl_conn *api)
{
cJSON *topics = JSON_NEW_ARRAY();
for(unsigned int i = 0; i < sizeof(config_topics)/sizeof(*config_topics); i++)
for(unsigned int i = 0; i < ArraySize(config_topics); i++)
{
cJSON *topic = JSON_NEW_OBJECT();
JSON_REF_STR_IN_OBJECT(topic, "name", config_topics[i].name);
@@ -857,7 +841,7 @@ int api_config_topics(struct ftl_conn *api)
int api_config_server(struct ftl_conn *api)
{
cJSON *servers = JSON_NEW_ARRAY();
for(unsigned int i = 0; i < sizeof(dns_server)/sizeof(*dns_server); i++)
for(unsigned int i = 0; i < ArraySize(dns_server); i++)
{
cJSON *server = JSON_NEW_OBJECT();
JSON_REF_STR_IN_OBJECT(server, "name", dns_server[i].name);
+78 -2
View File
@@ -8,7 +8,8 @@ components:
- "Pi-hole configuration"
operationId: "get_teleporter"
description: |
Request an archived copy of your Pi-hole's current configuration
Request an archived copy of your Pi-hole's current configuration.
Authentication via header or cookie is required for the endpoint.
responses:
'200':
description: OK
@@ -22,4 +23,79 @@ components:
content:
application/json:
schema:
$ref: 'common.yaml#/components/errors/unauthorized'
$ref: 'common.yaml#/components/errors/unauthorized'
post:
summary: Import Pi-hole settings
tags:
- "Pi-hole configuration"
operationId: "post_teleporter"
description: |
Upload a Pi-hole Teleporter archive to (partially) restore from it. Note that this will overwrite your current configuration.
Authentication via header or cookie is required for the endpoint.
requestBody:
content:
multipart/form-data:
schema:
properties:
file:
type: string
format: binary
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: 'teleporter.yaml#/components/schemas/teleporter/post'
examples:
teleporter:
$ref: 'teleporter.yaml#/components/examples/teleporter'
'400':
description: Bad request
content:
application/json:
schema:
$ref: 'teleporter.yaml#/components/errors/invalid_zip'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: 'common.yaml#/components/errors/unauthorized'
schemas:
teleporter:
post:
type: object
properties:
processed:
type: array
items:
type: string
errors:
invalid_zip:
type: object
description: Invalid ZIP archive uploaded
properties:
error:
type: object
properties:
key:
type: string
description: "Machine-readable error type"
example: "invalid_zip"
message:
type: string
description: "Human-readable error message"
example: "Invalid ZIP file uploaded"
hint:
type: string
nullable: true
description: "No additional data available"
example: null
examples:
teleporter:
value:
teleporter:
- a
- b
+1 -1
View File
@@ -397,7 +397,7 @@ int api_stats_upstreams(struct ftl_conn *api)
temparray[upstreamID][0] = upstreamID;
unsigned int count = 0;
for(unsigned i = 0; i < (sizeof(upstream->overTime)/sizeof(*upstream->overTime)); i++)
for(unsigned i = 0; i < ArraySize(upstream->overTime); i++)
count += upstream->overTime[i];
temparray[upstreamID][1] = count;
totalcount += count;
+212 -2
View File
@@ -11,8 +11,12 @@
#include "FTL.h"
#include "webserver/http-common.h"
#include "webserver/json_macros.h"
#include "miniz/teleporter.h"
#include "compression/teleporter.h"
#include "api/api.h"
// ERRBUF_SIZE
#include "config/dnsmasq_config.h"
#define MAXZIPSIZE (50u*1024*1024)
static int api_teleporter_GET(struct ftl_conn *api)
{
@@ -50,10 +54,216 @@ static int api_teleporter_GET(struct ftl_conn *api)
return 200;
}
// Struct to store the data we want to process
struct upload_data {
bool too_large;
char *sid;
char *zip_data;
char *zip_filename;
size_t zip_size;
};
// Callback function for CivetWeb to determine which fields we want to receive
static bool is_file = false;
static bool is_sid = false;
static int field_found(const char *key,
const char *filename,
char *path,
size_t pathlen,
void *user_data)
{
struct upload_data *data = (struct upload_data *)user_data;
log_debug(DEBUG_API, "Found field: \"%s\", filename: \"%s\"", key, filename);
is_file = false;
is_sid = false;
if(strcasecmp(key, "file") == 0 && filename && *filename)
{
data->zip_filename = strdup(filename);
is_file = true;
return MG_FORM_FIELD_STORAGE_GET;
}
else if(strcasecmp(key, "import") == 0)
{
is_sid = true;
return MG_FORM_FIELD_STORAGE_GET;
}
// Ignore any other fields
return MG_FORM_FIELD_STORAGE_SKIP;
}
// Callback function for CivetWeb to receive the data of the fields we want to process.
// This function might be called several times for the same field (large (> 8KB)
// or chunked data), so we may need to append new data to existing data.
static int field_get(const char *key, const char *value, size_t valuelen, void *user_data)
{
struct upload_data *data = (struct upload_data *)user_data;
log_debug(DEBUG_API, "Received field: \"%s\" (length %zu bytes)", key, valuelen);
if(is_file)
{
if(data->zip_size + valuelen > MAXZIPSIZE)
{
log_warn("Uploaded Teleporter ZIP archive is too large (limit is %u bytes)",
MAXZIPSIZE);
data->too_large = true;
return MG_FORM_FIELD_HANDLE_ABORT;
}
// Allocate memory for the raw ZIP archive data
data->zip_data = realloc(data->zip_data, data->zip_size + valuelen);
// Copy the raw ZIP archive data
memcpy(data->zip_data + data->zip_size, value, valuelen);
// Store the size of the ZIP archive raw data
data->zip_size += valuelen;
log_debug(DEBUG_API, "Received ZIP archive (%zu bytes, buffer is now %zu bytes)",
valuelen, data->zip_size);
}
else if(is_sid)
{
// Allocate memory for the SID
data->sid = calloc(valuelen + 1, sizeof(char));
// Copy the SID string
memcpy(data->sid, value, valuelen);
// Add terminating NULL byte (memcpy does not do this)
data->sid[valuelen] = '\0';
}
// If there is more data in this field, get the next chunk.
// Otherwise: handle the next field.
return MG_FORM_FIELD_HANDLE_GET;
}
// We don't use this function, but it is required by the CivetWeb API
static int field_stored(const char *path, long long file_size, void *user_data)
{
return 0;
}
static int free_upload_data(struct upload_data *data)
{
// Free allocated memory
if(data->zip_filename)
{
free(data->zip_filename);
data->zip_filename = NULL;
}
if(data->sid)
{
free(data->sid);
data->sid = NULL;
}
if(data->zip_data)
{
free(data->zip_data);
data->zip_data = NULL;
}
return 0;
}
static int api_teleporter_POST(struct ftl_conn *api)
{
struct upload_data data;
memset(&data, 0, sizeof(struct upload_data));
const struct mg_request_info *req_info = mg_get_request_info(api->conn);
struct mg_form_data_handler fdh = {field_found, field_get, field_stored, &data};
// Disallow large ZIP archives (> 50 MB) to prevent DoS attacks.
// Typically, the ZIP archive size should be around 30-100 kB.
if(req_info->content_length > MAXZIPSIZE)
{
free_upload_data(&data);
return send_json_error(api, 400,
"bad_request",
"ZIP archive too large",
NULL);
}
// Call the form handler to process the POST request content
const int ret = mg_handle_form_request(api->conn, &fdh);
if(ret < 0)
{
free_upload_data(&data);
return send_json_error(api, 400,
"bad_request",
"Invalid form request",
NULL);
}
// Check if we received something we consider being a file
if(data.zip_data == NULL || data.zip_size == 0)
{
free_upload_data(&data);
return send_json_error(api, 400,
"bad_request",
"No ZIP archive received",
NULL);
}
// Check if the file we received is too large
if(data.too_large)
{
free_upload_data(&data);
return send_json_error(api, 400,
"bad_request",
"ZIP archive too large",
NULL);
}
/*
// Set the payload to the SID we received (if available)
if(data.sid != NULL)
{
const size_t bufsize = strlen(data.sid) + 5;
api->payload.raw = calloc(bufsize, sizeof(char));
strncpy(api->payload.raw, "sid=", 5);
strncat(api->payload.raw, data.sid, bufsize - 4);
}
// Check if the client is authorized to use this API endpoint
if(check_client_auth(api) == API_AUTH_UNAUTHORIZED)
{
free_upload_data(&data);
return send_json_unauthorized(api);
}
*/
// Process what we received
char hint[ERRBUF_SIZE];
memset(hint, 0, sizeof(hint));
cJSON *json_files = JSON_NEW_ARRAY();
const char *error = read_teleporter_zip(data.zip_data, data.zip_size, hint, json_files);
if(error != NULL)
{
char msg[strlen(error) + strlen(hint) + 4];
memset(msg, 0, sizeof(msg));
strncpy(msg, error, sizeof(msg));
if(strlen(hint) > 0)
{
// Concatenate error message and hint into a single string
strcat(msg, ": ");
strcat(msg, hint);
}
free_upload_data(&data);
return send_json_error(api, 400,
"bad_request",
"Invalid ZIP archive",
msg);
}
// Free allocated memory
free_upload_data(&data);
// Send response
cJSON *json = JSON_NEW_OBJECT();
JSON_ADD_ITEM_TO_OBJECT(json, "files", json_files);
JSON_SEND_OBJECT(json);
}
int api_teleporter(struct ftl_conn *api)
{
if(api->method == HTTP_GET)
return api_teleporter_GET(api);
if(api->method == HTTP_POST)
return api_teleporter_POST(api);
return 0;
}
}
+6 -4
View File
@@ -43,9 +43,9 @@
#include "config/cli.h"
#include "config/config.h"
// compression functions
#include "miniz/compression.h"
#include "compression/gzip.h"
// teleporter functions
#include "miniz/teleporter.h"
#include "compression/teleporter.h"
// defined in dnsmasq.c
extern void print_dnsmasq_version(const char *yellow, const char *green, const char *bold, const char *normal);
@@ -234,7 +234,8 @@ void parse_args(int argc, char* argv[])
{
// Enable stdout printing
cli_mode = true;
readFTLconf(false);
log_ctrl(false, true);
readFTLconf(&config, false);
if(argc == 3)
exit(get_config_from_CLI(argv[2]) ? EXIT_SUCCESS : EXIT_FAILURE);
else if(argc == 4)
@@ -252,7 +253,8 @@ void parse_args(int argc, char* argv[])
{
// Enable stdout printing
cli_mode = true;
readFTLconf(false);
log_ctrl(false, true);
readFTLconf(&config, false);
exit(write_teleporter_zip_to_disk() ? EXIT_SUCCESS : EXIT_FAILURE);
}
+1 -2
View File
@@ -19,7 +19,6 @@
static const unsigned int capabilityIDs[] = { CAP_CHOWN , CAP_DAC_OVERRIDE , CAP_DAC_READ_SEARCH , CAP_FOWNER , CAP_FSETID , CAP_KILL , CAP_SETGID , CAP_SETUID , CAP_SETPCAP , CAP_LINUX_IMMUTABLE , CAP_NET_BIND_SERVICE , CAP_NET_BROADCAST , CAP_NET_ADMIN , CAP_NET_RAW , CAP_IPC_LOCK , CAP_IPC_OWNER , CAP_SYS_MODULE , CAP_SYS_RAWIO , CAP_SYS_CHROOT , CAP_SYS_PTRACE , CAP_SYS_PACCT , CAP_SYS_ADMIN , CAP_SYS_BOOT , CAP_SYS_NICE , CAP_SYS_RESOURCE , CAP_SYS_TIME , CAP_SYS_TTY_CONFIG , CAP_MKNOD , CAP_LEASE , CAP_AUDIT_WRITE , CAP_AUDIT_CONTROL , CAP_SETFCAP };
static const char* capabilityNames[] = {"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_DAC_READ_SEARCH", "CAP_FOWNER", "CAP_FSETID", "CAP_KILL", "CAP_SETGID", "CAP_SETUID", "CAP_SETPCAP", "CAP_LINUX_IMMUTABLE", "CAP_NET_BIND_SERVICE", "CAP_NET_BROADCAST", "CAP_NET_ADMIN", "CAP_NET_RAW", "CAP_IPC_LOCK", "CAP_IPC_OWNER", "CAP_SYS_MODULE", "CAP_SYS_RAWIO", "CAP_SYS_CHROOT", "CAP_SYS_PTRACE", "CAP_SYS_PACCT", "CAP_SYS_ADMIN", "CAP_SYS_BOOT", "CAP_SYS_NICE", "CAP_SYS_RESOURCE", "CAP_SYS_TIME", "CAP_SYS_TTY_CONFIG", "CAP_MKNOD", "CAP_LEASE", "CAP_AUDIT_WRITE", "CAP_AUDIT_CONTROL", "CAP_SETFCAP"};
static const unsigned int numCaps = sizeof(capabilityIDs) / sizeof(*capabilityIDs);
bool check_capabilities(void)
{
@@ -54,7 +53,7 @@ bool check_capabilities(void)
log_debug(DEBUG_CAPS, "***************************************");
log_debug(DEBUG_CAPS, "* Linux capability debugging enabled *");
for(unsigned int i = 0u; i < numCaps; i++)
for(unsigned int i = 0u; i < ArraySize(capabilityIDs); i++)
{
const unsigned int capid = capabilityIDs[i];
log_debug(DEBUG_CAPS, "* %-24s (%02u) = %s%s%s *",
+24 -24
View File
@@ -216,7 +216,7 @@ mg_handle_form_request(struct mg_connection *conn,
if (0 != strcmp(conn->request_info.request_method, "GET")) {
/* No body data, but not a GET request.
* This is not a valid form request. */
return -1;
return -2;
}
/* GET request: form data is in the query string. */
@@ -226,7 +226,7 @@ mg_handle_form_request(struct mg_connection *conn,
data = conn->request_info.query_string;
if (!data) {
/* No query string. */
return -1;
return -3;
}
/* Split data in a=1&b=xy&c=3&c=4 ... */
@@ -400,7 +400,7 @@ mg_handle_form_request(struct mg_connection *conn,
r = mg_read(conn, buf + (size_t)buf_fill, to_read);
if ((r < 0) || ((r == 0) && all_data_read)) {
/* read error */
return -1;
return -4;
}
if (r == 0) {
/* TODO: Create a function to get "all_data_read" from
@@ -538,7 +538,7 @@ mg_handle_form_request(struct mg_connection *conn,
mg_fclose(&fstore.access);
remove_bad_file(conn, path);
}
return -1;
return -5;
#endif /* NO_FILESYSTEMS */
}
if (r == 0) {
@@ -620,7 +620,7 @@ mg_handle_form_request(struct mg_connection *conn,
/* There has to be a BOUNDARY definition in the Content-Type header */
if (mg_strncasecmp(content_type + bl, "BOUNDARY=", 9)) {
/* Malformed request */
return -1;
return -6;
}
/* Copy boundary string to variable "boundary" */
@@ -633,7 +633,7 @@ mg_handle_form_request(struct mg_connection *conn,
"%s: Cannot allocate memory for boundary [%lu]",
__func__,
(unsigned long)bl);
return -1;
return -7;
}
memcpy(boundary, fbeg, bl);
boundary[bl] = 0;
@@ -645,7 +645,7 @@ mg_handle_form_request(struct mg_connection *conn,
if ((!hbuf) || (*hbuf != '"')) {
/* Malformed request */
mg_free(boundary);
return -1;
return -8;
}
*hbuf = 0;
memmove(boundary, boundary + 1, bl);
@@ -666,13 +666,13 @@ mg_handle_form_request(struct mg_connection *conn,
* Requests with long boundaries are not RFC compliant, maybe they
* are intended attacks to interfere with this algorithm. */
mg_free(boundary);
return -1;
return -9;
}
if (bl < 4) {
/* Sanity check: A boundary string of less than 4 bytes makes
* no sense either. */
mg_free(boundary);
return -1;
return -10;
}
for (part_no = 0;; part_no++) {
@@ -687,7 +687,7 @@ mg_handle_form_request(struct mg_connection *conn,
if ((r < 0) || ((r == 0) && all_data_read)) {
/* read error */
mg_free(boundary);
return -1;
return -11;
}
if (r == 0) {
all_data_read = (buf_fill == 0);
@@ -698,7 +698,7 @@ mg_handle_form_request(struct mg_connection *conn,
if (buf_fill < 1) {
/* No data */
mg_free(boundary);
return -1;
return -12;
}
if (part_no == 0) {
@@ -716,12 +716,12 @@ mg_handle_form_request(struct mg_connection *conn,
if (buf[0] != '-' || buf[1] != '-') {
/* Malformed request */
mg_free(boundary);
return -1;
return -13;
}
if (0 != strncmp(buf + 2, boundary, bl)) {
/* Malformed request */
mg_free(boundary);
return -1;
return -14;
}
if (buf[bl + 2] != '\r' || buf[bl + 3] != '\n') {
/* Every part must end with \r\n, if there is another part.
@@ -730,7 +730,7 @@ mg_handle_form_request(struct mg_connection *conn,
|| (strncmp(buf + bl + 2, "--\r\n", 4))) {
/* Malformed request */
mg_free(boundary);
return -1;
return -15;
}
/* End of the request */
break;
@@ -742,7 +742,7 @@ mg_handle_form_request(struct mg_connection *conn,
if (!hend) {
/* Malformed request */
mg_free(boundary);
return -1;
return -16;
}
part_header.num_headers =
@@ -750,7 +750,7 @@ mg_handle_form_request(struct mg_connection *conn,
if ((hend + 2) != hbuf) {
/* Malformed request */
mg_free(boundary);
return -1;
return -17;
}
/* Skip \r\n\r\n */
@@ -764,7 +764,7 @@ mg_handle_form_request(struct mg_connection *conn,
if (!content_disp) {
/* Malformed request */
mg_free(boundary);
return -1;
return -18;
}
/* Get the mandatory name="..." part of the Content-Disposition
@@ -788,7 +788,7 @@ mg_handle_form_request(struct mg_connection *conn,
if (!nend) {
/* Malformed request */
mg_free(boundary);
return -1;
return -19;
}
} else {
/* name= without quotes is also allowed */
@@ -800,7 +800,7 @@ mg_handle_form_request(struct mg_connection *conn,
if (!nbeg) {
/* Malformed request */
mg_free(boundary);
return -1;
return -20;
}
nbeg += 5;
@@ -835,7 +835,7 @@ mg_handle_form_request(struct mg_connection *conn,
/* Malformed request (the filename field is optional, but if
* it exists, it needs to be terminated correctly). */
mg_free(boundary);
return -1;
return -21;
}
/* TODO: check Content-Type */
@@ -868,7 +868,7 @@ mg_handle_form_request(struct mg_connection *conn,
if (!(((ptrdiff_t)fbeg > (ptrdiff_t)nend)
|| ((ptrdiff_t)nbeg > (ptrdiff_t)fend))) {
mg_free(boundary);
return -1;
return -22;
}
/* Call callback for new field */
@@ -917,7 +917,7 @@ mg_handle_form_request(struct mg_connection *conn,
/* Not enough data stored. */
/* Incomplete request. */
mg_free(boundary);
return -1;
return -23;
}
/* Subtract the boundary length, to deal with
@@ -980,7 +980,7 @@ mg_handle_form_request(struct mg_connection *conn,
}
#endif /* NO_FILESYSTEMS */
mg_free(boundary);
return -1;
return -24;
}
/* r==0 already handled, all_data_read is false here */
@@ -1076,7 +1076,7 @@ mg_handle_form_request(struct mg_connection *conn,
}
/* Unknown Content-Type */
return -1;
return -25;
}
/* End of handle_form.inl */
+21
View File
@@ -0,0 +1,21 @@
# 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
# /src/compression/CMakeList.txt
#
# This file is copyright under the latest version of the EUPL.
# Please see LICENSE file for your rights under this license.
set(sources
gzip.c
gzip.h
teleporter.c
teleporter.h
)
add_library(compression OBJECT ${sources})
target_compile_options(compression PRIVATE)
target_compile_options(compression PRIVATE ${EXTRAWARN})
target_include_directories(compression PRIVATE ${PROJECT_SOURCE_DIR}/src)
@@ -3,7 +3,7 @@
* Network-wide ad blocking via your own hardware.
*
* FTL Engine
* Compression routines
* GZIP compression routines
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
@@ -14,8 +14,8 @@
#include <string.h>
// le32toh and friends
#include <endian.h>
#include "miniz.h"
#include "compression.h"
#include "miniz/miniz.h"
#include "gzip.h"
#include "log.h"
static int mz_uncompress2_raw(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong *pSource_len);
@@ -152,7 +152,10 @@ static bool inflate_buffer(unsigned char *buffer_compressed, mz_ulong size_compr
// +---+---+---+---+---+---+---+---+
//
// XLEN: This contains the length of the extra field, in bytes.
const uint16_t xlen = le16toh(*(uint16_t*)(buffer_compressed + 10));
uint16_t xlen = 0u;
for(unsigned int i = 0; i < 2; i++)
xlen |= buffer_compressed[10 + i] << (i * 8);
xlen = le16toh(xlen);
if(size_compressed < 12u + xlen)
{
log_warn("Invalid GZIP header");
@@ -161,6 +164,7 @@ static bool inflate_buffer(unsigned char *buffer_compressed, mz_ulong size_compr
// Move compressed data to the left
memmove(buffer_compressed + 10, buffer_compressed + 12 + xlen, size_compressed - (12 + xlen));
size_compressed -= 2 + xlen;
}
// Skip file name (if present)
@@ -196,6 +200,7 @@ static bool inflate_buffer(unsigned char *buffer_compressed, mz_ulong size_compr
// Move compressed blocks to the beginning of the in buffer
memmove(buffer_compressed + 10, buffer_compressed + i, size_compressed - i);
size_compressed -= i - 10;
}
// Skip file comment (if present)
@@ -231,11 +236,15 @@ static bool inflate_buffer(unsigned char *buffer_compressed, mz_ulong size_compr
// Move compressed blocks to the beginning of the in
memmove(buffer_compressed + 10, buffer_compressed + i, size_compressed - i);
size_compressed -= i - 10;
}
// Get the size of the uncompressed file from the GZIP footer (last 4
// bytes of the file)
*size_uncompressed = le32toh(*(uint32_t*)(buffer_compressed + size_compressed - 4));
*size_uncompressed = 0u;
for(unsigned int i = 0; i < 4; i++)
*size_uncompressed |= buffer_compressed[size_compressed - 4 + i] << (i * 8);
*size_uncompressed = le32toh(*size_uncompressed);
if(*size_uncompressed == 0 || *size_uncompressed > 0x10000000)
{
log_warn("File is empty or too large");
@@ -248,7 +257,10 @@ static bool inflate_buffer(unsigned char *buffer_compressed, mz_ulong size_compr
// Extract checksum (stored in the first 4 of the last 8 bytes of the
// file)
uint32_t crc = le32toh(*(uint32_t*)(buffer_compressed + size_compressed - 8));
uint32_t crc = 0u;
for(unsigned int i = 0; i < 4; i++)
crc |= buffer_compressed[size_compressed - 8 + i] << (i * 8);
crc = le32toh(crc);
// ZLIB trailer/footer is an Adler-32 checksum of the uncompressed data.
// We have to strip the uncompressed size from the GZIP footer.
@@ -281,36 +293,44 @@ static bool inflate_buffer(unsigned char *buffer_compressed, mz_ulong size_compr
return true;
}
bool inflate_file(const char *infile, const char *outfile, bool verbose)
bool inflate_file(const char *infilename, const char *outfilename, bool verbose)
{
// Read entire file into memory
FILE *fp = fopen(infile, "rb");
if(fp == NULL)
FILE *infile = fopen(infilename, "rb");
if(infile == NULL)
{
log_warn("Failed to open %s: %s", infile, strerror(errno));
log_warn("Failed to open %s: %s", infilename, strerror(errno));
return false;
}
// Create compressed file
FILE *outfile = fopen(outfilename, "wb");
if(outfile == NULL)
{
log_warn("Failed to open %s: %s", outfilename, strerror(errno));
return false;
}
// Get file size
fseek(fp, 0, SEEK_END);
const mz_ulong size_compressed = ftell(fp);
fseek(fp, 0, SEEK_SET);
fseek(infile, 0, SEEK_END);
const mz_ulong size_compressed = ftell(infile);
fseek(infile, 0, SEEK_SET);
// Read file into memory
unsigned char *buffer_compressed = malloc(size_compressed);
if(buffer_compressed == NULL)
{
log_warn("Failed to allocate %lu bytes of memory", (unsigned long)size_compressed);
fclose(fp);
fclose(infile);
return false;
}
if(fread(buffer_compressed, 1, size_compressed, fp) != size_compressed)
if(fread(buffer_compressed, 1, size_compressed, infile) != size_compressed)
{
log_warn("Failed to read %lu bytes from %s", (unsigned long)size_compressed, infile);
fclose(fp);
log_warn("Failed to read %lu bytes from %s", (unsigned long)size_compressed, infilename);
fclose(infile);
return false;
}
fclose(fp);
fclose(infile);
unsigned char *buffer_uncompressed = NULL;
mz_ulong size_uncompressed = 0;
@@ -322,26 +342,20 @@ bool inflate_file(const char *infile, const char *outfile, bool verbose)
// Check if uncompression was successful
if(!success)
{
log_warn("Failed to uncompress %s", infile);
log_warn("Failed to uncompress %s", infilename);
free(buffer_uncompressed);
fclose(fp);
fclose(outfile);
return false;
}
// Create compressed file
fp = fopen(outfile, "wb");
if(fp == NULL)
// Write uncompressed file to disk
if(fwrite(buffer_uncompressed, sizeof(char), size_uncompressed, outfile) != size_uncompressed)
{
log_warn("Failed to open %s: %s", outfile, strerror(errno));
log_warn("Failed to write %lu bytes to %s", (unsigned long)size_uncompressed, outfilename);
fclose(outfile);
return false;
}
if(fwrite(buffer_uncompressed, sizeof(char), size_uncompressed, fp) != size_uncompressed)
{
log_warn("Failed to write %lu bytes to %s", (unsigned long)size_uncompressed, outfile);
fclose(fp);
return false;
}
fclose(fp);
fclose(outfile);
free(buffer_uncompressed);
@@ -353,44 +367,52 @@ bool inflate_file(const char *infile, const char *outfile, bool verbose)
format_memory_size(raw_prefix, size_compressed, &raw_size);
format_memory_size(comp_prefix, size_uncompressed, &comp_size);
log_info("Uncompressed %s (%.1f%sB) to %s (%.1f%sB), %.1f%% size increase",
infile, raw_size, raw_prefix, outfile, comp_size, comp_prefix,
infilename, raw_size, raw_prefix, outfilename, comp_size, comp_prefix,
100.0*size_uncompressed / size_compressed - 100.0);
}
return true;
}
bool deflate_file(const char *infile, const char *outfile, bool verbose)
bool deflate_file(const char *infilename, const char *outfilename, bool verbose)
{
// Read entire file into memory
FILE *fp = fopen(infile, "rb");
if(fp == NULL)
FILE *infile = fopen(infilename, "rb");
if(infile == NULL)
{
log_warn("Failed to open %s: %s", infile, strerror(errno));
log_warn("Failed to open %s for reading: %s", infilename, strerror(errno));
return false;
}
// Create compressed file
FILE* outfile = fopen(outfilename, "wb");
if(outfile == NULL)
{
log_warn("Failed to open %s for writing: %s", outfilename, strerror(errno));
return false;
}
// Get file size
fseek(fp, 0, SEEK_END);
const mz_ulong size_uncompressed = ftell(fp);
fseek(fp, 0, SEEK_SET);
fseek(infile, 0, SEEK_END);
const mz_ulong size_uncompressed = ftell(infile);
fseek(infile, 0, SEEK_SET);
// Read file into memory
unsigned char *buffer_uncompressed = malloc(size_uncompressed);
if(buffer_uncompressed == NULL)
{
log_warn("Failed to allocate %lu bytes of memory", (unsigned long)size_uncompressed);
fclose(fp);
fclose(infile);
return false;
}
if(fread(buffer_uncompressed, 1, size_uncompressed, fp) != size_uncompressed)
if(fread(buffer_uncompressed, 1, size_uncompressed, infile) != size_uncompressed)
{
log_warn("Failed to read %lu bytes from %s", (unsigned long)size_uncompressed, infile);
fclose(fp);
log_warn("Failed to read %lu bytes from %s", (unsigned long)size_uncompressed, infilename);
fclose(infile);
free(buffer_uncompressed);
return false;
}
fclose(fp);
fclose(infile);
unsigned char *buffer_compressed = NULL;
mz_ulong size_compressed = 0;
@@ -403,32 +425,24 @@ bool deflate_file(const char *infile, const char *outfile, bool verbose)
// Check if compression was successful
if(!success)
{
log_warn("Failed to compress %s", infile);
log_warn("Failed to compress %s", infilename);
free(buffer_uncompressed);
if(buffer_compressed)
free(buffer_compressed);
fclose(fp);
fclose(outfile);
return false;
}
// Create compressed file
fp = fopen(outfile, "wb");
if(fp == NULL)
// Write compressed data to file
if(fwrite(buffer_compressed, sizeof(char), size_compressed, outfile) != size_compressed)
{
log_warn("Failed to open %s: %s", outfile, strerror(errno));
log_warn("Failed to write %lu bytes to %s", (unsigned long)size_compressed, outfilename);
fclose(outfile);
free(buffer_uncompressed);
free(buffer_compressed);
return false;
}
if(fwrite(buffer_compressed, sizeof(char), size_compressed, fp) != size_compressed)
{
log_warn("Failed to write %lu bytes to %s", (unsigned long)size_compressed, outfile);
fclose(fp);
free(buffer_uncompressed);
free(buffer_compressed);
return false;
}
fclose(fp);
fclose(outfile);
free(buffer_compressed);
@@ -444,7 +458,8 @@ bool deflate_file(const char *infile, const char *outfile, bool verbose)
format_memory_size(raw_prefix, size_uncompressed, &raw_size);
format_memory_size(comp_prefix, csize, &comp_size);
log_info("Compressed %s (%.1f%sB) to %s (%.1f%sB), %.1f%% size reduction",
infile, raw_size, raw_prefix, outfile, comp_size, comp_prefix,
infilename, raw_size, raw_prefix,
outfilename, comp_size, comp_prefix,
100.0 - 100.0*csize / size_uncompressed);
}
@@ -3,16 +3,16 @@
* Network-wide ad blocking via your own hardware.
*
* FTL Engine
* Compression routines
* GZIP compression routines
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
#ifndef COMPRESSION_H
#define COMPRESSION_H
#ifndef GZIP_H
#define GZIP_H
#include <stdbool.h>
bool deflate_file(const char *in, const char *out, bool verbose);
bool inflate_file(const char *infile, const char *outfile, bool verbose);
#endif // COMPRESSION_H
#endif // GZIP_H
+678
View File
@@ -0,0 +1,678 @@
/* 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
* Teleporter un-/compression routines
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
#include "FTL.h"
#include "compression/teleporter.h"
#include "config/config.h"
// hostname()
#include "daemon.h"
// get_timestr(), TIMESTR_SIZE
#include "log.h"
// directory_exists()
#include "files.h"
// DIR, dirent, opendir(), readdir(), closedir()
#include <dirent.h>
// sqlite3
#include "database/sqlite3.h"
// toml_parse()
#include "config/tomlc99/toml.h"
// readFTLtoml()
#include "config/toml_reader.h"
// writeFTLtoml()
#include "config/toml_writer.h"
// write_dnsmasq_config()
#include "config/dnsmasq_config.h"
// lock_shm(), unlock_shm()
#include "shmem.h"
// rotate_file()
#include "files.h"
// cJSON
#include "cJSON/cJSON.h"
// set_event()
#include "events.h"
// Tables to copy from the gravity database to the Teleporter database
static const char *gravity_tables[] = {
"info",
"group",
"adlist",
"adlist_by_group",
"domainlist",
"domainlist_by_group",
"client",
"client_by_group",
"domain_audit"
};
// Tables to copy from the FTL database to the Teleporter database
static const char *ftl_tables[] = {
"message",
"aliasclient",
"network",
"network_addresses"
};
// List of files to process from a Teleporter ZIP archive
static const char *extract_files[] = {
"etc/pihole/pihole.toml",
"etc/pihole/dhcp.leases",
"etc/pihole/gravity.db"
};
// Create database in memory, copy selected tables to it, serialize and return a memory pointer to it
static bool create_teleporter_database(const char *filename, const char **tables, const unsigned int num_tables,
void **buffer, size_t *size)
{
// Open in-memory sqlite3 database
sqlite3 *db;
if(sqlite3_open_v2(":memory:", &db, SQLITE_OPEN_READWRITE, NULL) != SQLITE_OK)
{
log_warn("Failed to open in-memory database: %s", sqlite3_errmsg(db));
return false;
}
// Attach the FTL database to the in-memory database
char *err = NULL;
char attach_stmt[128] = "";
snprintf(attach_stmt, sizeof(attach_stmt), "ATTACH DATABASE '%s' AS \"disk\";", filename);
if(sqlite3_exec(db, attach_stmt, NULL, NULL, &err) != SQLITE_OK)
{
log_warn("Failed to attach database \"%s\" to in-memory database: %s", filename, err);
sqlite3_free(err);
sqlite3_close(db);
return false;
}
// Loop over the tables and copy them to the in-memory database
for(unsigned int i = 0; i < num_tables; i++)
{
char create_stmt[128] = "";
// Create in-memory table copy
snprintf(create_stmt, sizeof(create_stmt), "CREATE TABLE \"%s\" AS SELECT * FROM disk.\"%s\";", tables[i], tables[i]);
if(sqlite3_exec(db, create_stmt, NULL, NULL, &err) != SQLITE_OK)
{
log_warn("Failed to create %s in in-memory database: %s", tables[i], err);
sqlite3_free(err);
sqlite3_close(db);
return false;
}
}
// Detach the FTL database from the in-memory database
if(sqlite3_exec(db, "DETACH DATABASE 'disk';", NULL, NULL, &err) != SQLITE_OK)
{
log_warn("Failed to detach FTL database from in-memory database: %s", err);
sqlite3_free(err);
sqlite3_close(db);
return false;
}
// Serialize the in-memory database to a buffer
// The sqlite3_serialize(D,S,P,F) interface returns a pointer to memory that
// is a serialization of the S database on database connection D. If P is
// not a NULL pointer, then the size of the database in bytes is written
// into *P.
// For an ordinary on-disk database file, the serialization is just a copy
// of the disk file. For an in-memory database or a "TEMP" database, the
// serialization is the same sequence of bytes which would be written to
// disk if that database where backed up to disk.
// The usual case is that sqlite3_serialize() copies the serialization of
// the database into memory obtained from sqlite3_malloc64() and returns a
// pointer to that memory. The caller is responsible for freeing the
// returned value to avoid a memory leak.
sqlite3_int64 isize = 0;
*buffer = sqlite3_serialize(db, "main", &isize, 0);
*size = isize;
if(*buffer == NULL)
{
log_warn("Failed to serialize in-memory database to buffer: %s", sqlite3_errmsg(db));
sqlite3_close(db);
return false;
}
// Close the in-memory database
sqlite3_close(db);
return true;
}
const char *generate_teleporter_zip(mz_zip_archive *zip, char filename[128], void *ptr, size_t *size)
{
// Initialize ZIP archive
memset(zip, 0, sizeof(*zip));
// Start with 64KB allocation size (pihole.TOML is slightly larger than 32KB
// at the time of writing thjs)
if(!mz_zip_writer_init_heap(zip, 0, 64*1024))
{
return "Failed creating heap ZIP archive";
}
// Add pihole.toml to the ZIP archive
const char *file_comment = "Pi-hole's configuration";
const char *file_path = GLOBALTOMLPATH;
if(!mz_zip_writer_add_file(zip, file_path+1, file_path, file_comment, (uint16_t)strlen(file_comment), MZ_BEST_COMPRESSION))
{
mz_zip_writer_end(zip);
return "Failed to add "GLOBALTOMLPATH" to heap ZIP archive!";
}
// Add /etc/hosts to the ZIP archive
file_comment = "System's HOSTS file";
file_path = "/etc/hosts";
if(!mz_zip_writer_add_file(zip, file_path+1, file_path, file_comment, (uint16_t)strlen(file_comment), MZ_BEST_COMPRESSION))
{
mz_zip_writer_end(zip);
return "Failed to add /etc/hosts to heap ZIP archive!";
}
// Add /etc/pihole/dhcp.lease to the ZIP archive if it exists
file_comment = "DHCP leases file";
file_path = "/etc/pihole/dhcp.leases";
if(file_exists(file_path) && !mz_zip_writer_add_file(zip, file_path+1, file_path, file_comment, (uint16_t)strlen(file_comment), MZ_BEST_COMPRESSION))
{
mz_zip_writer_end(zip);
return "Failed to add /etc/hosts to heap ZIP archive!";
}
const char *directory = "/etc/dnsmasq.d";
if(directory_exists(directory))
{
// Loop over all files and add them to the ZIP archive
DIR *dir;
struct dirent *ent;
if((dir = opendir(directory)) != NULL)
{
// Loop over all files in the directory
while((ent = readdir(dir)) != NULL)
{
// Skip "." and ".."
if(strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
continue;
// Construct full path to file
char fullpath[128] = "";
snprintf(fullpath, 128, "%s/%s", directory, ent->d_name);
// Add file to ZIP archive
file_comment = "dnsmasq configuration file";
file_path = fullpath;
if(!mz_zip_writer_add_file(zip, file_path+1, file_path, file_comment, (uint16_t)strlen(file_comment), MZ_BEST_COMPRESSION))
continue;
}
closedir(dir);
}
}
// Add (a reduced version of) the gravity database to the ZIP archive
void *dbbuf = NULL;
size_t dbsize = 0u;
if(create_teleporter_database(config.files.gravity.v.s, gravity_tables, ArraySize(gravity_tables), &dbbuf, &dbsize))
{
// Add gravity database to ZIP archive
file_comment = "Pi-hole's gravity database";
file_path = config.files.gravity.v.s;
if(file_path[0] == '/')
file_path++;
if(!mz_zip_writer_add_mem_ex(zip, file_path, dbbuf, dbsize, file_comment, (uint16_t)strlen(file_comment), MZ_BEST_COMPRESSION, 0, 0))
{
sqlite3_free(dbbuf);
mz_zip_writer_end(zip);
return "Failed to add gravity database to heap ZIP archive!";
}
sqlite3_free(dbbuf);
}
else
{
mz_zip_writer_end(zip);
return "Failed to create gravity database for heap ZIP archive!";
}
if(create_teleporter_database(config.files.database.v.s, ftl_tables, ArraySize(ftl_tables), &dbbuf, &dbsize))
{
// Add FTL database to ZIP archive
file_comment = "Pi-hole's FTL database";
file_path = config.files.database.v.s;
if(file_path[0] == '/')
file_path++;
if(!mz_zip_writer_add_mem_ex(zip, file_path, dbbuf, dbsize, file_comment, (uint16_t)strlen(file_comment), MZ_BEST_COMPRESSION, 0, 0))
{
sqlite3_free(dbbuf);
mz_zip_writer_end(zip);
return "Failed to add FTL database to heap ZIP archive!";
}
sqlite3_free(dbbuf);
}
else
{
mz_zip_writer_end(zip);
return "Failed to create FTL database for heap ZIP archive!";
}
// Get the heap data so we can send it to the requesting client
if(!mz_zip_writer_finalize_heap_archive(zip, ptr, size))
{
mz_zip_writer_end(zip);
return "Failed to finalize heap ZIP archive!";
}
// Generate filename for ZIP archive (it has both the hostname and the
// current datetime)
char timestr[TIMESTR_SIZE] = "";
get_timestr(timestr, time(NULL), false, true);
snprintf(filename, 128, "pi-hole_%s_teleporter_%s.zip", hostname(), timestr);
// Everything worked well
return NULL;
}
static const char *test_and_import_pihole_toml(void *ptr, size_t size, char * const hint)
{
// Check if the file is empty
if(size == 0)
return "File etc/pihole/pihole.toml in ZIP archive is empty";
// Create a memory copy that is null-terminated
char *buffer = calloc(size+1, sizeof(char));
if(buffer == NULL)
return "Failed to allocate memory for null-terminated copy of etc/pihole/pihole.toml in ZIP archive";
memcpy(buffer, ptr, size);
buffer[size] = '\0';
// Check if the file is a valid TOML file
toml_table_t *toml = toml_parse(buffer, hint, ERRBUF_SIZE);
if(toml == NULL)
{
free(buffer);
return "File etc/pihole/pihole.toml in ZIP archive is not a valid TOML file";
}
free(buffer);
// Check if the file contains a valid configuration for Pi-hole by parsing it into
// a temporary config struct (teleporter_config)
struct config teleporter_config = { 0 };
duplicate_config(&teleporter_config, &config);
if(!readFTLtoml(&teleporter_config, toml, true))
return "File etc/pihole/pihole.toml in ZIP archive contains invalid TOML configuration";
// Test dnsmasq config in the imported configuration
// The dnsmasq configuration will be overwritten if the test succeeds
if(!write_dnsmasq_config(&teleporter_config, true, hint))
return "File etc/pihole/pihole.toml in ZIP archive contains invalid dnsmasq configuration";
// When we reach this point, we know that the file is a valid TOML file and contains
// a valid configuration for Pi-hole. We can now safely overwrite the current
// configuration with the one from the ZIP archive
// Install new configuration
replace_config(&teleporter_config);
// Write new pihole.toml to disk, the dnsmaq config was already written above
// Also write the custom list to disk
rotate_files(GLOBALTOMLPATH);
writeFTLtoml(true);
write_custom_list();
return NULL;
}
static const char *import_dhcp_leases(void *ptr, size_t size, char * const hint)
{
// We do not check if the file is empty here, as an empty dhcp.leases file is valid
// When we reach this point, we know that the file is a valid dhcp.leases file.
// We can now safely overwrite the current dhcp.leases file with the one from the ZIP archive
// Nevertheless, we rotate the current dhcp.leases file to keep a backup of the previous version
// Rotate current dhcp.leases file
rotate_files(DHCPLEASESFILE);
// Write new dhcp.leases file to disk
FILE *fp = fopen(DHCPLEASESFILE, "w");
if(fp == NULL)
{
strncpy(hint, strerror(errno), ERRBUF_SIZE);
return "Failed to open dhcp.leases file for writing";
}
if(fwrite(ptr, 1, size, fp) != size)
{
strncpy(hint, strerror(errno), ERRBUF_SIZE);
fclose(fp);
return "Failed to write to dhcp.leases file";
}
fclose(fp);
return NULL;
}
static const char *test_and_import_database(void *ptr, size_t size, const char *destination,
const char **tables, const unsigned int num_tables,
char * const hint)
{
// Check if the file is empty
// The first 100 bytes of the database file comprise the database file header.
// See https://www.sqlite.org/fileformat.html, section 1.3
if(size < 100)
{
return "File etc/pihole/gravity.db in ZIP archive is empty";
}
// Check file header to see if this is a SQLite3 database file
// Every valid SQLite database file begins with the following 16 bytes (in
// hex): 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00. This byte sequence
// corresponds to the UTF-8 string "SQLite format 3" including the nul
// terminator character at the end. The nul terminator character is not
// included in the 16 bytes of the header.
// See https://www.sqlite.org/fileformat.html, section 1.3.1
if(memcmp(ptr, "SQLite format 3", 15) != 0)
{
return "File etc/pihole/gravity.db in ZIP archive is not a SQLite3 database file (no header)";
}
// Check if the file is a valid SQlite3 database
// We do this by trying to deserialize the file into a SQLite3 database
// object. If this fails, the file is not a valid SQlite3 database.
sqlite3 *database = NULL;
if(sqlite3_open_v2(":memory:", &database, SQLITE_OPEN_READWRITE, NULL) != SQLITE_OK)
{
strncpy(hint, sqlite3_errmsg(database), ERRBUF_SIZE);
return "Failed to open temporary SQLite3 database";
}
if(sqlite3_deserialize(database, "main", ptr, size, size, SQLITE_DESERIALIZE_READONLY) != SQLITE_OK)
{
strncpy(hint, sqlite3_errmsg(database), ERRBUF_SIZE);
return "File etc/pihole/gravity.db in ZIP archive is not a valid SQLite3 database file";
}
// Run PRAGMA integrity_check on the database to check if the database is
// valid. If the database is valid, the result of the PRAGMA integrity_check
// is "ok". If the database is invalid, the result of the PRAGMA
// integrity_check is a string describing the error.
// See https://www.sqlite.org/pragma.html#pragma_integrity_check
sqlite3_stmt *statement = NULL;
if(sqlite3_prepare_v2(database, "PRAGMA integrity_check;", -1, &statement, NULL) != SQLITE_OK)
{
strncpy(hint, sqlite3_errmsg(database), ERRBUF_SIZE);
sqlite3_finalize(statement);
sqlite3_close(database);
return "Failed to prepare PRAGMA integrity_check statement";
}
if(sqlite3_step(statement) != SQLITE_ROW)
{
strncpy(hint, sqlite3_errmsg(database), ERRBUF_SIZE);
sqlite3_finalize(statement);
sqlite3_close(database);
return "Failed to execute PRAGMA integrity_check statement";
}
if(strcmp((const char *)sqlite3_column_text(statement, 0), "ok") != 0)
{
strncpy(hint, (const char *)sqlite3_column_text(statement, 0), ERRBUF_SIZE);
sqlite3_finalize(statement);
sqlite3_close(database);
return "Database file in ZIP archive is not a valid SQLite3 database (integrity check failed)";
}
// Finalize statement
sqlite3_finalize(statement);
// When we reach this point, we know that the file is a valid SQLite3 database
// ATTACH the database file to the in-memory database
char *err = NULL;
char attach_stmt[128] = "";
snprintf(attach_stmt, sizeof(attach_stmt), "ATTACH DATABASE '%s' AS disk;", destination);
if(sqlite3_exec(database, attach_stmt, NULL, NULL, &err) != SQLITE_OK)
{
sqlite3_free(err);
sqlite3_close(database);
return "Failed to attach database file to in-memory SQLite3 database";
}
// Disable foreign key checks for import
if(sqlite3_exec(database, "PRAGMA foreign_keys = 0;", NULL, NULL, &err) != SQLITE_OK)
{
sqlite3_free(err);
sqlite3_close(database);
return "Failed to disable foreign key checks for import";
}
// Start transaction
if(sqlite3_exec(database, "BEGIN TRANSACTION;", NULL, NULL, &err) != SQLITE_OK)
{
sqlite3_free(err);
sqlite3_close(database);
return "Failed to start transaction";
}
// Loop over the tables
for(unsigned int i = 0; i < num_tables; i++)
{
char stmt[256] = "";
// Delete all rows in the disk table
snprintf(stmt, sizeof(stmt), "DELETE FROM disk.\"%s\";", tables[i]);
if(sqlite3_exec(database, stmt, NULL, NULL, &err) != SQLITE_OK)
{
strncpy(hint, err, ERRBUF_SIZE);
sqlite3_free(err);
sqlite3_close(database);
return "Failed to delete from disk database table";
}
// Store the table in the disk database
// We have to use INSERT OR REPLACE here, because the gravity database
// has several triggers, e.g., immediately recreating the default group
// on (accidental) deletion. This would cause the import to fail due to
// a unique constraint violation.
snprintf(stmt, sizeof(stmt), "INSERT OR REPLACE INTO disk.\"%s\" SELECT * FROM \"%s\";", tables[i], tables[i]);
if(sqlite3_exec(database, stmt, NULL, NULL, &err) != SQLITE_OK)
{
strncpy(hint, err, ERRBUF_SIZE);
sqlite3_free(err);
sqlite3_close(database);
return "Failed to insert into disk database table";
}
log_debug(DEBUG_DATABASE, "Replaced table %s in %s", tables[i], destination);
}
// End transaction
if(sqlite3_exec(database, "END TRANSACTION;", NULL, NULL, &err) != SQLITE_OK)
{
sqlite3_free(err);
sqlite3_close(database);
return "Failed to end transaction";
}
// Detach the database file from the in-memory database
if(sqlite3_exec(database, "DETACH DATABASE disk;", NULL, NULL, &err) != SQLITE_OK)
{
log_warn("Failed to detach disk database: %s", err);
sqlite3_free(err);
sqlite3_close(database);
return "Failed to detach database file from in-memory SQLite3 database";
}
// Close the database
sqlite3_close(database);
// Add event to reload gravity database
set_event(RELOAD_GRAVITY);
return NULL;
}
const char *read_teleporter_zip(char *buffer, const size_t buflen, char * const hint, cJSON *imported_files)
{
// Initialize ZIP archive
mz_zip_archive zip = { 0 };
memset(&zip, 0, sizeof(zip));
log_debug(DEBUG_CONFIG, "Reading ZIP archive from memory buffer (size %zu)", buflen);
// Open ZIP archive from memory buffer
if(!mz_zip_reader_init_mem(&zip, buffer, buflen, 0))
{
strncpy(hint, mz_zip_get_error_string(mz_zip_get_last_error(&zip)), ERRBUF_SIZE);
return "Failed to parse received ZIP archive";
}
// Loop over all files in the ZIP archive
for(mz_uint i = 0; i < mz_zip_reader_get_num_files(&zip); i++)
{
// Get file information
mz_zip_archive_file_stat file_stat;
if(!mz_zip_reader_file_stat(&zip, i, &file_stat))
{
log_warn("Failed to get file information for file %d in ZIP archive: %s",
i, mz_zip_get_error_string(mz_zip_get_last_error(&zip)));
continue;
}
// Check if this file is one of the files we want to extract and process
bool extract = false;
for(size_t j = 0; j < ArraySize(extract_files); j++)
{
if(strcmp(file_stat.m_filename, extract_files[j]) == 0)
{
extract = true;
break;
}
}
if(!extract)
continue;
// Read file into its dedicated memory buffer
void *ptr = malloc(file_stat.m_uncomp_size);
if(ptr == NULL)
{
log_warn("Failed to allocate memory for file %d (%s) in ZIP archive: %s",
i, file_stat.m_filename, mz_zip_get_error_string(mz_zip_get_last_error(&zip)));
continue;
}
if(!mz_zip_reader_extract_to_mem(&zip, i, ptr, file_stat.m_uncomp_size, 0))
{
log_warn("Failed to read file %d (%s) in ZIP archive: %s",
i, file_stat.m_filename, mz_zip_get_error_string(mz_zip_get_last_error(&zip)));
free(ptr);
continue;
}
log_debug(DEBUG_CONFIG, "Processing file %d (%s) in ZIP archive (%zu/%zu bytes, comment: \"%s\", timestamp: %lu)",
i, file_stat.m_filename, file_stat.m_comp_size, file_stat.m_uncomp_size,
file_stat.m_comment, (unsigned long)file_stat.m_time);
// Process file
// Is this "etc/pihole/pihole.toml" ?
if(strcmp(file_stat.m_filename, "etc/pihole/pihole.toml") == 0)
{
// Import Pi-hole configuration
memset(hint, 0, ERRBUF_SIZE);
const char *err = test_and_import_pihole_toml(ptr, file_stat.m_uncomp_size, hint);
if(err != NULL)
{
free(ptr);
return err;
}
log_debug(DEBUG_CONFIG, "Imported Pi-hole configuration: %s", file_stat.m_filename);
}
// Is this "etc/pihole/dhcp.leases"?
else if(strcmp(file_stat.m_filename, "etc/pihole/dhcp.leases") == 0)
{
// Import DHCP leases
memset(hint, 0, ERRBUF_SIZE);
const char *err = import_dhcp_leases(ptr, file_stat.m_uncomp_size, hint);
if(err != NULL)
{
free(ptr);
return err;
}
log_debug(DEBUG_CONFIG, "Imported DHCP leases: %s", file_stat.m_filename);
}
else if(strcmp(file_stat.m_filename, "etc/pihole/gravity.db") == 0)
{
// Import gravity database
memset(hint, 0, ERRBUF_SIZE);
const char *err = test_and_import_database(ptr, file_stat.m_uncomp_size, config.files.gravity.v.s,
gravity_tables, ArraySize(gravity_tables), hint);
if(err != NULL)
{
free(ptr);
return err;
}
log_debug(DEBUG_CONFIG, "Imported database: %s", file_stat.m_filename);
}
else
{
log_warn("Ignoring file %s in Teleporter archive", file_stat.m_filename);
free(ptr);
continue;
}
// Add filename of processed files to JSON array
if(imported_files != NULL && !cJSON_AddItemToArray(imported_files, cJSON_CreateString(file_stat.m_filename)))
log_warn("Failed to add file %s to JSON array", file_stat.m_filename);
// Free allocated memory
free(ptr);
}
// Close ZIP archive
mz_zip_reader_end(&zip);
// Everything worked well
return NULL;
}
bool free_teleporter_zip(mz_zip_archive *zip)
{
return mz_zip_writer_end(zip);
}
bool write_teleporter_zip_to_disk(void)
{
// Generate in-memory ZIP file
mz_zip_archive zip = { 0 };
void *ptr = NULL;
size_t size = 0u;
char filename[128] = "";
const char *error = generate_teleporter_zip(&zip, filename, &ptr, &size);
if(error != NULL)
{
log_err("Failed to create Teleporter ZIP file: %s", error);
return false;
}
// Write file to disk
FILE *fp = fopen(filename, "w");
if(fp == NULL)
{
log_err("Failed to open %s for writing: %s", filename, strerror(errno));
free_teleporter_zip(&zip);
return false;
}
if(fwrite(ptr, 1, size, fp) != size)
{
log_err("Failed to write %zu bytes to %s: %s", size, filename, strerror(errno));
free_teleporter_zip(&zip);
return false;
}
fclose(fp);
// Free allocated ZIP memory
free_teleporter_zip(&zip);
/* Output filename on successful creation */
log_info("%s", filename);
return true;
}
@@ -11,8 +11,11 @@
#define TELEPORTER_H
#include "miniz/miniz.h"
#include "cJSON/cJSON.h"
const char *generate_teleporter_zip(mz_zip_archive *zip, char filename[128], void *ptr, size_t *size);
bool free_teleporter_zip(mz_zip_archive *zip);
bool write_teleporter_zip_to_disk(void);
const char *read_teleporter_zip(char *buffer, const size_t buflen, char *hint, cJSON *json_files);
#endif // TELEPORTER_H
#endif // TELEPORTER_H
+14 -20
View File
@@ -258,13 +258,13 @@ static bool readStringValue(struct conf_item *conf_item, const char *value)
if(!cJSON_IsString(item))
{
log_err("Config setting %s is invalid: element with index %d is not a string", conf_item->k, i);
cJSON_free(elem);
cJSON_Delete(elem);
return false;
}
}
// If we reach this point, all elements are valid
// Free previously allocated JSON array and replace with new
cJSON_free(conf_item->v.json);
cJSON_Delete(conf_item->v.json);
conf_item->v.json = elem;
break;
}
@@ -276,20 +276,20 @@ static bool readStringValue(struct conf_item *conf_item, const char *value)
bool set_config_from_CLI(const char *key, const char *value)
{
// Identify config option
struct config conf_copy;
duplicate_config(&conf_copy);
struct config newconf;
duplicate_config(&newconf, &config);
struct conf_item *conf_item = NULL;
struct conf_item *copy_item = NULL;
struct conf_item *new_item = NULL;
for(unsigned int i = 0; i < CONFIG_ELEMENTS; i++)
{
// Get pointer to (copied) memory location of this conf_item
struct conf_item *item = get_conf_item(&conf_copy, i);
struct conf_item *item = get_conf_item(&newconf, i);
if(strcmp(item->k, key) != 0)
continue;
// This is the config option we are looking for
copy_item = item;
new_item = item;
// Also get pointer to memory location of this conf_item
conf_item = get_conf_item(&config, i);
@@ -299,18 +299,18 @@ bool set_config_from_CLI(const char *key, const char *value)
}
// Check if we found the config option
if(copy_item == NULL)
if(new_item == NULL)
{
log_err("Unknown config option: %s", key);
return false;
}
// Parse value
if(!readStringValue(copy_item, value))
if(!readStringValue(new_item, value))
return false;
// Check if value changed compared to current value
if(!compare_config_item(copy_item, conf_item))
if(!compare_config_item(new_item, conf_item))
{
// Config item changed
@@ -318,7 +318,7 @@ bool set_config_from_CLI(const char *key, const char *value)
if(conf_item->f & FLAG_RESTART_DNSMASQ)
{
char errbuf[ERRBUF_SIZE] = { 0 };
if(!write_dnsmasq_config(&conf_copy, true, errbuf))
if(!write_dnsmasq_config(&newconf, true, errbuf))
{
// Test failed
log_debug(DEBUG_CONFIG, "Config item %s: dnsmasq config test failed", conf_item->k);
@@ -335,22 +335,16 @@ bool set_config_from_CLI(const char *key, const char *value)
}
// Install new configuration
// Backup old config struct (so we can free it)
struct config old_conf;
memcpy(&old_conf, &config, sizeof(struct config));
// Replace old config struct by changed one
memcpy(&config, &conf_copy, sizeof(struct config));
// Free old backup struct
free_config(&old_conf);
replace_config(&newconf);
// Print value
writeTOMLvalue(stdout, -1, copy_item->t, &copy_item->v);
writeTOMLvalue(stdout, -1, new_item->t, &new_item->v);
}
else
{
// No change
log_debug(DEBUG_CONFIG, "Config item %s: Unchanged", conf_item->k);
free_config(&conf_copy);
free_config(&newconf);
// Print value
writeTOMLvalue(stdout, -1, conf_item->t, &conf_item->v);
+653 -597
View File
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -276,19 +276,21 @@ extern struct config config;
// Defined in config.c
void set_all_debug(const bool status);
void initConfig(void);
void readFTLconf(const bool rewrite);
void initConfig(struct config *conf);
void readFTLconf(struct config *conf, const bool rewrite);
bool getLogFilePath(void);
struct conf_item *get_conf_item(struct config *conf, const unsigned int n);
struct conf_item *get_debug_item(const enum debug_flag debug);
unsigned int config_path_depth(char **paths) __attribute__ ((pure));
void duplicate_config(struct config *conf);
void duplicate_config(struct config *dst, struct config *src);
void free_config(struct config *conf);
bool compare_config_item(const struct conf_item *conf_item1, const struct conf_item *conf_item2);
char **gen_config_path(const char *pathin, const char delim);
void free_config_path(char **paths);
bool check_paths_equal(char **paths1, char **paths2, unsigned int max_level) __attribute__ ((pure));
const char *get_conf_type_str(const enum conf_type type) __attribute__ ((const));
void replace_config(struct config *newconf);
void reread_config(void);
// Defined in toml_reader.c
bool getPrivacyLevel(void);
@@ -301,7 +303,7 @@ void set_blockingstatus(bool enabled);
// Add enum items with descriptions
#define CONFIG_ADD_ENUM_OPTIONS(json, opts)({ \
json = cJSON_CreateArray(); \
for(unsigned int i = 0; i < sizeof(opts)/sizeof(*opts); i++) \
for(unsigned int i = 0; i < ArraySize(opts); i++) \
{ \
cJSON *jopt = cJSON_CreateObject(); \
if(opts[i].item[0] >= '0' && opts[i].item[0] <= '9') \
@@ -327,7 +329,7 @@ void set_blockingstatus(bool enabled);
cJSON_AddItemToArray(array, cJSON_Duplicate(item, true)); \
} \
output = cJSON_PrintUnformatted(array); \
cJSON_free(array); \
cJSON_Delete(array); \
} \
else if(cJSON_IsString(json)) \
{ \
+1 -1
View File
@@ -401,7 +401,7 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
{
fputs("# DHCP server setting\n", pihole_conf);
fputs("dhcp-authoritative\n", pihole_conf);
fputs("dhcp-leasefile=/etc/pihole/dhcp.leases\n", pihole_conf);
fputs("dhcp-leasefile="DHCPLEASESFILE"\n", pihole_conf);
fprintf(pihole_conf, "dhcp-range=%s,%s,%s\n",
conf->dhcp.start.v.s,
conf->dhcp.end.v.s,
+1
View File
@@ -27,5 +27,6 @@ bool write_custom_list(void);
#define DNSMASQ_STATIC_LEASES "/etc/pihole/04-pihole-static-dhcp.conf"
#define DNSMASQ_CNAMES "/etc/pihole/05-pihole-custom-cname.conf"
#define DNSMASQ_CUSTOM_LIST "/etc/pihole/custom.list"
#define DHCPLEASESFILE "/etc/pihole/dhcp.leases"
#endif //DNSMASQ_CONFIG_H
+99 -99
View File
@@ -49,7 +49,7 @@ static FILE * __attribute__((nonnull(1), malloc, warn_unused_result)) openFTLcon
return fp;
}
bool getLogFilePathLegacy(FILE *fp)
bool getLogFilePathLegacy(struct config *conf, FILE *fp)
{
const char *path = NULL;
if(fp == NULL)
@@ -67,13 +67,13 @@ bool getLogFilePathLegacy(FILE *fp)
if(buffer == NULL)
{
// Use standard path if no custom path was obtained from the config file
config.files.log.ftl.v.s = strdup("/var/log/pihole/FTL.log");
config.files.log.ftl.t = CONF_STRING_ALLOCATED;
conf->files.log.ftl.v.s = strdup("/var/log/pihole/FTL.log");
conf->files.log.ftl.t = CONF_STRING_ALLOCATED;
// Test if memory allocation was successful
if(config.files.log.ftl.v.s == NULL)
if(conf->files.log.ftl.v.s == NULL)
{
printf("FATAL: Allocating memory for config.files.log.ftl.v.s failed (%s, %i). Exiting.",
printf("FATAL: Allocating memory for conf->files.log.ftl.v.s failed (%s, %i). Exiting.",
strerror(errno), errno);
exit(EXIT_FAILURE);
}
@@ -82,24 +82,24 @@ bool getLogFilePathLegacy(FILE *fp)
else if(sscanf(buffer, "%127ms", &val_buffer) == 0)
{
// Free previously allocated memory (if any)
if(config.files.log.ftl.t == CONF_STRING_ALLOCATED)
free(config.files.log.ftl.v.s);
if(conf->files.log.ftl.t == CONF_STRING_ALLOCATED)
free(conf->files.log.ftl.v.s);
// Set empty file string
config.files.log.ftl.v.s = NULL;
config.files.log.ftl.t = CONF_STRING;
conf->files.log.ftl.v.s = NULL;
conf->files.log.ftl.t = CONF_STRING;
log_info("Using syslog facility");
}
if(val_buffer)
{
// Free previously allocated memory (if any)
if(config.files.log.ftl.t == CONF_STRING_ALLOCATED)
free(config.files.log.ftl.v.s);
if(conf->files.log.ftl.t == CONF_STRING_ALLOCATED)
free(conf->files.log.ftl.v.s);
// Set string
config.files.log.ftl.v.s = val_buffer;
config.files.log.ftl.t = CONF_STRING_ALLOCATED;
conf->files.log.ftl.v.s = val_buffer;
conf->files.log.ftl.t = CONF_STRING_ALLOCATED;
}
fclose(fp);
@@ -107,7 +107,7 @@ bool getLogFilePathLegacy(FILE *fp)
}
// Returns which file was read
const char *readFTLlegacy(void)
const char *readFTLlegacy(struct config *conf)
{
char *buffer;
const char *path = NULL;
@@ -131,18 +131,18 @@ const char *readFTLlegacy(void)
// Only use valid values
if(value == -1 || value >= 0)
config.database.maxDBdays.v.i = value;
conf->database.maxDBdays.v.i = value;
}
// RESOLVE_IPV6
// defaults to: Yes
buffer = parseFTLconf(fp, "RESOLVE_IPV6");
parseBool(buffer, &config.resolver.resolveIPv6.v.b);
parseBool(buffer, &conf->resolver.resolveIPv6.v.b);
// RESOLVE_IPV4
// defaults to: Yes
buffer = parseFTLconf(fp, "RESOLVE_IPV4");
parseBool(buffer, &config.resolver.resolveIPv4.v.b);
parseBool(buffer, &conf->resolver.resolveIPv4.v.b);
// DBINTERVAL
// How often do we store queries in FTL's database [minutes]?
@@ -156,25 +156,25 @@ const char *readFTLlegacy(void)
// - larger than 0.1min (6sec), and
// - smaller than 1440.0min (once a day)
if(fvalue >= 0.1f && fvalue <= 1440.0f)
config.database.DBinterval.v.ui = (int)(fvalue * 60);
conf->database.DBinterval.v.ui = (int)(fvalue * 60);
// DBFILE
// defaults to: "/etc/pihole/pihole-FTL.db"
buffer = parseFTLconf(fp, "DBFILE");
// Use sscanf() to obtain filename from config file parameter only if buffer != NULL
if(!(buffer != NULL && sscanf(buffer, "%127ms", &config.files.database.v.s)))
if(!(buffer != NULL && sscanf(buffer, "%127ms", &conf->files.database.v.s)))
{
// Use standard path if no custom path was obtained from the config file
config.files.database.v.s = config.files.database.d.s;
conf->files.database.v.s = conf->files.database.d.s;
}
if(config.files.database.v.s == NULL || strlen(config.files.database.v.s) == 0)
if(conf->files.database.v.s == NULL || strlen(conf->files.database.v.s) == 0)
{
// Use standard path if path was set to zero but override
// MAXDBDAYS=0 to ensure no queries are stored in the database
config.files.database.v.s = config.files.database.d.s;
config.database.maxDBdays.v.i = 0;
conf->files.database.v.s = conf->files.database.d.s;
conf->database.maxDBdays.v.i = 0;
}
// MAXLOGAGE
@@ -186,7 +186,7 @@ const char *readFTLlegacy(void)
if(buffer != NULL && sscanf(buffer, "%f", &fvalue))
{
if(fvalue >= 0.0f && fvalue <= 1.0f*MAXLOGAGE)
config.database.maxHistory.v.ui = (int)(fvalue * 3600);
conf->database.maxHistory.v.ui = (int)(fvalue * 3600);
}
// PRIVACYLEVEL
@@ -204,10 +204,10 @@ const char *readFTLlegacy(void)
// ignoreLocalhost
// defaults to: false
buffer = parseFTLconf(fp, "IGNORE_LOCALHOST");
parseBool(buffer, &config.dns.ignoreLocalhost.v.b);
parseBool(buffer, &conf->dns.ignoreLocalhost.v.b);
if(buffer != NULL && strcasecmp(buffer, "yes") == 0)
config.dns.ignoreLocalhost.v.b = true;
conf->dns.ignoreLocalhost.v.b = true;
// BLOCKINGMODE
// defaults to: MODE_IP
@@ -216,37 +216,37 @@ const char *readFTLlegacy(void)
// ANALYZE_ONLY_A_AND_AAAA
// defaults to: false
buffer = parseFTLconf(fp, "ANALYZE_ONLY_A_AND_AAAA");
parseBool(buffer, &config.dns.analyzeOnlyAandAAAA.v.b);
parseBool(buffer, &conf->dns.analyzeOnlyAandAAAA.v.b);
if(buffer != NULL && strcasecmp(buffer, "true") == 0)
config.dns.analyzeOnlyAandAAAA.v.b = true;
conf->dns.analyzeOnlyAandAAAA.v.b = true;
// DBIMPORT
// defaults to: Yes
buffer = parseFTLconf(fp, "DBIMPORT");
parseBool(buffer, &config.database.DBimport.v.b);
parseBool(buffer, &conf->database.DBimport.v.b);
// PIDFILE
config.files.pid.v.s = getPath(fp, "PIDFILE", config.files.pid.v.s);
conf->files.pid.v.s = getPath(fp, "PIDFILE", conf->files.pid.v.s);
// SETUPVARSFILE
config.files.setupVars.v.s = getPath(fp, "SETUPVARSFILE", config.files.setupVars.v.s);
conf->files.setupVars.v.s = getPath(fp, "SETUPVARSFILE", conf->files.setupVars.v.s);
// MACVENDORDB
config.files.macvendor.v.s = getPath(fp, "MACVENDORDB", config.files.macvendor.v.s);
conf->files.macvendor.v.s = getPath(fp, "MACVENDORDB", conf->files.macvendor.v.s);
// GRAVITYDB
config.files.gravity.v.s = getPath(fp, "GRAVITYDB", config.files.gravity.v.s);
conf->files.gravity.v.s = getPath(fp, "GRAVITYDB", conf->files.gravity.v.s);
// PARSE_ARP_CACHE
// defaults to: true
buffer = parseFTLconf(fp, "PARSE_ARP_CACHE");
parseBool(buffer, &config.database.network.parseARPcache.v.b);
parseBool(buffer, &conf->database.network.parseARPcache.v.b);
// CNAME_DEEP_INSPECT
// defaults to: true
buffer = parseFTLconf(fp, "CNAME_DEEP_INSPECT");
parseBool(buffer, &config.dns.CNAMEdeepInspect.v.b);
parseBool(buffer, &conf->dns.CNAMEdeepInspect.v.b);
// DELAY_STARTUP
// defaults to: zero (seconds)
@@ -254,15 +254,15 @@ const char *readFTLlegacy(void)
unsigned int unum;
if(buffer != NULL && sscanf(buffer, "%u", &unum) && unum > 0 && unum <= 300)
config.misc.delay_startup.v.ui = unum;
conf->misc.delay_startup.v.ui = unum;
// BLOCK_ESNI
// defaults to: true
buffer = parseFTLconf(fp, "BLOCK_ESNI");
parseBool(buffer, &config.dns.blockESNI.v.b);
parseBool(buffer, &conf->dns.blockESNI.v.b);
// WEBROOT
config.webserver.paths.webroot.v.s = getPath(fp, "WEBROOT", config.webserver.paths.webroot.v.s);
conf->webserver.paths.webroot.v.s = getPath(fp, "WEBROOT", conf->webserver.paths.webroot.v.s);
// WEBPORT
// On which port should FTL's API be listening?
@@ -271,12 +271,12 @@ const char *readFTLlegacy(void)
value = 0;
if(buffer != NULL && strlen(buffer) > 0)
config.webserver.port.v.s = strdup(buffer);
conf->webserver.port.v.s = strdup(buffer);
// WEBHOME
// From which sub-directory is the web interface served from?
// Defaults to: /admin/ (both slashes are needed!)
config.webserver.paths.webhome.v.s = getPath(fp, "WEBHOME", config.webserver.paths.webhome.v.s);
conf->webserver.paths.webhome.v.s = getPath(fp, "WEBHOME", conf->webserver.paths.webhome.v.s);
// WEBACL
// Default: allow all access
@@ -300,12 +300,12 @@ const char *readFTLlegacy(void)
//
buffer = parseFTLconf(fp, "WEBACL");
if(buffer != NULL)
config.webserver.acl.v.s = strdup(buffer);
conf->webserver.acl.v.s = strdup(buffer);
// API_AUTH_FOR_LOCALHOST
// defaults to: true
buffer = parseFTLconf(fp, "API_AUTH_FOR_LOCALHOST");
parseBool(buffer, &config.webserver.api.localAPIauth.v.b);
parseBool(buffer, &conf->webserver.api.localAPIauth.v.b);
// API_SESSION_TIMEOUT
// How long should a session be considered valid after login?
@@ -314,18 +314,18 @@ const char *readFTLlegacy(void)
value = 0;
if(buffer != NULL && sscanf(buffer, "%i", &value) && value > 0)
config.webserver.api.sessionTimeout.v.ui = value;
conf->webserver.api.sessionTimeout.v.ui = value;
// API_PRETTY_JSON
// defaults to: false
buffer = parseFTLconf(fp, "API_PRETTY_JSON");
parseBool(buffer, &config.webserver.api.prettyJSON.v.b);
parseBool(buffer, &conf->webserver.api.prettyJSON.v.b);
// API_ERROR_LOG
config.files.ph7_error.v.s = getPath(fp, "API_ERROR_LOG", config.files.ph7_error.v.s);
conf->files.ph7_error.v.s = getPath(fp, "API_ERROR_LOG", conf->files.ph7_error.v.s);
// API_INFO_LOG
config.files.http_info.v.s = getPath(fp, "API_INFO_LOG", config.files.http_info.v.s);
conf->files.http_info.v.s = getPath(fp, "API_INFO_LOG", conf->files.http_info.v.s);
// NICE
// Shall we change the nice of the current process?
@@ -350,7 +350,7 @@ const char *readFTLlegacy(void)
if(buffer != NULL &&
sscanf(buffer, "%i", &ivalue) &&
ivalue > 0 && ivalue <= 8760) // 8760 days = 24 years
config.database.network.expire.v.ui = ivalue;
conf->database.network.expire.v.ui = ivalue;
// NAMES_FROM_NETDB
// Should we use the fallback option to try to obtain client names from
@@ -361,30 +361,30 @@ const char *readFTLlegacy(void)
// device. This behavior can be disabled using NAMES_FROM_NETDB=false
// defaults to: true
buffer = parseFTLconf(fp, "NAMES_FROM_NETDB");
parseBool(buffer, &config.resolver.networkNames.v.b);
parseBool(buffer, &conf->resolver.networkNames.v.b);
// EDNS0_ECS
// Should we overwrite the query source when client information is
// provided through EDNS0 client subnet (ECS) information?
// defaults to: true
buffer = parseFTLconf(fp, "EDNS0_ECS");
parseBool(buffer, &config.dns.EDNS0ECS.v.b);
parseBool(buffer, &conf->dns.EDNS0ECS.v.b);
// REFRESH_HOSTNAMES
// defaults to: IPV4
buffer = parseFTLconf(fp, "REFRESH_HOSTNAMES");
if(buffer != NULL && strcasecmp(buffer, "ALL") == 0)
config.resolver.refreshNames.v.refresh_hostnames = REFRESH_ALL;
conf->resolver.refreshNames.v.refresh_hostnames = REFRESH_ALL;
else if(buffer != NULL && strcasecmp(buffer, "NONE") == 0)
config.resolver.refreshNames.v.refresh_hostnames = REFRESH_NONE;
conf->resolver.refreshNames.v.refresh_hostnames = REFRESH_NONE;
else if(buffer != NULL && strcasecmp(buffer, "UNKNOWN") == 0)
config.resolver.refreshNames.v.refresh_hostnames = REFRESH_UNKNOWN;
conf->resolver.refreshNames.v.refresh_hostnames = REFRESH_UNKNOWN;
else
config.resolver.refreshNames.v.refresh_hostnames = REFRESH_IPV4_ONLY;
conf->resolver.refreshNames.v.refresh_hostnames = REFRESH_IPV4_ONLY;
// WEBDOMAIN
config.webserver.domain.v.s = getPath(fp, "WEBDOMAIN", config.webserver.domain.v.s);
conf->webserver.domain.v.s = getPath(fp, "WEBDOMAIN", conf->webserver.domain.v.s);
// RATE_LIMIT
// defaults to: 1000 queries / 60 seconds
@@ -393,47 +393,47 @@ const char *readFTLlegacy(void)
unsigned int count = 0, interval = 0;
if(buffer != NULL && sscanf(buffer, "%u/%u", &count, &interval) == 2)
{
config.dns.rateLimit.count.v.ui = count;
config.dns.rateLimit.interval.v.ui = interval;
conf->dns.rateLimit.count.v.ui = count;
conf->dns.rateLimit.interval.v.ui = interval;
}
// LOCAL_IPV4
// Use a specific IP address instead of automatically detecting the
// IPv4 interface address a query arrived on for A hostname queries
// defaults to: not set
config.dns.reply.host.overwrite_v4.v.b = false;
config.dns.reply.host.v4.v.in_addr.s_addr = 0;
conf->dns.reply.host.overwrite_v4.v.b = false;
conf->dns.reply.host.v4.v.in_addr.s_addr = 0;
buffer = parseFTLconf(fp, "LOCAL_IPV4");
if(buffer != NULL && inet_pton(AF_INET, buffer, &config.dns.reply.host.v4.v.in_addr))
config.dns.reply.host.overwrite_v4.v.b = true;
if(buffer != NULL && inet_pton(AF_INET, buffer, &conf->dns.reply.host.v4.v.in_addr))
conf->dns.reply.host.overwrite_v4.v.b = true;
// LOCAL_IPV6
// Use a specific IP address instead of automatically detecting the
// IPv6 interface address a query arrived on for AAAA hostname queries
// defaults to: not set
config.dns.reply.host.overwrite_v6.v.b = false;
memset(&config.dns.reply.host.v6.v.in6_addr, 0, sizeof(config.dns.reply.host.v6.v.in6_addr));
conf->dns.reply.host.overwrite_v6.v.b = false;
memset(&conf->dns.reply.host.v6.v.in6_addr, 0, sizeof(conf->dns.reply.host.v6.v.in6_addr));
buffer = parseFTLconf(fp, "LOCAL_IPV6");
if(buffer != NULL && inet_pton(AF_INET6, buffer, &config.dns.reply.host.v6.v.in6_addr))
config.dns.reply.host.overwrite_v6.v.b = true;
if(buffer != NULL && inet_pton(AF_INET6, buffer, &conf->dns.reply.host.v6.v.in6_addr))
conf->dns.reply.host.overwrite_v6.v.b = true;
// BLOCK_IPV4
// Use a specific IPv4 address for IP blocking mode replies
// defaults to: REPLY_ADDR4 setting
config.dns.reply.blocking.overwrite_v4.v.b = false;
config.dns.reply.blocking.v4.v.in_addr.s_addr = 0;
conf->dns.reply.blocking.overwrite_v4.v.b = false;
conf->dns.reply.blocking.v4.v.in_addr.s_addr = 0;
buffer = parseFTLconf(fp, "BLOCK_IPV4");
if(buffer != NULL && inet_pton(AF_INET, buffer, &config.dns.reply.blocking.v4.v.in_addr))
config.dns.reply.blocking.overwrite_v4.v.b = true;
if(buffer != NULL && inet_pton(AF_INET, buffer, &conf->dns.reply.blocking.v4.v.in_addr))
conf->dns.reply.blocking.overwrite_v4.v.b = true;
// BLOCK_IPV6
// Use a specific IPv6 address for IP blocking mode replies
// defaults to: REPLY_ADDR6 setting
config.dns.reply.blocking.overwrite_v6.v.b = false;
memset(&config.dns.reply.blocking.v6.v.in6_addr, 0, sizeof(config.dns.reply.host.v6.v.in6_addr));
conf->dns.reply.blocking.overwrite_v6.v.b = false;
memset(&conf->dns.reply.blocking.v6.v.in6_addr, 0, sizeof(conf->dns.reply.host.v6.v.in6_addr));
buffer = parseFTLconf(fp, "BLOCK_IPV6");
if(buffer != NULL && inet_pton(AF_INET6, buffer, &config.dns.reply.blocking.v6.v.in6_addr))
config.dns.reply.blocking.overwrite_v6.v.b = true;
if(buffer != NULL && inet_pton(AF_INET6, buffer, &conf->dns.reply.blocking.v6.v.in6_addr))
conf->dns.reply.blocking.overwrite_v6.v.b = true;
// REPLY_ADDR4 (deprecated setting)
// Use a specific IP address instead of automatically detecting the
@@ -443,16 +443,16 @@ const char *readFTLlegacy(void)
buffer = parseFTLconf(fp, "REPLY_ADDR4");
if(buffer != NULL && inet_pton(AF_INET, buffer, &reply_addr4))
{
if(config.dns.reply.host.overwrite_v4.v.b || config.dns.reply.blocking.overwrite_v4.v.b)
if(conf->dns.reply.host.overwrite_v4.v.b || conf->dns.reply.blocking.overwrite_v4.v.b)
{
log_warn("Ignoring REPLY_ADDR4 as LOCAL_IPV4 or BLOCK_IPV4 has been specified.");
}
else
{
config.dns.reply.host.overwrite_v4.v.b = true;
memcpy(&config.dns.reply.host.v4.v.in_addr, &reply_addr4, sizeof(reply_addr4));
config.dns.reply.blocking.overwrite_v4.v.b = true;
memcpy(&config.dns.reply.blocking.v4.v.in_addr, &reply_addr4, sizeof(reply_addr4));
conf->dns.reply.host.overwrite_v4.v.b = true;
memcpy(&conf->dns.reply.host.v4.v.in_addr, &reply_addr4, sizeof(reply_addr4));
conf->dns.reply.blocking.overwrite_v4.v.b = true;
memcpy(&conf->dns.reply.blocking.v4.v.in_addr, &reply_addr4, sizeof(reply_addr4));
}
}
@@ -464,16 +464,16 @@ const char *readFTLlegacy(void)
buffer = parseFTLconf(fp, "REPLY_ADDR6");
if(buffer != NULL && inet_pton(AF_INET, buffer, &reply_addr6))
{
if(config.dns.reply.host.overwrite_v6.v.b || config.dns.reply.blocking.overwrite_v6.v.b)
if(conf->dns.reply.host.overwrite_v6.v.b || conf->dns.reply.blocking.overwrite_v6.v.b)
{
log_warn("Ignoring REPLY_ADDR6 as LOCAL_IPV6 or BLOCK_IPV6 has been specified.");
}
else
{
config.dns.reply.host.overwrite_v6.v.b = true;
memcpy(&config.dns.reply.host.v6.v.in6_addr, &reply_addr6, sizeof(reply_addr6));
config.dns.reply.blocking.overwrite_v6.v.b = true;
memcpy(&config.dns.reply.blocking.v6.v.in6_addr, &reply_addr6, sizeof(reply_addr6));
conf->dns.reply.host.overwrite_v6.v.b = true;
memcpy(&conf->dns.reply.host.v6.v.in6_addr, &reply_addr6, sizeof(reply_addr6));
conf->dns.reply.blocking.overwrite_v6.v.b = true;
memcpy(&conf->dns.reply.blocking.v6.v.in6_addr, &reply_addr6, sizeof(reply_addr6));
}
}
@@ -481,13 +481,13 @@ const char *readFTLlegacy(void)
// Should FTL analyze and include automatically generated DNSSEC queries in the Query Log?
// defaults to: true
buffer = parseFTLconf(fp, "SHOW_DNSSEC");
parseBool(buffer, &config.dns.showDNSSEC.v.b);
parseBool(buffer, &conf->dns.showDNSSEC.v.b);
// MOZILLA_CANARY
// Should FTL handle use-application-dns.net specifically and always return NXDOMAIN?
// defaults to: true
buffer = parseFTLconf(fp, "MOZILLA_CANARY");
parseBool(buffer, &config.dns.specialDomains.mozillaCanary.v.b);
parseBool(buffer, &conf->dns.specialDomains.mozillaCanary.v.b);
// PIHOLE_PTR
// Should FTL return "pi.hole" as name for PTR requests to local IP addresses?
@@ -498,18 +498,18 @@ const char *readFTLlegacy(void)
{
if(strcasecmp(buffer, "none") == 0 ||
strcasecmp(buffer, "false") == 0)
config.dns.piholePTR.v.ptr_type = PTR_NONE;
conf->dns.piholePTR.v.ptr_type = PTR_NONE;
else if(strcasecmp(buffer, "hostname") == 0)
config.dns.piholePTR.v.ptr_type = PTR_HOSTNAME;
conf->dns.piholePTR.v.ptr_type = PTR_HOSTNAME;
else if(strcasecmp(buffer, "hostnamefqdn") == 0)
config.dns.piholePTR.v.ptr_type = PTR_HOSTNAMEFQDN;
conf->dns.piholePTR.v.ptr_type = PTR_HOSTNAMEFQDN;
}
// ADDR2LINE
// Should FTL try to call addr2line when generating backtraces?
// defaults to: true
buffer = parseFTLconf(fp, "ADDR2LINE");
parseBool(buffer, &config.misc.addr2line.v.b);
parseBool(buffer, &conf->misc.addr2line.v.b);
// REPLY_WHEN_BUSY
// How should FTL handle queries when the gravity database is not available?
@@ -519,55 +519,55 @@ const char *readFTLlegacy(void)
if(buffer != NULL)
{
if(strcasecmp(buffer, "DROP") == 0)
config.dns.replyWhenBusy.v.busy_reply = BUSY_DROP;
conf->dns.replyWhenBusy.v.busy_reply = BUSY_DROP;
else if(strcasecmp(buffer, "REFUSE") == 0)
config.dns.replyWhenBusy.v.busy_reply = BUSY_REFUSE;
conf->dns.replyWhenBusy.v.busy_reply = BUSY_REFUSE;
else if(strcasecmp(buffer, "BLOCK") == 0)
config.dns.replyWhenBusy.v.busy_reply = BUSY_BLOCK;
conf->dns.replyWhenBusy.v.busy_reply = BUSY_BLOCK;
}
// BLOCK_TTL
// defaults to: 2 seconds
config.dns.blockTTL.v.ui = 2;
conf->dns.blockTTL.v.ui = 2;
buffer = parseFTLconf(fp, "BLOCK_TTL");
unsigned int uval = 0;
if(buffer != NULL && sscanf(buffer, "%u", &uval))
config.dns.blockTTL.v.ui = uval;
conf->dns.blockTTL.v.ui = uval;
// BLOCK_ICLOUD_PR
// Should FTL handle the iCloud privacy relay domains specifically and
// always return NXDOMAIN??
// defaults to: true
buffer = parseFTLconf(fp, "BLOCK_ICLOUD_PR");
parseBool(buffer, &config.dns.specialDomains.iCloudPrivateRelay.v.b);
parseBool(buffer, &conf->dns.specialDomains.iCloudPrivateRelay.v.b);
// CHECK_LOAD
// Should FTL check the 15 min average of CPU load and complain if the
// load is larger than the number of available CPU cores?
// defaults to: true
buffer = parseFTLconf(fp, "CHECK_LOAD");
parseBool(buffer, &config.misc.check.load.v.b);
parseBool(buffer, &conf->misc.check.load.v.b);
// CHECK_SHMEM
// Limit above which FTL should complain about a shared-memory shortage
// defaults to: 90%
config.misc.check.shmem.v.ui = 90;
conf->misc.check.shmem.v.ui = 90;
buffer = parseFTLconf(fp, "CHECK_SHMEM");
if(buffer != NULL && sscanf(buffer, "%i", &ivalue) &&
ivalue >= 0 && ivalue <= 100)
config.misc.check.shmem.v.ui = ivalue;
conf->misc.check.shmem.v.ui = ivalue;
// CHECK_DISK
// Limit above which FTL should complain about disk shortage for checked files
// defaults to: 90%
config.misc.check.disk.v.ui = 90;
conf->misc.check.disk.v.ui = 90;
buffer = parseFTLconf(fp, "CHECK_DISK");
if(buffer != NULL && sscanf(buffer, "%i", &ivalue) &&
ivalue >= 0 && ivalue <= 100)
config.misc.check.disk.v.ui = ivalue;
conf->misc.check.disk.v.ui = ivalue;
// Read DEBUG_... setting from pihole-FTL.conf
// This option should be the last one as it causes
+4 -2
View File
@@ -10,7 +10,9 @@
#ifndef LEGACY_READER_H
#define LEGACY_READER_H
bool getLogFilePathLegacy(FILE *fp);
const char *readFTLlegacy(void);
#include "config/config.h"
bool getLogFilePathLegacy(struct config *conf, FILE *fp);
const char *readFTLlegacy(struct config *conf);
#endif //LEGACY_READER_H
+1 -1
View File
@@ -578,7 +578,7 @@ void readTOMLvalue(struct conf_item *conf_item, const char* key, toml_table_t *t
case CONF_JSON_STRING_ARRAY:
{
// Free previously allocated JSON array
cJSON_free(conf_item->v.json);
cJSON_Delete(conf_item->v.json);
conf_item->v.json = cJSON_CreateArray();
// Parse TOML array and generate a JSON array
const toml_array_t *array = toml_array_in(toml, key);
+20 -15
View File
@@ -10,7 +10,6 @@
#include "FTL.h"
#include "toml_reader.h"
#include "config.h"
#include "setupVars.h"
#include "log.h"
// getprio(), setprio()
@@ -20,7 +19,6 @@
// INT_MAX
#include <limits.h>
#include "tomlc99/toml.h"
#include "../datastructure.h"
// openFTLtoml()
#include "toml_helper.h"
@@ -29,30 +27,37 @@
static toml_table_t *parseTOML(void);
static void reportDebugConfig(void);
bool readFTLtoml(const bool verbose)
bool readFTLtoml(struct config *conf, toml_table_t *toml, const bool verbose)
{
// Parse lines in the config file
toml_table_t *conf = parseTOML();
if(!conf)
return false;
// Parse lines in the config file if we did not receive a pointer to a TOML
// table (e.g. from an imported Teleporter file)
bool external = true;
if(toml == NULL)
{
external = false;
toml = parseTOML();
if(!toml)
return false;
// Initialize config with default values
initConfig();
// Initialize config with default values
initConfig(conf);
}
// Try to read debug config. This is done before the full config
// parsing to allow for debug output further down
toml_table_t *conf_debug = toml_table_in(conf, "debug");
toml_table_t *conf_debug = toml_table_in(toml, "debug");
if(conf_debug)
readTOMLvalue(&config.debug.config, "config", conf_debug);
set_debug_flags();
log_debug(DEBUG_CONFIG, "Reading TOML config file: full config");
log_debug(DEBUG_CONFIG, "Reading %s TOML config file: full config",
external ? "external" : "default");
// Read all known config items
for(unsigned int i = 0; i < CONFIG_ELEMENTS; i++)
{
// Get pointer to memory location of this conf_item
struct conf_item *conf_item = get_conf_item(&config, i);
struct conf_item *conf_item = get_conf_item(conf, i);
// Get config path depth
unsigned int level = config_path_depth(conf_item->p);
@@ -63,7 +68,7 @@ bool readFTLtoml(const bool verbose)
for(unsigned int j = 0; j < level-1; j++)
{
// Get table at this level
table[j] = toml_table_in(j > 0 ? table[j-1] : conf, conf_item->p[j]);
table[j] = toml_table_in(j > 0 ? table[j-1] : toml, conf_item->p[j]);
if(!table[j])
{
log_debug(DEBUG_CONFIG, "%s DOES NOT EXIST", conf_item->k);
@@ -86,7 +91,7 @@ bool readFTLtoml(const bool verbose)
reportDebugConfig();
// Free memory allocated by the TOML parser and return success
toml_free(conf);
toml_free(toml);
return true;
}
@@ -257,4 +262,4 @@ static void reportDebugConfig(void)
log_debug(DEBUG_ANY, "* %s:%*s %s *", name+6, spaces, "", debug_flags[debug_flag] ? "YES" : "NO ");
}
log_debug(DEBUG_ANY, "************************");
}
}
+4 -1
View File
@@ -10,7 +10,10 @@
#ifndef TOML_READER_H
#define TOML_READER_H
bool readFTLtoml(const bool verbose);
#include "config/config.h"
#include "tomlc99/toml.h"
bool readFTLtoml(struct config *conf, toml_table_t *toml, const bool verbose);
bool getLogFilePathTOML(void);
#endif //TOML_READER_H
+1 -1
View File
@@ -105,4 +105,4 @@ bool writeFTLtoml(const bool verbose)
closeFTLtoml(fp);
return true;
}
}
+5 -1
View File
@@ -242,6 +242,10 @@ static void terminate_threads(void)
log_info("Waiting for threads to join");
for(int i = 0; i < THREADS_MAX; i++)
{
// Skip threads that have never been started or which are already stopped
if(!thread_running[i])
continue;
// Cancel thread if it is idle
if(thread_cancellable[i])
{
@@ -326,7 +330,7 @@ void cleanup(const int ret)
// Remove shared memory objects
// Important: This invalidated all objects such as
// counters-> ... etc.
// This should be the last action when cleaning up
// This should be the last action when c
destroy_shmem();
char buffer[42] = { 0 };
+2
View File
@@ -55,6 +55,7 @@ void *DB_thread(void *val)
{
// Set thread name
thread_names[DB] = "database";
thread_running[DB] = true;
prctl(PR_SET_NAME, thread_names[DB], 0, 0, 0);
// Save timestamp as we do not want to store immediately
@@ -183,5 +184,6 @@ void *DB_thread(void *val)
dbclose(&db);
log_info("Terminating database thread");
thread_running[DB] = false;
return NULL;
}
+1 -1
View File
@@ -697,7 +697,7 @@ int run_dhcp_discover(void)
// Disable terminal output during config config file parsing
log_ctrl(false, false);
// Process pihole-FTL.conf to get gravity.db
readFTLconf(false);
readFTLconf(&config, false);
// Only print to terminal, disable log file
log_ctrl(false, true);
+20 -13
View File
@@ -56,6 +56,8 @@
#include "struct_size.h"
// query_to_database()
#include "database/query-table.h"
// reread_config()
#include "config/config.h"
// Private prototypes
static void print_flags(const unsigned int flags);
@@ -188,9 +190,9 @@ size_t _FTL_make_answer(struct dns_header *header, char *limit, const size_t len
// Debug logging
if(*ede != EDE_UNSET)
log_debug(DEBUG_FLAGS, "Preparing reply for \"%s\", EDE: %s (%d)", name, edestr(*ede), *ede);
log_debug(DEBUG_QUERIES, "Preparing reply for \"%s\", EDE: %s (%d)", name, edestr(*ede), *ede);
else
log_debug(DEBUG_FLAGS, "Preparing reply for \"%s\", EDE: N/A", name);
log_debug(DEBUG_QUERIES, "Preparing reply for \"%s\", EDE: N/A", name);
// Get question type
int qtype, flags = 0;
@@ -218,7 +220,7 @@ size_t _FTL_make_answer(struct dns_header *header, char *limit, const size_t len
force_next_DNS_reply = REPLY_UNKNOWN;
// Debug logging
log_debug(DEBUG_FLAGS, "Forced DNS reply to NXDOMAIN");
log_debug(DEBUG_QUERIES, "Forced DNS reply to NXDOMAIN");
}
else if(force_next_DNS_reply == REPLY_NODATA)
{
@@ -227,7 +229,7 @@ size_t _FTL_make_answer(struct dns_header *header, char *limit, const size_t len
force_next_DNS_reply = REPLY_UNKNOWN;
// Debug logging
log_debug(DEBUG_FLAGS, "Forced DNS reply to NODATA");
log_debug(DEBUG_QUERIES, "Forced DNS reply to NODATA");
}
else if(force_next_DNS_reply == REPLY_REFUSED)
{
@@ -237,7 +239,7 @@ size_t _FTL_make_answer(struct dns_header *header, char *limit, const size_t len
force_next_DNS_reply = REPLY_UNKNOWN;
// Debug logging
log_debug(DEBUG_FLAGS, "Forced DNS reply to REFUSED");
log_debug(DEBUG_QUERIES, "Forced DNS reply to REFUSED");
// Set EDE code to blocked
*ede = EDE_BLOCKED;
@@ -252,7 +254,7 @@ size_t _FTL_make_answer(struct dns_header *header, char *limit, const size_t len
force_next_DNS_reply = REPLY_UNKNOWN;
// Debug logging
log_debug(DEBUG_FLAGS, "Forced DNS reply to IP");
log_debug(DEBUG_QUERIES, "Forced DNS reply to IP");
}
else if(force_next_DNS_reply == REPLY_NONE)
{
@@ -260,7 +262,7 @@ size_t _FTL_make_answer(struct dns_header *header, char *limit, const size_t len
force_next_DNS_reply = REPLY_UNKNOWN;
// Debug logging
log_debug(DEBUG_FLAGS, "Forced DNS reply to NONE - dropping this query");
log_debug(DEBUG_QUERIES, "Forced DNS reply to NONE - dropping this query");
return 0;
}
@@ -272,7 +274,7 @@ size_t _FTL_make_answer(struct dns_header *header, char *limit, const size_t len
// If we block in NXDOMAIN mode, we set flags to NXDOMAIN
// (NEG will be added after setup_reply() below)
flags = F_NXDOMAIN;
log_debug(DEBUG_FLAGS, "Configured blocking mode is NXDOMAIN");
log_debug(DEBUG_QUERIES, "Configured blocking mode is NXDOMAIN");
}
else if(config.dns.blocking.mode.v.blocking_mode == MODE_NODATA ||
(config.dns.blocking.mode.v.blocking_mode == MODE_IP_NODATA_AAAA && (flags & F_IPV6)))
@@ -280,7 +282,7 @@ size_t _FTL_make_answer(struct dns_header *header, char *limit, const size_t len
// If we block in NODATA mode or NODATA for AAAA queries, we apply
// the NOERROR response flag. This ensures we're sending an empty response
flags = F_NOERR;
log_debug(DEBUG_FLAGS, "Configured blocking mode is NODATA%s",
log_debug(DEBUG_QUERIES, "Configured blocking mode is NODATA%s",
config.dns.blocking.mode.v.blocking_mode == MODE_IP_NODATA_AAAA ? "-IPv6" : "");
}
}
@@ -295,7 +297,7 @@ size_t _FTL_make_answer(struct dns_header *header, char *limit, const size_t len
last_regex_idx = -1;
// Debug logging
log_debug(DEBUG_FLAGS, "Regex match is %sredirected", redirecting ? "" : "NOT ");
log_debug(DEBUG_QUERIES, "Regex match is %sredirected", redirecting ? "" : "NOT ");
}
// Debug logging
@@ -1723,10 +1725,13 @@ static void FTL_forwarded(const unsigned int flags, const char *name, const unio
unlock_shm();
}
static unsigned int reload = 0u;
void FTL_dnsmasq_reload(void)
{
// This function is called by the dnsmasq code on receive of SIGHUP
// *before* clearing the cache and re-reading the lists
if(reload++ > 0)
log_info("Received SIGHUP, flushing chache and re-reading config");
// Gravity database updates
// - (Re-)open gravity database connection
@@ -1740,8 +1745,10 @@ void FTL_dnsmasq_reload(void)
if(config.debug.caps.v.b)
check_capabilities();
// Re-read configuration (incl. rewriting)
readFTLconf(true);
// Re-read pihole.toml (incl. rewriting) on every but the first reload
// (which is happening right after the start of dnsmasq)
if(reload > 1)
reread_config();
// Report blocking mode
log_info("Blocking status is %s", config.dns.blocking.active.v.b ? "enabled" : "disabled");
@@ -2551,7 +2558,7 @@ void print_flags(const unsigned int flags)
return;
char *flagstr = calloc(sizeof(flagnames) + 1, sizeof(char));
for (unsigned int i = 0; i < (sizeof(flagnames) / sizeof(*flagnames)); i++)
for (unsigned int i = 0; i < ArraySize(flagnames); i++)
if (flags & (1u << i))
strcat(flagstr, flagnames[i]);
log_debug(DEBUG_FLAGS, " Flags: %s", flagstr);
+1 -1
View File
@@ -27,7 +27,7 @@
// dirname()
#include <libgen.h>
// compression functions
#include "miniz/compression.h"
#include "compression/gzip.h"
#define BACKUP_DIR "/etc/pihole/config_backups"
+3 -1
View File
@@ -122,6 +122,7 @@ void *GC_thread(void *val)
{
// Set thread name
thread_names[GC] = "housekeeper";
thread_running[GC] = true;
prctl(PR_SET_NAME, thread_names[GC], 0, 0, 0);
// Remember when we last ran the actions
@@ -255,7 +256,7 @@ void *GC_thread(void *val)
break;
}
// Update reply counters
// Update reply countersthread_running[GC] = false;
counters->reply[query->reply]--;
// Update type counters
@@ -322,5 +323,6 @@ void *GC_thread(void *val)
}
log_info("Terminating GC thread");
thread_running[GC] = false;
return NULL;
}
+3 -1
View File
@@ -69,7 +69,7 @@ int main (int argc, char *argv[])
// Process pihole.toml configuration file
// The file is rewritten after parsing to ensure that all
// settings are present and have a valid value
readFTLconf(true);
readFTLconf(&config, true);
// Set process priority
set_nice();
@@ -132,6 +132,8 @@ int main (int argc, char *argv[])
// Start the resolver
startup = false;
// Stop writing to STDOUT
log_ctrl(true, false);
main_dnsmasq(argc_dnsmasq, argv_dnsmasq);
log_info("Shutting down... // exit code %d", exit_code);
+1 -5
View File
@@ -9,13 +9,9 @@
# Please see LICENSE file for your rights under this license.
set(sources
compression.c
compression.h
miniz.c
miniz.h
teleporter.c
teleporter.h
)
)
add_library(miniz OBJECT ${sources})
target_compile_options(miniz PRIVATE)
-297
View File
@@ -1,297 +0,0 @@
/* 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
* Teleporter un-/compression routines
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
#include "FTL.h"
#include "miniz/teleporter.h"
#include "config/config.h"
// hostname()
#include "daemon.h"
// get_timestr(), TIMESTR_SIZE
#include "log.h"
// directory_exists()
#include "files.h"
// DIR, dirent, opendir(), readdir(), closedir()
#include <dirent.h>
// sqlite3
#include "database/sqlite3.h"
// Tables to copy from the gravity database to the Teleporter database
static const char *gravity_tables[] = {
"info",
"group",
"adlist",
"adlist_by_group",
"domainlist",
"domainlist_by_group",
"client",
"client_by_group",
"domain_audit"
};
// Tables to copy from the FTL database to the Teleporter database
static const char *ftl_tables[] = {
"message",
"aliasclient",
"network",
"network_addresses"
};
// Create database in memory, copy selected tables to it, serialize and return a memory pointer to it
static bool create_teleporter_database(const char *filename, const char **tables, const unsigned int num_tables,
void **buffer, size_t *size)
{
// Open in-memory sqlite3 database
sqlite3 *db;
if(sqlite3_open_v2(":memory:", &db, SQLITE_OPEN_READWRITE, NULL) != SQLITE_OK)
{
log_warn("Failed to open in-memory database: %s", sqlite3_errmsg(db));
return false;
}
// Attach the FTL database to the in-memory database
char *err = NULL;
char attach_stmt[128] = "";
snprintf(attach_stmt, sizeof(attach_stmt), "ATTACH DATABASE '%s' AS \"disk\";", filename);
if(sqlite3_exec(db, attach_stmt, NULL, NULL, &err) != SQLITE_OK)
{
log_warn("Failed to attach database \"%s\" to in-memory database: %s", filename, err);
sqlite3_free(err);
sqlite3_close(db);
return false;
}
// Loop over the tables and copy them to the in-memory database
for(unsigned int i = 0; i < num_tables; i++)
{
char *err = NULL;
char create_stmt[128] = "";
// Create in-memory table copy
snprintf(create_stmt, sizeof(create_stmt), "CREATE TABLE \"%s\" AS SELECT * FROM disk.\"%s\";", tables[i], tables[i]);
if(sqlite3_exec(db, create_stmt, NULL, NULL, &err) != SQLITE_OK)
{
log_warn("Failed to create %s in in-memory database: %s", tables[i], err);
sqlite3_free(err);
sqlite3_close(db);
return false;
}
}
// Detach the FTL database from the in-memory database
if(sqlite3_exec(db, "DETACH DATABASE 'disk';", NULL, NULL, &err) != SQLITE_OK)
{
log_warn("Failed to detach FTL database from in-memory database: %s", err);
sqlite3_free(err);
sqlite3_close(db);
return false;
}
// Serialize the in-memory database to a buffer
// The sqlite3_serialize(D,S,P,F) interface returns a pointer to memory that
// is a serialization of the S database on database connection D. If P is
// not a NULL pointer, then the size of the database in bytes is written
// into *P.
// For an ordinary on-disk database file, the serialization is just a copy
// of the disk file. For an in-memory database or a "TEMP" database, the
// serialization is the same sequence of bytes which would be written to
// disk if that database where backed up to disk.
// The usual case is that sqlite3_serialize() copies the serialization of
// the database into memory obtained from sqlite3_malloc64() and returns a
// pointer to that memory. The caller is responsible for freeing the
// returned value to avoid a memory leak.
sqlite3_int64 isize = 0;
*buffer = sqlite3_serialize(db, "main", &isize, 0);
*size = isize;
if(*buffer == NULL)
{
log_warn("Failed to serialize in-memory database to buffer: %s", sqlite3_errmsg(db));
sqlite3_close(db);
return false;
}
// Close the in-memory database
sqlite3_close(db);
return true;
}
const char *generate_teleporter_zip(mz_zip_archive *zip, char filename[128], void *ptr, size_t *size)
{
// Initialize ZIP archive
memset(zip, 0, sizeof(*zip));
// Start with 64KB allocation size (pihole.TOML is slightly larger than 32KB
// at the time of writing thjs)
if(!mz_zip_writer_init_heap(zip, 0, 64*1024))
{
return "Failed creating heap ZIP archive";
}
// Add pihole.toml to the ZIP archive
const char *file_comment = "Pi-hole's configuration";
const char *file_path = GLOBALTOMLPATH;
if(!mz_zip_writer_add_file(zip, file_path+1, file_path, file_comment, (uint16_t)strlen(file_comment), MZ_BEST_COMPRESSION))
{
mz_zip_writer_end(zip);
return "Failed to add "GLOBALTOMLPATH" to heap ZIP archive!";
}
// Add /etc/hosts to the ZIP archive
file_comment = "System's HOSTS file";
file_path = "/etc/hosts";
if(!mz_zip_writer_add_file(zip, file_path+1, file_path, file_comment, (uint16_t)strlen(file_comment), MZ_BEST_COMPRESSION))
{
mz_zip_writer_end(zip);
return "Failed to add /etc/hosts to heap ZIP archive!";
}
// Add /etc/pihole/dhcp.lease to the ZIP archive if it exists
file_comment = "DHCP leases file";
file_path = "/etc/pihole/dhcp.leases";
if(file_exists(file_path) && !mz_zip_writer_add_file(zip, file_path+1, file_path, file_comment, (uint16_t)strlen(file_comment), MZ_BEST_COMPRESSION))
{
mz_zip_writer_end(zip);
return "Failed to add /etc/hosts to heap ZIP archive!";
}
const char *directory = "/etc/dnsmasq.d";
if(directory_exists(directory))
{
// Loop over all files and add them to the ZIP archive
DIR *dir;
struct dirent *ent;
if((dir = opendir(directory)) != NULL)
{
// Loop over all files in the directory
while((ent = readdir(dir)) != NULL)
{
// Skip "." and ".."
if(strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
continue;
// Construct full path to file
char fullpath[128] = "";
snprintf(fullpath, 128, "%s/%s", directory, ent->d_name);
// Add file to ZIP archive
file_comment = "dnsmasq configuration file";
file_path = fullpath;
if(!mz_zip_writer_add_file(zip, file_path+1, file_path, file_comment, (uint16_t)strlen(file_comment), MZ_BEST_COMPRESSION))
continue;
}
closedir(dir);
}
}
// Add (a reduced version of) the gravity database to the ZIP archive
void *dbbuf = NULL;
size_t dbsize = 0u;
if(create_teleporter_database(config.files.gravity.v.s, gravity_tables, ArraySize(gravity_tables), &dbbuf, &dbsize))
{
// Add gravity database to ZIP archive
file_comment = "Pi-hole's gravity database";
file_path = config.files.gravity.v.s;
if(file_path[0] == '/')
file_path++;
if(!mz_zip_writer_add_mem(zip, file_path, dbbuf, dbsize, MZ_BEST_COMPRESSION))
{
sqlite3_free(dbbuf);
mz_zip_writer_end(zip);
return "Failed to add gravity database to heap ZIP archive!";
}
sqlite3_free(dbbuf);
}
else
{
mz_zip_writer_end(zip);
return "Failed to create gravity database for heap ZIP archive!";
}
if(create_teleporter_database(config.files.database.v.s, ftl_tables, ArraySize(ftl_tables), &dbbuf, &dbsize))
{
// Add FTL database to ZIP archive
file_comment = "Pi-hole's FTL database";
file_path = config.files.database.v.s;
if(file_path[0] == '/')
file_path++;
if(!mz_zip_writer_add_mem(zip, file_path, dbbuf, dbsize, MZ_BEST_COMPRESSION))
{
sqlite3_free(dbbuf);
mz_zip_writer_end(zip);
return "Failed to add FTL database to heap ZIP archive!";
}
sqlite3_free(dbbuf);
}
else
{
mz_zip_writer_end(zip);
return "Failed to create FTL database for heap ZIP archive!";
}
// Get the heap data so we can send it to the requesting client
if(!mz_zip_writer_finalize_heap_archive(zip, ptr, size))
{
mz_zip_writer_end(zip);
return "Failed to finalize heap ZIP archive!";
}
char timestr[TIMESTR_SIZE] = "";
get_timestr(timestr, time(NULL), false, true);
snprintf(filename, 128, "pi-hole_%s_teleporter_%s.zip", hostname(), timestr);
// Everything worked well
return NULL;
}
bool free_teleporter_zip(mz_zip_archive *zip)
{
return mz_zip_writer_end(zip);
}
bool write_teleporter_zip_to_disk(void)
{
// Generate in-memory ZIP file
mz_zip_archive zip = { 0 };
void *ptr = NULL;
size_t size = 0u;
char filename[128] = "";
const char *error = generate_teleporter_zip(&zip, filename, &ptr, &size);
if(error != NULL)
{
log_err("Failed to create Teleporter ZIP file: %s", error);
return false;
}
// Write file to disk
FILE *fp = fopen(filename, "w");
if(fp == NULL)
{
log_err("Failed to open %s for writing: %s", filename, strerror(errno));
free_teleporter_zip(&zip);
return false;
}
if(fwrite(ptr, 1, size, fp) != size)
{
log_err("Failed to write %zu bytes to %s: %s", size, filename, strerror(errno));
free_teleporter_zip(&zip);
return false;
}
fclose(fp);
// Free allocated ZIP memory
free_teleporter_zip(&zip);
/* Output filename on successful creation */
log_info("%s", filename);
return true;
}
+1 -1
View File
@@ -701,7 +701,7 @@ int regex_test(const bool debug_mode, const bool quiet, const char *domainin, co
// Process pihole-FTL.conf to get gravity.db path
// Do not overwrite the file after reading it
readFTLconf(false);
readFTLconf(&config, false);
// Disable all debugging output if not explicitly in debug mode (CLI argument "d")
if(!debug_mode)
+2
View File
@@ -603,6 +603,7 @@ void *DNSclient_thread(void *val)
{
// Set thread name
thread_names[DNSclient] = "DNS client";
thread_running[DNSclient] = true;
prctl(PR_SET_NAME, thread_names[DNSclient], 0, 0, 0);
// Initial delay until we first try to resolve anything
@@ -662,5 +663,6 @@ void *DNSclient_thread(void *val)
}
log_info("Terminating resolver thread");
thread_running[DNSclient] = false;
return NULL;
}
+12 -9
View File
@@ -89,7 +89,6 @@ static SharedMemory *sharedMemories[] = { &shm_lock,
&shm_dns_cache,
&shm_per_client_regex,
&shm_fifo_log };
#define NUM_SHMEM (sizeof(sharedMemories)/sizeof(SharedMemory*))
// Variable size array structs
static queriesData *queries = NULL;
@@ -399,12 +398,14 @@ static void remap_shm(void)
// Obtain SHMEM lock
void _lock_shm(const char *func, const int line, const char *file)
{
log_debug(DEBUG_LOCKS, "Waiting for SHM lock in %s() (%s:%i)", func, file, line);
log_debug(DEBUG_LOCKS, "SHM lock: %p", shmLock);
// There is no need to lock if we are the only thread
// (e.g., when running pihole-FTL --config a.b.c def)
if(shmLock == NULL)
return;
log_debug(DEBUG_LOCKS, "Waiting for SHM lock in %s() (%s:%i)", func, file, line);
log_debug(DEBUG_LOCKS, "SHM lock: %p", shmLock);
int result = pthread_mutex_lock(&shmLock->lock.outer);
if(result != 0)
@@ -457,15 +458,17 @@ void _lock_shm(const char *func, const int line, const char *file)
// Release SHM lock
void _unlock_shm(const char* func, const int line, const char * file)
{
// There is no need to unlock if we are the only thread
// (e.g., when running pihole-FTL --config a.b.c def)
if(shmLock == NULL)
return;
if(config.debug.locks.v.b && !is_our_lock())
{
log_err("Tried to unlock but lock is owned by %li/%li",
(long int)shmLock->owner.pid, (long int)shmLock->owner.tid);
}
if(shmLock == NULL)
return;
// Unlock mutex
int result = pthread_mutex_unlock(&shmLock->lock.inner);
shmLock->owner.pid = 0;
@@ -617,7 +620,7 @@ bool init_shmem()
// CHOWN all shared memory objects to supplied user/group
void chown_all_shmem(struct passwd *ent_pw)
{
for(unsigned int i = 0; i < NUM_SHMEM; i++)
for(unsigned int i = 0; i < ArraySize(sharedMemories); i++)
chown_shmem(sharedMemories[i], ent_pw);
}
@@ -633,7 +636,7 @@ void destroy_shmem(void)
shmLock = NULL;
// Then, we delete the shared memory objects
for(unsigned int i = 0; i < NUM_SHMEM; i++)
for(unsigned int i = 0; i < ArraySize(sharedMemories); i++)
delete_shm(sharedMemories[i]);
}
+3 -2
View File
@@ -33,7 +33,8 @@ static volatile pid_t mpid = -1;
static time_t FTLstarttime = 0;
volatile int exit_code = EXIT_SUCCESS;
volatile sig_atomic_t thread_cancellable[THREADS_MAX] = { true };
volatile sig_atomic_t thread_cancellable[THREADS_MAX] = { false };
volatile sig_atomic_t thread_running[THREADS_MAX] = { false };
const char *thread_names[THREADS_MAX] = { "" };
// Return the (null-terminated) name of the calling thread
@@ -321,7 +322,7 @@ void handle_signals(void)
struct sigaction old_action;
const int signals[] = { SIGSEGV, SIGBUS, SIGILL, SIGFPE };
for(unsigned int i = 0; i < sizeof(signals)/sizeof(signals[0]); i++)
for(unsigned int i = 0; i < ArraySize(signals); i++)
{
// Catch this signal
sigaction (signals[i], NULL, &old_action);
+1
View File
@@ -24,6 +24,7 @@ extern volatile sig_atomic_t want_to_reimport_aliasclients;
extern volatile sig_atomic_t want_to_reload_lists;
extern volatile sig_atomic_t thread_cancellable[THREADS_MAX];
extern volatile sig_atomic_t thread_running[THREADS_MAX];
extern const char *thread_names[THREADS_MAX];
#endif //SIGNALS_H
+1
View File
@@ -35,6 +35,7 @@ enum http_method {
struct api_options {
bool domains :1;
bool parse_json :1;
enum fifo_logs which;
};
+3 -3
View File
@@ -8,9 +8,9 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
#include "../cJSON/cJSON.h"
#include "cJSON/cJSON.h"
// logging routines
#include "../log.h"
#include "log.h"
#define JSON_NEW_OBJECT() cJSON_CreateObject();
#define JSON_NEW_ARRAY() cJSON_CreateArray();
@@ -243,4 +243,4 @@
free(additional_headers); \
return code; \
})
*/
*/
+7 -5
View File
@@ -8,17 +8,19 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
// ArraySize()
#include "FTL.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// strncpy()
#include <string.h>
#include "../log.h"
#include "../ph7/ph7.h"
#include "../civetweb/civetweb.h"
#include "log.h"
#include "ph7/ph7.h"
#include "civetweb/civetweb.h"
#include "ph7.h"
// struct config.http
#include "../config/config.h"
#include "config/config.h"
// mmap
#include <sys/mman.h>
// stat
@@ -130,7 +132,7 @@ int ph7_handler(struct mg_connection *conn, void *cbdata)
ph7_vm_config(pVm, PH7_VM_CONFIG_IMPORT_PATH, webroot_with_home_and_scripts);
// Register Pi-hole's PH7 extensions (defined in subdirectory "ph7_ext/")
for(unsigned int i = 0; i < sizeof(aFunc)/sizeof(aFunc[0]); i++ )
for(unsigned int i = 0; i < ArraySize(aFunc); i++ )
{
rc = ph7_create_function(pVm, aFunc[i].zName, aFunc[i].xProc, NULL /* NULL: No private data */);
if( rc != PH7_OK ){
+2 -2
View File
@@ -159,11 +159,11 @@ void http_init(void)
// Add access control list if configured (last two options)
if(strlen(config.webserver.acl.v.s) > 0)
{
options[(sizeof(options)/sizeof(*options)) - 3] = "access_control_list";
options[ArraySize(options) - 3] = "access_control_list";
// Note: The string is duplicated by CivetWeb, so it doesn't matter if
// the original string is freed (config changes) after mg_start()
// returns below.
options[(sizeof(options)/sizeof(*options)) - 2] = config.webserver.acl.v.s;
options[ArraySize(options) - 2] = config.webserver.acl.v.s;
}
// Configure logging handlers
+2 -3
View File
@@ -58,9 +58,9 @@ if __name__ == "__main__":
verifyer = ResponseVerifyer(ftl, openapi)
errors = verifyer.verify_endpoint(path)
if len(errors) == 0:
print(" " + path + ": OK")
print(" " + path + " (" + verifyer.auth_method + " auth): OK")
else:
print(" " + path + ":")
print(" " + path + " (" + verifyer.auth_method + " auth):")
for error in errors:
print(" - " + error)
errs[2] += len(errors)
@@ -79,6 +79,5 @@ if __name__ == "__main__":
exit(1)
# If there are no errors, exit with success
# (this is important for the CI)
print("Everything okay!")
exit(0)
+17 -4
View File
@@ -9,6 +9,7 @@
# This file is copyright under the latest version of the EUPL.
# Please see LICENSE file for your rights under this license.
from enum import Enum
import requests
from typing import List
import json
@@ -25,6 +26,11 @@ valid = session["session"]["valid"] # True / False
sid = session["session"]["sid"] # SID string if succesful, null otherwise
"""
class AuthenticationMethods(Enum):
HEADER = 1
BODY = 2
COOKIE = 3
# Class to query the FTL API
class FTLAPI():
def __init__(self, api_url: str):
@@ -95,7 +101,7 @@ class FTLAPI():
self.session = response["session"]
# Query the FTL API (GET) and return the response
def GET(self, uri: str, params: List[str] = [], expected_mimetype: str = "application/json"):
def GET(self, uri: str, params: List[str] = [], expected_mimetype: str = "application/json", authenticate: AuthenticationMethods = AuthenticationMethods.BODY):
self.errors = []
try:
# Add parameters to the URI (if any)
@@ -104,13 +110,20 @@ class FTLAPI():
# Add session ID to the request (if any)
data = None
headers = None
cookies = None
if self.session is not None and 'sid' in self.session:
data = {"sid": self.session['sid'] }
if authenticate == AuthenticationMethods.HEADER:
headers = {"X-FTL-SID": self.session['sid']}
elif authenticate == AuthenticationMethods.BODY:
data = {"sid": self.session['sid'] }
elif authenticate == AuthenticationMethods.COOKIE:
cookies = {"sid": self.session['sid'] }
# Query the API
if self.verbose:
print("GET " + self.api_url + uri + " with data: " + json.dumps(data))
with requests.get(url = self.api_url + uri, json = data) as response:
with requests.get(url = self.api_url + uri, json = data, headers=headers, cookies=cookies) as response:
if self.verbose:
print(json.dumps(response.json(), indent=4))
if expected_mimetype == "application/json":
@@ -149,4 +162,4 @@ class FTLAPI():
print("Exception when pre-processing endpoints from FTL: " + str(e))
exit(1)
return self.endpoints
return self.endpoints
+11 -2
View File
@@ -10,10 +10,11 @@
# Please see LICENSE file for your rights under this license.
import io
import random
import zipfile
from libs.openAPI import openApi
import urllib.request, urllib.parse
from libs.FTLAPI import FTLAPI
from libs.FTLAPI import FTLAPI, AuthenticationMethods
from collections.abc import MutableMapping
class ResponseVerifyer():
@@ -22,6 +23,8 @@ class ResponseVerifyer():
YAML_TYPES = { "string": [str], "integer": [int], "number": [int, float], "boolean": [bool], "array": [list] }
TELEPORTER_FILES = ["etc/pihole/gravity.db", "etc/pihole/pihole.toml", "etc/pihole/pihole-FTL.db", "etc/hosts"]
auth_method = "?"
def __init__(self, ftl: FTLAPI, openapi: openApi):
self.ftl = ftl
self.openapi = openapi
@@ -62,6 +65,10 @@ class ResponseVerifyer():
# Get YAML response schema and examples (if applicable)
expected_mimetype = True
# Assign random authentication method so we can test them all
authentication_method = random.choice([a for a in AuthenticationMethods])
self.auth_method = authentication_method.name
# Check if the expected response is defined in the API specs
response_rcode = self.openapi.paths[endpoint][method]['responses'][str(rcode)]
if 'content' in response_rcode:
content = response_rcode['content']
@@ -73,6 +80,8 @@ class ResponseVerifyer():
elif 'application/zip' in content:
expected_mimetype = 'application/zip'
jsonData = content[expected_mimetype]
# Thie endpoint requires HEADER authentication
authentication_method = AuthenticationMethods.HEADER
YAMLresponseSchema = None
YAMLresponseExamples = None
else:
@@ -93,7 +102,7 @@ class ResponseVerifyer():
FTLparameters.append(param['name'] + "=" + urllib.parse.quote_plus(str(param['example'])))
# Get FTL response
FTLresponse = self.ftl.GET("/api" + endpoint, FTLparameters, expected_mimetype)
FTLresponse = self.ftl.GET("/api" + endpoint, FTLparameters, expected_mimetype, authentication_method)
if FTLresponse is None:
return self.ftl.errors
+10 -5
View File
@@ -1165,13 +1165,18 @@
@test "Pi-hole uses dns.reply.blocking.IPv4/6 for blocked domain" {
run bash -c 'grep "mode = \"NULL\"" /etc/pihole/pihole.toml'
printf "grep output: %s\n" "${lines[@]}"
[[ "${lines[0]}" == ' mode = "NULL"' ]]
run bash -c './pihole-FTL --config dns.blocking.mode IP'
printf "setting config: %s\n" "${lines[@]}"
run bash -c 'grep "mode = \"IP" /etc/pihole/pihole.toml'
printf "grep output (before reload): %s\n" "${lines[@]}"
[[ "${lines[0]}" == *'mode = "IP" ### CHANGED, default = "NULL"' ]]
run bash -c "kill -HUP $(cat /run/pihole-FTL.pid)"
sleep 2
run bash -c 'grep "mode = \"IP\"" /etc/pihole/pihole.toml'
[[ "${lines[0]}" == ' mode = "IP" ### CHANGED, default = "NULL"' ]]
sleep 1
run bash -c 'grep "mode = \"IP" /etc/pihole/pihole.toml'
printf "grep output (after reload): %s\n" "${lines[@]}"
[[ "${lines[0]}" == *'mode = "IP" ### CHANGED, default = "NULL"' ]]
run bash -c "dig A denied.ftl +short @127.0.0.1"
printf "A: %s\n" "${lines[@]}"
[[ "${lines[0]}" == "10.100.0.11" ]]
@@ -1225,12 +1230,12 @@
printf "Compression output:\n"
printf "%s\n" "${lines[@]}"
[[ $status == 0 ]]
[[ ${lines[0]} == "Compressed test/pihole-FTL.db.sql ("* ]]
[[ ${lines[0]} == "Compressed test/pihole-FTL.db.sql (2.0KB) to test/pihole-FTL.db.sql.gz (689.0B), 66.0% size reduction" ]]
printf "Uncompress (FTL) output:\n"
run bash -c './pihole-FTL gzip test/pihole-FTL.db.sql.gz test/pihole-FTL.db.sql.1'
printf "%s\n" "${lines[@]}"
[[ $status == 0 ]]
[[ ${lines[0]} == "Uncompressed test/pihole-FTL.db.sql.gz ("* ]]
[[ ${lines[0]} == "Uncompressed test/pihole-FTL.db.sql.gz (677.0B) to test/pihole-FTL.db.sql.1 (2.0KB), 199.3% size increase" ]]
printf "Uncompress (gzip) output:\n"
run bash -c 'gzip -dkc test/pihole-FTL.db.sql.gz > test/pihole-FTL.db.sql.2'
printf "%s\n" "${lines[@]}"