fix: clean up terminated HTTP requests and bound autoload lists

This commit is contained in:
najdek committed 2026-10-12 17:54:16 +02:00
1 parent 9b863e8020
commit 608d7fa5f2
8 files changed
+797 -224

No files matched your search

+16 -4
View File
@@ -41,8 +41,10 @@ For a quick build & deploy cycle, use the `deploy.sh` script. It will automatica
The HTTP response suite compiles the actual `src/http_server.c` with stubbed
PS5 services and real libmicrohttpd response objects. It injects response-buffer,
response-creation, CORS-header, and queue failures, then sends 10,000 requests
through a real loopback HTTP server. ASan (including leak detection) and UBSan
response-creation, CORS-header, queue, reallocation, and file I/O failures, then
sends 10,000 requests through a real loopback HTTP server. It also checks
disconnected/chunked requests, concurrent uploads, and server-stop cleanup in
thread-per-connection mode. ASan (including leak detection) and UBSan
are enabled by default. Test files, storage paths, and binaries are temporary;
the suite does not connect to a console or launch payloads.
@@ -68,8 +70,18 @@ the default sanitizer run for verification.
`.github/workflows/verify.yml` runs the HTTP suite, existing frontend tests,
and frontend build without publishing or deploying. Frontend lint still reports existing errors. The host suite tests response
ownership/status handling; it does not yet cover aborted request cleanup,
storage transactions, autoload concurrency, or PS5 firmware behavior.
ownership/status handling, request cleanup, and autoload-list bounds. Storage
transactions, autoload worker concurrency, and PS5 firmware behavior remain
separate validation scopes. Backend storage/service calls in the HTTP tests are
stubbed; successful mock imports move actual files inside temporary fixtures.
The HTTP server accepts config/source bodies up to **1 MiB** and streamed uploads
up to **512 MiB**. Larger declared or accumulated bodies return HTTP 413; empty
request bodies return HTTP 400. Upload staging files use private, unique
`.upload-*` names and are removed by request teardown if uncommitted. This does
not purge older orphan files or guarantee cleanup after power loss/SIGKILL.
Autoload CSV responses currently allow 4,095 bytes; larger/unreadable lists return
HTTP 500 with JSON rather than a truncated list.
Native compilation remains a separate gate. To preserve the previous ELF,
build a candidate under `scratch/` rather than run `make clean`:
+8
View File
@@ -3,10 +3,18 @@
#include <microhttpd.h>
/* Bound accumulated JSON and streamed payloads independently. */
#define HTTP_POST_BODY_LIMIT ((size_t)1024 * 1024)
#define HTTP_UPLOAD_BODY_LIMIT ((size_t)512 * 1024 * 1024)
/* MHD request handler callback — dispatches all routes. */
enum MHD_Result http_on_request(void *cls, struct MHD_Connection *conn,
const char *url, const char *method,
const char *version, const char *upload_data,
size_t *upload_data_size, void **con_cls);
/* Registered with MHD_OPTION_NOTIFY_COMPLETED for every daemon instance. */
void http_request_completed(void *cls, struct MHD_Connection *conn,
void **con_cls, enum MHD_RequestTerminationCode reason);
#endif
+293 -205
View File
@@ -10,7 +10,9 @@
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>
#include <microhttpd.h>
#include "http_server.h"
@@ -75,6 +77,17 @@ static enum MHD_Result send_out_of_memory(struct MHD_Connection *conn) {
return send_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, resp);
}
/* Messages passed here are fixed application strings, not user input. */
static enum MHD_Result send_request_error(struct MHD_Connection *conn,
unsigned int status, const char *message) {
char json[256];
snprintf(json, sizeof(json), "{\"error\":\"%s\"}", message);
struct MHD_Response *resp = create_buffer_response(
strlen(json), json, MHD_RESPMEM_MUST_COPY);
add_response_header(resp, "Content-Type", "application/json");
return send_response(conn, status, resp);
}
/* Success is separate from response creation: a failed OOM response must not
* be mistaken for a successful buffer allocation. */
static int alloc_response_buffer_sized(char **out, size_t size) {
@@ -152,19 +165,202 @@ static void url_decode_in_place(char *str) {
struct PostStatus {
char *data;
size_t size;
int error;
size_t capacity;
unsigned int error;
};
struct UploadStatus {
FILE *fp;
size_t total_size;
int error;
unsigned int error;
char filename[256];
char temp_path[512];
char mode[32];
char repo_url[512];
};
enum RequestKind { REQUEST_CONFIG, REQUEST_SOURCES, REQUEST_UPLOAD };
struct RequestContext {
enum RequestKind kind;
int finished;
union {
struct PostStatus post;
struct UploadStatus upload;
} state;
};
/* Read-only requests need no allocation. This marker is never dereferenced. */
#define REQUEST_MARKER ((void *)1)
void http_request_completed(void *cls, struct MHD_Connection *conn,
void **con_cls, enum MHD_RequestTerminationCode reason) {
(void)cls;
(void)conn;
(void)reason;
if (!con_cls || !*con_cls)
return;
void *context = *con_cls;
*con_cls = NULL;
if (context == REQUEST_MARKER)
return;
struct RequestContext *request = context;
if (request->kind == REQUEST_UPLOAD) {
struct UploadStatus *upload = &request->state.upload;
if (upload->fp && fclose(upload->fp) != 0)
pldmgr_log("[PLDMGR] Warning: Failed to close upload during cleanup\n");
/* Each request owns a unique path. A committed file was renamed out
* of this path; remove any uncommitted remainder on every exit. */
if (upload->temp_path[0] && remove(upload->temp_path) != 0 && errno != ENOENT)
pldmgr_log("[PLDMGR] Warning: Failed to remove upload temp file: %s\n",
upload->temp_path);
} else {
free(request->state.post.data);
}
free(request);
}
static unsigned int check_body_length(struct MHD_Connection *conn, size_t limit) {
const char *value = MHD_lookup_connection_value(conn, MHD_HEADER_KIND, "Content-Length");
if (!value)
return 0; /* Chunked/unknown length is checked while receiving data. */
if (!isdigit((unsigned char)*value))
return MHD_HTTP_BAD_REQUEST;
errno = 0;
char *end;
unsigned long long length = strtoull(value, &end, 10);
if (errno == ERANGE || *end)
return MHD_HTTP_BAD_REQUEST;
return length > limit ? MHD_HTTP_CONTENT_TOO_LARGE : 0;
}
static void append_post_data(struct PostStatus *post, const char *data, size_t size) {
if (post->error)
return;
if (size > HTTP_POST_BODY_LIMIT - post->size) {
post->error = MHD_HTTP_CONTENT_TOO_LARGE;
return;
}
size_t needed = post->size + size + 1;
if (needed > post->capacity) {
size_t capacity = post->capacity ? post->capacity : 4096;
while (capacity < needed) {
capacity = capacity > (HTTP_POST_BODY_LIMIT + 1) / 2
? HTTP_POST_BODY_LIMIT + 1 : capacity * 2;
}
char *new_data = realloc(post->data, capacity);
if (!new_data) {
post->error = MHD_HTTP_INTERNAL_SERVER_ERROR;
return;
}
post->data = new_data;
post->capacity = capacity;
}
memcpy(post->data + post->size, data, size);
post->size += size;
post->data[post->size] = '\0';
}
static void prepare_upload(struct MHD_Connection *conn, struct UploadStatus *upload) {
const char *filename = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "filename");
const char *mode = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "mode");
if (!mode)
mode = "new";
if (!is_safe_filename(filename) || strlen(filename) >= sizeof(upload->filename) ||
strlen(mode) >= sizeof(upload->mode)) {
upload->error = MHD_HTTP_BAD_REQUEST;
return;
}
strcpy(upload->filename, filename);
strcpy(upload->mode, mode);
upload->error = check_body_length(conn, HTTP_UPLOAD_BODY_LIMIT);
if (upload->error)
return;
if ((mkdir(BASE_DATA_DIR, 0777) != 0 && errno != EEXIST) ||
(mkdir(PAYLOADS_STORAGE_DIR, 0777) != 0 && errno != EEXIST)) {
upload->error = MHD_HTTP_INTERNAL_SERVER_ERROR;
return;
}
int n = snprintf(upload->temp_path, sizeof(upload->temp_path),
"%s/.upload-XXXXXX", PAYLOADS_STORAGE_DIR);
if (n < 0 || (size_t)n >= sizeof(upload->temp_path)) {
upload->temp_path[0] = '\0';
upload->error = MHD_HTTP_INTERNAL_SERVER_ERROR;
return;
}
int fd = mkstemp(upload->temp_path);
if (fd < 0) {
upload->temp_path[0] = '\0'; /* No file was acquired to clean up. */
upload->error = MHD_HTTP_INTERNAL_SERVER_ERROR;
return;
}
upload->fp = fdopen(fd, "wb");
if (!upload->fp) {
close(fd);
upload->error = MHD_HTTP_INTERNAL_SERVER_ERROR;
return;
}
pldmgr_log("[PLDMGR] Starting upload: %s (mode: %s)\n", filename, mode);
}
static enum MHD_Result finish_request_error(struct MHD_Connection *conn,
struct RequestContext *request,
unsigned int status) {
request->finished = 1;
if (status == MHD_HTTP_CONTENT_TOO_LARGE)
return send_request_error(conn, status, "Request body exceeds the size limit");
if (status == MHD_HTTP_BAD_REQUEST)
return send_request_error(conn, status, "Invalid or empty request");
return send_request_error(conn, status, "Failed to receive or save request");
}
/* Build complete CSV entries within the output limit, including lines that
* span read chunks. Excess input is reported rather than silently truncated. */
static int read_autoload_list(char *out, size_t size, int include_delays) {
if (!size)
return -1;
out[0] = '\0';
FILE *file = fopen(AUTOLOAD_CONFIG_PATH, "r");
if (!file)
return errno == ENOENT ? 0 : -1;
size_t pos = 0;
int line_start = 1, has_data = 0, skip = 0, result = 0;
int c;
while ((c = fgetc(file)) != EOF) {
if (c == '\0') {
result = -1;
break;
}
if (c == '\n') {
line_start = 1; has_data = 0; skip = 0;
continue;
}
if (line_start) {
skip = !include_delays && c == '!';
line_start = 0;
}
if (c == '\r')
skip = 1;
if (skip)
continue;
size_t needed = (!has_data && pos ? 1 : 0) + 1;
if (needed >= size - pos) {
result = -1;
break;
}
if (!has_data && pos)
out[pos++] = ',';
out[pos++] = (char)c;
has_data = 1;
}
out[pos] = '\0';
if (ferror(file))
result = -1;
fclose(file);
return result;
}
/* ── Helper: Noisy routes ──────────────────────────────────── */
/* Filter out endpoints that the frontend polls automatically to reduce log spam */
@@ -212,188 +408,112 @@ enum MHD_Result http_on_request(void *cls, struct MHD_Connection *conn,
/* Set flag that we received a request (meaning browser is open) */
log_server_set_active();
/* ── Initial call for a new request ────────────────────── */
/* The completion callback owns request state on success, error, disconnect,
* and daemon stop. Keep it alive until MHD finishes the request. */
if (*con_cls == NULL) {
if (strncmp(url, ROUTE_UPLOAD, strlen(ROUTE_UPLOAD)) == 0) {
struct UploadStatus *status = malloc(sizeof(struct UploadStatus));
status->fp = NULL;
status->total_size = 0;
status->error = 0;
status->mode[0] = '\0';
const char *mode =
MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "mode");
strncpy(status->mode, mode ? mode : "new", sizeof(status->mode) - 1);
status->mode[sizeof(status->mode) - 1] = '\0';
const char *filename =
MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "filename");
if (filename && !strstr(filename, "/") && !strstr(filename, "..")) {
char path[512];
mkdir(BASE_DATA_DIR, 0777);
mkdir(PAYLOADS_STORAGE_DIR, 0777);
snprintf(path, sizeof(path), "%s/%s.tmp", PAYLOADS_STORAGE_DIR, filename);
strncpy(status->filename, filename, sizeof(status->filename) - 1);
status->filename[sizeof(status->filename) - 1] = '\0';
strncpy(status->temp_path, path, sizeof(status->temp_path) - 1);
status->temp_path[sizeof(status->temp_path) - 1] = '\0';
status->fp = fopen(path, "wb");
if (!status->fp) {
pldmgr_log("[PLDMGR] !!! FAILED to open temp file: %s\n", path);
status->error = 1;
} else {
pldmgr_log("[PLDMGR] Starting upload to temp: %s (mode: %s)\n", path, status->mode);
}
} else {
pldmgr_log("[PLDMGR] !!! Upload failed: Missing filename parameter\n");
status->error = 1;
int upload = strncmp(url, ROUTE_UPLOAD, strlen(ROUTE_UPLOAD)) == 0;
int config = strcmp(url, ROUTE_SET_CONFIG) == 0 && strcmp(method, "POST") == 0;
int sources = strcmp(url, ROUTE_SOURCES_SET) == 0 && strcmp(method, "POST") == 0;
if (upload || config || sources) {
struct RequestContext *request = malloc(sizeof(*request));
if (!request) {
*con_cls = REQUEST_MARKER;
return send_out_of_memory(conn);
}
*con_cls = status;
return MHD_YES;
memset(request, 0, sizeof(*request));
request->kind = upload ? REQUEST_UPLOAD : (config ? REQUEST_CONFIG : REQUEST_SOURCES);
*con_cls = request;
unsigned int error;
if (upload) {
prepare_upload(conn, &request->state.upload);
error = request->state.upload.error;
} else {
error = check_body_length(conn, HTTP_POST_BODY_LIMIT);
request->state.post.error = error;
}
if (error)
return finish_request_error(conn, request, error);
} else {
*con_cls = REQUEST_MARKER;
}
if (strcmp(url, ROUTE_SET_CONFIG) == 0 && strcmp(method, "POST") == 0) {
struct PostStatus *status = malloc(sizeof(struct PostStatus));
status->data = NULL;
status->size = 0;
status->error = 0;
*con_cls = status;
return MHD_YES;
}
if (strcmp(url, ROUTE_SOURCES_SET) == 0 && strcmp(method, "POST") == 0) {
struct PostStatus *status = malloc(sizeof(struct PostStatus));
status->data = NULL;
status->size = 0;
status->error = 0;
*con_cls = status;
return MHD_YES;
}
*con_cls = (void *)1;
return MHD_YES;
}
/* ── Handle POST data for /set_config ──────────────────── */
if (strcmp(url, ROUTE_SET_CONFIG) == 0 && strcmp(method, "POST") == 0) {
struct PostStatus *status = (struct PostStatus *)*con_cls;
if (*upload_data_size != 0) {
char *new_data = realloc(status->data, status->size + *upload_data_size + 1);
if (!new_data) {
status->error = 1;
} else {
status->data = new_data;
memcpy(status->data + status->size, upload_data, *upload_data_size);
status->size += *upload_data_size;
status->data[status->size] = '\0';
}
if (*con_cls != REQUEST_MARKER) {
struct RequestContext *request = *con_cls;
if (request->finished) {
*upload_data_size = 0;
return MHD_YES;
} else {
/* Finished receiving JSON */
pldmgr_log("[PLDMGR] Received config update: %s\n",
status->data ? status->data : "(null)");
if (status->data && !status->error) {
config_handle_set_json(status->data);
}
if (request->kind != REQUEST_UPLOAD) {
struct PostStatus *post = &request->state.post;
if (*upload_data_size) {
append_post_data(post, upload_data, *upload_data_size);
*upload_data_size = 0;
if (post->error)
return finish_request_error(conn, request, post->error);
return MHD_YES;
}
if (status->data)
free(status->data);
free(status);
*con_cls = NULL;
if (!post->size)
return finish_request_error(conn, request, MHD_HTTP_BAD_REQUEST);
int rc = request->kind == REQUEST_CONFIG
? config_handle_set_json(post->data) : sources_save(post->data, post->size);
if (rc != 0)
return finish_request_error(conn, request, MHD_HTTP_INTERNAL_SERVER_ERROR);
request->finished = 1;
struct MHD_Response *resp = create_buffer_response(
strlen(MSG_OK), (void *)MSG_OK, MHD_RESPMEM_MUST_COPY);
strlen(MSG_OK), (void *)MSG_OK, MHD_RESPMEM_PERSISTENT);
add_response_header(resp, "Content-Type", "text/plain");
return send_response(conn, MHD_HTTP_OK, resp);
}
}
/* ── Handle POST data for /sources_set ─────────────────── */
if (strcmp(url, ROUTE_SOURCES_SET) == 0 && strcmp(method, "POST") == 0) {
struct PostStatus *status = (struct PostStatus *)*con_cls;
if (*upload_data_size != 0) {
char *nd = realloc(status->data, status->size + *upload_data_size + 1);
if (!nd) {
status->error = 1;
struct UploadStatus *status = &request->state.upload;
if (*upload_data_size) {
if (*upload_data_size > HTTP_UPLOAD_BODY_LIMIT - status->total_size) {
status->error = MHD_HTTP_CONTENT_TOO_LARGE;
} else {
status->data = nd;
memcpy(status->data + status->size, upload_data, *upload_data_size);
status->size += *upload_data_size;
status->data[status->size] = '\0';
}
*upload_data_size = 0;
return MHD_YES;
} else {
int ok = -1;
if (status->data && !status->error)
ok = sources_save(status->data, status->size);
if (status->data)
free(status->data);
free(status);
*con_cls = NULL;
const char *msg = ok == 0 ? MSG_OK : "Failed to save sources";
struct MHD_Response *resp_ss = create_buffer_response(
strlen(msg), (void *)msg, MHD_RESPMEM_MUST_COPY);
return send_response(
conn, ok == 0 ? MHD_HTTP_OK : MHD_HTTP_INTERNAL_SERVER_ERROR, resp_ss);
}
}
/* ── Chunked upload for /manage:upload ─────────────────── */
if (strncmp(url, ROUTE_UPLOAD, strlen(ROUTE_UPLOAD)) == 0) {
struct UploadStatus *status = (struct UploadStatus *)*con_cls;
if (*upload_data_size != 0) {
if (status->fp && !status->error) {
size_t written = fwrite(upload_data, 1, *upload_data_size, status->fp);
if (written != *upload_data_size) {
pldmgr_log("[PLDMGR] !!! Write error: expected %zu, got %zu\n",
*upload_data_size, written);
status->error = 1;
}
status->total_size += written;
if (written != *upload_data_size)
status->error = MHD_HTTP_INTERNAL_SERVER_ERROR;
}
*upload_data_size = 0;
if (status->error)
return finish_request_error(conn, request, status->error);
return MHD_YES;
} else {
/* Upload finished */
if (status->fp) {
fflush(status->fp);
fclose(status->fp);
status->fp = NULL;
if (!status->error) {
char msg[256];
char out_slot[128];
if (payload_mgr_import_to_storage_mode(status->filename, status->temp_path,
status->mode, "web_upload", "",
msg, sizeof(msg),
out_slot, sizeof(out_slot)) != 0) {
pldmgr_log("[PLDMGR] !!! Failed to commit upload: %s\n", msg);
status->error = 1;
}
}
}
pldmgr_log("[PLDMGR] Upload finished. Total bytes: %zu, Error: %d\n",
status->total_size, status->error);
int err = status->error;
free(status);
*con_cls = NULL;
const char *msg = err ? "Error during upload\n" : MSG_OK;
struct MHD_Response *resp = create_buffer_response(
strlen(msg), (void *)msg, MHD_RESPMEM_MUST_COPY);
return send_response(
conn, err ? MHD_HTTP_INTERNAL_SERVER_ERROR : MHD_HTTP_OK, resp);
}
/* Both operations must run: a failed flush must not skip fclose. */
if (fflush(status->fp) != 0)
status->error = MHD_HTTP_INTERNAL_SERVER_ERROR;
if (fclose(status->fp) != 0)
status->error = MHD_HTTP_INTERNAL_SERVER_ERROR;
status->fp = NULL;
if (!status->total_size && !status->error)
status->error = MHD_HTTP_BAD_REQUEST;
if (!status->error) {
char msg[256] = "";
char out_slot[128];
if (payload_mgr_import_to_storage_mode(status->filename, status->temp_path,
status->mode, "web_upload", "",
msg, sizeof(msg), out_slot, sizeof(out_slot)) != 0) {
pldmgr_log("[PLDMGR] !!! Failed to commit upload: %s\n", msg);
status->error = MHD_HTTP_INTERNAL_SERVER_ERROR;
} else {
/* Import consumed the temp file. Relinquish its pathname so
* completion cannot unlink a later owner of the same name. */
status->temp_path[0] = '\0';
}
}
pldmgr_log("[PLDMGR] Upload finished. Total bytes: %zu, Error: %u\n",
status->total_size, status->error);
if (status->error)
return finish_request_error(conn, request, status->error);
request->finished = 1;
struct MHD_Response *resp = create_buffer_response(
strlen(MSG_OK), (void *)MSG_OK, MHD_RESPMEM_PERSISTENT);
add_response_header(resp, "Content-Type", "text/plain");
return send_response(conn, MHD_HTTP_OK, resp);
}
/* ── Log significant requests ──────────────────────────── */
@@ -773,21 +893,10 @@ enum MHD_Result http_on_request(void *cls, struct MHD_Connection *conn,
int remaining = pldmgr_autoload_get_remaining_seconds();
long long remaining_ms = pldmgr_autoload_get_remaining_ms();
char list_buf[4096] = "";
FILE *f = fopen(AUTOLOAD_CONFIG_PATH, "r");
if (f) {
char line[256];
int first = 1;
while (fgets(line, sizeof(line), f)) {
line[strcspn(line, "\r\n")] = 0;
if (strlen(line) == 0 || line[0] == '!')
continue;
if (!first) strcat(list_buf, ",");
strcat(list_buf, line);
first = 0;
}
fclose(f);
}
char list_buf[4096];
if (read_autoload_list(list_buf, sizeof(list_buf), 0) != 0)
return send_request_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR,
"Autoload list is unreadable or exceeds the size limit");
PldmgrConfig cfg;
config_read(&cfg);
@@ -821,35 +930,14 @@ enum MHD_Result http_on_request(void *cls, struct MHD_Connection *conn,
resp = create_buffer_response(strlen(msg), (void *)msg,
MHD_RESPMEM_PERSISTENT);
add_response_header(resp, "Content-Type", "text/plain");
} else if (strcmp(url, "/autoload_status") == 0) {
char resp_buf[64];
snprintf(resp_buf, sizeof(resp_buf), "{\"remaining\":%d}",
pldmgr_autoload_get_remaining_seconds());
resp = create_buffer_response(strlen(resp_buf),
(void *)resp_buf,
MHD_RESPMEM_MUST_COPY);
add_response_header(resp, "Content-Type", "application/json");
} else if (strcmp(url, ROUTE_GET_CONFIG) == 0) {
PldmgrConfig cfg;
config_read(&cfg);
/* Get list */
char list_buf[4096] = {0};
FILE *f = fopen(AUTOLOAD_CONFIG_PATH, "r");
if (f) {
char line[256];
int first = 1;
while (fgets(line, sizeof(line), f)) {
line[strcspn(line, "\r\n")] = 0;
if (strlen(line) == 0)
continue;
if (!first)
strncat(list_buf, ",", sizeof(list_buf) - strlen(list_buf) - 1);
strncat(list_buf, line, sizeof(list_buf) - strlen(list_buf) - 1);
first = 0;
}
fclose(f);
}
char list_buf[4096];
if (read_autoload_list(list_buf, sizeof(list_buf), 1) != 0)
return send_request_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR,
"Autoload list is unreadable or exceeds the size limit");
char list_escaped[8192];
pldmgr_json_escape(list_buf, list_escaped, sizeof(list_escaped));
+1
View File
@@ -83,6 +83,7 @@ static struct MHD_Daemon *start_http_daemon(unsigned int port) {
return MHD_start_daemon(
MHD_USE_THREAD_PER_CONNECTION | MHD_USE_DEBUG, port,
NULL, NULL, &http_on_request, NULL,
MHD_OPTION_NOTIFY_COMPLETED, &http_request_completed, NULL,
MHD_OPTION_THREAD_STACK_SIZE, (size_t)(1024 * 1024),
MHD_OPTION_END);
}
+19 -3
View File
@@ -18,11 +18,18 @@ static size_t json(char *out, size_t size, const char *value) {
return n < 0 ? 0 : (size_t)n;
}
unsigned int test_config_saves, test_source_saves, test_imports;
size_t test_post_size;
void pldmgr_log(const char *fmt, ...) { (void)fmt; }
void log_server_set_active(void) {}
void config_read(PldmgrConfig *cfg) { memset(cfg, 0, sizeof(*cfg)); cfg->autoload_delay = 5; }
int config_read_bool(const char *key, int fallback) { (void)key; return fallback; }
int config_handle_set_json(const char *data) { (void)data; return 0; }
int config_handle_set_json(const char *data) {
test_config_saves++;
test_post_size = strlen(data);
return strcmp(data, "fail_test") == 0 ? -1 : 0;
}
int config_get_favorites_csv(char *out, size_t size, int *exists) {
if (size) out[0] = '\0';
*exists = 0;
@@ -47,7 +54,12 @@ int payload_mgr_import_to_storage_mode(const char *filename, const char *path,
const char *mode, const char *origin, const char *detail,
char *msg, size_t msg_size, char *slot, size_t slot_size) {
(void)filename; (void)path; (void)mode; (void)origin; (void)detail;
snprintf(msg, msg_size, "Test import"); snprintf(slot, slot_size, "local/test"); return 0;
snprintf(msg, msg_size, "Test import"); snprintf(slot, slot_size, "local/test");
test_imports++;
if (mode && strcmp(mode, "fail_test") == 0) return -1;
char destination[1024];
snprintf(destination, sizeof(destination), "%s/%s", PAYLOADS_STORAGE_DIR, filename);
return rename(path, destination);
}
int payload_mgr_check_existing(const char *filename, char *out, size_t size) {
(void)filename; json(out, size, "{\"status\":\"ready\"}"); return 0;
@@ -73,7 +85,11 @@ int sources_repo_install(const char *url, const char *id, const char *mode,
(void)url; (void)id; (void)mode; (void)replace; json(out, size, "{\"ok\":true}"); return 0;
}
int sources_list_json(char *out, size_t size) { json(out, size, "{\"sources\":[]}"); return 0; }
int sources_save(const char *data, size_t size) { (void)data; (void)size; return 0; }
int sources_save(const char *data, size_t size) {
test_source_saves++;
test_post_size = size;
return strcmp(data, "fail_test") == 0 ? -1 : 0;
}
int sources_add(const char *url, char *msg, size_t size) {
(void)url; snprintf(msg, size, "Test source"); return 0;
}
+298 -7
View File
@@ -4,12 +4,30 @@
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <microhttpd.h>
#include "http_server.h"
#include "pldmgr.h"
/* Keep regression comparisons against pre-cleanup source snapshots runnable. */
void http_request_completed(void *cls, struct MHD_Connection *connection,
void **context, enum MHD_RequestTerminationCode reason);
#pragma weak http_request_completed
static void complete_request(void *cls, struct MHD_Connection *connection,
void **context, enum MHD_RequestTerminationCode reason) {
if (http_request_completed)
http_request_completed(cls, connection, context, reason);
}
static int serving;
static int fail_malloc, fail_create, fail_queue, fail_cors;
static int fail_realloc, fail_mkstemp, fail_fdopen, fail_write, fail_flush, fail_close;
static char last_temp[1024];
static int last_fd = -1;
static unsigned int queued_status, cors_count;
static char body[256];
static _Atomic unsigned int created, destroyed;
@@ -28,6 +46,38 @@ void *http_test_malloc(size_t size) {
return malloc(size);
}
void *http_test_realloc(void *pointer, size_t size) {
return fail_realloc ? NULL : realloc(pointer, size);
}
int http_test_mkstemp(char *path) {
if (fail_mkstemp) { errno = ENOSPC; return -1; }
int fd = mkstemp(path);
if (!serving) {
snprintf(last_temp, sizeof(last_temp), "%s", path);
last_fd = fd;
}
return fd;
}
FILE *http_test_fdopen(int fd, const char *mode) {
return fail_fdopen ? NULL : fdopen(fd, mode);
}
size_t http_test_fwrite(const void *data, size_t size, size_t count, FILE *file) {
return fail_write ? 0 : fwrite(data, size, count, file);
}
int http_test_fflush(FILE *file) {
int result = fflush(file);
return fail_flush ? EOF : result;
}
int http_test_fclose(FILE *file) {
int result = fclose(file);
return fail_close ? EOF : result;
}
static void remember(struct MHD_Response *response) {
if (!response) return;
created++;
@@ -125,10 +175,12 @@ static void run_request(struct Request request, int oom, int creation_failure, i
size_t upload_size = 0;
enum MHD_Result result = http_on_request(NULL, (struct MHD_Connection *)&request,
request.url, request.method, "HTTP/1.1", "", &upload_size, &context);
if (strcmp(request.method, "OPTIONS") != 0 && result == MHD_YES) {
if (strcmp(request.method, "OPTIONS") != 0 && result == MHD_YES && !queued_status) {
result = http_on_request(NULL, (struct MHD_Connection *)&request,
request.url, request.method, "HTTP/1.1", "", &upload_size, &context);
}
complete_request(NULL, (struct MHD_Connection *)&request, &context,
MHD_REQUEST_TERMINATED_COMPLETED_OK);
unsigned int expected_status = oom ? MHD_HTTP_INTERNAL_SERVER_ERROR : request.status;
int failed = (created != destroyed);
if (creation_failure || fail_cors || (oom && strcmp(request.url, "/events") == 0)) {
@@ -195,9 +247,9 @@ static int unit_tests(int ownership_only, int oom_only) {
{"/abort", "GET", 200, {{0}}},
{"/get_config", "GET", 200, {{0}}},
{"/events", "GET", 200, {{0}}},
{"/set_config", "POST", 200, {{0}}},
{"/sources_set", "POST", 500, {{0}}},
{"/manage:upload", "POST", 500, {{0}}},
{"/set_config", "POST", 400, {{0}}},
{"/sources_set", "POST", 400, {{0}}},
{"/manage:upload", "POST", 400, {{0}}},
{"/shutdown", "GET", 200, {{0}}},
};
for (size_t i = 0; i < sizeof(requests) / sizeof(requests[0]); i++) {
@@ -227,6 +279,9 @@ static int unit_tests(int ownership_only, int oom_only) {
{"/autoload_status", "GET", 200, {{0}}},
{"/get_config", "GET", 200, {{0}}},
{"/events", "GET", 200, {{0}}},
{"/set_config", "POST", 400, {{0}}},
{"/sources_set", "POST", 400, {{0}}},
{"/manage:upload", "POST", 400, {{0}}},
};
for (size_t i = 0; i < sizeof(allocated) / sizeof(allocated[0]); i++) {
if (oom_only && strcmp(allocated[i].url, "/events") == 0) continue;
@@ -239,13 +294,246 @@ static int unit_tests(int ownership_only, int oom_only) {
return failures ? 1 : 0;
}
extern unsigned int test_config_saves, test_source_saves, test_imports;
extern size_t test_post_size;
static void expect(int condition, const char *description) {
checks++;
if (!condition) {
fprintf(stderr, "FAIL request lifecycle: %s\n", description);
failures++;
}
}
struct Transaction { struct Request request; void *context; };
static enum MHD_Result invoke(struct Transaction *transaction, const char *data, size_t size) {
size_t remaining = size;
enum MHD_Result result = http_on_request(NULL, (struct MHD_Connection *)&transaction->request,
transaction->request.url, "POST", "HTTP/1.1", data, &remaining, &transaction->context);
expect(!size || !remaining, "body chunk consumed");
return result;
}
static struct Transaction begin(struct Request request) {
fail_malloc = fail_create = fail_queue = fail_cors = 0;
fail_realloc = fail_mkstemp = fail_fdopen = fail_write = fail_flush = fail_close = 0;
queued_status = cors_count = 0;
test_config_saves = test_source_saves = test_imports = 0;
test_post_size = 0;
last_temp[0] = '\0'; last_fd = -1;
struct Transaction transaction = {request, NULL};
invoke(&transaction, "", 0);
return transaction;
}
static void finish(struct Transaction *transaction, enum MHD_RequestTerminationCode reason) {
complete_request(NULL, (struct MHD_Connection *)&transaction->request,
&transaction->context, reason);
expect(transaction->context == NULL, "completion clears the context");
complete_request(NULL, (struct MHD_Connection *)&transaction->request,
&transaction->context, reason);
expect(created == destroyed, "completion leaves no response references");
if (last_temp[0]) expect(access(last_temp, F_OK) != 0, "owned temporary file removed");
if (last_fd >= 0) expect(fcntl(last_fd, F_GETFD) == -1 && errno == EBADF, "upload descriptor closed");
}
static void request_tests(void) {
int initial_checks = checks;
struct Request upload = {"/manage:upload", "POST", 200, {{"filename", "test.elf"}}};
for (int cancelled = 0; cancelled < 2; cancelled++) {
struct Transaction transaction = begin(upload);
expect(last_temp[0] && access(last_temp, F_OK) == 0, "upload owns a file");
invoke(&transaction, "payload", 7);
if (!cancelled) {
invoke(&transaction, "", 0);
expect(queued_status == 200 && test_imports == 1, "successful upload imports once");
invoke(&transaction, "", 0);
expect(test_imports == 1, "repeat callback does not import twice");
}
finish(&transaction, cancelled ? MHD_REQUEST_TERMINATED_CLIENT_ABORT : MHD_REQUEST_TERMINATED_COMPLETED_OK);
if (cancelled) expect(test_imports == 0, "aborted upload never imports");
}
for (int fault = 0; fault < 6; fault++) {
struct Transaction transaction = begin(upload);
if (fault == 0) fail_write = 1;
if (fault == 1) fail_flush = 1;
if (fault == 2) fail_close = 1;
if (fault == 3) fail_create = 1;
if (fault == 4) fail_queue = 1;
if (fault == 5) fail_cors = 1;
invoke(&transaction, "payload", 7);
if (!queued_status) invoke(&transaction, "", 0);
if (fault < 3) {
expect(queued_status == 500, "write/flush/close error is HTTP 500");
expect(test_imports == 0, "file I/O error prevents import");
}
finish(&transaction, MHD_REQUEST_TERMINATED_WITH_ERROR);
}
struct Transaction committed = begin(upload);
invoke(&committed, "payload", 7);
invoke(&committed, "", 0);
char reused_path[1024];
snprintf(reused_path, sizeof(reused_path), "%s", last_temp);
FILE *new_owner = fopen(reused_path, "w");
assert(new_owner);
fputs("new owner", new_owner);
fclose(new_owner);
last_temp[0] = '\0';
finish(&committed, MHD_REQUEST_TERMINATED_COMPLETED_OK);
expect(access(reused_path, F_OK) == 0, "successful import relinquishes the consumed temp pathname");
remove(reused_path);
for (int fault = 0; fault < 2; fault++) {
struct Transaction transaction = begin((struct Request){"/version", "GET", 200, {{0}}});
finish(&transaction, MHD_REQUEST_TERMINATED_COMPLETED_OK);
if (fault == 0) fail_mkstemp = 1; else fail_fdopen = 1;
transaction = (struct Transaction){upload, NULL};
queued_status = 0;
invoke(&transaction, "", 0);
expect(queued_status == 500, "temporary-file creation/open failure is HTTP 500");
finish(&transaction, MHD_REQUEST_TERMINATED_WITH_ERROR);
}
char length[32];
snprintf(length, sizeof(length), "%zu", HTTP_UPLOAD_BODY_LIMIT + 1);
struct Request too_large = upload;
too_large.args[1] = (struct Argument){"Content-Length", length};
struct Transaction transaction = begin(too_large);
expect(queued_status == 413 && !last_temp[0], "oversized declared upload opens no file");
finish(&transaction, MHD_REQUEST_TERMINATED_COMPLETED_OK);
transaction = begin(upload);
invoke(&transaction, "x", HTTP_UPLOAD_BODY_LIMIT + 1);
expect(queued_status == 413 && !test_imports, "oversized chunk rejected before reading input");
finish(&transaction, MHD_REQUEST_TERMINATED_WITH_ERROR);
transaction = begin(upload);
invoke(&transaction, "", 0);
expect(queued_status == 400 && !test_imports, "empty upload cannot overwrite a payload");
finish(&transaction, MHD_REQUEST_TERMINATED_COMPLETED_OK);
const char *invalid_lengths[] = {"", "-1", "1x", "18446744073709551616"};
for (size_t i = 0; i < sizeof(invalid_lengths) / sizeof(invalid_lengths[0]); i++) {
struct Request invalid = upload;
invalid.args[1] = (struct Argument){"Content-Length", invalid_lengths[i]};
transaction = begin(invalid);
expect(queued_status == 400 && !last_temp[0], "invalid declared length opens no file");
finish(&transaction, MHD_REQUEST_TERMINATED_COMPLETED_OK);
}
char long_filename[257];
memset(long_filename, 'a', sizeof(long_filename) - 1);
long_filename[sizeof(long_filename) - 1] = '\0';
struct Request invalid_name = upload;
invalid_name.args[0].value = long_filename;
transaction = begin(invalid_name);
expect(queued_status == 400 && !last_temp[0], "overlong filename is rejected rather than truncated");
finish(&transaction, MHD_REQUEST_TERMINATED_COMPLETED_OK);
for (int sources = 0; sources < 2; sources++) {
struct Request post = {sources ? "/sources_set" : "/set_config", "POST", 200, {{0}}};
transaction = begin(post);
invoke(&transaction, "{\"value\":", 9);
invoke(&transaction, "true}", 5);
invoke(&transaction, "", 0);
expect(queued_status == 200 && test_post_size == 14, "POST chunks accumulated and saved");
invoke(&transaction, "", 0);
expect(test_config_saves + test_source_saves == 1, "POST saves only once");
finish(&transaction, MHD_REQUEST_TERMINATED_COMPLETED_OK);
transaction = begin(post);
invoke(&transaction, "partial", 7);
finish(&transaction, MHD_REQUEST_TERMINATED_CLIENT_ABORT);
expect(test_config_saves + test_source_saves == 0, "aborted POST is not saved");
transaction = begin(post);
invoke(&transaction, "fail_test", 9);
invoke(&transaction, "", 0);
expect(queued_status == 500, "backend save failure is propagated");
finish(&transaction, MHD_REQUEST_TERMINATED_COMPLETED_OK);
transaction = begin(post);
invoke(&transaction, "partial", 7);
fail_realloc = 1;
char data[5000] = {0};
invoke(&transaction, data, sizeof(data));
expect(queued_status == 500 && test_config_saves + test_source_saves == 0,
"reallocation failure preserves context without saving partial data");
finish(&transaction, MHD_REQUEST_TERMINATED_WITH_ERROR);
transaction = begin(post);
invoke(&transaction, "x", HTTP_POST_BODY_LIMIT + 1);
expect(queued_status == 413 && test_config_saves + test_source_saves == 0,
"oversized unknown-length POST rejected before copying input");
finish(&transaction, MHD_REQUEST_TERMINATED_WITH_ERROR);
snprintf(length, sizeof(length), "%zu", HTTP_POST_BODY_LIMIT + 1);
post.args[0] = (struct Argument){"Content-Length", length};
transaction = begin(post);
expect(queued_status == 413, "oversized declared POST rejected initially");
finish(&transaction, MHD_REQUEST_TERMINATED_COMPLETED_OK);
post.args[0] = (struct Argument){NULL, NULL};
char *boundary = malloc(HTTP_POST_BODY_LIMIT);
assert(boundary);
memset(boundary, ' ', HTTP_POST_BODY_LIMIT);
boundary[HTTP_POST_BODY_LIMIT - 2] = '{';
boundary[HTTP_POST_BODY_LIMIT - 1] = '}';
transaction = begin(post);
invoke(&transaction, boundary, HTTP_POST_BODY_LIMIT / 2);
invoke(&transaction, boundary + HTTP_POST_BODY_LIMIT / 2, HTTP_POST_BODY_LIMIT / 2);
invoke(&transaction, "", 0);
expect(queued_status == 200 && test_post_size == HTTP_POST_BODY_LIMIT,
"POST at the exact body limit is complete and terminated");
finish(&transaction, MHD_REQUEST_TERMINATED_COMPLETED_OK);
transaction = begin(post);
invoke(&transaction, boundary, HTTP_POST_BODY_LIMIT);
invoke(&transaction, "x", 1);
expect(queued_status == 413 && test_config_saves + test_source_saves == 0,
"cumulative POST limit prevents a partial save");
finish(&transaction, MHD_REQUEST_TERMINATED_WITH_ERROR);
free(boundary);
}
fail_malloc = fail_create = fail_queue = fail_cors = 0;
fail_realloc = fail_mkstemp = fail_fdopen = fail_write = fail_flush = fail_close = 0;
FILE *autoload = fopen(AUTOLOAD_CONFIG_PATH, "w");
assert(autoload);
fputs("\r\nlocal/one\r\n!500\nlocal/two\n", autoload);
fclose(autoload);
run_request((struct Request){"/autoload_status", "GET", 200, {{0}}}, 0, 0, 0);
expect(strstr(body, "local/one,local/two") != NULL, "status excludes delays and CRLF blank lines");
run_request((struct Request){"/get_config", "GET", 200, {{0}}}, 0, 0, 0);
expect(strstr(body, "local/one,!500,local/two") != NULL, "config retains delay entries");
for (int size = 4095; size <= 4096; size++) {
autoload = fopen(AUTOLOAD_CONFIG_PATH, "w");
assert(autoload);
for (int i = 0; i < size; i++) fputc('a', autoload);
fclose(autoload);
for (int config = 0; config < 2; config++) {
run_request((struct Request){config ? "/get_config" : "/autoload_status", "GET",
size == 4095 ? 200 : 500, {{0}}}, 0, 0, 0);
}
}
autoload = fopen(AUTOLOAD_CONFIG_PATH, "w");
assert(autoload);
fputs("local/one", autoload);
fputc('\0', autoload);
fputs("local/two", autoload);
fclose(autoload);
run_request((struct Request){"/autoload_status", "GET", 500, {{0}}}, 0, 0, 0);
remove(AUTOLOAD_CONFIG_PATH);
printf("HTTP request lifecycle: %d additional checks, %d total failures\n", checks - initial_checks, failures);
}
static int serve(void) {
serving = 1;
struct sockaddr_in address = {0};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
struct MHD_Daemon *daemon = MHD_start_daemon(MHD_USE_INTERNAL_POLLING_THREAD,
struct MHD_Daemon *daemon = MHD_start_daemon(
MHD_USE_THREAD_PER_CONNECTION | MHD_USE_INTERNAL_POLLING_THREAD,
0, NULL, NULL, http_on_request, NULL,
MHD_OPTION_NOTIFY_COMPLETED, complete_request, NULL,
MHD_OPTION_SOCK_ADDR, &address, MHD_OPTION_END);
if (!daemon) return 1;
const union MHD_DaemonInfo *info = MHD_get_daemon_info(daemon, MHD_DAEMON_INFO_BIND_PORT);
@@ -259,6 +547,9 @@ static int serve(void) {
int main(int argc, char **argv) {
if (argc > 1 && strcmp(argv[1], "--serve") == 0) return serve();
return unit_tests(argc > 1 && strcmp(argv[1], "--ownership-only") == 0,
argc > 1 && strcmp(argv[1], "--oom-only") == 0);
int selected = argc > 1;
int result = unit_tests(selected && strcmp(argv[1], "--ownership-only") == 0,
selected && strcmp(argv[1], "--oom-only") == 0);
if (!selected) request_tests();
return result || failures ? 1 : 0;
}
+13
View File
@@ -2,11 +2,18 @@
#define HTTP_TEST_HOOKS_H
#include <stdlib.h>
#include <stdio.h>
#include <microhttpd.h>
/* Inject faults only into the production HTTP translation unit. The test
* implementations use the real libmicrohttpd response objects/destructors. */
void *http_test_malloc(size_t size);
void *http_test_realloc(void *pointer, size_t size);
int http_test_mkstemp(char *path);
FILE *http_test_fdopen(int fd, const char *mode);
size_t http_test_fwrite(const void *data, size_t size, size_t count, FILE *file);
int http_test_fflush(FILE *file);
int http_test_fclose(FILE *file);
struct MHD_Response *http_test_create_response(size_t size, void *buffer,
enum MHD_ResponseMemoryMode mode);
struct MHD_Response *http_test_create_callback(uint64_t size, size_t block_size,
@@ -20,6 +27,12 @@ const char *http_test_lookup(struct MHD_Connection *connection,
enum MHD_ValueKind kind, const char *key);
#define malloc http_test_malloc
#define realloc http_test_realloc
#define mkstemp http_test_mkstemp
#define fdopen http_test_fdopen
#define fwrite http_test_fwrite
#define fflush http_test_fflush
#define fclose http_test_fclose
#define MHD_create_response_from_buffer http_test_create_response
#define MHD_create_response_from_callback http_test_create_callback
#define MHD_queue_response http_test_queue
+149 -5
View File
@@ -10,8 +10,10 @@ import json
import os
from pathlib import Path
import shlex
import socket
import subprocess
import tempfile
import time
ROOT = Path(__file__).resolve().parents[1]
@@ -20,14 +22,147 @@ def run(args):
subprocess.run(args, cwd=ROOT, check=True)
def network_tests(binary):
def eventually(predicate, message, timeout=5):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if predicate():
return
time.sleep(0.02)
raise AssertionError(message)
def lifecycle_tests(port, storage, server):
payloads = storage / "payloads"
def temporary_uploads():
return list(payloads.glob(".upload-*")) + list(payloads.glob("*.tmp"))
def partial_upload(filename="cancelled.elf", size=100000):
sock = socket.create_connection(("127.0.0.1", port), timeout=10)
sock.sendall(f"POST /manage:upload?filename={filename} HTTP/1.1\r\n"
f"Host: localhost\r\nContent-Length: {size}\r\n\r\n".encode() + b"first chunk")
return sock
# Abort after the daemon has opened its file, not before accepting the socket.
for _ in range(8):
sock = partial_upload()
try:
eventually(lambda: bool(temporary_uploads()), "Upload never opened a temporary file")
finally:
sock.close()
eventually(lambda: not temporary_uploads(), "Disconnected upload left a temporary file")
assert not (payloads / "cancelled.elf").exists()
# Incomplete config/source bodies must be released without being saved.
fd_directory = Path(f"/proc/{server.pid}/fd")
baseline_fds = len(list(fd_directory.iterdir())) if fd_directory.exists() else None
for route in ["/set_config", "/sources_set"] * 8:
with socket.create_connection(("127.0.0.1", port), timeout=10) as sock:
sock.sendall(f"POST {route} HTTP/1.1\r\nHost: localhost\r\n"
"Content-Length: 100000\r\n\r\n".encode() + b'{"incomplete":')
if baseline_fds is not None:
eventually(lambda: len(list(fd_directory.iterdir())) <= baseline_fds,
"Aborted requests leaked file descriptors")
# Two in-flight requests for the same destination must have separate files.
first = partial_upload("same.elf")
second = None
try:
eventually(lambda: len(temporary_uploads()) == 1, "First upload did not open")
second = partial_upload("same.elf")
eventually(lambda: len(temporary_uploads()) == 2, "Same-name uploads shared a temporary file")
first.close()
eventually(lambda: len(temporary_uploads()) == 1, "Cancelling one upload affected the other")
finally:
first.close()
if second:
second.close()
eventually(lambda: not temporary_uploads(), "Concurrent upload cleanup failed")
# A normal import consumes the file; a failed import must still remove it.
for filename, mode, status in [("success.elf", "new", 200), ("failed.elf", "fail_test", 500)]:
connection = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
try:
connection.request("POST", f"/manage:upload?filename={filename}&mode={mode}", b"test payload")
response = connection.getresponse()
data = response.read()
assert response.status == status, (response.status, data)
finally:
connection.close()
eventually(lambda: not temporary_uploads(), "Completed upload left its temporary file")
assert (payloads / "success.elf").read_bytes() == b"test payload"
assert not (payloads / "failed.elf").exists()
# Real chunked bodies exercise the unknown-length path, not just mock callbacks.
for route in ["/set_config", "/sources_set", "/manage:upload?filename=chunked.elf"]:
connection = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
try:
connection.request("POST", route, iter([b"{", b'"value":', b"true}"]), encode_chunked=True)
response = connection.getresponse()
assert response.status == 200, (response.status, response.read())
response.read()
finally:
connection.close()
eventually(lambda: not temporary_uploads(), "Chunked upload left a temporary file")
assert (payloads / "chunked.elf").read_bytes() == b'{"value":true}'
for route, limit in [("/set_config", 1024 * 1024), ("/sources_set", 1024 * 1024),
("/manage:upload?filename=oversized.elf", 512 * 1024 * 1024)]:
connection = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
try:
connection.putrequest("POST", route)
connection.putheader("Content-Length", str(limit + 1))
connection.endheaders()
response = connection.getresponse()
data = response.read()
assert response.status == 413, (response.status, data)
json.loads(data)
finally:
connection.close()
assert not (payloads / "oversized.elf").exists()
eventually(lambda: not temporary_uploads(), "Rejected oversized upload created a file")
# Check large-list error responses using the actual HTTP transport and JSON parser.
autoload = storage / "autoload.txt"
try:
for size in [4095, 4096, 20000]:
autoload.write_text("a" * size)
for route in ["/get_config", "/autoload_status"]:
connection = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
try:
connection.request("GET", route)
response = connection.getresponse()
data = json.loads(response.read())
assert response.status == (200 if size == 4095 else 500), (response.status, data)
if response.status == 200:
assert data["AUTOLOAD_LIST" if route == "/get_config" else "list"] == "a" * size
else:
assert "error" in data
finally:
connection.close()
finally:
autoload.unlink(missing_ok=True)
# Daemon stop must clean requests whose clients remain connected.
pending = [partial_upload("shutdown.elf")]
eventually(lambda: bool(temporary_uploads()), "Shutdown test upload did not open")
for route in ["/set_config", "/sources_set"]:
sock = socket.create_connection(("127.0.0.1", port), timeout=10)
sock.sendall(f"POST {route} HTTP/1.1\r\nHost: localhost\r\n"
"Content-Length: 100000\r\n\r\n".encode() + b'{"incomplete":')
pending.append(sock)
return pending, temporary_uploads
def network_tests(binary, storage):
server = subprocess.Popen([str(binary), "--serve"], stdin=subprocess.PIPE,
stdout=subprocess.PIPE, text=True)
try:
line = server.stdout.readline().strip()
if not line.startswith("PORT "):
raise RuntimeError(f"Test HTTP server did not start: {line}")
connection = http.client.HTTPConnection("127.0.0.1", int(line.split()[1]), timeout=10)
port = int(line.split()[1])
connection = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
routes = ["/manage:check?filename=test.elf", "/history_list", "/update_check",
"/repository_payloads", "/sources_list", "/get_config", "/autoload_status"]
try:
@@ -42,12 +177,19 @@ def network_tests(binary):
json.loads(data)
finally:
connection.close()
pending, temporary_uploads = lifecycle_tests(port, storage, server)
server.stdin.write("\n")
server.stdin.flush()
output = server.communicate(timeout=15)[0]
try:
output = server.communicate(timeout=15)[0]
finally:
for sock in pending:
sock.close()
print(output.strip())
assert server.returncode == 0, "Unreleased responses or sanitizer failure during soak"
assert not temporary_uploads(), "Daemon stop left an upload temporary file"
print("HTTP integration: 10,000 real loopback requests passed")
print("HTTP lifecycle: disconnects, concurrent uploads, failed commits, and daemon stop passed")
finally:
if server.poll() is None:
server.kill()
@@ -61,6 +203,7 @@ def main():
parser.add_argument("--oom-only", action="store_true",
help="Reproduce OOM status/header bugs without deeper failure injection")
parser.add_argument("--no-sanitize", action="store_true")
parser.add_argument("--network-only", action="store_true")
parser.add_argument("--source", default="src/http_server.c",
help="Alternate HTTP source snapshot for regression reproduction")
args = parser.parse_args()
@@ -98,9 +241,10 @@ def main():
run(common + cflags + [str(object_file), "src/utils.c", "tests/http_backend_stubs.c",
"tests/http_responses_test.c", "-o", str(binary)] + flags)
selected = ["--ownership-only"] if args.ownership_only else (["--oom-only"] if args.oom_only else [])
run([str(binary)] + selected)
if not args.network_only:
run([str(binary)] + selected)
if not selected:
network_tests(binary)
network_tests(binary, storage)
if __name__ == "__main__":