From 5e96022e63e3bf3406aebe3837dcc0ddd224c908 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Mon, 16 Jan 2023 20:32:49 +0100 Subject: [PATCH] Group endpoints in /api/ftl/endpoints by supported methods Signed-off-by: DL6ER --- src/api/api.c | 133 +++++++++++++++++++--------- src/api/dns.c | 3 +- src/api/docs/content/specs/ftl.yaml | 31 ++++++- src/webserver/http-common.h | 9 +- test/api/checkAPI.py | 4 +- test/api/libs/FTLAPI.py | 17 +++- 6 files changed, 140 insertions(+), 57 deletions(-) diff --git a/src/api/api.c b/src/api/api.c index efad6093..1c3b896f 100644 --- a/src/api/api.c +++ b/src/api/api.c @@ -28,47 +28,49 @@ static struct { const char *parameters; int (*func)(struct ftl_conn *api); const bool opts[2]; + enum http_method methods; } api_request[] = { - // URI ARGUMENTS FUNCTION OPTIONS + // URI ARGUMENTS FUNCTION OPTIONS ALLOWED METHODS // 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, false } }, - { "/api/dns/cache", "", api_dns_cache, { false, false } }, - { "/api/dns/port", "", api_dns_port, { false, false } }, - { "/api/dns/entries", "/{ip}/{host}", api_dns_entries, { false, false } }, - { "/api/clients", "/{client}", api_list, { false, false } }, - { "/api/domains", "/{type}/{kind}/{domain}", api_list, { false, false } }, - { "/api/groups", "/{name}", api_list, { false, false } }, - { "/api/lists", "/{list}", api_list, { false, false } }, - { "/api/ftl/client", "", api_ftl_client, { false, false } }, - { "/api/ftl/logs/dns", "", api_ftl_logs_dns, { false, false } }, - { "/api/ftl/sysinfo", "", api_ftl_sysinfo, { false, false } }, - { "/api/ftl/dbinfo", "", api_ftl_dbinfo, { false, false } }, - { "/api/ftl/endpoints", "", api_ftl_endpoints, { false, false } }, - { "/api/history/clients", "", api_history_clients, { false, false } }, - { "/api/history/database/clients", "", api_history_database_clients, { false, false } }, - { "/api/history/database", "", api_history_database, { false, false } }, - { "/api/history", "", api_history, { false, false } }, - { "/api/queries/suggestions", "", api_queries_suggestions, { false, false } }, - { "/api/queries", "", api_queries, { false, false } }, - { "/api/stats/summary", "", api_stats_summary, { false, false } }, - { "/api/stats/query_types", "", api_stats_query_types, { false, false } }, - { "/api/stats/upstreams", "", api_stats_upstreams, { false, false } }, - { "/api/stats/top_domains", "", api_stats_top_domains, { false, false } }, - { "/api/stats/top_clients", "", api_stats_top_clients, { false, false } }, - { "/api/stats/recent_blocked", "", api_stats_recentblocked, { false, false } }, - { "/api/stats/database/top_domains", "", api_stats_database_top_items, { false, true } }, - { "/api/stats/database/top_clients", "", api_stats_database_top_items, { false, false } }, - { "/api/stats/database/summary", "", api_stats_database_summary, { false, false } }, - { "/api/stats/database/query_types", "", api_stats_database_query_types, { false, false } }, - { "/api/stats/database/upstreams", "", api_stats_database_upstreams, { false, false } }, - { "/api/version", "", api_version, { false, false } }, - { "/api/auth", "", api_auth, { false, false } }, - { "/api/config", "", api_config, { false, false } }, - { "/api/network/gateway", "", api_network_gateway, { false, false } }, - { "/api/network/interfaces", "", api_network_interfaces, { false, false } }, - { "/api/network/devices", "", api_network_devices, { false, false } }, - { "/api/docs", "", api_docs, { false, false } }, + { "/api/dns/blocking", "", api_dns_blocking, { false, false }, HTTP_GET | HTTP_POST }, + { "/api/dns/cache", "", api_dns_cache, { false, false }, HTTP_GET }, + { "/api/dns/port", "", api_dns_port, { false, false }, HTTP_GET }, + { "/api/dns/entries", "", api_dns_entries, { false, false }, HTTP_GET }, + { "/api/dns/entries", "/{ip}/{host}", api_dns_entries, { false, false }, HTTP_PUT | HTTP_DELETE }, + { "/api/clients", "/{client}", api_list, { false, false }, HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_DELETE }, + { "/api/domains", "/{type}/{kind}/{domain}", api_list, { false, false }, HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_DELETE }, + { "/api/groups", "/{name}", api_list, { false, false }, HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_DELETE }, + { "/api/lists", "/{list}", api_list, { false, false }, HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_DELETE }, + { "/api/ftl/client", "", api_ftl_client, { false, false }, HTTP_GET }, + { "/api/ftl/logs/dns", "", api_ftl_logs_dns, { false, false }, HTTP_GET }, + { "/api/ftl/sysinfo", "", api_ftl_sysinfo, { false, false }, HTTP_GET }, + { "/api/ftl/dbinfo", "", api_ftl_dbinfo, { false, false }, HTTP_GET }, + { "/api/ftl/endpoints", "", api_ftl_endpoints, { false, false }, HTTP_GET }, + { "/api/history/clients", "", api_history_clients, { false, false }, HTTP_GET }, + { "/api/history/database/clients", "", api_history_database_clients, { false, false }, HTTP_GET }, + { "/api/history/database", "", api_history_database, { false, false }, HTTP_GET }, + { "/api/history", "", api_history, { false, false }, HTTP_GET }, + { "/api/queries/suggestions", "", api_queries_suggestions, { false, false }, HTTP_GET }, + { "/api/queries", "", api_queries, { false, false }, HTTP_GET }, + { "/api/stats/summary", "", api_stats_summary, { false, false }, HTTP_GET }, + { "/api/stats/query_types", "", api_stats_query_types, { false, false }, HTTP_GET }, + { "/api/stats/upstreams", "", api_stats_upstreams, { false, false }, HTTP_GET }, + { "/api/stats/top_domains", "", api_stats_top_domains, { false, false }, HTTP_GET }, + { "/api/stats/top_clients", "", api_stats_top_clients, { false, false }, HTTP_GET }, + { "/api/stats/recent_blocked", "", api_stats_recentblocked, { false, false }, HTTP_GET }, + { "/api/stats/database/top_domains", "", api_stats_database_top_items, { false, true }, HTTP_GET }, + { "/api/stats/database/top_clients", "", api_stats_database_top_items, { false, false }, HTTP_GET }, + { "/api/stats/database/summary", "", api_stats_database_summary, { false, false }, HTTP_GET }, + { "/api/stats/database/query_types", "", api_stats_database_query_types, { false, false }, HTTP_GET }, + { "/api/stats/database/upstreams", "", api_stats_database_upstreams, { false, false }, HTTP_GET }, + { "/api/version", "", api_version, { false, false }, HTTP_GET }, + { "/api/auth", "", api_auth, { false, false }, HTTP_GET | HTTP_POST | HTTP_DELETE }, + { "/api/config", "", api_config, { false, false }, HTTP_GET | HTTP_PATCH }, + { "/api/network/gateway", "", api_network_gateway, { false, false }, HTTP_GET }, + { "/api/network/interfaces", "", api_network_interfaces, { false, false }, HTTP_GET }, + { "/api/network/devices", "", api_network_devices, { false, false }, HTTP_GET }, + { "/api/docs", "", api_docs, { false, false }, HTTP_GET }, }; int api_handler(struct mg_connection *conn, void *ignored) @@ -108,6 +110,10 @@ int api_handler(struct mg_connection *conn, void *ignored) // Loop over all API endpoints and check if the requested URI matches for(unsigned int i = 0; i < sizeof(api_request)/sizeof(api_request[0]); i++) { + // Check if the requested method is allowed + if(!(api_request[i].methods & api.method)) + continue; + // Check if the requested URI starts with the API endpoint if((api.item = startsWith(api_request[i].uri, &api)) != NULL) { @@ -164,19 +170,58 @@ static int api_ftl_endpoints(struct ftl_conn *api) if(check_client_auth(api) == API_AUTH_UNAUTHORIZED) return send_json_unauthorized(api); - cJSON *json = JSON_NEW_OBJECT(); - cJSON *endpoints = JSON_NEW_ARRAY(); + 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 < sizeof(api_request)/sizeof(api_request[0]); i++) { - 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); - JSON_ADD_ITEM_TO_ARRAY(endpoints, endpoint); + 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: + 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 diff --git a/src/api/dns.c b/src/api/dns.c index 90d6d89c..7a5e2b37 100644 --- a/src/api/dns.c +++ b/src/api/dns.c @@ -125,8 +125,7 @@ int api_dns_blocking(struct ftl_conn *api) } else { - // This results in error 404 - return 0; + return send_json_error(api, 405, "method_not_allowed", "Method not allowed", NULL); } } diff --git a/src/api/docs/content/specs/ftl.yaml b/src/api/docs/content/specs/ftl.yaml index b097e042..1c16f73a 100644 --- a/src/api/docs/content/specs/ftl.yaml +++ b/src/api/docs/content/specs/ftl.yaml @@ -464,11 +464,34 @@ components: type: object properties: endpoints: - type: array + type: object description: Endpoints - items: - type: string - example: ["/api/dns/blocking", "/api/dns/cache", "/api/dns/port", "/api/domains", "/api/groups", "/api/lists"] + properties: + get: + type: array + items: + type: string + example: ["/api/dns/blocking", "/api/dns/cache", "/api/dns/port", "/api/domains", "/api/groups", "/api/lists"] + post: + type: array + items: + type: string + example: ["/api/dns/blocking", "/api/dns/cache", "/api/dns/port", "/api/domains", "/api/groups", "/api/lists"] + put: + type: array + items: + type: string + example: ["/api/dns/blocking", "/api/dns/cache", "/api/dns/port", "/api/domains", "/api/groups", "/api/lists"] + patch: + type: array + items: + type: string + example: ["/api/dns/blocking", "/api/dns/cache", "/api/dns/port", "/api/domains", "/api/groups", "/api/lists"] + delete: + type: array + items: + type: string + example: ["/api/dns/blocking", "/api/dns/cache", "/api/dns/port", "/api/domains", "/api/groups", "/api/lists"] parameters: logs: diff --git a/src/webserver/http-common.h b/src/webserver/http-common.h index 89757b97..399cb808 100644 --- a/src/webserver/http-common.h +++ b/src/webserver/http-common.h @@ -21,7 +21,14 @@ // Maximum size of received and processed payload: 64 KB #define MAX_PAYLOAD_BYTES 64*1024 -enum http_method { HTTP_UNKNOWN, HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_PATCH, HTTP_DELETE }; +enum http_method { + HTTP_UNKNOWN = 0, + HTTP_GET = 1 << 0, + HTTP_POST = 1 << 1, + HTTP_PUT = 1 << 2, + HTTP_PATCH = 1 << 3, + HTTP_DELETE = 1 << 4, +}; struct ftl_conn { struct mg_connection *conn; const struct mg_request_info *request; diff --git a/test/api/checkAPI.py b/test/api/checkAPI.py index 810a8a70..2adc8e47 100644 --- a/test/api/checkAPI.py +++ b/test/api/checkAPI.py @@ -27,7 +27,7 @@ if __name__ == "__main__": print("Endpoints in OpenAPI specs but not in FTL:") # Check for endpoints in OpenAPI specs that are not defined in FTL for path in openapi.endpoints["get"]: - if path not in ftl.endpoints: + if path not in ftl.endpoints["get"]: print(" Missing GET endpoint in FTL: " + path) errs[0] += 1 if errs[0] == 0: @@ -35,7 +35,7 @@ if __name__ == "__main__": # Check for endpoints in FTL that are not in the OpenAPI specs print("Endpoints in FTL but not in OpenAPI specs:") - for path in ftl.endpoints: + for path in ftl.endpoints["get"]: if path not in openapi.endpoints["get"]: # Ignore the docs endpoint if path in ["/api/docs"]: diff --git a/test/api/libs/FTLAPI.py b/test/api/libs/FTLAPI.py index cf2c0177..1a11834b 100644 --- a/test/api/libs/FTLAPI.py +++ b/test/api/libs/FTLAPI.py @@ -29,7 +29,13 @@ sid = session["session"]["sid"] # SID string if succesful, null otherwise class FTLAPI(): def __init__(self, api_url: str): self.api_url = api_url - self.endpoints = [] + self.endpoints = { + "get": [], + "post": [], + "put": [], + "patch": [], + "delete": [] + } self.errors = [] self.session = None self.verbose = False @@ -130,9 +136,12 @@ class FTLAPI(): def get_endpoints(self): try: # Get all endpoints from FTL and sort them for comparison - for endpoint in self.GET("/api/ftl/endpoints")["endpoints"]: - self.endpoints.append(endpoint["uri"] + endpoint["parameters"]) - self.endpoints = sorted(self.endpoints) + response = self.GET("/api/ftl/endpoints") + for method in response["endpoints"]: + for endpoint in response["endpoints"][method]: + self.endpoints[method].append(endpoint["uri"] + endpoint["parameters"]) + for method in self.endpoints: + self.endpoints[method] = sorted(self.endpoints[method]) except Exception as e: print("Exception when pre-processing endpoints from FTL: " + str(e)) exit(1)