Complete Phase 12 dual-stack networking
Add IPv6-aware Wi-Fi state, HTTPS/SSH listeners, mDNS service reconciliation, and browser Wi-Fi administration. Include a guarded build-local fix for mDNS 1.12.0 membership handling, focused regression suites, and Phase 12 acceptance documentation.
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile production server lifecycle and URI tables against fixed host fakes.
|
||||
|
||||
No HTTP handlers, TLS/HTTPD runtime, transport implementation or scheduler is
|
||||
executed. Assertions cover server orchestration and values passed to registration
|
||||
No complete HTTP handlers, TLS/HTTPD runtime, transport implementation or scheduler
|
||||
are executed. The Wi-Fi status JSON projection is compiled separately from its
|
||||
production format and arguments. Assertions cover server orchestration and values passed to registration
|
||||
and SSL-start fakes, not actual requests/101, socket eviction or concurrent stop.
|
||||
No firmware build, network access or device operation. CC selects the compiler.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
@@ -91,6 +93,18 @@ static int mutex_storage, server_storage, locked;
|
||||
static void esp_restart(void);
|
||||
void web_lifecycle_settings_stopped(httpd_handle_t server);
|
||||
static bool mutex_fail, auth_live, ssl_live, admin_owned, serial_live, mutex_busy;
|
||||
static bool https_available;
|
||||
static unsigned https_publications, https_withdrawals;
|
||||
static void mdns_service_set_https_available(bool available) {
|
||||
assert(locked);
|
||||
if (available) {
|
||||
assert(auth_live && ssl_live && !https_available);
|
||||
++https_publications;
|
||||
} else {
|
||||
++https_withdrawals;
|
||||
}
|
||||
https_available = available;
|
||||
}
|
||||
static void (*unlock_hook)(void);
|
||||
static esp_err_t serial_detach_error;
|
||||
static unsigned ssl_stop_fail_at;
|
||||
@@ -271,7 +285,7 @@ static bool generation_fail;
|
||||
static esp_err_t route_error_handler(httpd_req_t *r, httpd_err_code_t c) { (void)r; (void)c; assert(0); return ESP_FAIL; }
|
||||
static esp_err_t web_serial_transport_init(void) { assert(!locked); ++serial_inits; return serial_init_error; }
|
||||
static esp_err_t web_cookie_auth_start(void) { assert(!locked); ++auth_starts; auth_live = auth_error == ESP_OK; return auth_error; }
|
||||
static void web_cookie_auth_stop(void) { event('A'); ++auth_stops; auth_live = false; }
|
||||
static void web_cookie_auth_stop(void) { assert(!https_available); event('A'); ++auth_stops; auth_live = false; }
|
||||
static esp_err_t web_security_copy_tls_material(uint8_t *cert, size_t nc, size_t *lc,
|
||||
uint8_t *key, size_t nk, size_t *lk) {
|
||||
assert(!locked && auth_live && nc && nk); cert[0] = 1; key[0] = 2; *lc = *lk = 1; return ESP_OK;
|
||||
@@ -416,6 +430,7 @@ static void reset(void) {
|
||||
s_server_mutex = NULL; s_server = NULL; s_initialized = s_transitioning = false;
|
||||
s_generation = 1U; mutex_busy = false; unlock_hook = NULL; serial_detach_error = ESP_OK;
|
||||
ssl_stop_fail_at = 0;
|
||||
https_available = false; https_publications = https_withdrawals = 0;
|
||||
s_serial_transport_init_attempted = s_serial_transport_initialized = false;
|
||||
s_serial_transport_attached = s_admin_transport_owned = false;
|
||||
s_last_error = s_serial_transport_error = ESP_ERR_INVALID_STATE;
|
||||
@@ -451,6 +466,7 @@ static void start(void) {
|
||||
assert(web_server_start() == ESP_OK);
|
||||
assert(s_server == SERVER && s_admin_transport_owned && s_serial_transport_attached);
|
||||
assert(auth_live && ssl_live && admin_owned && serial_live && !s_transitioning && idle_owned);
|
||||
assert(https_available);
|
||||
}
|
||||
static const httpd_uri_t *route(const char *uri) {
|
||||
const httpd_uri_t *found = NULL;
|
||||
@@ -515,6 +531,32 @@ static void other_domains_complete(void) {
|
||||
}
|
||||
}
|
||||
int main(void) {
|
||||
reset();
|
||||
assert(web_server_stop() == ESP_ERR_INVALID_STATE);
|
||||
assert(!https_available && !https_publications && !https_withdrawals);
|
||||
start();
|
||||
assert(https_publications == 1 && !https_withdrawals);
|
||||
assert(web_server_start() == ESP_ERR_INVALID_STATE);
|
||||
assert(stop_server(s_generation + 1, false, false) == ESP_ERR_INVALID_STATE);
|
||||
s_transitioning = true;
|
||||
assert(web_server_stop() == ESP_ERR_INVALID_STATE);
|
||||
s_transitioning = false;
|
||||
assert(https_available && https_publications == 1 && !https_withdrawals);
|
||||
for (unsigned failure = 0; failure < 4; ++failure) {
|
||||
idle_detach_error = failure == 0 ? ESP_ERR_TIMEOUT : ESP_OK;
|
||||
admin_detach_error = failure == 1 ? ESP_ERR_TIMEOUT : ESP_OK;
|
||||
serial_detach_error = failure == 2 ? ESP_FAIL : ESP_OK;
|
||||
ssl_stop_error = failure == 3 ? ESP_FAIL : ESP_OK;
|
||||
assert(web_server_stop() != ESP_OK);
|
||||
assert(!https_available && https_publications == 1 && https_withdrawals == failure + 1);
|
||||
assert(web_server_start() == ESP_ERR_INVALID_STATE && !https_available);
|
||||
}
|
||||
ssl_stop_error = ESP_OK;
|
||||
assert(web_server_stop() == ESP_OK && !https_available && https_withdrawals == 5);
|
||||
fresh_registration(); start();
|
||||
assert(https_publications == 2 && https_withdrawals == 5);
|
||||
assert(web_server_stop() == ESP_OK && !https_available && https_withdrawals == 6);
|
||||
puts("PASS mDNS HTTPS publication under mutex, rejected calls unchanged, withdrawal before cookie stop through all teardown failures/retry/restart");
|
||||
reset(); mutex_fail = true;
|
||||
assert(web_server_init() == ESP_ERR_NO_MEM && !s_initialized && !serial_inits);
|
||||
mutex_fail = false; serial_init_error = ESP_FAIL;
|
||||
@@ -578,6 +620,7 @@ int main(void) {
|
||||
assert(registration_calls == failure && !admin_inits && !admin_attaches && !serial_attaches);
|
||||
assert(!auth_live && !ssl_live && ssl_stops == 1 && !s_server && !s_admin_transport_owned);
|
||||
assert(!admin_detaches && !admin_stoppeds && !s_transitioning && s_counters.start_failures == 1);
|
||||
assert(!https_available && !https_publications && https_withdrawals == 1);
|
||||
}
|
||||
puts("PASS required registration positions 1..17 fail fatally before transport attachment");
|
||||
|
||||
@@ -621,6 +664,7 @@ int main(void) {
|
||||
reset(); registration_fail_at = 6; ssl_stop_error = ESP_FAIL;
|
||||
assert(web_server_start() == ESP_FAIL && s_server == SERVER && ssl_live);
|
||||
assert(!s_admin_transport_owned && !admin_attaches && !auth_live);
|
||||
assert(!https_available && !https_publications && https_withdrawals == 1);
|
||||
assert(web_server_start() == ESP_ERR_INVALID_STATE && ssl_starts == 1);
|
||||
ssl_stop_error = ESP_OK; clear_events();
|
||||
assert(web_server_stop() == ESP_OK && !strcmp(events, "AH") && !admin_stoppeds);
|
||||
@@ -866,7 +910,7 @@ int main(void) {
|
||||
puts("PASS every other settings route failure leaves the complete Network domain available");
|
||||
management_tests();
|
||||
pipeline_tests();
|
||||
puts("44 lifecycle groups passed (34 prior owner/route, 7 lifecycle integration, 3 HTTPS identity owner groups)");
|
||||
puts("45 lifecycle groups passed (34 prior owner/route, 7 lifecycle integration, 3 HTTPS identity owner groups, 1 mDNS availability group)");
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
@@ -1049,6 +1093,60 @@ with tempfile.TemporaryDirectory(prefix='web-admin-server-lifecycle-') as direct
|
||||
subprocess.run([str(executable)], check=True, timeout=15)
|
||||
print('Compiled production init/start/stop, URI initializers and configuration; dependency behavior is faked.')
|
||||
|
||||
# Keep the production Wi-Fi JSON format and all its arguments together, without
|
||||
# doubling every unrelated /api/status subsystem or claiming full HTTP coverage.
|
||||
status = function('status_handler')
|
||||
wifi_format = status[status.index(' " \\"wifi\\":'):status.index(' " \\"serial\\":')]
|
||||
wifi_arguments = status[status.index(' wifi_available ? "true"'):status.index(' serial_config_available ?')].rstrip().removesuffix(',')
|
||||
wifi_unit = r'''
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
static const char *wifi_manager_state_to_string(int state) {
|
||||
assert(state == 1); return "connected";
|
||||
}
|
||||
int main(void) {
|
||||
struct { bool ipv6_linklocal, ipv6_routable, ap_running;
|
||||
int state, sta_rssi; unsigned sta_channel, ap_client_count; } wifi = {
|
||||
.state = 1, .sta_rssi = -42, .sta_channel = 6,
|
||||
.ap_running = true, .ap_client_count = 2 };
|
||||
const char *ipv4 = "192.0.2.1";
|
||||
char response[512];
|
||||
for (unsigned available = 0; available < 2; ++available)
|
||||
for (unsigned flags = 0; flags < 4; ++flags) {
|
||||
bool wifi_available = available;
|
||||
wifi.ipv6_linklocal = flags & 1;
|
||||
wifi.ipv6_routable = flags & 2;
|
||||
int written = snprintf(response, sizeof(response),
|
||||
''' + wifi_format + ',\n' + wifi_arguments + r''');
|
||||
assert(written > 0 && (size_t)written < sizeof(response));
|
||||
fputs(response, stdout);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='web-status-wifi-') as directory:
|
||||
temporary = Path(directory)
|
||||
(temporary / 'status.c').write_text(wifi_unit)
|
||||
executable = temporary / 'status'
|
||||
subprocess.run([os.environ.get('CC', 'cc'), '-std=c11', '-Wall', '-Wextra', '-Werror',
|
||||
str(temporary / 'status.c'), '-o', str(executable)], check=True, timeout=30)
|
||||
result = subprocess.run([str(executable)], check=True, capture_output=True, text=True, timeout=15)
|
||||
rows = result.stdout.splitlines()
|
||||
assert len(rows) == 8
|
||||
for index, row in enumerate(rows):
|
||||
wifi = json.loads('{' + row.rstrip().removesuffix(',') + '}')['wifi']
|
||||
available, flags = divmod(index, 4)
|
||||
assert wifi == {
|
||||
'available': bool(available), 'state': 'connected' if available else 'unavailable',
|
||||
'sta_ipv4': '192.0.2.1', 'ipv6_linklocal': bool(available and flags & 1),
|
||||
'ipv6_routable': bool(available and flags & 2), 'rssi': -42 if available else 0,
|
||||
'channel': 6 if available else 0, 'ap_running': bool(available),
|
||||
'ap_clients': 2 if available else 0,
|
||||
}
|
||||
assert type(wifi['ipv6_linklocal']) is bool and type(wifi['ipv6_routable']) is bool
|
||||
print('PASS production /api/status Wi-Fi JSON projection: eight availability/IPv6 combinations, native booleans and unavailable masking')
|
||||
|
||||
# Second executable links the same production server functions to the COMPLETE
|
||||
# security implementation and real mbedTLS. Only NVS/HTTPD/scheduler are doubles.
|
||||
import ast
|
||||
|
||||
Reference in New Issue
Block a user