From 13168c377bc83e2bced9822aedbf83a17eaea8d5 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Mon, 23 Jan 2023 21:56:11 +0100 Subject: [PATCH] Add GET /api/teleporter Signed-off-by: DL6ER --- src/api/CMakeLists.txt | 1 + src/api/api.c | 1 + src/api/api.h | 3 + src/api/auth.c | 4 +- src/api/docs/content/specs/main.yaml | 3 + src/api/docs/content/specs/teleporter.yaml | 25 ++++++ src/api/teleporter.c | 59 +++++++++++++ src/config/dnsmasq_config.c | 4 +- src/config/toml_writer.c | 4 +- src/gc.c | 4 +- src/log.c | 29 ++++--- src/log.h | 3 +- src/miniz/CMakeLists.txt | 2 + src/miniz/compression.h | 2 +- src/miniz/teleporter.c | 49 +++++++++++ src/miniz/teleporter.h | 17 ++++ src/overTime.c | 8 +- src/procps.c | 6 +- test/api/libs/FTLAPI.py | 7 +- test/api/libs/responseVerifyer.py | 96 +++++++++++++++------- 20 files changed, 266 insertions(+), 61 deletions(-) create mode 100644 src/api/docs/content/specs/teleporter.yaml create mode 100644 src/api/teleporter.c create mode 100644 src/miniz/teleporter.c create mode 100644 src/miniz/teleporter.h diff --git a/src/api/CMakeLists.txt b/src/api/CMakeLists.txt index 124d98e5..f47c9ba5 100644 --- a/src/api/CMakeLists.txt +++ b/src/api/CMakeLists.txt @@ -22,6 +22,7 @@ set(sources queries.c stats_database.c stats.c + teleporter.c ) add_library(api OBJECT ${sources}) diff --git a/src/api/api.c b/src/api/api.c index b738db37..af782020 100644 --- a/src/api/api.c +++ b/src/api/api.c @@ -78,6 +78,7 @@ static struct { { "/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 }, false, HTTP_GET | HTTP_POST }, { "/api/docs", "", api_docs, { false, 0 }, false, HTTP_GET }, }; diff --git a/src/api/api.h b/src/api/api.h index 185bcdbf..920f9ca1 100644 --- a/src/api/api.h +++ b/src/api/api.h @@ -82,4 +82,7 @@ int api_auth(struct ftl_conn *api); // Documentation methods int api_docs(struct ftl_conn *api); +// Teleporter methods +int api_teleporter(struct ftl_conn *api); + #endif // ROUTES_H diff --git a/src/api/auth.c b/src/api/auth.c index 7dadb24d..f6721e0a 100644 --- a/src/api/auth.c +++ b/src/api/auth.c @@ -199,7 +199,7 @@ int check_client_auth(struct ftl_conn *api) if(config.debug.api.v.b) { char timestr[128]; - get_timestr(timestr, auth_data[user_id].valid_until, false); + get_timestr(timestr, auth_data[user_id].valid_until, false, false); log_debug(DEBUG_API, "Recognized known user: user_id %i valid_until: %s remote_addr %s", user_id, timestr, auth_data[user_id].remote_addr); } @@ -510,7 +510,7 @@ int api_auth(struct ftl_conn *api) if(config.debug.api.v.b && user_id > API_AUTH_UNAUTHORIZED) { char timestr[128]; - get_timestr(timestr, auth_data[user_id].valid_until, false); + get_timestr(timestr, auth_data[user_id].valid_until, false, false); log_debug(DEBUG_API, "API: Registered new user: user_id %i valid_until: %s remote_addr %s (accepted due to %s)", user_id, timestr, auth_data[user_id].remote_addr, response_correct ? "correct response" : "empty password"); diff --git a/src/api/docs/content/specs/main.yaml b/src/api/docs/content/specs/main.yaml index 509693bd..dde1c005 100644 --- a/src/api/docs/content/specs/main.yaml +++ b/src/api/docs/content/specs/main.yaml @@ -194,6 +194,9 @@ paths: /network/interfaces: $ref: 'network.yaml#/components/paths/interfaces' + /teleporter: + $ref: 'teleporter.yaml#/components/paths/teleporter' + components: securitySchemes: sidHeader: diff --git a/src/api/docs/content/specs/teleporter.yaml b/src/api/docs/content/specs/teleporter.yaml new file mode 100644 index 00000000..a60489b6 --- /dev/null +++ b/src/api/docs/content/specs/teleporter.yaml @@ -0,0 +1,25 @@ +openapi: 3.0.2 +components: + paths: + teleporter: + get: + summary: Export Pi-hole settings + tags: + - "Pi-hole configuration" + operationId: "get_teleporter" + description: | + Request an archived copy of your Pi-hole's current configuration + responses: + '200': + description: OK + content: + application/zip: + schema: + type: string + format: binary + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: 'common.yaml#/components/errors/unauthorized' \ No newline at end of file diff --git a/src/api/teleporter.c b/src/api/teleporter.c new file mode 100644 index 00000000..a44f3ba2 --- /dev/null +++ b/src/api/teleporter.c @@ -0,0 +1,59 @@ +/* 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 +* API Implementation /api/teleporter +* +* 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 "miniz/teleporter.h" +#include "api/api.h" +// hostname() +#include "daemon.h" + +static int api_teleporter_GET(struct ftl_conn *api) +{ + mz_zip_archive zip = { 0 }; + void *ptr = NULL; + size_t size = 0u; + const char *error = generate_teleporter_zip(&zip, &ptr, &size); + if(error != NULL) + return send_json_error(api, 500, + "compression_error", + error, + NULL); + + // Add header indicating that this is a file to be downloaded and stored as + // teleporter.zip (rather than showing the binary data in teh browser + // window). This client is free to ignore and do whatever it wants with this + // data stream. + char timestr[TIMESTR_SIZE] = ""; + get_timestr(timestr, time(NULL), false, true); + snprintf(pi_hole_extra_headers, sizeof(pi_hole_extra_headers), + "Content-Disposition: attachment; filename=\"pi-hole_%s_teleporter_%s.zip\"", + hostname(), timestr); + + // Send 200 OK with appropriate headers + mg_send_http_ok(api->conn, "application/zip", size); + + // Send raw (binary) ZIP content + mg_write(api->conn, ptr, size); + + // Free allocated ZIP memory + free_teleporter_zip(&zip); + + return 200; +} + +int api_teleporter(struct ftl_conn *api) +{ + if(api->method == HTTP_GET) + return api_teleporter_GET(api); + + return 0; +} \ No newline at end of file diff --git a/src/config/dnsmasq_config.c b/src/config/dnsmasq_config.c index 0227fa87..410c43ee 100644 --- a/src/config/dnsmasq_config.c +++ b/src/config/dnsmasq_config.c @@ -184,8 +184,8 @@ char *get_dnsmasq_line(const unsigned int lineno) static void write_config_header(FILE *fp, const char *description) { const time_t now = time(NULL); - char timestring[84] = ""; - get_timestr(timestring, now, false); + char timestring[TIMESTR_SIZE] = ""; + get_timestr(timestring, now, false, false); fputs("# Pi-hole: A black hole for Internet advertisements\n", fp); fprintf(fp, "# (c) %u Pi-hole, LLC (https://pi-hole.net)\n", get_year(now)); fputs("# Network-wide ad blocking via your own hardware.\n", fp); diff --git a/src/config/toml_writer.c b/src/config/toml_writer.c index 375d34bb..e996bc4f 100644 --- a/src/config/toml_writer.c +++ b/src/config/toml_writer.c @@ -37,8 +37,8 @@ bool writeFTLtoml(const bool verbose) fputs("# This file is managed by pihole-FTL\n#\n", fp); fputs("# Do not edit the file while FTL is\n", fp); fputs("# running or your changes may be overwritten\n#\n", fp); - char timestring[84] = ""; - get_timestr(timestring, time(NULL), false); + char timestring[TIMESTR_SIZE] = ""; + get_timestr(timestring, time(NULL), false, false); fputs("# Last updated on ", fp); fputs(timestring, fp); fputs("\n# by FTL ", fp); diff --git a/src/gc.c b/src/gc.c index db983b84..2329b5d6 100644 --- a/src/gc.c +++ b/src/gc.c @@ -183,8 +183,8 @@ void *GC_thread(void *val) if(config.debug.gc.v.b) { timer_start(GC_TIMER); - char timestring[84] = ""; - get_timestr(timestring, mintime, false); + char timestring[TIMESTR_SIZE] = ""; + get_timestr(timestring, mintime, false, false); log_info("GC starting, mintime: %s (%llu)", timestring, (long long)mintime); } diff --git a/src/log.c b/src/log.c index 09f59c75..c4e66690 100644 --- a/src/log.c +++ b/src/log.c @@ -100,10 +100,17 @@ double double_time(void) // The size of 84 bytes has been carefully selected for all possible timestamps // to always fit into the available space without buffer overflows -void get_timestr(char * const timestring, const time_t timein, const bool millis) +void get_timestr(char timestring[TIMESTR_SIZE], const time_t timein, const bool millis, const bool uri_compatible) { struct tm tm; localtime_r(&timein, &tm); + char space = ' '; + char colon = ':'; + if(uri_compatible) + { + space = '_'; + colon = '-'; + } if(millis) { @@ -111,15 +118,15 @@ void get_timestr(char * const timestring, const time_t timein, const bool millis gettimeofday(&tv, NULL); const int millisec = tv.tv_usec/1000; - sprintf(timestring,"%d-%02d-%02d %02d:%02d:%02d.%03i", - tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, - tm.tm_hour, tm.tm_min, tm.tm_sec, millisec); + sprintf(timestring,"%d-%02d-%02d%c%02d%c%02d%c%02d.%03i", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, space, + tm.tm_hour, colon, tm.tm_min, colon, tm.tm_sec, millisec); } else { - sprintf(timestring,"%d-%02d-%02d %02d:%02d:%02d", - tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, - tm.tm_hour, tm.tm_min, tm.tm_sec); + sprintf(timestring,"%d-%02d-%02d%c%02d%c%02d%c%02d", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, space, + tm.tm_hour, colon, tm.tm_min, colon, tm.tm_sec); } } @@ -254,7 +261,7 @@ void debugstr(const enum debug_flag flag, const char **name) void __attribute__ ((format (gnu_printf, 3, 4))) _FTL_log(const int priority, const enum debug_flag flag, const char *format, ...) { - char timestring[84] = ""; + char timestring[TIMESTR_SIZE] = ""; va_list args; // We have been explicitly asked to not print anything to the log @@ -262,7 +269,7 @@ void __attribute__ ((format (gnu_printf, 3, 4))) _FTL_log(const int priority, co return; // Get human-readable time - get_timestr(timestring, time(NULL), true); + get_timestr(timestring, time(NULL), true, false); // Get and log PID of current process to avoid ambiguities when more than one // pihole-FTL instance is logging into the same file @@ -372,7 +379,7 @@ static FILE * __attribute__((malloc, warn_unused_result)) open_web_log(const enu void __attribute__ ((format (gnu_printf, 2, 3))) logg_web(enum fifo_logs which, const char *format, ...) { - char timestring[84] = ""; + char timestring[TIMESTR_SIZE] = ""; const time_t now = time(NULL); va_list args; @@ -384,7 +391,7 @@ void __attribute__ ((format (gnu_printf, 2, 3))) logg_web(enum fifo_logs which, add_to_fifo_buffer(which, buffer, len > MAX_MSG_FIFO ? MAX_MSG_FIFO : len); // Get human-readable time - get_timestr(timestring, now, true); + get_timestr(timestring, now, true, false); // Get and log PID of current process to avoid ambiguities when more than one // pihole-FTL instance is logging into the same file diff --git a/src/log.h b/src/log.h index 08e012f5..db27492c 100644 --- a/src/log.h +++ b/src/log.h @@ -17,6 +17,7 @@ #include #define DEBUG_ANY 0 +#define TIMESTR_SIZE 84 // Credit: https://stackoverflow.com/a/75116514 #define LEFT(str, w) \ @@ -49,7 +50,7 @@ unsigned int get_year(const time_t timein); const char *get_FTL_version(void); void log_FTL_version(bool crashreport); double double_time(void); -void get_timestr(char * const timestring, const time_t timein, const bool millis); +void get_timestr(char timestring[TIMESTR_SIZE], const time_t timein, const bool millis, const bool uri_compatible); void debugstr(const enum debug_flag flag, const char **name); void logg_web(enum fifo_logs which, const char *format, ...) __attribute__ ((format (gnu_printf, 2, 3))); const char *get_ordinal_suffix(unsigned int number) __attribute__ ((const)); diff --git a/src/miniz/CMakeLists.txt b/src/miniz/CMakeLists.txt index 98a73cf4..965b4513 100644 --- a/src/miniz/CMakeLists.txt +++ b/src/miniz/CMakeLists.txt @@ -13,6 +13,8 @@ set(sources compression.h miniz.c miniz.h + teleporter.c + teleporter.h ) add_library(miniz OBJECT ${sources}) diff --git a/src/miniz/compression.h b/src/miniz/compression.h index 84e88b6e..458aa77c 100644 --- a/src/miniz/compression.h +++ b/src/miniz/compression.h @@ -1,5 +1,5 @@ /* Pi-hole: A black hole for Internet advertisements -* (c) 2323 Pi-hole, LLC (https://pi-hole.net) +* (c) 2023 Pi-hole, LLC (https://pi-hole.net) * Network-wide ad blocking via your own hardware. * * FTL Engine diff --git a/src/miniz/teleporter.c b/src/miniz/teleporter.c new file mode 100644 index 00000000..a4c4b139 --- /dev/null +++ b/src/miniz/teleporter.c @@ -0,0 +1,49 @@ +/* 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" + +const char *generate_teleporter_zip(mz_zip_archive *zip, 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"; + if(!mz_zip_writer_add_file(zip, "pihole.toml", GLOBALTOMLPATH, file_comment, (uint16_t)strlen(file_comment), MZ_BEST_COMPRESSION)) + { + mz_zip_writer_end(zip); + return "Failed to add "GLOBALTOMLPATH" to 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!"; + } + + // Everything worked well + return NULL; +} + +bool free_teleporter_zip(mz_zip_archive *zip) +{ + return mz_zip_writer_end(zip); +} \ No newline at end of file diff --git a/src/miniz/teleporter.h b/src/miniz/teleporter.h new file mode 100644 index 00000000..9be59a0c --- /dev/null +++ b/src/miniz/teleporter.h @@ -0,0 +1,17 @@ +/* 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 +* Compression routines +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ +#ifndef TELEPORTER_H +#define TELEPORTER_H + +#include "miniz/miniz.h" +const char *generate_teleporter_zip(mz_zip_archive *zip, void *ptr, size_t *size); +bool free_teleporter_zip(mz_zip_archive *zip); + +#endif // TELEPORTER_H \ No newline at end of file diff --git a/src/overTime.c b/src/overTime.c index c632e0af..9e12952d 100644 --- a/src/overTime.c +++ b/src/overTime.c @@ -131,12 +131,12 @@ unsigned int _getOverTimeID(time_t timestamp, const char *file, const int line) // This is definitely wrong. We warn about this (but only once) if(!warned_about_hwclock) { - char timestampStr[84] = ""; - get_timestr(timestampStr, timestamp, false); + char timestampStr[TIMESTR_SIZE] = ""; + get_timestr(timestampStr, timestamp, false, false); const time_t lastTimestamp = overTime[OVERTIME_SLOTS-1].timestamp; - char lastTimestampStr[84] = ""; - get_timestr(lastTimestampStr, lastTimestamp, false); + char lastTimestampStr[TIMESTR_SIZE] = ""; + get_timestr(lastTimestampStr, lastTimestamp, false, false); log_warn("Found database entries in the future (%s (%llu), last timestamp for importing: %s (%llu)). " "Your over-time statistics may be incorrect (found in %s:%d)", diff --git a/src/procps.c b/src/procps.c index a7a33b37..5a7a1c3d 100644 --- a/src/procps.c +++ b/src/procps.c @@ -63,7 +63,7 @@ static bool get_process_ppid(const pid_t pid, pid_t *ppid) return true; } -static bool get_process_creation_time(const pid_t pid, char timestr[84]) +static bool get_process_creation_time(const pid_t pid, char timestr[TIMESTR_SIZE]) { // Try to open comm file char filename[sizeof("/proc/%u/task/%u/comm") + sizeof(int)*3 * 2]; @@ -71,7 +71,7 @@ static bool get_process_creation_time(const pid_t pid, char timestr[84]) struct stat st; if(stat(filename, &st) < 0) return false; - get_timestr(timestr, st.st_ctim.tv_sec, false); + get_timestr(timestr, st.st_ctim.tv_sec, false, false); return true; } @@ -128,7 +128,7 @@ bool check_running_FTL(void) if(!get_process_name(ppid, ppid_name)) continue; - char timestr[84] = { 0 }; + char timestr[TIMESTR_SIZE] = { 0 }; get_process_creation_time(pid, timestr); // If this is the first process we log, add a header diff --git a/test/api/libs/FTLAPI.py b/test/api/libs/FTLAPI.py index 938897f9..11803328 100644 --- a/test/api/libs/FTLAPI.py +++ b/test/api/libs/FTLAPI.py @@ -95,7 +95,7 @@ class FTLAPI(): self.session = response["session"] # Query the FTL API (GET) and return the response - def GET(self, uri: str, params: List[str] = []): + def GET(self, uri: str, params: List[str] = [], expected_mimetype: str = "application/json"): self.errors = [] try: # Add parameters to the URI (if any) @@ -113,7 +113,10 @@ class FTLAPI(): with requests.get(url = self.api_url + uri, json = data) as response: if self.verbose: print(json.dumps(response.json(), indent=4)) - return response.json() + if expected_mimetype == "application/json": + return response.json() + else: + return response.content except Exception as e: self.errors.append("Exception when GETing from FTL: " + str(e)) return None diff --git a/test/api/libs/responseVerifyer.py b/test/api/libs/responseVerifyer.py index ae0bf4ef..ed921faf 100644 --- a/test/api/libs/responseVerifyer.py +++ b/test/api/libs/responseVerifyer.py @@ -9,6 +9,8 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. +import io +import zipfile from libs.openAPI import openApi import urllib.request, urllib.parse from libs.FTLAPI import FTLAPI @@ -58,10 +60,20 @@ class ResponseVerifyer(): return self.errors # Get YAML response schema and examples (if applicable) - if 'content' in self.openapi.paths[endpoint][method]['responses'][str(rcode)]: - jsonData = self.openapi.paths[endpoint][method]['responses'][str(rcode)]['content']['application/json'] - YAMLresponseSchema = jsonData['schema'] - YAMLresponseExamples = jsonData['examples'] if 'examples' in jsonData else None + expected_mimetype = True + response_rcode = self.openapi.paths[endpoint][method]['responses'][str(rcode)] + if 'content' in response_rcode: + content = response_rcode['content'] + if 'application/json' in content: + expected_mimetype = 'application/json' + jsonData = content[expected_mimetype] + YAMLresponseSchema = jsonData['schema'] + YAMLresponseExamples = jsonData['examples'] if 'examples' in jsonData else None + elif 'application/zip' in content: + expected_mimetype = 'application/zip' + jsonData = content[expected_mimetype] + YAMLresponseSchema = None + YAMLresponseExamples = None else: # No response defined return self.errors @@ -80,40 +92,62 @@ class ResponseVerifyer(): FTLparameters.append(param['name'] + "=" + urllib.parse.quote_plus(str(param['example']))) # Get FTL response - FTLresponse = self.ftl.GET("/api" + endpoint, FTLparameters) + FTLresponse = self.ftl.GET("/api" + endpoint, FTLparameters, expected_mimetype) if FTLresponse is None: return self.ftl.errors self.YAMLresponse = {} - # Check if the response is an object. If so, we have to check it - # recursively - if 'type' in YAMLresponseSchema and YAMLresponseSchema['type'] == 'object': - # Loop over all properties of the object - for prop in YAMLresponseSchema['properties']: - self.verify_property(YAMLresponseSchema['properties'], YAMLresponseExamples, FTLresponse, [prop]) + # Checking depends on the expected mimetype + if expected_mimetype == "application/json": + # Check if the response is an object. If so, we have to check it + # recursively + if 'type' in YAMLresponseSchema and YAMLresponseSchema['type'] == 'object': + # Loop over all properties of the object + for prop in YAMLresponseSchema['properties']: + self.verify_property(YAMLresponseSchema['properties'], YAMLresponseExamples, FTLresponse, [prop]) - # Check if the response is a gather-all object. If so, we have - # to check all objects in the array individually - elif 'allOf' in YAMLresponseSchema and len(YAMLresponseSchema['allOf']) > 0: - for i in range(len(YAMLresponseSchema['allOf'])): - for prop in YAMLresponseSchema['allOf'][i]['properties']: - self.verify_property(YAMLresponseSchema['allOf'][i]['properties'], YAMLresponseExamples, FTLresponse, [prop]) + # Check if the response is a gather-all object. If so, we have + # to check all objects in the array individually + elif 'allOf' in YAMLresponseSchema and len(YAMLresponseSchema['allOf']) > 0: + for i in range(len(YAMLresponseSchema['allOf'])): + for prop in YAMLresponseSchema['allOf'][i]['properties']: + self.verify_property(YAMLresponseSchema['allOf'][i]['properties'], YAMLresponseExamples, FTLresponse, [prop]) - # If neither of the above is true, thie definition is invalid + # If neither of the above is true, thie definition is invalid + else: + self.errors.append("Top-level response should be either an object or a non-empty allOf/anyOf/oneOf") + + # Finally, we check if there are extra properties in the FTL response + # that are not defined in the API specs + + # Flatten the FTL response + FTLflat = self.flatten_dict(FTLresponse) + YAMLflat = self.YAMLresponse + + # Check for properties in FTL that are not in the API specs + for property in FTLflat.keys(): + if property not in YAMLflat.keys(): + self.errors.append("Property '" + property + "' missing in the API specs") + + elif expected_mimetype == "application/zip": + file_like_object = io.BytesIO(FTLresponse) + with zipfile.ZipFile(file_like_object) as zipfile_obj: + # Read all the files in the archive and check their CRC’s and + # file headers. Returns the name of the first bad file, or else + # returns None. + bad_filename = zipfile_obj.testzip() + if bad_filename is not None: + self.errors.append("File " + bad_filename + " in received archive is corrupt.") + # Try to read pihole.toml and see if it starts with the expected + # header block + try: + pihole_toml = zipfile_obj.read("pihole.toml") + if not pihole_toml.startswith(b"# This file is managed by pihole-FTL"): + self.errors.append("Received ZIP file starts with wrong header") + except Exception as err: + self.errors.append("Error during ZIP analysis: " + str(err)) else: - self.errors.append("Top-level response should be either an object or a non-empty allOf/anyOf/oneOf") - - # Finally, we check if there are extra properties in the FTL response - # that are not defined in the API specs - - # Flatten the FTL response - FTLflat = self.flatten_dict(FTLresponse) - YAMLflat = self.YAMLresponse - - # Check for properties in FTL that are not in the API specs - for property in FTLflat.keys(): - if property not in YAMLflat.keys(): - self.errors.append("Property '" + property + "' missing in the API specs") + self.errors.append("Checker script does not know how to check for mimetype \"" + expected_mimetype + "\"") # Return all errors return self.errors