/* Production store dependency doubles and its existing public API suite. */ #define main store_tests #include "../web_session_store/test.c" #undef main #include "web_cookie_auth.h" #include "web_httpd_adapter.h" #include "esp_httpd_priv.h" #ifdef HOST_ADMIN #include "web_admin_transport.h" #endif 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], output[1024], cookie_values[2][200]; static const char *request_body; static size_t body_offset; static unsigned password_calls, cookie_count, sends, upgrades; static unsigned fail_header, setter_calls; static bool send_fail, recv_fail; static void (*password_hook)(void); static char response_status[48]; static struct httpd_req_aux aux; static httpd_req_t req; size_t host_read_pending(httpd_req_t *r, char *out, size_t n); esp_err_t httpd_resp_set_status(httpd_req_t *r, const char *status) { (void)r; if (fail_header && ++setter_calls == fail_header) return ESP_FAIL; snprintf(response_status, sizeof(response_status), "%s", status); return ESP_OK; } esp_err_t httpd_resp_set_type(httpd_req_t *r, const char *type) { (void)r; (void)type; return ESP_OK; } esp_err_t httpd_resp_sendstr(httpd_req_t *r, const char *body) { (void)r; ++sends; assert(strlen(body) < sizeof(output)); strcpy(output, body); cookie_count = 0; for (unsigned i = 0; i < aux.resp_hdrs_count; ++i) { assert(response_headers[i].value); assert(strcmp(response_headers[i].field, "WWW-Authenticate")); if (!strcmp(response_headers[i].field, "Set-Cookie")) { assert(cookie_count < 2); snprintf(cookie_values[cookie_count++], 200, "%s", response_headers[i].value); } } return send_fail ? ESP_FAIL : ESP_OK; } int httpd_req_recv(httpd_req_t *r, char *out, size_t size) { (void)r; if (recv_fail) return -1; if (size > 7) size = 7; /* Fragment every login body. */ memcpy(out, request_body + body_offset, size); body_offset += size; aux.remaining_len -= size; return (int)size; } esp_err_t web_login_ui_send_response(httpd_req_t *r) { return httpd_resp_sendstr(r, "login document"); } esp_err_t web_serial_transport_revoke_web_session(web_session_id_t id) { web_session_store_invalidate(id); #ifdef HOST_ADMIN web_admin_transport_revoke(id, NULL, 0); #endif return ESP_OK; } esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *r, const char *protocol) { (void)r; (void)protocol; ++upgrades; return ESP_OK; } esp_err_t user_database_authenticate_password(const uint8_t *u, size_t un, const uint8_t *p, size_t pn, user_principal_t *principal, bool *authenticated) { assert(!host_lock_depth); ++password_calls; if (password_hook) { void (*hook)(void) = password_hook; password_hook = NULL; hook(); } *authenticated = un == 5 && !memcmp(u, "alice", 5) && pn == 12 && !memcmp(p, "password1234", 12); if (*authenticated) *principal = alice; return db_fail ? ESP_FAIL : ESP_OK; } static void begin(const char *uri, int method, const char *body) { memset(scratch, 0, sizeof(scratch)); memset(response_headers, 0, sizeof(response_headers)); memset(&socket_state, 0, sizeof(socket_state)); aux = (struct httpd_req_aux){.sd = &socket_state, .scratch = scratch, .scratch_cur_size = sizeof(scratch), .resp_hdrs = response_headers}; req = (httpd_req_t){.handle = &server, .aux = &aux, .uri = uri, .method = method, .content_len = body ? strlen(body) : 0}; aux.remaining_len = req.content_len; request_body = body; body_offset = 0; response_status[0] = output[0] = 0; cookie_count = 0; } static void add(const char *key, const char *value) { char *at = scratch; for (unsigned i = 0; i < aux.req_hdrs_count; ++i) at += strlen(at) + 1; assert((size_t)(at - scratch) + strlen(key) + strlen(value) + 3 < sizeof(scratch)); sprintf(at, "%s: %s", key, value); ++aux.req_hdrs_count; } static void same_origin(void) { add("Host", "device.example"); add("Origin", origin); } static void expect(const char *status) { (void)web_cookie_auth_handler(&req); assert(!strcmp(response_status, status)); } static void token_from(const char *value, char token[65]) { const char *start = strchr(value, '='); assert(start && strlen(start + 1) >= 64); memcpy(token, start + 1, 64); token[64] = 0; } static void csrf_from(char csrf[65]) { const char *start = strstr(output, "\"csrf\":\""); assert(start); memcpy(csrf, start + 8, 64); csrf[64] = 0; } static void challenge(char token[65], char csrf[65]) { begin("/api/login-challenge", HTTP_GET, NULL); add("Host", "device.example"); add("X-Login-Bootstrap", "1"); expect("200 OK"); assert(cookie_count == 1); token_from(cookie_values[0], token); csrf_from(csrf); } static const char good_body[] = "{\"username\":\"alice\",\"password\":\"password1234\"}"; static void login_request(const char *token, const char *csrf, const char *body) { begin("/api/login", HTTP_POST, body); same_origin(); add("Content-Type", "application/json"); add("X-CSRF-Token", csrf); char cookies[100]; snprintf(cookies, sizeof(cookies), "__Host-sak-prelogin=%s", token); add("Cookie", cookies); } static void auth_reset(void) { web_cookie_auth_stop(); reset(); assert(web_cookie_auth_start() == ESP_OK); password_calls = 0; password_hook = NULL; } #ifdef HOST_ADMIN #include "admin_test.c" #endif #ifdef HOST_SETTINGS #include "settings_test.c" #endif int main(void) { assert(store_tests() == 0); auth_reset(); char token[65], csrf[65], session[65], cookies[200]; challenge(token, csrf); begin("/api/login-challenge", HTTP_GET, NULL); add("Host", "device.example"); add("X-Login-Bootstrap", "1"); snprintf(cookies, sizeof(cookies), "__Host-sak-prelogin=%s", token); add("Cookie", cookies); expect("200 OK"); assert(cookie_count == 0); login_request(token, csrf, good_body); expect("200 OK"); assert(password_calls == 1 && cookie_count == 2 && snapshot().active == 1); assert(strstr(cookie_values[0], "__Host-sak-prelogin=")); assert(strstr(cookie_values[0], "Max-Age=0")); assert(strstr(cookie_values[1], "Secure; HttpOnly; SameSite=Strict; Path=/; Max-Age=3600")); token_from(cookie_values[1], session); begin("/api/session", HTTP_GET, NULL); same_origin(); snprintf(cookies, sizeof(cookies), "__Host-sak-session=%s", session); add("Cookie", cookies); expect("200 OK"); csrf_from(csrf); issued_t other = mint(&alice); begin("/api/logout", HTTP_POST, NULL); same_origin(); add("Cookie", cookies); add("X-CSRF-Token", csrf); expect("204 No Content"); present(&other); assert(snapshot().active == 1); begin("/api/session", HTTP_GET, NULL); same_origin(); add("Cookie", cookies); expect("401 Unauthorized"); puts("PASS: challenge reuse, fragmented login, two independent Set-Cookie fields, session and isolated logout"); auth_reset(); for (unsigned i = 0; i < 5; ++i) { challenge(token, csrf); login_request(token, csrf, "{\"username\":\"alice\",\"password\":\"wrong\"}"); expect("401 Unauthorized"); login_request(token, csrf, good_body); expect("403 Forbidden"); } challenge(token, csrf); login_request(token, csrf, good_body); expect("429 Too Many Requests"); assert(password_calls == 5); now += 60000000; challenge(token, csrf); login_request(token, csrf, good_body); expect("200 OK"); auth_reset(); for (unsigned i = 0; i < 4; ++i) challenge(token, csrf); begin("/api/login-challenge", HTTP_GET, NULL); same_origin(); add("X-Login-Bootstrap", "1"); expect("503 Service Unavailable"); now += 120000000; challenge(token, csrf); auth_reset(); for (unsigned i = 0; i < 4; ++i) (void)mint(&alice); challenge(token, csrf); login_request(token, csrf, good_body); expect("503 Service Unavailable"); assert(snapshot().active == 4); puts("PASS: consumed challenges, global five/60s throttle, expiry and no live challenge/session eviction"); auth_reset(); challenge(token, csrf); const char *keys[] = {"Host", "Origin", "Cookie", "Content-Type", "X-CSRF-Token"}; for (unsigned i = 0; i < sizeof(keys)/sizeof(keys[0]); ++i) { login_request(token, csrf, good_body); add(keys[i], "ambiguous"); expect("400 Bad Request"); } assert(password_calls == 0); login_request(token, csrf, good_body); add("Transfer-Encoding", "chunked"); expect("400 Bad Request"); login_request(token, csrf, good_body); add("Sec-Fetch-Site", "cross-site"); expect("403 Forbidden"); login_request(token, "invalid", good_body); expect("403 Forbidden"); login_request(token, csrf, good_body); req.method = HTTP_GET; expect("400 Bad Request"); login_request(token, csrf, good_body); req.content_len = 513; expect("413 Payload Too Large"); login_request(token, csrf, "{\"username\":\"alice\",\"password\":\"x\",\"unknown\":1}"); expect("400 Bad Request"); assert(password_calls == 0); begin("/", HTTP_GET, NULL); same_origin(); add("Authorization", "Basic ignored"); web_session_view_t view; bool allowed; assert(web_cookie_auth_require(&req, false, false, &view, &allowed) == ESP_OK && !allowed); assert(!strcmp(response_status, "303 See Other")); begin("/assets/app.js", HTTP_GET, NULL); same_origin(); assert(web_cookie_auth_require(&req, false, false, &view, &allowed) == ESP_OK && !allowed); assert(!strcmp(response_status, "401 Unauthorized")); puts("PASS: duplicate security headers, framing, methods, metadata, CSRF, strict JSON and no Basic bypass"); auth_reset(); other = mint(&alice); snprintf(cookies, sizeof(cookies), "__Host-sak-session=%s", other.token); begin("/api/logout", HTTP_POST, NULL); same_origin(); add("Cookie", cookies); expect("403 Forbidden"); present(&other); begin("/api/logout", HTTP_POST, NULL); add("Host", "device.example"); add("Cookie", cookies); add("X-CSRF-Token", other.view.csrf); expect("403 Forbidden"); present(&other); begin("/api/logout", HTTP_POST, NULL); add("Host", "device.example"); add("Origin", "https://foreign.example"); add("Cookie", cookies); add("X-CSRF-Token", other.view.csrf); expect("403 Forbidden"); present(&other); begin("/api/session", HTTP_GET, NULL); add("Host", "device.example"); add("Cookie", cookies); expect("200 OK"); begin("/api/session", HTTP_GET, NULL); add("Host", "alias.local"); add("Cookie", cookies); expect("401 Unauthorized"); present(&other); challenge(token, csrf); begin("/api/login", HTTP_POST, good_body); same_origin(); add("Content-Type", "application/json"); add("X-CSRF-Token", csrf); snprintf(cookies, sizeof(cookies), "__Host-sak-session=%s; __Host-sak-prelogin=%s", other.token, token); add("Cookie", cookies); expect("409 Conflict"); assert(password_calls == 0); present(&other); begin("/api/login", HTTP_POST, good_body); same_origin(); add("Content-Type", "application/json"); add("X-CSRF-Token", csrf); snprintf(cookies, sizeof(cookies), "__Host-sak-session=%s; __Host-sak-session=%s", other.token, other.token); add("Cookie", cookies); expect("400 Bad Request"); assert(password_calls == 0); snprintf(cookies, sizeof(cookies), "__Host-sak-session=%s", other.token); now = other.view.expires_at_us; begin("/api/session", HTTP_GET, NULL); same_origin(); add("Cookie", cookies); expect("401 Unauthorized"); auth_reset(); other = mint(&alice); stale_user = alice.user_id; snprintf(cookies, sizeof(cookies), "__Host-sak-session=%s", other.token); begin("/api/session", HTTP_GET, NULL); same_origin(); add("Cookie", cookies); expect("401 Unauthorized"); puts("PASS: mandatory mutation Origin/CSRF, origin binding, explicit account switching, duplicate named cookies, expiry/currentness"); auth_reset(); challenge(token, csrf); login_request(token, csrf, good_body); password_hook = web_cookie_auth_stop; expect("503 Service Unavailable"); assert(!snapshot().active); auth_reset(); rng_fail = true; begin("/api/login-challenge", HTTP_GET, NULL); same_origin(); add("X-Login-Bootstrap", "1"); expect("503 Service Unavailable"); auth_reset(); challenge(token, csrf); login_request(token, csrf, good_body); send_fail = true; assert(web_cookie_auth_handler(&req) != ESP_OK); send_fail = false; assert(!snapshot().active); auth_reset(); challenge(token, csrf); login_request(token, csrf, good_body); recv_fail = true; expect("400 Bad Request"); recv_fail = false; assert(!password_calls); begin("/api/session", HTTP_GET, NULL); add("Host", "first"); add("host", "second"); char value[32]; assert(httpd_req_get_hdr_value_str(&req, "Host", value, sizeof(value)) == ESP_OK); assert(!strcmp(value, "first") && !web_httpd_headers_valid(&req)); begin("/ws/serial", HTTP_GET, NULL); same_origin(); add("Sec-WebSocket-Version", "13"); add("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ=="); assert(!web_httpd_upgrade_requested(&req)); aux.ws_handshake_detect = true; assert(web_httpd_upgrade(&req, web_cookie_auth_handler) == ESP_OK && upgrades == 1); assert(socket_state.ws_handshake_done && !web_httpd_upgrade_requested(&req)); begin("/ws/serial", HTTP_GET, NULL); same_origin(); aux.ws_handshake_detect = true; add("Sec-WebSocket-Version", "130"); add("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ=="); assert(!web_httpd_upgrade_requested(&req)); for (size_t keep = 0; keep <= sizeof(socket_state.pending_data); ++keep) { memset(socket_state.pending_data, 's', sizeof(socket_state.pending_data)); size_t offset = sizeof(socket_state.pending_data) - keep; memset(socket_state.pending_data + offset, 'p', keep); socket_state.pending_len = keep; web_httpd_wipe_request(&req, false); zero(socket_state.pending_data, offset); char received[128]; size_t first = host_read_pending(&req, received, 1); assert(first == (keep ? 1U : 0U)); if (first) assert(received[0] == 'p'); web_httpd_wipe_request(&req, false); zero(socket_state.pending_data, offset + first); size_t remaining = host_read_pending(&req, received, sizeof(received)); assert(remaining == keep - first); for (size_t i = 0; i < remaining; ++i) assert(received[i] == 'p'); } memset(socket_state.pending_data, 's', sizeof(socket_state.pending_data)); socket_state.pending_len = 3; web_httpd_wipe_request(&req, true); zero(socket_state.pending_data, sizeof(socket_state.pending_data)); zero(scratch, sizeof(scratch)); puts("PASS: request wiping preserves right-aligned pending data through actual IDF reader (all lengths/partial reads)"); puts("PASS: stop/login race, RNG/send/receive failure, actual IDF first-header semantics and explicit upgrade adapter"); for (unsigned limit = 0; limit < 6; ++limit) { auth_reset(); challenge(token, csrf); login_request(token, csrf, good_body); server.config.max_resp_headers = limit; assert(web_cookie_auth_handler(&req) != ESP_OK); assert(snapshot().active == 0); server.config.max_resp_headers = 8; } auth_reset(); challenge(token, csrf); login_request(token, csrf, good_body); server.config.max_resp_headers = 6; expect("200 OK"); assert(cookie_count == 2); server.config.max_resp_headers = 8; puts("PASS: exact six-header successful login budget; all smaller header capacities invalidate unpublished login"); #ifdef HOST_ADMIN admin_tests(); #endif #ifdef HOST_SETTINGS settings_tests(); #endif return 0; }