- Add admin account list, create, role, delete, and password workflows - Execute identity-checked mutations through the existing dispatcher - Bound queued credential lifetime and wipe transient secrets - Add explicit password generation with saved-value acknowledgement - Handle self-revocation and uncertain outcomes without automatic retries - Register optional account routes without disrupting terminal transports - Expand host regressions and document contracts and pending target checks Validated host suites and pio run; hardware validation remains pending.
176 lines
9.7 KiB
Python
176 lines
9.7 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
|
|
accounts = "--accounts" in sys.argv
|
|
if accounts:
|
|
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 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 []),
|
|
"-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)
|