Route bounded admin mutations through the existing administration dispatcher, covering apply, lifecycle, persistence, authorization, and result tracking. Add the browser controls, automatic result refresh, regression coverage, and phase documentation.
161 lines
8.8 KiB
Python
161 lines
8.8 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 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 "esp_http_server.h"
|
|
struct sock_db { 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; void *uri_match_fn; } config;
|
|
httpd_uri_t **hd_calls; };
|
|
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
|
|
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 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 []),
|
|
"-I" + str(tmp), "-I" + str(ROOT / "src"), *map(str, sources), "-lcrypto",
|
|
"-o", str(tmp / "test")], check=True, timeout=30)
|
|
subprocess.run([str(tmp / "test")], check=True, timeout=20)
|