#!/usr/bin/env python3 """Production policy/store/adapter with bounded HTTPD and database doubles. Getter and append-header functions are extracted verbatim from installed IDF, not reimplemented with convenient merging or overwrite semantics. """ import os import pathlib import subprocess import sys import tempfile sys.dont_write_bytecode = True os.environ["CCACHE_DISABLE"] = "1" HERE = pathlib.Path(__file__).resolve().parent ROOT = HERE.parents[1] sys.path.insert(0, str(HERE.parent / "web_session_store")) from run import HEADERS from serial_headers import SERIAL_HEADERS HEADERS.update(SERIAL_HEADERS) HEADERS["esp_http_server.h"] += """ #define HTTPD_SOCK_ERR_FAIL -1 #define ESP_ERR_HTTPD_INVALID_REQ 0x200 #define ESP_ERR_HTTPD_RESULT_TRUNC 0x201 #define ESP_ERR_HTTPD_RESP_HDR 0x202 esp_err_t httpd_resp_sendstr(httpd_req_t *, const char *); int httpd_req_recv(httpd_req_t *, char *, size_t); """ HEADERS["esp_idf_version.h"] = """ #define ESP_IDF_VERSION_VAL(a,b,c) ((a)*10000+(b)*100+(c)) #define ESP_IDF_VERSION ESP_IDF_VERSION_VAL(5,5,0) """ HEADERS["esp_httpd_priv.h"] = """#pragma once #include #include "esp_http_server.h" static inline void *httpd_os_thread_handle(void) { return (void *)1; } struct sock_db { int fd; bool for_async_req, ws_close; uint64_t lru_counter; void *ctx; int (*send_fn)(httpd_handle_t, int, const char *, size_t, int); int (*pending_fn)(httpd_handle_t, int); bool ws_handshake_done; esp_err_t (*ws_handler)(httpd_req_t *); bool ws_control_frames; void *ws_user_ctx; char pending_data[128]; size_t pending_len; }; struct httpd_req_aux { struct sock_db *sd; char *scratch; size_t scratch_cur_size, remaining_len; unsigned req_hdrs_count, resp_hdrs_count; bool ws_handshake_detect; struct resp_hdr { const char *field, *value; } *resp_hdrs; }; struct httpd_data { struct { unsigned max_resp_headers, max_uri_handlers, max_open_sockets; void *uri_match_fn; } config; struct { void *handle; } hd_td; struct httpd_req_aux hd_req_aux; struct sock_db *hd_sd; httpd_uri_t **hd_calls; }; static inline struct sock_db *httpd_sess_get(httpd_handle_t h, int fd) { struct httpd_data *hd = h; for (unsigned i = 0; i < hd->config.max_open_sockets; ++i) if (hd->hd_sd[i].fd == fd) return &hd->hd_sd[i]; return NULL; } esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *, const char *); """ admin = "--admin" in sys.argv settings = "--settings" in sys.argv serial_settings = "--serial-settings" in sys.argv accounts = "--accounts" in sys.argv broker = "--broker" in sys.argv ssh_settings = "--ssh" in sys.argv lifecycle = "--lifecycle" in sys.argv display = "--display" in sys.argv if display: HEADERS["nvs_flash.h"] = '#pragma once\n#include "esp_err.h"\nesp_err_t nvs_flash_init(void);\n' HEADERS["nvs.h"] = '''#pragma once #include #include "esp_err.h" typedef int nvs_handle_t; #define NVS_READONLY 0 #define NVS_READWRITE 1 #define ESP_ERR_NVS_NOT_FOUND 0x1102 #define ESP_ERR_NVS_TYPE_MISMATCH 0x1103 #define ESP_ERR_NVS_INVALID_LENGTH 0x110c esp_err_t nvs_open(const char *, int, nvs_handle_t *); esp_err_t nvs_get_blob(nvs_handle_t, const char *, void *, size_t *); esp_err_t nvs_set_blob(nvs_handle_t, const char *, const void *, size_t); esp_err_t nvs_commit(nvs_handle_t); void nvs_close(nvs_handle_t); ''' network = "--network" in sys.argv if network: HEADERS["esp_wifi_types.h"] = "#pragma once\ntypedef int wifi_auth_mode_t;\n" HEADERS["esp_err.h"] += "\n#define ESP_ERR_TIMEOUT 0x107\n" HEADERS["esp_timer.h"] += """ #include typedef void *esp_timer_handle_t; typedef struct { void (*callback)(void *); const char *name; bool skip_unhandled_events; } esp_timer_create_args_t; int esp_timer_create(const esp_timer_create_args_t *, esp_timer_handle_t *); int esp_timer_start_periodic(esp_timer_handle_t, uint64_t); """ if accounts: HEADERS["mbedtls/base64.h"] = """ #pragma once #include #include static inline int mbedtls_base64_decode(unsigned char *out, size_t capacity, size_t *length, const unsigned char *in, size_t n) { unsigned char decoded[132]; if (!n || n>172 || n%4) return -1; int result=EVP_DecodeBlock(decoded,in,(int)n); if (result<0) return -1; if (in[n-1]=='=') --result; if (in[n-2]=='=') --result; if ((size_t)result>capacity) return -1; memcpy(out,decoded,(size_t)result); *length=(size_t)result; return 0; } static inline int mbedtls_base64_encode(unsigned char *out, size_t capacity, size_t *length, const unsigned char *in, size_t n) { if (capacity < 4*((n+2)/3)+1) return -1; *length=(size_t)EVP_EncodeBlock(out,in,(int)n); return 0; } """ HEADERS["esp_timer.h"] += """ #include typedef void *esp_timer_handle_t; typedef struct { void (*callback)(void *); const char *name; bool skip_unhandled_events; } esp_timer_create_args_t; int esp_timer_create(const esp_timer_create_args_t *, esp_timer_handle_t *); int esp_timer_start_periodic(esp_timer_handle_t, uint64_t); """ if admin: HEADERS["esp_system.h"] = "#pragma once\nvoid esp_restart(void);\n" HEADERS["esp_heap_caps.h"] = """#pragma once #include #define MALLOC_CAP_SPIRAM 1 #define MALLOC_CAP_8BIT 2 void *heap_caps_calloc(size_t, size_t, unsigned); void heap_caps_free(void *); """ HEADERS["esp_timer.h"] += """ #include typedef void *esp_timer_handle_t; typedef struct { void (*callback)(void *); const char *name; bool skip_unhandled_events; } esp_timer_create_args_t; int esp_timer_create(const esp_timer_create_args_t *, esp_timer_handle_t *); int esp_timer_start_periodic(esp_timer_handle_t, uint64_t); int esp_timer_delete(esp_timer_handle_t); """ def function(source, name): start = source.index(name + "(") start = source.rfind("\n", 0, start) + 1 end = source.index("\n}", start) + 2 return source[start:end] idf = pathlib.Path(os.environ.get("IDF_PATH", str(pathlib.Path.home() / ".platformio/packages/framework-espidf"))) parse = (idf / "components/esp_http_server/src/httpd_parse.c").read_text() txrx = (idf / "components/esp_http_server/src/httpd_txrx.c").read_text() extracted = """ #pragma GCC diagnostic ignored "-Wsign-compare" #include #include #include #include "esp_httpd_priv.h" #define ESP_LOGD(...) ((void)0) #define ESP_LOGW(...) ((void)0) #define MIN(a,b) ((a) < (b) ? (a) : (b)) static bool httpd_valid_req(httpd_req_t *r) { return r && r->aux; } static size_t strlcpy(char *d, const char *s, size_t n) { size_t len = strlen(s); if (n) { size_t m = len < n-1 ? len : n-1; memcpy(d,s,m); d[m]=0; } return len; } """ for name in ["httpd_req_get_hdr_value_len", "httpd_req_get_hdr_value_str"]: # The declarations start at line beginning; avoid earlier calls in parser. prefix = "size_t " if name.endswith("len") else "esp_err_t " extracted += function(parse[parse.index(prefix + name):], name) + "\n" extracted += function(txrx[txrx.index("esp_err_t httpd_resp_set_hdr"):], "httpd_resp_set_hdr") extracted += "\n" + function(txrx[txrx.index("static size_t httpd_recv_pending"):], "httpd_recv_pending") extracted += "\nsize_t host_read_pending(httpd_req_t *r, char *out, size_t n) { return httpd_recv_pending(r, out, n); }\n" if settings: uri_source = (idf / 'components/esp_http_server/src/httpd_uri.c').read_text() extracted += '\n' + function(uri_source[uri_source.index('esp_err_t httpd_unregister_uri_handler'):], 'httpd_unregister_uri_handler') with tempfile.TemporaryDirectory(prefix="web-cookie-auth-") as directory: tmp = pathlib.Path(directory) for name, text in HEADERS.items(): path = tmp / name path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text) (tmp / "installed_httpd.c").write_text(extracted) if settings: # Compile exact production handler/helpers and nonblocking snapshot body. server_source = (ROOT / 'src/web_server.c').read_text() service_source = (ROOT / 'src/serial_service.c').read_text() config_source = (ROOT / 'src/serial_config.c').read_text() settings_source = function(service_source, 'serial_service_get_snapshot') + '\n' for name in ('data_bits', 'parity', 'stop_bits', 'flow_control', 'dtr_behavior'): settings_source += function(config_source[config_source.index('const char *serial_config_' + name + '_to_string'):], 'serial_config_' + name + '_to_string') + '\n' for name in ('set_common_headers', 'send_plain_error', 'authorize_or_respond', 'safe_string', 'serial_settings_handler'): settings_source += function(server_source, name) + '\n' # Exercise the serial projection of /api/status without doubling every # unrelated subsystem. Copy its acquisition, format and arguments verbatim. status = function(server_source, 'status_handler') assert 'serial_service_get_config(' not in status, 'status must not use the blocking config getter' assert 'serial_service_is_running(' not in status, 'status must use snapshot running state' assert status.count('serial_service_get_snapshot(') == 1 acquisition = status[status.index(' bool serial_config_available ='): status.index(' serial_service_get_counters(')] serial_format = status[status.index(' " \\"serial\\":'): status.index(' " \\"broker\\":')] argument_start = status.index(' wifi_available ? (unsigned int)wifi.ap_client_count : 0U,') argument_start = status.index('\n', argument_start) + 1 serial_arguments = status[argument_start:status.index(' broker_available ? "true"')].rstrip().removesuffix(',') settings_source += '\nstatic int status_serial_projection(char *response, size_t capacity) {\n' settings_source += ' serial_service_snapshot_t serial_snapshot = {0};\n' settings_source += ' serial_service_counters_t serial_counters = {0};\n' + acquisition settings_source += ' return snprintf(response, capacity,\n' + serial_format + ',\n' + serial_arguments + ');\n}\n' (tmp / 'settings_production.h').write_text(settings_source) if display: ui_source = (ROOT / 'src/local_status_ui.c').read_text() names = ('local_status_ui_get_config', 'local_status_ui_get_settings', 'local_status_ui_update_settings', 'local_status_ui_apply_config', 'local_status_ui_hold_for_diagnostics') (tmp / 'display_owner_production.h').write_text('\n'.join(function(ui_source, name) for name in names)) console_source = (ROOT / 'src/local_ui_console.c').read_text() names = ('print_usage', 'print_config', 'parse_timeout', 'apply_parameter', 'command_display') (tmp / 'display_console_production.h').write_text('\n'.join(function(console_source, name) for name in names)) if network: wifi_source = (ROOT / 'src/wifi_config.c').read_text() mdns_source = (ROOT / 'src/mdns_config.c').read_text() names = ['wifi_config_parse_ap_policy', 'wifi_config_ap_policy_to_string', 'wifi_config_parse_security', 'wifi_config_security_to_string'] (tmp / 'network_parse_production.h').write_text('\n'.join(function(wifi_source, name) for name in names) + '\n' + '\n'.join(function(mdns_source, name) for name in ('suffix_character_is_valid', 'mdns_config_validate'))) if accounts: db_source = (ROOT / 'src/user_database.c').read_text() alphabet_start = db_source.index('static const uint8_t s_generated_alphabet') alphabet = db_source[alphabet_start:db_source.index(';', alphabet_start) + 1] (tmp / 'account_parse_production.h').write_text(alphabet + '\n' + '\n'.join(function(db_source, name) for name in ('user_database_username_valid', 'user_database_password_valid', 'user_role_parse', 'user_role_to_string', 'user_database_generate_password_value'))) if serial_settings: config_source = (ROOT / 'src/serial_config.c').read_text() names = ['serial_config_defaults', 'serial_config_validate'] names += ['serial_config_parse_' + name for name in ('data_bits', 'parity', 'stop_bits', 'flow_control', 'dtr_behavior')] (tmp / 'serial_config_production.h').write_text('\n'.join(function(config_source, name) for name in names)) console_source = (ROOT / 'src/serial_console.c').read_text() (tmp / 'serial_console_production.h').write_text('\n'.join(function(console_source, name) for name in ('parse_unsigned', 'set_parameter', 'command_serial'))) sources = [HERE / "test.c", tmp / "installed_httpd.c"] sources += [ROOT / "src" / name for name in ["web_session_store.c", "web_auth_parse.c", "web_cookie_auth.c", "web_httpd_adapter.c"]] if admin: sources += [ROOT / "src" / name for name in ["web_admin_tickets.c", "web_admin_transport.c"]] if settings: adapter = ROOT / 'src/web_httpd_adapter.c' sources.remove(adapter) subprocess.run(['cc', '-std=c11', '-D_GNU_SOURCE', '-Wall', '-Wextra', '-Werror', '-Dmalloc=settings_malloc', '-Dfree=settings_free', '-I' + str(tmp), '-I' + str(ROOT / 'src'), '-c', str(adapter), '-o', str(tmp / 'adapter.o')], check=True, timeout=30) sources.append(tmp / 'adapter.o') subprocess.run(["cc", "-std=c11", "-Wall", "-Wextra", "-Werror", "-g", "-DHOST_OPENSSL", *(["-DHOST_ADMIN"] if admin else []), *(["-DHOST_SETTINGS"] if settings else []), *(["-DHOST_SERIAL_SETTINGS"] if serial_settings else []), *(["-DHOST_ACCOUNTS"] if accounts else []), *(["-DHOST_NETWORK"] if network else []), *(["-DHOST_DISPLAY"] if display else []), *(["-DHOST_BROKER"] if broker else []), *(["-DHOST_SSH_SETTINGS"] if ssh_settings else []), *(["-DHOST_LIFECYCLE"] if lifecycle else []), "-I" + str(tmp), "-I" + str(ROOT / "src"), *map(str, sources), "-lcrypto", *(["-lmbedcrypto"] if ssh_settings else []), "-o", str(tmp / "test")], check=True, timeout=30) subprocess.run([str(tmp / "test")], check=True, timeout=20) if lifecycle: guard = subprocess.run(["cc", "-E", "-DCONFIG_HTTPD_QUEUE_WORK_BLOCKING=1", "-I" + str(tmp), "-I" + str(ROOT / "src"), str(ROOT / "src/web_lifecycle_settings.c")], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, timeout=30) assert guard.returncode and b"Lifecycle ACK handoff requires nonblocking" in guard.stderr print("PASS lifecycle compile-time rejection of blocking HTTPD work submission")