Files
FTL/src/api/list.c
T
DL6ER fa4c194045 Improve URI matching algorithm
Signed-off-by: DL6ER <dl6er@dl6er.de>
2021-01-26 17:46:11 +01:00

476 lines
14 KiB
C

/* Pi-hole: A black hole for Internet advertisements
* (c) 2020 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* FTL Engine
* API Implementation /api/{allow,deny}list
*
* 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 "routes.h"
#include "../database/gravity-db.h"
#include "../events.h"
// getNameFromIP()
#include "../database/network-table.h"
static int api_list_read(struct ftl_conn *api,
const int code,
const enum gravity_list_type listtype,
const char *argument)
{
const char *sql_msg = NULL;
if(!gravityDB_readTable(listtype, argument, &sql_msg))
{
cJSON *json = JSON_NEW_OBJ();
// Add argument (may be NULL = not available)
JSON_OBJ_REF_STR(json, "argument", argument);
// Add SQL message (may be NULL = not available)
if (sql_msg != NULL) {
JSON_OBJ_REF_STR(json, "sql_msg", sql_msg);
} else {
JSON_OBJ_ADD_NULL(json, "sql_msg");
}
return send_json_error(api, 400, // 400 Bad Request
"database_error",
"Could not read domains from database table",
json);
}
tablerow row;
cJSON *items = JSON_NEW_ARRAY();
while(gravityDB_readTableGetRow(&row, &sql_msg))
{
cJSON *item = JSON_NEW_OBJ();
// Special fields
if(listtype == GRAVITY_GROUPS)
{
JSON_OBJ_COPY_STR(item, "name", row.name);
if(row.description != NULL) {
JSON_OBJ_COPY_STR(item, "description", row.description);
} else {
JSON_OBJ_ADD_NULL(item, "description");
}
}
else if(listtype == GRAVITY_ADLISTS)
{
JSON_OBJ_COPY_STR(item, "address", row.address);
if(row.comment != NULL) {
JSON_OBJ_COPY_STR(item, "comment", row.comment);
} else {
JSON_OBJ_ADD_NULL(item, "comment");
}
}
else if(listtype == GRAVITY_CLIENTS)
{
if(row.ip != NULL)
{
JSON_OBJ_COPY_STR(item, "ip", row.ip);
char *name = getNameFromIP(row.ip);
if(name != NULL)
{
JSON_OBJ_COPY_STR(item, "name", name);
free(name);
}
else
{
JSON_OBJ_ADD_NULL(item, "name");
}
}
else
{
JSON_OBJ_ADD_NULL(item, "ip");
JSON_OBJ_ADD_NULL(item, "name");
}
if(row.comment != NULL) {
JSON_OBJ_COPY_STR(item, "comment", row.comment);
} else {
JSON_OBJ_ADD_NULL(item, "comment");
}
}
else // domainlists
{
JSON_OBJ_COPY_STR(item, "domain", row.domain);
JSON_OBJ_REF_STR(item, "type", row.type);
if(row.comment != NULL) {
JSON_OBJ_COPY_STR(item, "comment", row.comment);
} else {
JSON_OBJ_ADD_NULL(item, "comment");
}
}
if(row.group_ids != NULL) {
// Black magic at work here: We build a JSON array from
// the group_concat result delivered from the database,
// parse it as valid array and append it as item to the
// data
logg("row.group_ids = %p \"%s\"", row.group_ids, row.group_ids);
char group_ids_str[strlen(row.group_ids)+3u];
group_ids_str[0] = '[';
strcpy(group_ids_str+1u , row.group_ids);
group_ids_str[sizeof(group_ids_str)-2u] = ']';
group_ids_str[sizeof(group_ids_str)-1u] = '\0';
cJSON * group_ids = cJSON_Parse(group_ids_str);
JSON_OBJ_ADD_ITEM(item, "groups", group_ids);
} else {
// Empty group set
cJSON *group_ids = JSON_NEW_ARRAY();
JSON_OBJ_ADD_ITEM(item, "groups", group_ids);
}
// Clients don't have the enabled property
if(listtype != GRAVITY_CLIENTS)
JSON_OBJ_ADD_BOOL(item, "enabled", row.enabled);
cJSON *database = JSON_NEW_OBJ();
JSON_OBJ_ADD_NUMBER(database, "id", row.id);
JSON_OBJ_ADD_NUMBER(database, "date_added", row.date_added);
JSON_OBJ_ADD_NUMBER(database, "date_modified", row.date_modified);
JSON_OBJ_ADD_ITEM(item, "database", database);
JSON_ARRAY_ADD_ITEM(items, item);
}
gravityDB_readTableFinalize();
if(sql_msg == NULL)
{
// No error, send domains array
const char *objname;
cJSON *json = JSON_NEW_OBJ();
if(listtype == GRAVITY_GROUPS)
objname = "groups";
else if(listtype == GRAVITY_ADLISTS)
objname = "lists";
else if(listtype == GRAVITY_CLIENTS)
objname = "clients";
else // domainlists
objname = "domains";
JSON_OBJ_ADD_ITEM(json, objname, items);
JSON_SEND_OBJECT_CODE(json, code);
}
else
{
JSON_DELETE(items);
cJSON *json = JSON_NEW_OBJ();
// Add argument (may be NULL = not available)
JSON_OBJ_REF_STR(json, "argument", argument);
// Add SQL message (may be NULL = not available)
if (sql_msg != NULL) {
JSON_OBJ_REF_STR(json, "sql_msg", sql_msg);
} else {
JSON_OBJ_ADD_NULL(json, "sql_msg");
}
return send_json_error(api, 400, // 400 Bad Request
"database_error",
"Could not read from gravity database",
json);
}
}
static int api_list_write(struct ftl_conn *api,
const enum gravity_list_type listtype,
const char *argument,
char payload[MAX_PAYLOAD_BYTES])
{
tablerow row = { 0 };
// Set argument
row.argument = argument;
// Check argument is not empty
if (api->method == HTTP_PUT && strlen(argument) < 1) {
return send_json_error(api, 400,
"bad_request",
"Missing argument, check URI",
NULL);
}
// Check if valid JSON payload is available
if (api->payload.json == NULL) {
return send_json_error(api, 400,
"bad_request",
"Invalid request body data",
NULL);
}
// Clients don't have the enabled property
cJSON *json_enabled = cJSON_GetObjectItemCaseSensitive(api->payload.json, "enabled");
if (cJSON_IsBool(json_enabled))
row.enabled = cJSON_IsTrue(json_enabled);
else
row.enabled = true; // Default value
cJSON *json_item = cJSON_GetObjectItemCaseSensitive(api->payload.json, "item");
if(cJSON_IsString(json_item) && strlen(json_item->valuestring) > 0)
row.argument = json_item->valuestring;
else if (api->method != HTTP_PUT) // PUT uses the URI argument
{
return send_json_error(api, 400,
"bad_request",
"No \"item\" string in body data",
NULL);
}
cJSON *json_comment = cJSON_GetObjectItemCaseSensitive(api->payload.json, "comment");
if(cJSON_IsString(json_comment) && strlen(json_comment->valuestring) > 0)
row.comment = json_comment->valuestring;
else
row.comment = NULL; // Default value
cJSON *json_description = cJSON_GetObjectItemCaseSensitive(api->payload.json, "description");
if(cJSON_IsString(json_description) && strlen(json_description->valuestring) > 0)
row.description = json_description->valuestring;
else
row.description = NULL; // Default value
cJSON *json_oldtype = cJSON_GetObjectItemCaseSensitive(api->payload.json, "oldtype");
if(cJSON_IsString(json_oldtype) && strlen(json_oldtype->valuestring) > 0)
row.oldtype = json_oldtype->valuestring;
else
row.oldtype = NULL; // Default value
bool okay = true;
char *regex_msg = NULL;
if(listtype == GRAVITY_DOMAINLIST_ALLOW_REGEX || listtype == GRAVITY_DOMAINLIST_DENY_REGEX)
{
// Test validity of this regex
regexData regex = { 0 };
okay = compile_regex(argument, &regex, &regex_msg);
}
// Try to add item to table
const char *sql_msg = NULL;
if(okay && gravityDB_addToTable(listtype, &row, &sql_msg, api->method))
{
if(listtype != GRAVITY_GROUPS)
{
cJSON *groups = cJSON_GetObjectItemCaseSensitive(api->payload.json, "groups");
if(groups != NULL)
okay = gravityDB_edit_groups(listtype, groups, &row, &sql_msg);
else
// The groups array is optional, we still succeed if it
// is omitted (groups stay as they are)
okay = true;
}
else
{
// Groups cannot be assigned to groups
okay = true;
}
}
if(!okay)
{
// Error adding item, prepare error object
cJSON *json = JSON_NEW_OBJ();
JSON_OBJ_REF_STR(json, "argument", argument);
JSON_OBJ_ADD_BOOL(json, "enabled", row.enabled);
if(row.comment != NULL)
JSON_OBJ_REF_STR(json, "comment", row.comment);
if(row.description != NULL)
JSON_OBJ_REF_STR(json, "description", row.description);
if(row.name != NULL)
JSON_OBJ_REF_STR(json, "name", row.name);
if(row.oldtype != NULL)
JSON_OBJ_REF_STR(json, "oldtype", row.oldtype);
// Add SQL message (may be NULL = not available)
if (sql_msg != NULL) {
JSON_OBJ_REF_STR(json, "sql_msg", sql_msg);
} else {
JSON_OBJ_ADD_NULL(json, "sql_msg");
}
// Add regex error (may not be available)
if (regex_msg != NULL) {
JSON_OBJ_COPY_STR(json, "regex_msg", regex_msg);
free(regex_msg);
regex_msg = NULL;
} else {
JSON_OBJ_ADD_NULL(json, "regex_msg");
}
// Send error reply
return send_json_error(api, 400, // 400 Bad Request
"database_error",
"Could not add to gravity database",
json);
}
// else: everything is okay
// Inform the resolver that it needs to reload the domainlists
set_event(RELOAD_GRAVITY);
int response_code = 201; // 201 - Created
if(api->method == HTTP_PUT)
response_code = 200; // 200 - OK
// Send GET style reply
return api_list_read(api, response_code, listtype, argument);
}
static int api_list_remove(struct ftl_conn *api,
const enum gravity_list_type listtype,
const char *argument)
{
cJSON *json = JSON_NEW_OBJ();
const char *sql_msg = NULL;
if(gravityDB_delFromTable(listtype, argument, &sql_msg))
{
// Inform the resolver that it needs to reload the domainlists
set_event(RELOAD_GRAVITY);
// Send empty reply with code 204 No Content
JSON_SEND_OBJECT_CODE(json, 204);
}
else
{
// Add argument
JSON_OBJ_REF_STR(json, "argument", argument);
// Add SQL message (may be NULL = not available)
if (sql_msg != NULL) {
JSON_OBJ_REF_STR(json, "sql_msg", sql_msg);
} else {
JSON_OBJ_ADD_NULL(json, "sql_msg");
}
// Send error reply
return send_json_error(api, 400,
"database_error",
"Could not remove domain from database table",
json);
}
}
int api_list(struct ftl_conn *api)
{
// Verify requesting client is allowed to see this ressource
char payload[MAX_PAYLOAD_BYTES] = { 0 };
if(check_client_auth(api) == API_AUTH_UNAUTHORIZED)
{
return send_json_unauthorized(api);
}
enum gravity_list_type listtype;
bool can_modify = false;
const char *argument = NULL;
if((argument = startsWith("/api/groups", api)) != NULL)
{
listtype = GRAVITY_GROUPS;
can_modify = true;
}
else if((argument = startsWith("/api/lists", api)) != NULL)
{
listtype = GRAVITY_ADLISTS;
can_modify = true;
}
else if((argument = startsWith("/api/clients", api)) != NULL)
{
listtype = GRAVITY_CLIENTS;
can_modify = true;
}
else if((argument = startsWith("/api/domains/allow/exact", api)) != NULL)
{
listtype = GRAVITY_DOMAINLIST_ALLOW_EXACT;
can_modify = true;
}
else if((argument = startsWith("/api/domains/allow/regex", api)) != NULL)
{
listtype = GRAVITY_DOMAINLIST_ALLOW_REGEX;
can_modify = true;
}
else if((argument = startsWith("/api/domains/allow", api)) != NULL)
{
listtype = GRAVITY_DOMAINLIST_ALLOW_ALL;
}
else if((argument = startsWith("/api/domains/deny/exact", api)) != NULL)
{
listtype = GRAVITY_DOMAINLIST_DENY_EXACT;
can_modify = true;
}
else if((argument = startsWith("/api/domains/deny/regex", api)) != NULL)
{
listtype = GRAVITY_DOMAINLIST_DENY_REGEX;
can_modify = true;
}
else if((argument = startsWith("/api/domains/deny", api)) != NULL)
{
listtype = GRAVITY_DOMAINLIST_DENY_ALL;
}
else if((argument = startsWith("/api/domains/exact", api)) != NULL)
{
listtype = GRAVITY_DOMAINLIST_ALL_EXACT;
}
else if((argument = startsWith("/api/domains/regex", api)) != NULL)
{
listtype = GRAVITY_DOMAINLIST_ALL_REGEX;
}
else if((argument = startsWith("/api/domains", api)) != NULL)
{
listtype = GRAVITY_DOMAINLIST_ALL_ALL;
}
else
{
cJSON *json = JSON_NEW_OBJ();
JSON_OBJ_REF_STR(json, "uri", api->request->local_uri);
return send_json_error(api, 400,
"bad_request",
"Invalid request: Specified endpoint not available",
json);
}
if(api->method == HTTP_GET)
{
// Read list item identified by URI (or read them all)
return api_list_read(api, 200, listtype, argument);
}
else if(can_modify && api->method == HTTP_PUT)
{
// Add/update item identified by URI
return api_list_write(api, listtype, argument, payload);
}
else if(can_modify && api->method == HTTP_POST)
{
// Add item to list identified by payload
if(strlen(argument) != 0)
{
cJSON *json = JSON_NEW_OBJ();
JSON_OBJ_REF_STR(json, "uri_param", argument);
return send_json_error(api, 400,
"bad_request",
"Invalid request: Specify item in payload, not as URI parameter",
json);
}
else
return api_list_write(api, listtype, argument, payload);
}
else if(can_modify && api->method == HTTP_DELETE)
{
// Delete item from list
return api_list_remove(api, listtype, argument);
}
else if(!can_modify)
{
// This list type cannot be modified (e.g., ALL_ALL)
return send_json_error(api, 400,
"bad_request",
"Invalid request: Specify list to modify",
NULL);
}
else
{
// This results in error 404
return 0;
}
}