Add broker management and writer transfer UI

This commit is contained in:
2026-09-13 13:59:28 +02:00
parent 29a4953df0
commit fa12440606
26 changed files with 1037 additions and 48 deletions
@@ -0,0 +1,129 @@
/* Real HTTP policy/store/parser/operation module, deterministic broker owner double.
* Canonical lock/lease/ID tests are in session_broker_diagnostics. */
#include "../../src/web_broker_settings.c"
static bool on_dispatcher, queue_fail;
static uint32_t queued_id;
static unsigned assignments, snapshots;
static esp_err_t broker_error;
static session_broker_management_snapshot_t broker_snapshot;
esp_err_t session_broker_get_management_snapshot(session_broker_management_snapshot_t *out) {
assert(!host_lock_depth && !on_dispatcher); ++snapshots;
*out = broker_snapshot; return broker_error;
}
esp_err_t session_broker_assign_writer_current(session_broker_client_id_t target, uint32_t generation) {
assert(on_dispatcher && !host_lock_depth && target == 9 && generation == 7);
++assignments; return broker_error;
}
esp_err_t admin_ssh_console_submit_broker_settings(uint32_t id) {
assert(id && !on_dispatcher && !host_lock_depth);
if (queue_fail) return ESP_FAIL;
queued_id = id; return ESP_OK;
}
static void operation_begin(const issued_t *identity, const char *body) {
begin("/api/settings/broker-operation", body ? HTTP_POST : HTTP_GET, body);
same_origin(); if (body) add("Content-Type", "application/json");
if (identity) {
char cookie[100]; snprintf(cookie, sizeof(cookie), "__Host-sak-session=%s", identity->token);
add("Cookie", cookie); if (body) add("X-CSRF-Token", identity->view.csrf);
}
}
static void broker_expect(const char *status, bool snapshot) {
unsigned before = assignments;
esp_err_t e = snapshot ? web_broker_settings_handler(&req) : web_broker_operation_handler(&req);
assert(e == (send_fail || aux.remaining_len ? ESP_FAIL : ESP_OK));
if (strcmp(response_status, status)) fprintf(stderr, "expected %s got %s: %s\n", status, response_status, output);
assert(!strcmp(response_status, status) && assignments == before);
assert(strlen(output) < (snapshot ? 2048 : 96)); zero(scratch, sizeof(scratch));
}
static void execute(void) { on_dispatcher = true; web_broker_settings_execute(queued_id); on_dispatcher = false; }
static const char *assign_body = "{\"action\":\"assign\",\"generation\":7,\"target\":9}";
static void submit(const issued_t *who) {
operation_begin(who, assign_body); broker_expect("202 Accepted", false); assert(s_operation.state == PENDING);
}
static void broker_settings_tests(void) {
auth_reset(); issued_t admin = mint(&alice), user = mint(&bob), other = mint(&alice);
receive_fragment = 64;
operation_begin(NULL, assign_body); broker_expect("401 Unauthorized", false);
operation_begin(&user, assign_body); broker_expect("403 Forbidden", false);
operation_begin(&user, NULL); broker_expect("403 Forbidden", false);
for (unsigned mode = 0; mode < 8; ++mode) {
operation_begin(&admin, assign_body);
if (mode == 0) req.content_len = aux.remaining_len = 257;
if (mode == 1) req.uri = "/api/settings/broker-operation?x=1";
if (mode == 2) req.method = HTTP_GET;
if (mode == 3) add("X-CSRF-Token", "duplicate");
if (mode == 4) add("Origin", "https://evil.example");
if (mode == 5) add("Transfer-Encoding", "chunked");
if (mode == 6) add("Content-Type", "text/plain");
if (mode == 7) add("Sec-Fetch-Site", "cross-site");
(void)web_broker_operation_handler(&req);
assert(response_status[0] == '4' && !s_next_id && !assignments);
}
puts("PASS Broker HTTP admin/cookie/Origin/CSRF, duplicate/query/body/framing bounds");
const char *invalid[] = {"{}", "[]", "{\"action\":\"assign\"}",
"{\"action\":\"assign\",\"generation\":0,\"target\":9}",
"{\"action\":\"assign\",\"generation\":4294967295,\"target\":9}",
"{\"action\":\"assign\",\"generation\":7,\"target\":0}",
"{\"action\":\"assign\",\"generation\":7,\"target\":4294967296}",
"{\"action\":\"assign\",\"generation\":7,\"target\":09}",
"{\"action\":\"assign\",\"generation\":7,\"target\":9.0}",
"{\"action\":\"assign\",\"generation\":7,\"target\":9e0}",
"{\"action\":\"assign\",\"generation\":7,\"target\":-9}",
"{\"action\":\"assign\",\"generation\":7,\"generation\":9}",
"{\"action\":\"assign\",\"generation\":7,\"target\":9,\"extra\":1}",
"{\"action\":\"as\\u0073ign\",\"generation\":7,\"target\":9}"};
for (unsigned i = 0; i < sizeof(invalid)/sizeof(*invalid); ++i) {
operation_begin(&admin, invalid[i]); broker_expect("400 Bad Request", false);
}
broker_operation_t parsed = {0};
for (size_t n = 0; n < strlen(assign_body); ++n) assert(!parse(assign_body, n, &parsed));
assert(parse(assign_body, strlen(assign_body), &parsed));
assert(!parse(assign_body, strlen(assign_body)+1, &parsed));
const char *reordered = " { \"target\":4294967295, \"generation\":4294967294, \"action\":\"assign\" } ";
assert(parse(reordered, strlen(reordered), &parsed));
receive_fragment = 1; operation_begin(&admin, assign_body); broker_expect("400 Bad Request", false); assert(body_offset == 4); receive_fragment = 64;
char full[257]; memset(full, ' ', 256); memcpy(full, assign_body, strlen(assign_body)); full[256] = 0;
queue_fail = true; operation_begin(&admin, full); broker_expect("503 Service Unavailable", false); queue_fail = false;
assert(body_offset == 256 && s_operation.state == IDLE);
puts("PASS Broker strict parser, truncation/order/integer limits, exact 256 bytes and four receives");
broker_snapshot.generation = 7; broker_snapshot.writer_id = 8; broker_snapshot.count = 8;
for (unsigned i = 0; i < 8; ++i) {
session_broker_management_client_t *c = &broker_snapshot.clients[i];
c->id = i + 8; c->type = i % 5; memset(c->name, '\"', 23); c->name[23] = 0;
c->pending = c->high_water = 4096; c->dropped = UINT64_MAX;
}
for (unsigned mode = 0; mode < 5; ++mode) {
unsigned before = snapshots;
operation_begin(mode == 0 ? NULL : mode == 1 ? &user : &admin, NULL); req.uri = "/api/settings/broker";
broker_error = mode >= 3 ? ESP_FAIL : ESP_OK;
broker_expect(mode == 0 ? "401 Unauthorized" : mode == 1 ? "403 Forbidden" : mode >= 3 ? "503 Service Unavailable" : "200 OK", true);
if (mode < 2) assert(snapshots == before);
if (mode == 2) assert(strstr(output, "18446744073709551615") && strstr(output, "2222222222222222222222222222222222222222222222"));
}
broker_error = ESP_OK;
puts("PASS Broker bounded eight-row admin snapshot, exact 64-bit strings/safe name encoding and unavailable owner");
submit(&admin); uint32_t first = queued_id;
operation_begin(&other, assign_body); broker_expect("503 Service Unavailable", false);
operation_begin(&other, NULL); broker_expect("200 OK", false); assert(strstr(output, "idle"));
execute(); assert(s_operation.state == OK && assignments == 1); zero(&s_operation.principal, sizeof(s_operation.principal));
execute(); assert(assignments == 1);
esp_err_t failures[] = {ESP_ERR_INVALID_STATE, ESP_ERR_NOT_FOUND, ESP_FAIL};
for (unsigned i = 0; i < 3; ++i) { broker_error = failures[i]; submit(&admin); execute(); assert(s_operation.state == (i < 2 ? CONFLICT : FAILED)); }
broker_error = ESP_OK;
puts("PASS Broker typed dispatcher-only execution, replay suppression, login-isolated results, stale/absent target conflicts");
submit(&admin); unsigned before = assignments;
web_broker_settings_execute(0); web_broker_settings_execute(first); assert(assignments == before && s_operation.state == PENDING);
now += 30000000; execute(); assert(s_operation.state == CANCELLED && assignments == before);
submit(&admin); web_session_store_invalidate(admin.view.id); execute(); assert(s_operation.state == CANCELLED);
admin = mint(&alice); submit(&admin); db_fail = true; execute(); db_fail = false; assert(s_operation.state == CANCELLED);
admin = mint(&alice); submit(&admin); stale_user = alice.user_id; execute(); stale_user = 0; assert(s_operation.state == CANCELLED);
admin = mint(&alice); now = admin.view.expires_at_us - 1; submit(&admin); now = admin.view.expires_at_us; execute(); assert(s_operation.state == CANCELLED);
admin = mint(&alice); submit(&admin); web_cookie_auth_stop(); assert(web_cookie_auth_start() == ESP_OK); execute(); assert(s_operation.state == CANCELLED);
admin = mint(&alice); operation_begin(&admin, NULL); broker_expect("200 OK", false); assert(strstr(output, "idle"));
assert(assignments == before);
puts("PASS Broker deadline/expiry/revocation/currentness failure and auth stop/restart fencing");
send_fail = true; submit(&admin); send_fail = false; execute(); assert(s_operation.state == OK);
operation_begin(&admin, NULL); broker_expect("200 OK", false); assert(strstr(output, "ok"));
s_next_id = UINT32_MAX; operation_begin(&admin, assign_body); broker_expect("503 Service Unavailable", false);
puts("PASS Broker lost acknowledgement retained result and nonwrapping operation IDs");
}
+2
View File
@@ -59,6 +59,7 @@ 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
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'
@@ -247,6 +248,7 @@ with tempfile.TemporaryDirectory(prefix="web-cookie-auth-") as directory:
*(["-DHOST_ACCOUNTS"] if accounts else []),
*(["-DHOST_NETWORK"] if network else []),
*(["-DHOST_DISPLAY"] if display else []),
*(["-DHOST_BROKER"] if broker 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)
+7 -1
View File
@@ -19,7 +19,7 @@ static struct httpd_data server = {.config.max_resp_headers = 8};
static struct sock_db socket_state;
static struct resp_hdr response_headers[8];
static char scratch[1024], cookie_values[2][200];
#ifdef HOST_NETWORK
#if defined(HOST_NETWORK) || defined(HOST_BROKER)
static char output[2048];
#else
static char output[1024];
@@ -146,6 +146,9 @@ static void auth_reset(void) {
#ifdef HOST_DISPLAY
#include "display_settings_test.c"
#endif
#ifdef HOST_BROKER
#include "broker_settings_test.c"
#endif
int main(void) {
assert(store_tests() == 0); auth_reset();
@@ -308,6 +311,9 @@ int main(void) {
#endif
#ifdef HOST_DISPLAY
display_settings_tests();
#endif
#ifdef HOST_BROKER
broker_settings_tests();
#endif
return 0;
}