mirror of
https://github.com/pi-hole/FTL.git
synced 2026-07-07 11:25:11 +01:00
48fc06d46b
Signed-off-by: DL6ER <dl6er@dl6er.de>
871 lines
26 KiB
C
871 lines
26 KiB
C
/* Pi-hole: A black hole for Internet advertisements
|
|
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
|
|
* Network-wide ad blocking via your own hardware.
|
|
*
|
|
* FTL Engine
|
|
* API Implementation /api/ftl
|
|
*
|
|
* 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 "webserver/http-common.h"
|
|
#include "webserver/json_macros.h"
|
|
#include "api/api.h"
|
|
// config struct
|
|
#include "config/config.h"
|
|
// struct clientsData
|
|
#include "datastructure.h"
|
|
// INT_MIN, INT_MAX, ...
|
|
#include <limits.h>
|
|
// writeFTLtoml()
|
|
#include "config/toml_writer.h"
|
|
// write_dnsmasq_config()
|
|
#include "config/dnsmasq_config.h"
|
|
// shm_lock()
|
|
#include "shmem.h"
|
|
|
|
static struct {
|
|
const char *name;
|
|
const char *description;
|
|
} config_topics[] =
|
|
{
|
|
{ "dns", "DNS server settings" },
|
|
{ "dhcp", "DHCP server settings" },
|
|
{ "resolver", "Resolver settings" },
|
|
{ "database", "Database settings" },
|
|
{ "webserver", "Webserver and API settings" },
|
|
{ "files", "File locations" },
|
|
{ "misc", "Miscellaneous settings" },
|
|
{ "debug", "Debug settings" }
|
|
};
|
|
|
|
static struct {
|
|
const char *name;
|
|
struct {
|
|
const char *addr1;
|
|
const char *addr2;
|
|
} v4;
|
|
struct {
|
|
const char *addr1;
|
|
const char *addr2;
|
|
} v6;
|
|
} dns_server[] =
|
|
{
|
|
{ "Google (ECS, DNSSEC)", { "8.8.8.8", "8.8.4.4" }, { "2001:4860:4860:0:0:0:0:8888", "2001:4860:4860:0:0:0:0:8844" } },
|
|
{ "OpenDNS (ECS, DNSSEC)", { "208.67.222.222", "208.67.220.220" }, {"2620:119:35::35", "2620:119:53::53"} },
|
|
{ "Level3", { "4.2.2.1", "4.2.2.2" }, { NULL, NULL } },
|
|
{ "Comodo", { "8.26.56.26", "8.20.247.20" }, { NULL, NULL} },
|
|
{ "DNS.WATCH (DNSSEC)", { "84.200.69.80", "84.200.70.40" }, { "2001:1608:10:25:0:0:1c04:b12f", "2001:1608:10:25:0:0:9249:d69b" } },
|
|
{ "Quad9 (filtered, DNSSEC)", {"9.9.9.9", "149.112.112.112" }, { "2620:fe::fe", "2620:fe::9" } },
|
|
{ "Quad9 (unfiltered, no DNSSEC)", { "9.9.9.10", "149.112.112.10" }, { "2620:fe::10", "2620:fe::fe:10" } },
|
|
{ "Quad9 (filtered, ECS, DNSSEC)", { "9.9.9.11", "149.112.112.11" }, { "2620:fe::11", "2620:fe::fe:11" } },
|
|
{ "Cloudflare (DNSSEC)", { "1.1.1.1", "1.0.0.1" }, { "2606:4700:4700::1111", "2606:4700:4700::1001" } }
|
|
};
|
|
|
|
// The following functions are used to create the JSON output
|
|
// of the /api/config endpoint.
|
|
|
|
// This function is used to build the object architecture. It is called
|
|
// recursively to build the tree of objects.
|
|
static cJSON *get_or_create_object(cJSON *parent, const char *path_element)
|
|
{
|
|
// Check if this object already exists
|
|
cJSON *object = cJSON_GetObjectItem(parent, path_element);
|
|
|
|
// If not, create and append it to the parent
|
|
if(object == NULL)
|
|
{
|
|
object = JSON_NEW_OBJECT();
|
|
JSON_ADD_ITEM_TO_OBJECT(parent, path_element, object);
|
|
}
|
|
|
|
// Return the object
|
|
return object;
|
|
}
|
|
|
|
// This function is used to add a property to the JSON output using the
|
|
// appropriate type of the config item to add.
|
|
static cJSON *addJSONvalue(const enum conf_type conf_type, union conf_value *val)
|
|
{
|
|
switch(conf_type)
|
|
{
|
|
case CONF_BOOL:
|
|
return cJSON_CreateBool(val->b);
|
|
case CONF_INT:
|
|
return cJSON_CreateNumber(val->i);
|
|
case CONF_UINT:
|
|
case CONF_ENUM_PRIVACY_LEVEL:
|
|
return cJSON_CreateNumber(val->ui);
|
|
case CONF_UINT16:
|
|
return cJSON_CreateNumber(val->u16);
|
|
case CONF_LONG:
|
|
return cJSON_CreateNumber(val->l);
|
|
case CONF_ULONG:
|
|
return cJSON_CreateNumber(val->ul);
|
|
case CONF_DOUBLE:
|
|
return cJSON_CreateNumber(val->d);
|
|
case CONF_STRING:
|
|
case CONF_STRING_ALLOCATED:
|
|
return val->s ? cJSON_CreateStringReference(val->s) : cJSON_CreateNull();
|
|
case CONF_ENUM_PTR_TYPE:
|
|
return cJSON_CreateStringReference(get_ptr_type_str(val->ptr_type));
|
|
case CONF_ENUM_BUSY_TYPE:
|
|
return cJSON_CreateStringReference(get_busy_reply_str(val->busy_reply));
|
|
case CONF_ENUM_BLOCKING_MODE:
|
|
return cJSON_CreateStringReference(get_blocking_mode_str(val->blocking_mode));
|
|
case CONF_ENUM_REFRESH_HOSTNAMES:
|
|
return cJSON_CreateStringReference(get_refresh_hostnames_str(val->refresh_hostnames));
|
|
case CONF_ENUM_LISTENING_MODE:
|
|
return cJSON_CreateStringReference(get_listening_mode_str(val->listening_mode));
|
|
case CONF_STRUCT_IN_ADDR:
|
|
{
|
|
char addr4[INET_ADDRSTRLEN] = { 0 };
|
|
inet_ntop(AF_INET, &val->in_addr, addr4, INET_ADDRSTRLEN);
|
|
return cJSON_CreateString(addr4); // Performs a copy
|
|
}
|
|
case CONF_STRUCT_IN6_ADDR:
|
|
{
|
|
char addr6[INET6_ADDRSTRLEN] = { 0 };
|
|
inet_ntop(AF_INET6, &val->in6_addr, addr6, INET6_ADDRSTRLEN);
|
|
return cJSON_CreateString(addr6); // Performs a copy
|
|
}
|
|
case CONF_JSON_STRING_ARRAY:
|
|
{
|
|
// Return a duplicate to ensure our instance isn't getting freed
|
|
// after returning the reply
|
|
return cJSON_Duplicate(val->json, true);
|
|
}
|
|
default:
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
static const char *getJSONvalue(struct conf_item *conf_item, cJSON *elem)
|
|
{
|
|
if(conf_item == NULL || elem == NULL)
|
|
{
|
|
log_debug(DEBUG_CONFIG, "getJSONvalue(%p, %p) called with invalid arguments, skipping",
|
|
conf_item, elem);
|
|
return "invalid arguments";
|
|
}
|
|
switch(conf_item->t)
|
|
{
|
|
case CONF_BOOL:
|
|
{
|
|
// Check type
|
|
if(!cJSON_IsBool(elem))
|
|
return "not of type bool";
|
|
// Set item
|
|
conf_item->v.b = elem->valueint;
|
|
log_debug(DEBUG_CONFIG, "Set %s to %s", conf_item->k, conf_item->v.b ? "true" : "false");
|
|
break;
|
|
}
|
|
case CONF_INT:
|
|
{
|
|
// 1. Check it is a number
|
|
// 2. Check the number is within the allowed range for the given data type
|
|
if(!cJSON_IsNumber(elem) ||
|
|
elem->valuedouble < INT_MIN || elem->valuedouble > INT_MAX)
|
|
return "not of type integer";
|
|
// Set item
|
|
conf_item->v.i = elem->valueint;
|
|
log_debug(DEBUG_CONFIG, "Set %s to %i", conf_item->k, conf_item->v.i);
|
|
break;
|
|
}
|
|
case CONF_UINT:
|
|
{
|
|
// 1. Check it is a number
|
|
// 2. Check the number is within the allowed range for the given data type
|
|
if(!cJSON_IsNumber(elem) ||
|
|
elem->valuedouble < 0 || elem->valuedouble > UINT_MAX)
|
|
return "not of type unsigned integer";
|
|
// Set item
|
|
conf_item->v.ui = elem->valuedouble;
|
|
log_debug(DEBUG_CONFIG, "Set %s to %u", conf_item->k, conf_item->v.ui);
|
|
break;
|
|
}
|
|
case CONF_UINT16:
|
|
{
|
|
// 1. Check it is a number
|
|
// 2. Check the number is within the allowed range for the given data type
|
|
if(!cJSON_IsNumber(elem) ||
|
|
elem->valuedouble < 0 || elem->valuedouble > UINT16_MAX)
|
|
return "not of type unsigned integer (16bit)";
|
|
// Set item
|
|
conf_item->v.ui = elem->valuedouble;
|
|
log_debug(DEBUG_CONFIG, "Set %s to %u", conf_item->k, conf_item->v.ui);
|
|
break;
|
|
}
|
|
case CONF_LONG:
|
|
{
|
|
// 1. Check it is a number
|
|
// 2. Check the number is within the allowed range for the given data type
|
|
if(!cJSON_IsNumber(elem) ||
|
|
elem->valuedouble < LONG_MIN || elem->valuedouble > LONG_MAX)
|
|
return "not of type long";
|
|
// Set item
|
|
conf_item->v.l = elem->valuedouble;
|
|
log_debug(DEBUG_CONFIG, "Set %s to %li", conf_item->k, conf_item->v.l);
|
|
break;
|
|
}
|
|
case CONF_ULONG:
|
|
{
|
|
// 1. Check it is a number
|
|
// 2. Check the number is within the allowed range for the given data type
|
|
if(!cJSON_IsNumber(elem) ||
|
|
elem->valuedouble < 0 || elem->valuedouble > ULONG_MAX)
|
|
return "not of type unsigned long";
|
|
// Set item
|
|
conf_item->v.ul = elem->valuedouble;
|
|
log_debug(DEBUG_CONFIG, "Set %s to %lu", conf_item->k, conf_item->v.ul);
|
|
break;
|
|
}
|
|
case CONF_DOUBLE:
|
|
{
|
|
// Check it is a number
|
|
if(!cJSON_IsNumber(elem))
|
|
return "not of type unsigned long";
|
|
// Set item
|
|
conf_item->v.d = elem->valuedouble;
|
|
log_debug(DEBUG_CONFIG, "Set %s to %f", conf_item->k, conf_item->v.d);
|
|
break;
|
|
}
|
|
case CONF_STRING:
|
|
case CONF_STRING_ALLOCATED:
|
|
{
|
|
// Check type
|
|
if(!cJSON_IsString(elem))
|
|
return "not of type string";
|
|
// Free previously allocated memory (if applicable)
|
|
if(conf_item->t == CONF_STRING_ALLOCATED)
|
|
free(conf_item->v.s);
|
|
// Set item
|
|
conf_item->v.s = strdup(elem->valuestring);
|
|
log_debug(DEBUG_CONFIG, "Set %s to \"%s\"", conf_item->k, conf_item->v.s);
|
|
break;
|
|
}
|
|
case CONF_ENUM_PTR_TYPE:
|
|
{
|
|
// Check type
|
|
if(!cJSON_IsString(elem))
|
|
return "not of type string";
|
|
const int ptr_type = get_ptr_type_val(elem->valuestring);
|
|
if(ptr_type == -1)
|
|
return "invalid option";
|
|
// Set item
|
|
conf_item->v.ptr_type = ptr_type;
|
|
log_debug(DEBUG_CONFIG, "Set %s to %d", conf_item->k, conf_item->v.ptr_type);
|
|
break;
|
|
}
|
|
case CONF_ENUM_BUSY_TYPE:
|
|
{
|
|
// Check type
|
|
if(!cJSON_IsString(elem))
|
|
return "not of type string";
|
|
const int busy_reply = get_busy_reply_val(elem->valuestring);
|
|
if(busy_reply == -1)
|
|
return "invalid option";
|
|
// Set item
|
|
conf_item->v.busy_reply = busy_reply;
|
|
log_debug(DEBUG_CONFIG, "Set %s to %d", conf_item->k, conf_item->v.busy_reply);
|
|
break;
|
|
}
|
|
case CONF_ENUM_BLOCKING_MODE:
|
|
{
|
|
// Check type
|
|
if(!cJSON_IsString(elem))
|
|
return "not of type string";
|
|
const int blocking_mode = get_blocking_mode_val(elem->valuestring);
|
|
if(blocking_mode == -1)
|
|
return "invalid option";
|
|
// Set item
|
|
conf_item->v.blocking_mode = blocking_mode;
|
|
log_debug(DEBUG_CONFIG, "Set %s to %d", conf_item->k, conf_item->v.blocking_mode);
|
|
break;
|
|
}
|
|
case CONF_ENUM_REFRESH_HOSTNAMES:
|
|
{
|
|
// Check type
|
|
if(!cJSON_IsString(elem))
|
|
return "not of type string";
|
|
const int refresh_hostnames = get_refresh_hostnames_val(elem->valuestring);
|
|
if(refresh_hostnames == -1)
|
|
return "invalid option";
|
|
// Set item
|
|
conf_item->v.refresh_hostnames = refresh_hostnames;
|
|
log_debug(DEBUG_CONFIG, "Set %s to %d", conf_item->k, conf_item->v.refresh_hostnames );
|
|
break;
|
|
}
|
|
case CONF_ENUM_LISTENING_MODE:
|
|
{
|
|
// Check type
|
|
if(!cJSON_IsString(elem))
|
|
return "not of type string";
|
|
const int listening_mode = get_listening_mode_val(elem->valuestring);
|
|
if(listening_mode == -1)
|
|
return "invalid option";
|
|
// Set item
|
|
conf_item->v.listening_mode = listening_mode;
|
|
log_debug(DEBUG_CONFIG, "Set %s to %d", conf_item->k, conf_item->v.listening_mode);
|
|
break;
|
|
}
|
|
case CONF_ENUM_PRIVACY_LEVEL:
|
|
{
|
|
// Check type
|
|
if(!cJSON_IsNumber(elem))
|
|
return "not of type integer";
|
|
// Check allowed interval
|
|
if(elem->valuedouble < PRIVACY_SHOW_ALL || elem->valuedouble > PRIVACY_MAXIMUM)
|
|
return "not within valid range";
|
|
// Set item
|
|
conf_item->v.i = elem->valueint;
|
|
log_debug(DEBUG_CONFIG, "Set %s to %d", conf_item->k, conf_item->v.i);
|
|
break;
|
|
}
|
|
case CONF_STRUCT_IN_ADDR:
|
|
{
|
|
struct in_addr addr4 = { 0 };
|
|
if(!cJSON_IsString(elem))
|
|
return "not of type string";
|
|
if(!inet_pton(AF_INET, elem->valuestring, &addr4))
|
|
return "not a valid IPv4 address";
|
|
// Set item
|
|
memcpy(&conf_item->v.in_addr, &addr4, sizeof(addr4));
|
|
log_debug(DEBUG_CONFIG, "Set %s to %s", conf_item->k, elem->valuestring);
|
|
break;
|
|
}
|
|
case CONF_STRUCT_IN6_ADDR:
|
|
{
|
|
struct in6_addr addr6 = { 0 };
|
|
if(!cJSON_IsString(elem))
|
|
return "not of type string";
|
|
if(!inet_pton(AF_INET6, elem->valuestring, &addr6))
|
|
return "not a valid IPv6 address";
|
|
// Set item
|
|
memcpy(&conf_item->v.in6_addr, &addr6, sizeof(addr6));
|
|
log_debug(DEBUG_CONFIG, "Set %s to %s", conf_item->k, elem->valuestring);
|
|
break;
|
|
}
|
|
case CONF_JSON_STRING_ARRAY:
|
|
{
|
|
if(!cJSON_IsArray(elem))
|
|
return "not of type array";
|
|
const unsigned int elems = cJSON_GetArraySize(elem);
|
|
for(unsigned int i = 0; i < elems; i++)
|
|
{
|
|
const cJSON *item = cJSON_GetArrayItem(elem, i);
|
|
if(!cJSON_IsString(item))
|
|
return "array has invalid elements";
|
|
}
|
|
// If we reach this point, all elements are valid
|
|
conf_item->v.json = cJSON_Duplicate(elem, true);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static int api_config_get(struct ftl_conn *api)
|
|
{
|
|
// Parse query string parameters
|
|
bool detailed = false;
|
|
if(api->request->query_string != NULL)
|
|
{
|
|
// Check if we should return detailed config information
|
|
get_bool_var(api->request->query_string, "detailed", &detailed);
|
|
}
|
|
|
|
// Create root JSON object
|
|
cJSON *config_j = JSON_NEW_OBJECT();
|
|
|
|
// Does the user request only a subset of /config?
|
|
char **requested_path = NULL;
|
|
unsigned int min_level = 0;
|
|
if(api->item != NULL && strlen(api->item) > 0)
|
|
{
|
|
requested_path = gen_config_path(api->item, '/');
|
|
min_level = config_path_depth(requested_path);
|
|
}
|
|
|
|
// Iterate over all known config elements and create appropriate JSON
|
|
// objects + items for each of them
|
|
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);
|
|
|
|
// Get path depth
|
|
unsigned int level = config_path_depth(conf_item->p);
|
|
|
|
// Subset checking (if requested)
|
|
if(min_level > 0)
|
|
{
|
|
// Skip entry if level is too deep (don't skip arrays here)
|
|
if(min_level > level && conf_item->t != CONF_JSON_STRING_ARRAY)
|
|
continue;
|
|
// Skip entry if level if too deep (add one level for the array itself)
|
|
if(min_level > level+1 && conf_item->t == CONF_JSON_STRING_ARRAY)
|
|
continue;
|
|
// 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, min_level - 1))
|
|
continue;
|
|
}
|
|
|
|
cJSON *parent = config_j;
|
|
// Parse tree of properties and create JSON objects for each
|
|
// path element if they do not exist yet. We do not create the
|
|
// leaf object itself here (level - 1) as we want to add the
|
|
// actual value of the config item to it.
|
|
for(unsigned int j = 0; j < level - 1; j++)
|
|
parent = get_or_create_object(parent, conf_item->p[j]);
|
|
|
|
// Create the config item leaf object
|
|
if(detailed)
|
|
{
|
|
// Create the config item leaf object
|
|
cJSON *leaf = JSON_NEW_OBJECT();
|
|
|
|
// Add description
|
|
JSON_REF_STR_IN_OBJECT(leaf, "description", conf_item->h);
|
|
|
|
// Add allowed properties (if applicable)
|
|
if(conf_item->a != NULL)
|
|
JSON_ADD_ITEM_TO_OBJECT(leaf, "allowed", conf_item->a);
|
|
else
|
|
JSON_ADD_NULL_TO_OBJECT(leaf, "allowed");
|
|
|
|
// Add config item type
|
|
JSON_REF_STR_IN_OBJECT(leaf, "type", get_conf_type_str(conf_item->t));
|
|
|
|
// Add current value
|
|
cJSON *val = addJSONvalue(conf_item->t, &conf_item->v);
|
|
if(val == NULL)
|
|
{
|
|
log_warn("Cannot format config item type %s of type %i",
|
|
conf_item->k, conf_item->t);
|
|
continue;
|
|
}
|
|
JSON_ADD_ITEM_TO_OBJECT(leaf, "value", val);
|
|
|
|
// Add default value
|
|
cJSON *dval = addJSONvalue(conf_item->t, &conf_item->d);
|
|
if(dval == NULL)
|
|
{
|
|
log_warn("Cannot format config item type %s of type %i",
|
|
conf_item->k, conf_item->t);
|
|
continue;
|
|
}
|
|
JSON_ADD_ITEM_TO_OBJECT(leaf, "default", dval);
|
|
|
|
// Add config item flags
|
|
cJSON *flags = JSON_NEW_OBJECT();
|
|
JSON_ADD_BOOL_TO_OBJECT(flags, "restart_dnsmasq", conf_item->f & FLAG_RESTART_DNSMASQ);
|
|
JSON_ADD_BOOL_TO_OBJECT(flags, "advanced", conf_item->f & FLAG_ADVANCED_SETTING);
|
|
JSON_ADD_ITEM_TO_OBJECT(leaf, "flags", flags);
|
|
|
|
// Attach leave object to tree of objects
|
|
JSON_ADD_ITEM_TO_OBJECT(parent, conf_item->p[level - 1], leaf);
|
|
}
|
|
else
|
|
{
|
|
// Create the config item leaf object
|
|
cJSON *leaf = addJSONvalue(conf_item->t, &conf_item->v);
|
|
if(leaf == NULL)
|
|
{
|
|
log_warn("Cannot format config item type %s of type %i",
|
|
conf_item->k, conf_item->t);
|
|
continue;
|
|
}
|
|
JSON_ADD_ITEM_TO_OBJECT(parent, conf_item->p[level - 1], leaf);
|
|
}
|
|
}
|
|
|
|
// Release allocated memory
|
|
if(requested_path != NULL)
|
|
free_config_path(requested_path);
|
|
|
|
// Build and return JSON response
|
|
cJSON *json = JSON_NEW_OBJECT();
|
|
JSON_ADD_ITEM_TO_OBJECT(json, "config", config_j);
|
|
JSON_SEND_OBJECT(json);
|
|
}
|
|
|
|
static int api_config_patch(struct ftl_conn *api)
|
|
{
|
|
// Is there a payload with valid JSON data?
|
|
if (api->payload.json == NULL)
|
|
{
|
|
if (api->payload.json_error == NULL)
|
|
return send_json_error(api, 400,
|
|
"bad_request",
|
|
"No request body data",
|
|
NULL);
|
|
else
|
|
return send_json_error(api, 400,
|
|
"bad_request",
|
|
"Invalid request body data (no valid JSON), error before hint",
|
|
api->payload.json_error);
|
|
}
|
|
|
|
// Is there a "config" object at the root of the received JSON payload?
|
|
cJSON *conf = cJSON_GetObjectItem(api->payload.json, "config");
|
|
if (!cJSON_IsObject(conf))
|
|
{
|
|
return send_json_error(api, 400,
|
|
"body_error",
|
|
"No \"config\" object in body data",
|
|
NULL);
|
|
}
|
|
|
|
// Read all known config items
|
|
bool config_changed = false;
|
|
bool dnsmasq_changed = false;
|
|
bool rewrite_custom_list = false;
|
|
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 *new_item = get_conf_item(&newconf, i);
|
|
|
|
// Get path depth
|
|
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, 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", new_item->k);
|
|
continue;
|
|
}
|
|
|
|
// Try to set value and report error on failure
|
|
const char *response = getJSONvalue(new_item, elem);
|
|
if(response != NULL)
|
|
{
|
|
log_err("/api/config: %s invalid: %s", new_item->k, response);
|
|
continue;
|
|
}
|
|
|
|
// Get pointer to memory location of this conf_item (global)
|
|
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(new_item, conf_item))
|
|
{
|
|
log_debug(DEBUG_CONFIG, "Config item %s: Unchanged", conf_item->k);
|
|
continue;
|
|
}
|
|
|
|
// Memorize that at least one config item actually changed
|
|
config_changed = true;
|
|
|
|
// If we reach this point, a valid setting was found and changed
|
|
// Check if this item requires a config-rewrite + restart of dnsmasq
|
|
if(conf_item->f & FLAG_RESTART_DNSMASQ)
|
|
dnsmasq_changed = true;
|
|
|
|
// Check if this item requires a rewrite of the custom.list file
|
|
if(conf_item == &newconf.dns.hosts)
|
|
rewrite_custom_list = true;
|
|
}
|
|
|
|
// Process new config only when at least one value changed
|
|
if(config_changed)
|
|
{
|
|
// Request restart of FTL
|
|
if(dnsmasq_changed)
|
|
{
|
|
char errbuf[ERRBUF_SIZE] = { 0 };
|
|
if(write_dnsmasq_config(&newconf, true, errbuf))
|
|
api->ftl.restart = true;
|
|
else
|
|
{
|
|
return send_json_error(api, 400,
|
|
"bad_request",
|
|
"Invalid configuration",
|
|
errbuf);
|
|
}
|
|
}
|
|
else if(rewrite_custom_list)
|
|
{
|
|
// We need to rewrite the custom.list file but do not need to
|
|
// restart dnsmasq. If dnsmasq is going to be restarted anyway,
|
|
// this is not necessary as the file will be rewritten during
|
|
// the restart
|
|
write_custom_list();
|
|
}
|
|
|
|
// Install new configuration
|
|
replace_config(&newconf);
|
|
|
|
// Store changed configuration to disk
|
|
writeFTLtoml(true);
|
|
|
|
// Reload debug levels
|
|
set_debug_flags();
|
|
}
|
|
else
|
|
{
|
|
// Nothing changed, merely release copied config memory
|
|
free_config(&newconf);
|
|
}
|
|
|
|
// Return full config after possible changes above
|
|
return api_config_get(api);
|
|
}
|
|
|
|
// Inspired by https://stackoverflow.com/a/32496721
|
|
//static void replace_char(char* str, char find, char replace)
|
|
//{
|
|
// for (char *current_pos = strchr(str, find); (current_pos = strchr(str+1, find)) != NULL; *current_pos = replace);
|
|
//}
|
|
|
|
static int api_config_put_delete(struct ftl_conn *api)
|
|
{
|
|
if(api->item == NULL || strlen(api->item) == 0)
|
|
return 0;
|
|
|
|
char **requested_path = gen_config_path(api->item, '/');
|
|
const unsigned int min_level = config_path_depth(requested_path);
|
|
|
|
const char *hint = NULL, *message = NULL;
|
|
if(api->method == HTTP_PUT)
|
|
hint = "Use, e.g., PUT /config/dnsmasq/upstreams/127.0.0.1 to add \"127.0.0.1\" to config.dns.upstreams";
|
|
else
|
|
hint = "Use, e.g., DELETE /config/dnsmasq/upstreams/127.0.0.1 to remove \"127.0.0.1\" from config.dns.upstreams";
|
|
|
|
if(min_level < 2)
|
|
{
|
|
// Release allocated memory
|
|
if(requested_path != NULL)
|
|
free_config_path(requested_path);
|
|
|
|
return send_json_error(api, 400,
|
|
"bad_request",
|
|
"Invalid path depth",
|
|
hint);
|
|
}
|
|
|
|
char *new_item_str = requested_path[min_level - 1];
|
|
|
|
// Convert path to items, e.g.,
|
|
// dnsmasq/dhcp/active -> dhcp.active
|
|
//char *item = strdup(api->item);
|
|
//replace_char(item, '/', '.');
|
|
|
|
// Read all known config items
|
|
bool dnsmasq_changed = false;
|
|
bool rewrite_custom_list = false;
|
|
bool found = false;
|
|
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 *new_item = get_conf_item(&newconf, i);
|
|
|
|
// We support PUT only for adding to string arrays
|
|
if(new_item->t != CONF_JSON_STRING_ARRAY)
|
|
continue;
|
|
|
|
// Get path depth
|
|
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(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
|
|
if(min_level != level + 1)
|
|
continue;
|
|
|
|
// Check if this entry does already exist in the array
|
|
int idx = 0;
|
|
for(; idx < cJSON_GetArraySize(new_item->v.json); idx++)
|
|
{
|
|
cJSON *elem = cJSON_GetArrayItem(new_item->v.json, idx);
|
|
if(elem != NULL && elem->valuestring != NULL &&
|
|
strcmp(elem->valuestring, new_item_str) == 0)
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(api->method == HTTP_PUT)
|
|
{
|
|
if(found)
|
|
{
|
|
// Item already present
|
|
message = "Item already present";
|
|
hint = "Uniqueness of items is enforced";
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
// Add new item to array
|
|
JSON_COPY_STR_TO_ARRAY(new_item->v.json, new_item);
|
|
found = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(found)
|
|
{
|
|
// Remove item from array
|
|
cJSON_DeleteItemFromArray(new_item->v.json, idx);
|
|
}
|
|
else
|
|
{
|
|
// Item not found
|
|
message = "Item not found";
|
|
hint = "Can only delete existing items";
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If we reach this point, a valid setting was found and changed
|
|
// Check if this item requires a config-rewrite + restart of dnsmasq
|
|
if(new_item->f & FLAG_RESTART_DNSMASQ)
|
|
dnsmasq_changed = true;
|
|
|
|
// Check if this item requires a rewrite of the custom.list file
|
|
if(new_item == &newconf.dns.hosts)
|
|
rewrite_custom_list = true;
|
|
break;
|
|
}
|
|
|
|
// Release allocated memory
|
|
if(requested_path != NULL)
|
|
free_config_path(requested_path);
|
|
|
|
// Error 404 if not found
|
|
if(!found || message != NULL)
|
|
{
|
|
// For any other error, a more specific message will have been added
|
|
// above
|
|
if(!message)
|
|
message = "No item specified";
|
|
return send_json_error(api, 400,
|
|
"bad_request",
|
|
message,
|
|
hint);
|
|
}
|
|
|
|
// We need to build a new config (and carefully test it!) whenever dnsmasq
|
|
// options have changed that need a restart of the resolver
|
|
if(dnsmasq_changed)
|
|
{
|
|
char errbuf[ERRBUF_SIZE] = { 0 };
|
|
// Request restart of FTL
|
|
if(write_dnsmasq_config(&newconf, true, errbuf))
|
|
api->ftl.restart = true;
|
|
else
|
|
{
|
|
// The new config did not work
|
|
return send_json_error(api, 400,
|
|
"bad_request",
|
|
"Invalid configuration",
|
|
errbuf);
|
|
}
|
|
}
|
|
else if(rewrite_custom_list)
|
|
{
|
|
// We need to rewrite the custom.list file but do not need to
|
|
// restart dnsmasq. If dnsmasq is going to be restarted anyway,
|
|
// this is not necessary as the file will be rewritten during
|
|
// the restart
|
|
write_custom_list();
|
|
}
|
|
|
|
// Install new configuration
|
|
replace_config(&newconf);
|
|
|
|
// Store changed configuration to disk
|
|
writeFTLtoml(true);
|
|
|
|
// Reload debug levels
|
|
set_debug_flags();
|
|
|
|
return api->method == HTTP_PUT ? 201 : 204; // 201 - Created or 204 - No content
|
|
}
|
|
|
|
// Endpoint /api/config router
|
|
int api_config(struct ftl_conn *api)
|
|
{
|
|
if(api->method == HTTP_GET)
|
|
return api_config_get(api);
|
|
|
|
// POST: Create a new config (not supported)
|
|
// PATCH: Replace parts of the the config with the provided one
|
|
// PUT: Replaces the entire config with the provided one (not supported
|
|
// but PATCH with a full config is the same)
|
|
else if(api->method == HTTP_PATCH)
|
|
return api_config_patch(api);
|
|
else if(api->method == HTTP_PUT || api->method == HTTP_DELETE)
|
|
return api_config_put_delete(api);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int api_config_topics(struct ftl_conn *api)
|
|
{
|
|
cJSON *topics = JSON_NEW_ARRAY();
|
|
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);
|
|
JSON_REF_STR_IN_OBJECT(topic, "description", config_topics[i].description);
|
|
JSON_ADD_ITEM_TO_ARRAY(topics, topic);
|
|
}
|
|
|
|
// Build and return JSON response
|
|
cJSON *json = JSON_NEW_OBJECT();
|
|
JSON_ADD_ITEM_TO_OBJECT(json, "topics", topics);
|
|
JSON_SEND_OBJECT(json);
|
|
}
|
|
|
|
int api_config_server(struct ftl_conn *api)
|
|
{
|
|
cJSON *servers = JSON_NEW_ARRAY();
|
|
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);
|
|
|
|
cJSON *v4 = JSON_NEW_ARRAY();
|
|
if(dns_server[i].v4.addr1 != NULL)
|
|
JSON_REF_STR_IN_ARRAY(v4, dns_server[i].v4.addr1);
|
|
if(dns_server[i].v4.addr2 != NULL)
|
|
JSON_REF_STR_IN_ARRAY(v4, dns_server[i].v4.addr2);
|
|
JSON_ADD_ITEM_TO_OBJECT(server, "v4", v4);
|
|
|
|
cJSON *v6 = JSON_NEW_ARRAY();
|
|
if(dns_server[i].v6.addr1 != NULL)
|
|
JSON_REF_STR_IN_ARRAY(v6, dns_server[i].v6.addr1);
|
|
if(dns_server[i].v6.addr2 != NULL)
|
|
JSON_REF_STR_IN_ARRAY(v6, dns_server[i].v6.addr2);
|
|
JSON_ADD_ITEM_TO_OBJECT(server, "v6", v6);
|
|
|
|
JSON_ADD_ITEM_TO_ARRAY(servers, server);
|
|
}
|
|
|
|
// Build and return JSON response
|
|
cJSON *json = JSON_NEW_OBJECT();
|
|
JSON_ADD_ITEM_TO_OBJECT(json, "server", servers);
|
|
JSON_SEND_OBJECT(json);
|
|
}
|