mirror of
https://github.com/itsPLK/ps5-payload-manager.git
synced 2026-10-19 02:19:53 -05:00
fix: clean up terminated HTTP requests and bound autoload lists
This commit is contained in:
1 parent
9b863e8020
commit
608d7fa5f2
8 files changed
+797
-224
No files matched your search
+16
-4
@@ -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`:
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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__":
|
||||
|
||||
Reference in new issue
Block a user