Files
ps5-payload-manager/tests/http_responses_test.c
T

556 lines
24 KiB
C

#include <assert.h>
#include <stdatomic.h>
#include <stdio.h>
#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;
static struct MHD_Response *outstanding[32];
struct Argument { const char *key; const char *value; };
struct Request {
const char *url;
const char *method;
unsigned int status;
struct Argument args[4];
};
void *http_test_malloc(size_t size) {
if (fail_malloc) return NULL;
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++;
if (!serving) {
for (size_t i = 0; i < 32; i++) {
if (!outstanding[i]) { outstanding[i] = response; return; }
}
abort();
}
}
struct MHD_Response *http_test_create_response(size_t size, void *buffer,
enum MHD_ResponseMemoryMode mode) {
if (fail_create) return NULL; /* MHD leaves the buffer with its caller on failure. */
struct MHD_Response *response = MHD_create_response_from_buffer(size, buffer, mode);
remember(response);
if (!serving && response) {
size_t n = size < sizeof(body) - 1 ? size : sizeof(body) - 1;
if (n) memcpy(body, buffer, n);
body[n] = '\0';
}
return response;
}
struct MHD_Response *http_test_create_callback(uint64_t size, size_t block_size,
MHD_ContentReaderCallback reader, void *cls, MHD_ContentReaderFreeCallback cleanup) {
if (fail_create) return NULL;
struct MHD_Response *response = MHD_create_response_from_callback(
size, block_size, reader, cls, cleanup);
remember(response);
return response;
}
static enum MHD_Result count_header(void *cls, enum MHD_ValueKind kind,
const char *key, const char *value) {
(void)cls; (void)kind;
if (strcasecmp(key, "Access-Control-Allow-Origin") == 0) {
assert(strcmp(value, "*") == 0);
cors_count++;
}
return MHD_YES;
}
enum MHD_Result http_test_add_header(struct MHD_Response *response,
const char *name, const char *value) {
if (fail_cors && strcmp(name, "Access-Control-Allow-Origin") == 0)
return MHD_NO;
return MHD_add_response_header(response, name, value);
}
enum MHD_Result http_test_queue(struct MHD_Connection *connection,
unsigned int status, struct MHD_Response *response) {
assert(response); /* A failed response allocation must never be queued. */
if (serving) return MHD_queue_response(connection, status, response);
queued_status = status;
cors_count = 0;
MHD_get_response_headers(response, count_header, NULL);
return fail_queue ? MHD_NO : MHD_YES;
}
void http_test_destroy(struct MHD_Response *response) {
assert(response);
if (!serving) {
int found = 0;
for (size_t i = 0; i < 32; i++) {
if (outstanding[i] == response) { outstanding[i] = NULL; found = 1; break; }
}
assert(found); /* Detect double destruction. */
}
destroyed++;
MHD_destroy_response(response);
}
const char *http_test_lookup(struct MHD_Connection *connection,
enum MHD_ValueKind kind, const char *key) {
if (serving) return MHD_lookup_connection_value(connection, kind, key);
struct Request *request = (struct Request *)connection;
for (size_t i = 0; i < 4; i++) {
if (request->args[i].key && strcmp(request->args[i].key, key) == 0)
return request->args[i].value;
}
return NULL;
}
static int failures, checks;
static void run_request(struct Request request, int oom, int creation_failure, int queue_failure) {
created = destroyed = 0;
queued_status = cors_count = 0;
body[0] = '\0';
fail_malloc = oom;
fail_create = creation_failure;
fail_queue = queue_failure;
void *context = NULL;
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 && !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)) {
failed |= result != MHD_NO || queued_status != 0;
} else {
failed |= result != (queue_failure ? MHD_NO : MHD_YES);
failed |= queued_status != expected_status || cors_count != 1;
if (oom) failed |= strstr(body, "memory") == NULL;
}
checks++;
if (failed) {
fprintf(stderr, "FAIL %s %s [malloc=%d create=%d queue=%d]: result=%d status=%u cors=%u created=%u released=%u\n",
request.method, request.url, oom, creation_failure, queue_failure,
result, queued_status, cors_count, created, destroyed);
failures++;
}
/* Permit running against the original leaking source without exhausting RAM. */
for (size_t i = 0; i < 32; i++) {
if (outstanding[i]) { MHD_destroy_response(outstanding[i]); outstanding[i] = NULL; }
}
fail_malloc = fail_create = fail_queue = 0;
}
static int unit_tests(int ownership_only, int oom_only) {
struct Request requests[] = {
{"/", "OPTIONS", 200, {{0}}},
{"/", "GET", 200, {{0}}},
{"/cache.appcache", "GET", 200, {{0}}},
{"/favicon.svg", "GET", 200, {{0}}},
{"/icon.png", "GET", 200, {{0}}},
{"/update_check", "GET", 200, {{0}}},
{"/manage:check", "GET", 400, {{0}}},
{"/manage:check", "GET", 400, {{"filename", "../bad.elf"}}},
{"/manage:check", "GET", 200, {{"filename", "test.elf"}}},
{"/list_payloads", "GET", 200, {{0}}},
{"/processes_list", "GET", 200, {{0}}},
{"/history_list", "GET", 200, {{0}}},
{"/process_kill", "GET", 400, {{0}}},
{"/process_kill", "GET", 200, {{"pid", "123"}}},
{"/process_kill", "GET", 500, {{"pid", "-1"}}},
{"/repository_payloads", "GET", 200, {{0}}},
{"/repository_refresh", "GET", 200, {{0}}},
{"/repository_install", "GET", 400, {{0}}},
{"/repository_install", "GET", 200, {{"id", "test"}}},
{"/repository_install", "GET", 200, {{"filename", "test.elf"}}},
{"/sources_list", "GET", 200, {{0}}},
{"/sources_add", "GET", 400, {{0}}},
{"/sources_add", "GET", 200, {{"url", "http://example.test/repo.json"}}},
{"/sources_remove", "GET", 400, {{0}}},
{"/sources_remove", "GET", 200, {{"index", "1"}}},
{"/usb_move_check", "GET", 400, {{0}}},
{"/usb_move_check", "GET", 200, {{"path", "/mnt/usb0/test.elf"}}},
{"/usb_move_perform", "GET", 400, {{0}}},
{"/usb_move_perform", "GET", 200, {{"path", "/mnt/usb0/test.elf"}}},
{"/loadpayload:missing.elf", "GET", 400, {{0}}},
{"/loadpayload:pldmgr.elf", "GET", 400, {{0}}},
{"/manage:delete", "GET", 400, {{0}}},
{"/manage:delete", "GET", 200, {{"slot", "local/test"}}},
{"/log", "GET", 200, {{0}}},
{"/version", "GET", 200, {{0}}},
{"/getip", "GET", 200, {{0}}},
{"/autoload_status", "GET", 200, {{0}}},
{"/autoload_clear", "GET", 200, {{0}}},
{"/abort", "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}}},
{"/shutdown", "GET", 200, {{0}}},
};
for (size_t i = 0; i < sizeof(requests) / sizeof(requests[0]); i++) {
if (oom_only) break;
run_request(requests[i], 0, 0, 0);
run_request(requests[i], 0, 0, 1);
if (!ownership_only) {
run_request(requests[i], 0, 1, 0);
fail_cors = 1;
run_request(requests[i], 0, 0, 0);
fail_cors = 0;
}
}
if (!ownership_only) {
struct Request allocated[] = {
{"/manage:check", "GET", 200, {{"filename", "test.elf"}}},
{"/list_payloads", "GET", 200, {{0}}},
{"/processes_list", "GET", 200, {{0}}},
{"/history_list", "GET", 200, {{0}}},
{"/repository_payloads", "GET", 200, {{0}}},
{"/repository_refresh", "GET", 200, {{0}}},
{"/repository_install", "GET", 200, {{"id", "test"}}},
{"/sources_list", "GET", 200, {{0}}},
{"/usb_move_check", "GET", 200, {{"path", "test"}}},
{"/usb_move_perform", "GET", 200, {{"path", "test"}}},
{"/log", "GET", 200, {{0}}},
{"/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;
run_request(allocated[i], 1, 0, 0);
if (!oom_only) run_request(allocated[i], 1, 1, 0);
}
}
printf("HTTP response tests: %d checks, %d failures (libmicrohttpd %s)\n",
checks, failures, MHD_get_version());
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_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);
printf("PORT %u\n", info->port);
fflush(stdout);
(void)getchar();
MHD_stop_daemon(daemon);
printf("Application response references: created=%u released=%u\n", created, destroyed);
return created == destroyed ? 0 : 1;
}
int main(int argc, char **argv) {
if (argc > 1 && strcmp(argv[1], "--serve") == 0) return serve();
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;
}