Add admin firmware upload support
Implement authenticated HTTPS OTA uploads with bounded streaming, image validation, reboot coordination, and lifecycle exclusion. Add the admin UI, regression tests, and Phase 10 acceptance documentation.
This commit is contained in:
@@ -0,0 +1,452 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#include "fakes.h"
|
||||
#include "web_auth_parse.h"
|
||||
#include "esp_app_format.h"
|
||||
#include "esp_app_desc.h"
|
||||
#include "esp_image_format.h"
|
||||
#include "web_firmware_update.h"
|
||||
|
||||
static bool httpd_valid_req(httpd_req_t *r) { return r && r->aux; }
|
||||
static size_t strlcpy(char *out, const char *in, size_t size)
|
||||
{
|
||||
size_t length = strlen(in);
|
||||
if (size) { size_t n = length < size - 1 ? length : size - 1; memcpy(out, in, n); out[n] = 0; }
|
||||
return length;
|
||||
}
|
||||
/* IDF getters compare ptrdiff_t with size_t; retain their exact source. */
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wsign-compare"
|
||||
#include "httpd_production.h"
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
static int s_lock;
|
||||
static bool s_ready;
|
||||
static web_cookie_auth_snapshot_t s_counts;
|
||||
#include "auth_production.h"
|
||||
|
||||
static SemaphoreHandle_t s_server_mutex;
|
||||
static httpd_handle_t s_server;
|
||||
static bool s_transitioning;
|
||||
static esp_err_t s_last_error;
|
||||
static uint32_t s_generation;
|
||||
#include "server_production.h"
|
||||
|
||||
static void *test_malloc(size_t);
|
||||
static void test_free(void *);
|
||||
#define malloc test_malloc
|
||||
#define free test_free
|
||||
#include "web_firmware_update.c"
|
||||
#undef malloc
|
||||
#undef free
|
||||
|
||||
static uint8_t image[12000], flash[12000];
|
||||
static httpd_req_t request;
|
||||
static esp_partition_t running, target;
|
||||
static bool missing_target, missing_running;
|
||||
static bool lookup_current, final_current, lookup_unavailable, final_unavailable;
|
||||
static int role, stage_fail, response_fail, response_step;
|
||||
static bool malloc_fail, task_fail, identity_busy, locked, mutex_busy;
|
||||
static unsigned allocations, allocations_live, tasks_created, tasks_deleted;
|
||||
static unsigned begins, writes, ends, aborts, metadata_reads, commits, receives, wipes;
|
||||
static unsigned restarts, response_sends, releases, identity_releases;
|
||||
static size_t flash_size, max_chunk, cutoff, stall_at, parsed_size;
|
||||
static int64_t now_us, read_us;
|
||||
static char response_status[64], response_body[160];
|
||||
static uint32_t notification, identity_token;
|
||||
static void (*task_entry)(void *);
|
||||
static int task_storage, server_storage, mutex_storage;
|
||||
static bool receive_race, commit_race, revoke_at_metadata;
|
||||
static unsigned cases;
|
||||
static bool ota_live;
|
||||
static char raw_length[32], header_scratch[2048];
|
||||
static struct httpd_req_aux request_aux;
|
||||
|
||||
static void *test_malloc(size_t size)
|
||||
{
|
||||
assert(size == BUFFER_SIZE); ++allocations;
|
||||
if (malloc_fail) return NULL;
|
||||
++allocations_live; return malloc(size);
|
||||
}
|
||||
static void test_free(void *p) { if (p) { assert(allocations_live == 1); --allocations_live; } free(p); }
|
||||
void secure_wipe(void *p, size_t n) { memset(p, 0, n); }
|
||||
int64_t esp_timer_get_time(void) { return now_us; }
|
||||
int xSemaphoreTake(SemaphoreHandle_t m, unsigned ticks)
|
||||
{
|
||||
assert(m == &mutex_storage && !locked);
|
||||
if (mutex_busy) { assert(ticks == 0); return 0; }
|
||||
locked = true; return pdTRUE;
|
||||
}
|
||||
void xSemaphoreGive(SemaphoreHandle_t m) { assert(m == &mutex_storage && locked); locked = false; }
|
||||
|
||||
int xTaskCreate(void (*entry)(void *), const char *name, unsigned stack, void *arg,
|
||||
unsigned priority, TaskHandle_t *out)
|
||||
{
|
||||
assert(!strcmp(name, "fw_reboot") && stack == 2048 && !arg && priority == 5);
|
||||
assert(s_transitioning && identity_token && !commits && !task_entry);
|
||||
if (task_fail) return 0;
|
||||
task_entry = entry; *out = &task_storage; ++tasks_created; return pdPASS;
|
||||
}
|
||||
int xTaskNotify(TaskHandle_t t, uint32_t value, int action)
|
||||
{
|
||||
assert(t == &task_storage && task_entry && !notification && action == eSetValueWithOverwrite);
|
||||
assert(value == 1 || value == 2);
|
||||
if (value == 1) assert(commits == 1 && response_sends == 1 && !response_fail && !aborts);
|
||||
notification = value; return pdPASS;
|
||||
}
|
||||
int xTaskNotifyWait(uint32_t clear_in, uint32_t clear_out, uint32_t *value, uint32_t wait)
|
||||
{
|
||||
assert(!clear_in && clear_out == UINT32_MAX && wait == portMAX_DELAY && notification);
|
||||
*value = notification; return pdTRUE;
|
||||
}
|
||||
void vTaskDelay(unsigned ticks) { assert(ticks == 500 && notification == 1); }
|
||||
void vTaskDelete(TaskHandle_t task) { assert(!task); ++tasks_deleted; }
|
||||
void esp_restart(void) { assert(!locked); ++restarts; }
|
||||
static void finish_owner(void)
|
||||
{
|
||||
if (task_entry) {
|
||||
assert(notification); task_entry(NULL); task_entry = NULL;
|
||||
assert(tasks_deleted == tasks_created);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void set_header(const char *name, const char *value)
|
||||
{
|
||||
for (size_t i = 0; i < request.header_count; ++i) {
|
||||
if (!strcmp(name, request.headers[i].name)) { request.headers[i].value = value; return; }
|
||||
}
|
||||
assert(request.header_count < 12);
|
||||
request.headers[request.header_count++] = (test_header_t){name, value};
|
||||
}
|
||||
static void set_length(size_t length)
|
||||
{
|
||||
request.content_len = length;
|
||||
snprintf(raw_length, sizeof(raw_length), "%zu", length);
|
||||
set_header("Content-Length", raw_length);
|
||||
}
|
||||
bool web_httpd_headers_valid(httpd_req_t *r)
|
||||
{
|
||||
request_aux = (struct httpd_req_aux){.scratch = header_scratch};
|
||||
for (size_t i = 0; i < r->header_count; ++i) {
|
||||
if (!r->headers[i].value) continue;
|
||||
size_t used = request_aux.scratch_cur_size;
|
||||
int written = snprintf(header_scratch + used, sizeof(header_scratch) - used,
|
||||
"%s:%s", r->headers[i].name, r->headers[i].value);
|
||||
assert(written >= 0 && (size_t)written + 1 <= sizeof(header_scratch) - used);
|
||||
request_aux.scratch_cur_size += (size_t)written + 1;
|
||||
++request_aux.req_hdrs_count;
|
||||
}
|
||||
r->aux = &request_aux;
|
||||
return r->headers_valid && adapter_headers_valid(r);
|
||||
}
|
||||
bool web_httpd_unread_body(httpd_req_t *r) { return r->received < r->content_len; }
|
||||
void web_httpd_wipe_request(httpd_req_t *r, bool unread)
|
||||
{
|
||||
assert(unread == (r->received < r->content_len)); ++wipes;
|
||||
}
|
||||
static esp_err_t response_result(void) { return ++response_step == response_fail ? ESP_FAIL : ESP_OK; }
|
||||
esp_err_t httpd_resp_set_status(httpd_req_t *r, const char *s)
|
||||
{
|
||||
assert(r == &request && strlen(s) < sizeof(response_status)); strcpy(response_status, s);
|
||||
return response_result();
|
||||
}
|
||||
esp_err_t httpd_resp_set_type(httpd_req_t *r, const char *s)
|
||||
{
|
||||
assert(r == &request && !strcmp(s, "application/json; charset=utf-8")); return response_result();
|
||||
}
|
||||
esp_err_t httpd_resp_set_hdr(httpd_req_t *r, const char *name, const char *value)
|
||||
{
|
||||
assert(r == &request && name && value); return response_result();
|
||||
}
|
||||
esp_err_t httpd_resp_sendstr(httpd_req_t *r, const char *body)
|
||||
{
|
||||
assert(r == &request && strlen(body) < sizeof(response_body));
|
||||
++response_sends; strcpy(response_body, body); return response_result();
|
||||
}
|
||||
static void competing_lifecycle(void)
|
||||
{
|
||||
assert(s_transitioning && !locked);
|
||||
assert(web_firmware_update_reserve(request.handle) == ESP_ERR_INVALID_STATE);
|
||||
assert(web_server_reboot_current(s_generation) == ESP_ERR_INVALID_STATE);
|
||||
assert(!restarts);
|
||||
uint32_t token = 0;
|
||||
assert(web_security_reserve_identity(0, false, &token) == ESP_ERR_INVALID_STATE && !token);
|
||||
}
|
||||
int httpd_req_recv(httpd_req_t *r, char *out, size_t want)
|
||||
{
|
||||
assert(r == &request && want && want <= BUFFER_SIZE && s_transitioning && identity_token);
|
||||
assert(web_firmware_update_reserve_reboot() == ESP_ERR_INVALID_STATE);
|
||||
++receives; now_us += read_us;
|
||||
if (receive_race) competing_lifecycle();
|
||||
if (r->received >= stall_at) return HTTPD_SOCK_ERR_TIMEOUT;
|
||||
if (r->received >= cutoff) return 0;
|
||||
size_t count = want < max_chunk ? want : max_chunk;
|
||||
if (count > cutoff - r->received) count = cutoff - r->received;
|
||||
assert(r->received + count <= sizeof(image));
|
||||
memcpy(out, image + r->received, count); r->received += count; return (int)count;
|
||||
}
|
||||
esp_err_t web_session_store_lookup(const char *token, size_t length, const char *origin,
|
||||
size_t origin_length, web_session_view_t *view)
|
||||
{
|
||||
assert(length == 64 && token[0] == 'a' && origin_length == strlen(origin));
|
||||
assert(!strcmp(origin, "https://device"));
|
||||
if (lookup_unavailable) return ESP_FAIL;
|
||||
if (!lookup_current) return ESP_ERR_NOT_FOUND;
|
||||
view->id = 42; view->principal.role = role; view->principal.user_id = 9;
|
||||
memset(view->csrf, 'b', 64); view->csrf[64] = 0; return ESP_OK;
|
||||
}
|
||||
esp_err_t web_session_store_check_principal(web_session_id_t id, const user_principal_t *principal, bool *current)
|
||||
{
|
||||
assert(id == 42 && principal->role == USER_ROLE_ADMIN && principal->user_id == 9);
|
||||
assert(ends == 1 && metadata_reads == 1 && !commits);
|
||||
*current = final_current; return final_unavailable ? ESP_FAIL : ESP_OK;
|
||||
}
|
||||
esp_err_t web_security_reserve_identity(uint32_t expected, bool reset, uint32_t *token)
|
||||
{
|
||||
assert(!expected && !reset && s_transitioning); *token = 0;
|
||||
if (identity_busy || identity_token) return ESP_ERR_INVALID_STATE;
|
||||
*token = identity_token = 7; return ESP_OK;
|
||||
}
|
||||
void web_security_release_identity(uint32_t token)
|
||||
{
|
||||
assert(token && token == identity_token && s_transitioning);
|
||||
identity_token = 0; ++identity_releases;
|
||||
}
|
||||
const esp_partition_t *esp_ota_get_running_partition(void) { return missing_running ? NULL : &running; }
|
||||
const esp_partition_t *esp_ota_get_next_update_partition(const esp_partition_t *p)
|
||||
{
|
||||
assert(!p); return missing_target ? NULL : ⌖
|
||||
}
|
||||
esp_err_t esp_ota_begin(const esp_partition_t *p, size_t size, esp_ota_handle_t *handle)
|
||||
{
|
||||
assert(p == &target && p->address != running.address && size == request.content_len);
|
||||
assert(size <= target.size && request.received == PREFIX_SIZE && s_transitioning && identity_token);
|
||||
assert(tasks_created == 1 && !locked); ++begins;
|
||||
assert(!ota_live && *handle == 0);
|
||||
if (stage_fail == 1) return ESP_FAIL;
|
||||
*handle = 123; ota_live = true;
|
||||
/* IDF publishes the handle before erasing; this models erase failure. */
|
||||
return stage_fail == 6 ? ESP_FAIL : ESP_OK;
|
||||
}
|
||||
esp_err_t esp_ota_write(esp_ota_handle_t handle, const void *data, size_t size)
|
||||
{
|
||||
assert(handle == 123 && s_transitioning && !locked && size <= BUFFER_SIZE);
|
||||
assert(flash_size + size <= sizeof(flash)); ++writes;
|
||||
if (stage_fail == 2) return ESP_FAIL;
|
||||
memcpy(flash + flash_size, data, size); flash_size += size; return ESP_OK;
|
||||
}
|
||||
esp_err_t esp_ota_end(esp_ota_handle_t handle)
|
||||
{
|
||||
assert(handle == 123 && flash_size == request.content_len && !memcmp(flash, image, flash_size));
|
||||
assert(!aborts && !locked && s_transitioning && tasks_created == 1 && ota_live); ++ends;
|
||||
ota_live = false; /* End consumes on success AND validation failure. */
|
||||
return stage_fail == 3 ? ESP_FAIL : ESP_OK;
|
||||
}
|
||||
esp_err_t esp_ota_abort(esp_ota_handle_t handle)
|
||||
{
|
||||
assert(handle == 123 && begins == 1 && !ends && !commits && ota_live);
|
||||
ota_live = false; ++aborts; return ESP_OK;
|
||||
}
|
||||
esp_err_t esp_image_get_metadata(const esp_partition_pos_t *p, esp_image_metadata_t *metadata)
|
||||
{
|
||||
assert(p->offset == target.address && p->size == target.size && ends == 1);
|
||||
++metadata_reads; metadata->image_len = (uint32_t)parsed_size;
|
||||
if (revoke_at_metadata) final_current = false;
|
||||
return stage_fail == 4 ? ESP_FAIL : ESP_OK;
|
||||
}
|
||||
esp_err_t esp_ota_set_boot_partition(const esp_partition_t *p)
|
||||
{
|
||||
assert(p == &target && ends == 1 && metadata_reads == 1 && final_current && !final_unavailable);
|
||||
assert(tasks_created == 1 && !response_sends && s_transitioning && identity_token && !locked);
|
||||
assert(web_firmware_update_reserve_reboot() == ESP_ERR_INVALID_STATE);
|
||||
if (commit_race) competing_lifecycle();
|
||||
if (stage_fail == 5) return ESP_FAIL;
|
||||
++commits; return ESP_OK;
|
||||
}
|
||||
|
||||
static void reset(void)
|
||||
{
|
||||
assert(!allocations_live && !task_entry && !locked && !ota_live);
|
||||
atomic_store(&s_reboot_gate, false);
|
||||
s_firmware_selected = false;
|
||||
++cases;
|
||||
memset(&request, 0, sizeof(request));
|
||||
request.uri = WEB_FIRMWARE_UPDATE_URI; request.method = HTTP_POST;
|
||||
request.content_len = sizeof(image); request.handle = &server_storage; request.headers_valid = true;
|
||||
set_header("Host", "device"); set_header("Origin", "https://device");
|
||||
set_header("Content-Type", "application/octet-stream");
|
||||
set_length(sizeof(image));
|
||||
set_header("Cookie", SESSION_COOKIE "=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
set_header("X-CSRF-Token", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
|
||||
memset(image, 0x55, sizeof(image)); memset(flash, 0, sizeof(flash));
|
||||
esp_image_header_t header = {.magic = ESP_IMAGE_HEADER_MAGIC, .chip_id = ESP_CHIP_ID_ESP32S3,
|
||||
.segment_count = 3, .hash_appended = 1};
|
||||
esp_image_segment_header_t segment = {.data_len = sizeof(esp_app_desc_t)};
|
||||
uint32_t magic = ESP_APP_DESC_MAGIC_WORD;
|
||||
memcpy(image, &header, sizeof(header)); memcpy(image + sizeof(header), &segment, sizeof(segment));
|
||||
memcpy(image + sizeof(header) + sizeof(segment), &magic, sizeof(magic));
|
||||
running = (esp_partition_t){.type = 0, .subtype = 0x10, .address = 0x10000, .size = 0x400000};
|
||||
target = (esp_partition_t){.type = 0, .subtype = 0x11, .address = 0x410000, .size = 0x400000};
|
||||
missing_target = missing_running = false;
|
||||
s_ready = lookup_current = final_current = true;
|
||||
s_server_mutex = &mutex_storage; s_server = &server_storage;
|
||||
s_transitioning = false; s_last_error = ESP_OK; s_generation = 11;
|
||||
lookup_unavailable = final_unavailable = malloc_fail = task_fail = identity_busy = mutex_busy = false;
|
||||
receive_race = commit_race = revoke_at_metadata = false;
|
||||
role = USER_ROLE_ADMIN; stage_fail = response_fail = response_step = 0;
|
||||
allocations = allocations_live = tasks_created = tasks_deleted = 0;
|
||||
begins = writes = ends = aborts = metadata_reads = commits = receives = wipes = 0;
|
||||
restarts = response_sends = releases = identity_releases = 0;
|
||||
flash_size = 0; max_chunk = BUFFER_SIZE; cutoff = stall_at = SIZE_MAX; parsed_size = sizeof(image);
|
||||
now_us = 0; read_us = 1000; notification = identity_token = 0;
|
||||
response_status[0] = response_body[0] = 0;
|
||||
}
|
||||
static void rejected(const char *status, const char *code)
|
||||
{
|
||||
bool pre_reserved = s_transitioning;
|
||||
bool gate_reserved = atomic_load(&s_reboot_gate);
|
||||
esp_err_t result = web_firmware_update_handler(&request);
|
||||
assert(!strcmp(response_status, status) && strstr(response_body, code));
|
||||
assert(result == (request.received < request.content_len ? ESP_FAIL : ESP_OK));
|
||||
assert(!commits && !allocations_live && !restarts && wipes == 1 && !ota_live);
|
||||
assert(s_transitioning == pre_reserved && !identity_token);
|
||||
assert(atomic_load(&s_reboot_gate) == gate_reserved);
|
||||
if (task_entry) assert(notification == 2);
|
||||
finish_owner(); assert(!restarts);
|
||||
}
|
||||
static void success(void)
|
||||
{
|
||||
assert(web_firmware_update_handler(&request) == ESP_OK);
|
||||
assert(!strcmp(response_status, "200 OK") && !strcmp(response_body, "{\"ok\":true,\"rebooting\":true}"));
|
||||
assert(commits == 1 && ends == 1 && !aborts && s_transitioning && identity_token);
|
||||
assert(!allocations_live && notification == 1 && !restarts && wipes == 1);
|
||||
assert(s_firmware_selected && web_firmware_update_reserve_reboot() == ESP_ERR_INVALID_STATE);
|
||||
assert(request.received == request.content_len && !memcmp(image, flash, sizeof(image)));
|
||||
finish_owner(); assert(restarts == 1);
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
reset();
|
||||
assert(web_firmware_update_reserve_reboot() == ESP_OK);
|
||||
rejected("503 Service Unavailable", "busy");
|
||||
assert(!receives && !begins && !allocations);
|
||||
reset(); s_server_mutex = NULL; s_server = NULL;
|
||||
assert(web_firmware_update_reserve_reboot() == ESP_OK);
|
||||
assert(web_firmware_update_reserve_reboot() == ESP_ERR_INVALID_STATE);
|
||||
reset(); success();
|
||||
reset(); max_chunk = 1; receive_race = commit_race = true; success();
|
||||
assert(receives == sizeof(image));
|
||||
reset(); max_chunk = 17; success();
|
||||
puts("PASS bounded streaming, bytewise partial prefix, exact writes, delayed owner restart and reservation races");
|
||||
|
||||
reset(); lookup_current = false; rejected("401 Unauthorized", "authentication_required"); assert(!receives && !allocations);
|
||||
reset(); lookup_unavailable = true; rejected("503 Service Unavailable", "unavailable");
|
||||
reset(); role = USER_ROLE_USER; rejected("403 Forbidden", "admin_required"); assert(!receives && !allocations);
|
||||
reset(); set_header("X-CSRF-Token", NULL); rejected("403 Forbidden", "csrf");
|
||||
reset(); set_header("X-CSRF-Token", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); rejected("403 Forbidden", "csrf");
|
||||
reset(); set_header("Origin", NULL); rejected("403 Forbidden", "origin");
|
||||
reset(); set_header("Origin", "null"); rejected("403 Forbidden", "origin");
|
||||
reset(); set_header("Origin", "https://attacker"); rejected("403 Forbidden", "origin");
|
||||
reset(); set_header("Sec-Fetch-Site", "cross-site"); rejected("403 Forbidden", "origin");
|
||||
reset(); set_header("Cookie", NULL); rejected("401 Unauthorized", "authentication_required");
|
||||
reset(); request.headers_valid = false; rejected("400 Bad Request", "invalid_request");
|
||||
reset(); request.uri = "/api/firmware?x=1"; rejected("400 Bad Request", "invalid_request");
|
||||
reset(); request.method = HTTP_GET; rejected("400 Bad Request", "invalid_request");
|
||||
reset(); s_ready = false; rejected("503 Service Unavailable", "unavailable");
|
||||
puts("PASS production cookie/Origin/CSRF/admin admission before receive or flash (SDK getters/adapter real, database doubled)");
|
||||
|
||||
const char *invalid_lengths[] = {NULL, "", " ", "+12000", "-12000", "12000x", "1.2e4",
|
||||
"12 000", "12000,12000", "12000 ", "12000\t", "\t12000", "11999", "12001",
|
||||
"000000000000000012000"};
|
||||
for (size_t i = 0; i < sizeof(invalid_lengths) / sizeof(invalid_lengths[0]); ++i) {
|
||||
reset(); set_header("Content-Length", invalid_lengths[i]);
|
||||
rejected("400 Bad Request", "invalid_request");
|
||||
assert(!receives && !begins && !allocations && !notification);
|
||||
}
|
||||
const char *oversized_lengths[] = {"4194305", "4296815136", "4294967296",
|
||||
"18446744073709551615", "18446744073709551616", "99999999999999999999"};
|
||||
for (size_t i = 0; i < sizeof(oversized_lengths) / sizeof(oversized_lengths[0]); ++i) {
|
||||
reset(); set_header("Content-Length", oversized_lengths[i]);
|
||||
/* Explicitly model HTTPD's 64-bit -> ESP32 size_t narrowing. */
|
||||
request.content_len = i == 1 ? 1847840 : i == 2 ? 0 : sizeof(image);
|
||||
rejected("413 Payload Too Large", "firmware_too_large");
|
||||
assert(!receives && !begins && !allocations && !notification);
|
||||
}
|
||||
reset();
|
||||
request.headers[request.header_count++] = (test_header_t){"content-length", "12000"};
|
||||
rejected("400 Bad Request", "invalid_request"); assert(!receives && !begins && !allocations);
|
||||
reset(); set_header("Content-Length", " 12000"); success();
|
||||
reset(); set_header("Content-Length", "00000000000000012000"); success();
|
||||
puts("PASS raw Content-Length missing/duplicate/malformed/oversized/64-bit wrap and overflow reject before receive/erase; IDF leading-space semantics");
|
||||
|
||||
reset(); set_header("Content-Type", "multipart/form-data"); rejected("415 Unsupported Media Type", "firmware_content_type");
|
||||
reset(); set_header("Content-Type", NULL); rejected("415 Unsupported Media Type", "firmware_content_type");
|
||||
reset(); set_length(0); rejected("400 Bad Request", "invalid_firmware");
|
||||
reset(); set_length(PREFIX_SIZE - 1); rejected("400 Bad Request", "invalid_firmware");
|
||||
reset(); set_length(target.size + 1); rejected("413 Payload Too Large", "firmware_too_large"); assert(!allocations);
|
||||
reset(); target.size = sizeof(image); success();
|
||||
reset(); missing_target = true; rejected("503 Service Unavailable", "firmware_unavailable");
|
||||
reset(); missing_running = true; rejected("503 Service Unavailable", "firmware_unavailable");
|
||||
reset(); target.address = running.address; rejected("503 Service Unavailable", "firmware_unavailable");
|
||||
reset(); target.type = 1; rejected("503 Service Unavailable", "firmware_unavailable");
|
||||
reset(); target.subtype = 0; rejected("503 Service Unavailable", "firmware_unavailable");
|
||||
for (unsigned field = 0; field < 6; ++field) {
|
||||
reset(); esp_image_header_t header; memcpy(&header, image, sizeof(header));
|
||||
if (field == 0) header.magic = 0;
|
||||
if (field == 1) header.chip_id = ESP_CHIP_ID_ESP32;
|
||||
if (field == 2) header.segment_count = 0;
|
||||
if (field == 3) header.hash_appended = 0;
|
||||
memcpy(image, &header, sizeof(header));
|
||||
if (field == 4) memset(image + sizeof(header) + sizeof(esp_image_segment_header_t), 0, 4);
|
||||
if (field == 5) memset(image + sizeof(header), 0, sizeof(esp_image_segment_header_t));
|
||||
rejected("400 Bad Request", "invalid_firmware"); assert(!begins && !aborts);
|
||||
}
|
||||
puts("PASS content type, length/capacity, inactive app selection, format/chip/descriptor/hash header rejection before erase");
|
||||
|
||||
reset(); s_transitioning = true; rejected("503 Service Unavailable", "busy");
|
||||
reset(); identity_busy = true; rejected("503 Service Unavailable", "busy");
|
||||
reset(); mutex_busy = true; rejected("503 Service Unavailable", "busy");
|
||||
reset(); s_server = NULL; rejected("503 Service Unavailable", "busy");
|
||||
reset(); malloc_fail = true; rejected("503 Service Unavailable", "firmware_resources"); assert(!tasks_created && !begins);
|
||||
reset(); task_fail = true; rejected("503 Service Unavailable", "firmware_resources"); assert(!tasks_created && !begins);
|
||||
for (int stage = 1; stage <= 5; ++stage) {
|
||||
reset(); stage_fail = stage;
|
||||
rejected(stage == 3 || stage == 4 ? "400 Bad Request" : "500 Internal Server Error",
|
||||
stage == 3 || stage == 4 ? "invalid_firmware" : stage == 5 ? "firmware_commit_failed" : "firmware_write_failed");
|
||||
assert(aborts == (unsigned)(stage == 2));
|
||||
}
|
||||
reset(); stage_fail = 6;
|
||||
rejected("500 Internal Server Error", "firmware_write_failed");
|
||||
assert(begins == 1 && aborts == 1 && !writes && !ends && !ota_live);
|
||||
puts("PASS begin failure after handle publication aborts once; unpublished failure and consumed end never double-abort");
|
||||
reset(); parsed_size--; rejected("400 Bad Request", "invalid_firmware");
|
||||
reset(); parsed_size++; rejected("400 Bad Request", "invalid_firmware");
|
||||
reset(); revoke_at_metadata = true; rejected("401 Unauthorized", "authentication_required");
|
||||
reset(); final_unavailable = true; rejected("401 Unauthorized", "authentication_required");
|
||||
puts("PASS busy/resources/OTA begin-write-end-metadata-commit failures, final auth revocation and exact SDK image length");
|
||||
|
||||
reset(); cutoff = 10; rejected("400 Bad Request", "firmware_incomplete"); assert(!begins);
|
||||
reset(); cutoff = 300; rejected("400 Bad Request", "firmware_incomplete"); assert(aborts == 1);
|
||||
reset(); stall_at = 0; read_us = 1000000; rejected("408 Request Timeout", "firmware_timeout"); assert(!begins && receives == 10);
|
||||
reset(); stall_at = PREFIX_SIZE; read_us = 1000000; rejected("408 Request Timeout", "firmware_timeout"); assert(aborts == 1);
|
||||
reset(); read_us = STALL_US; rejected("408 Request Timeout", "firmware_timeout"); assert(!begins);
|
||||
reset(); max_chunk = 1; read_us = 40000; rejected("408 Request Timeout", "firmware_timeout"); assert(aborts == 1 && now_us == TOTAL_US);
|
||||
puts("PASS incomplete body, stalled receive and total slow-drip deadline; no selection/reboot, handle abort when live");
|
||||
|
||||
for (int step = 1; step <= 6; ++step) {
|
||||
reset(); response_fail = step;
|
||||
assert(web_firmware_update_handler(&request) == ESP_FAIL);
|
||||
assert(commits == 1 && !s_transitioning && !identity_token && !allocations_live);
|
||||
assert(notification == 2 && !restarts && !aborts); finish_owner(); assert(!restarts);
|
||||
assert(s_firmware_selected && !atomic_load(&s_reboot_gate));
|
||||
request.received = 0; response_fail = 0;
|
||||
assert(web_firmware_update_handler(&request) == ESP_FAIL);
|
||||
assert(!strcmp(response_status, "409 Conflict") && strstr(response_body, "firmware_selected_reboot_required"));
|
||||
assert(commits == 1 && begins == 1 && !atomic_load(&s_reboot_gate));
|
||||
assert(web_firmware_update_reserve_reboot() == ESP_OK);
|
||||
assert(web_firmware_update_reserve_reboot() == ESP_ERR_INVALID_STATE);
|
||||
}
|
||||
puts("PASS postcommit response failure: selected image retained, no automatic reboot, resources/reservations released");
|
||||
printf("PASS %u firmware backend cases; SDK validation, flash and scheduling are mocked, not device evidence\n", cases);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user