Pin PlatformIO packages and toolchains, rebase protected SDK overrides, and add WebSocket receive regression coverage. Document isolated candidate validation, archive provenance, and remaining gates.
268 lines
15 KiB
Python
268 lines
15 KiB
Python
#!/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,3)
|
|
"""
|
|
HEADERS["esp_httpd_priv.h"] = """#pragma once
|
|
#include <stdint.h>
|
|
#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 <stddef.h>
|
|
#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 <stdbool.h>
|
|
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 <stddef.h>
|
|
#include <openssl/evp.h>
|
|
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 <stdbool.h>
|
|
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 <stddef.h>
|
|
#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 <stdbool.h>
|
|
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 <string.h>
|
|
#include <strings.h>
|
|
#include <stdlib.h>
|
|
#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),
|
|
"-Wl,--wrap=web_auth_parse_login,--wrap=secure_wipe,--wrap=httpd_resp_set_hdr",
|
|
"-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")
|