mirror of
https://github.com/pi-hole/FTL.git
synced 2024-10-26 16:52:18 +02:00
b5a6ae44aa
Signed-off-by: DL6ER <dl6er@dl6er.de>
315 lines
17 KiB
C
315 lines
17 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 routes
|
|
*
|
|
* This file is copyright under the latest version of the EUPL.
|
|
* Please see LICENSE file for your rights under this license. */
|
|
|
|
#include "FTL.h"
|
|
// struct mg_connection
|
|
#include "webserver/civetweb/civetweb.h"
|
|
#include "webserver/http-common.h"
|
|
#include "webserver/json_macros.h"
|
|
#include "api.h"
|
|
#include "shmem.h"
|
|
// exit_code
|
|
#include "signals.h"
|
|
|
|
static int api_endpoints(struct ftl_conn *api);
|
|
|
|
static struct {
|
|
const char *uri;
|
|
const char *parameters;
|
|
int (*func)(struct ftl_conn *api);
|
|
struct api_options opts;
|
|
bool require_auth;
|
|
enum http_method methods;
|
|
} api_request[] = {
|
|
// 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/auth/sessions", "", api_auth_sessions, { false, true, 0 }, true, HTTP_GET },
|
|
{ "/api/auth/session", "/{id}", api_auth_session_delete, { false, true, 0 }, true, HTTP_DELETE },
|
|
{ "/api/auth/totp", "", generateTOTP, { false, true, 0 }, false, HTTP_GET },
|
|
{ "/api/auth", "", api_auth, { false, true, 0 }, false, HTTP_GET | HTTP_POST | HTTP_DELETE },
|
|
{ "/api/dns/blocking", "", api_dns_blocking, { false, true, 0 }, true, HTTP_GET | HTTP_POST },
|
|
{ "/api/clients/_suggestions", "", api_client_suggestions, { 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/search", "/{domain}", api_search, { false, true, 0 }, true, HTTP_GET },
|
|
{ "/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/info/messages/count", "", api_info_messages_count, { false, true, 0 }, true, HTTP_GET },
|
|
{ "/api/info/messages", "/{message_id}", api_info_messages, { false, true, 0 }, true, HTTP_DELETE },
|
|
{ "/api/info/messages", "", api_info_messages, { false, true, 0 }, true, HTTP_GET },
|
|
{ "/api/info/metrics", "", api_info_metrics, { 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/webserver", "", api_logs, { false, true, FIFO_WEBSERVER }, 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/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/network/devices", "/{device_id}", api_network_devices, { false, true, 0 }, true, HTTP_DELETE },
|
|
{ "/api/endpoints", "", api_endpoints, { false, true, 0 }, true, HTTP_GET },
|
|
{ "/api/teleporter", "", api_teleporter, { false, false, 0 }, true, HTTP_GET | HTTP_POST },
|
|
{ "/api/dhcp/leases", "", api_dhcp_leases_GET, { false, true, 0 }, true, HTTP_GET },
|
|
{ "/api/dhcp/leases", "/{hwaddr}/{ip}/{clid}", api_dhcp_leases_DELETE, { false, true, 0 }, true, HTTP_DELETE },
|
|
{ "/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/action/restartdns", "", api_action_restartDNS, { false, true, 0 }, true, HTTP_POST },
|
|
{ "/api/action/flush/logs", "", api_action_flush_logs, { false, true, 0 }, true, HTTP_POST },
|
|
{ "/api/action/flush/arp", "", api_action_flush_arp, { 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)
|
|
{
|
|
// Prepare API info struct
|
|
struct ftl_conn api = {
|
|
conn,
|
|
mg_get_request_info(conn),
|
|
http_method(conn),
|
|
NULL,
|
|
NULL,
|
|
API_AUTH_UNAUTHORIZED,
|
|
double_time(),
|
|
{ false, NULL, NULL, NULL, 0u },
|
|
{ false },
|
|
{ false, false, 0 }
|
|
};
|
|
|
|
log_debug(DEBUG_API, "Requested API URI: %s %s ? %s (Content-Type %s)",
|
|
api.request->request_method,
|
|
api.request->local_uri_raw,
|
|
api.request->query_string,
|
|
mg_get_header(conn, "Content-Type"));
|
|
|
|
int ret = 0;
|
|
|
|
// 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 < ArraySize(api_request); i++)
|
|
{
|
|
// Check if the requested method is allowed
|
|
if(!(api_request[i].methods & api.method) && api.method != HTTP_OPTIONS)
|
|
continue;
|
|
|
|
// Check if the requested URI starts with the API endpoint
|
|
if((api.item = startsWith(api_request[i].uri, &api)) != NULL)
|
|
{
|
|
|
|
// Copy options to API struct
|
|
memcpy(&api.opts, &api_request[i].opts, sizeof(api.opts));
|
|
|
|
// If this is an OPTIONS request, we add the supported
|
|
// options of this endpoint and continue
|
|
if(api.method == HTTP_OPTIONS)
|
|
{
|
|
allowed_methods |= api_request[i].methods;
|
|
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)
|
|
{
|
|
unauthorized = true;
|
|
break;
|
|
}
|
|
|
|
// Call the API function and get the return code
|
|
log_debug(DEBUG_API, "Processing %s %s in %s",
|
|
api.request->request_method,
|
|
api.request->local_uri_raw,
|
|
api_request[i].uri);
|
|
ret = api_request[i].func(&api);
|
|
log_debug(DEBUG_API, "Done");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Free memory allocated for action path (if allocated)
|
|
if(api.action_path != NULL)
|
|
{
|
|
free(api.action_path);
|
|
api.action_path = NULL;
|
|
}
|
|
|
|
// Free JSON-parsed payload memory (if allocated)
|
|
if(api.payload.json != NULL)
|
|
{
|
|
cJSON_Delete(api.payload.json);
|
|
api.payload.json = 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)
|
|
{
|
|
// Return with unauthorized payload
|
|
// Do this only after having cleaned up above
|
|
return send_json_unauthorized(&api);
|
|
}
|
|
|
|
// The HTTP OPTIONS method requests permitted communication options for
|
|
// a given URL or server. We no not implement the wildcard OPTIONS method
|
|
// but instead return the allowed methods for the requested endpoint
|
|
// in the Allow header.
|
|
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS
|
|
if(api.method == HTTP_OPTIONS)
|
|
{
|
|
// Send Allow header
|
|
mg_printf(conn, "HTTP/1.1 204 No Content\r\n"
|
|
"Allow: ");
|
|
|
|
// Loop over all possible methods
|
|
unsigned int m = 0;
|
|
for(enum http_method j = HTTP_GET; j < HTTP_OPTIONS; j <<= 1)
|
|
{
|
|
// Check if this method is allowed for this endpoint
|
|
if(allowed_methods & j)
|
|
mg_printf(conn, "%s%s", m++ > 0 ? ", " : "", get_http_method_str(j));
|
|
}
|
|
|
|
// Finish header and send empty body
|
|
mg_printf(conn, "\r\n"
|
|
"Content-Length: 0\r\n"
|
|
"Connection: close\r\n\r\n");
|
|
return 204;
|
|
}
|
|
|
|
// Check if we need to return with not found payload
|
|
if(ret == 0)
|
|
{
|
|
// not found or invalid request
|
|
ret = send_json_error(&api, 404,
|
|
"not_found",
|
|
"Not found",
|
|
api.request->local_uri_raw);
|
|
}
|
|
|
|
// Restart FTL if requested
|
|
if(api.ftl.restart)
|
|
{
|
|
exit_code = RESTART_FTL_CODE;
|
|
FTL_terminate = 1;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int api_endpoints(struct ftl_conn *api)
|
|
{
|
|
cJSON *get = JSON_NEW_ARRAY();
|
|
cJSON *post = JSON_NEW_ARRAY();
|
|
cJSON *put = JSON_NEW_ARRAY();
|
|
cJSON *patch = JSON_NEW_ARRAY();
|
|
cJSON *delete = JSON_NEW_ARRAY();
|
|
|
|
// Add endpoints to JSON array
|
|
for(unsigned int i = 0; i < ArraySize(api_request); i++)
|
|
{
|
|
for(enum http_method method = HTTP_GET; method <= HTTP_DELETE; method <<= 1)
|
|
{
|
|
if(!(api_request[i].methods & method))
|
|
continue;
|
|
cJSON *endpoint = JSON_NEW_OBJECT();
|
|
JSON_REF_STR_IN_OBJECT(endpoint, "uri", api_request[i].uri);
|
|
JSON_REF_STR_IN_OBJECT(endpoint, "parameters", api_request[i].parameters);
|
|
|
|
// Add endpoint to the correct array
|
|
switch(method)
|
|
{
|
|
case HTTP_UNKNOWN: // fall through
|
|
case HTTP_OPTIONS:
|
|
cJSON_Delete(endpoint);
|
|
break;
|
|
case HTTP_GET:
|
|
JSON_ADD_ITEM_TO_ARRAY(get, endpoint);
|
|
break;
|
|
case HTTP_POST:
|
|
JSON_ADD_ITEM_TO_ARRAY(post, endpoint);
|
|
break;
|
|
case HTTP_PUT:
|
|
JSON_ADD_ITEM_TO_ARRAY(put, endpoint);
|
|
break;
|
|
case HTTP_PATCH:
|
|
JSON_ADD_ITEM_TO_ARRAY(patch, endpoint);
|
|
break;
|
|
case HTTP_DELETE:
|
|
JSON_ADD_ITEM_TO_ARRAY(delete, endpoint);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add endpoints to JSON object
|
|
cJSON *endpoints = JSON_NEW_OBJECT();
|
|
JSON_ADD_ITEM_TO_OBJECT(endpoints, "get", get);
|
|
JSON_ADD_ITEM_TO_OBJECT(endpoints, "post", post);
|
|
JSON_ADD_ITEM_TO_OBJECT(endpoints, "put", put);
|
|
JSON_ADD_ITEM_TO_OBJECT(endpoints, "patch", patch);
|
|
JSON_ADD_ITEM_TO_OBJECT(endpoints, "delete", delete);
|
|
cJSON *json = JSON_NEW_OBJECT();
|
|
JSON_ADD_ITEM_TO_OBJECT(json, "endpoints", endpoints);
|
|
|
|
// Send response
|
|
JSON_SEND_OBJECT(json);
|
|
}
|