Update cHTTP and rewrite web/main.c

This commit is contained in:
2025-11-22 12:30:47 +01:00
parent 747f851155
commit b2b1ed540b
4 changed files with 3808 additions and 3865 deletions
+4 -1
View File
@@ -17,7 +17,7 @@ OFILES = $(CFILES:.c=.o)
.PHONY: all clean coverage coverage-report coverage-html
all: toastyfs$(EXT) toastyfs_random_test$(EXT) example_async_api$(EXT) example_blocking_api$(EXT) libtoastyfs.a
all: toastyfs$(EXT) toastyfs_web$(EXT) toastyfs_random_test$(EXT) example_async_api$(EXT) example_blocking_api$(EXT) libtoastyfs.a
coverage: toastyfs_random_test_coverage$(EXT)
@@ -42,6 +42,9 @@ example_async_api$(EXT): libtoastyfs.a examples/async_api.c
example_blocking_api$(EXT): libtoastyfs.a examples/blocking_api.c
gcc -o $@ examples/blocking_api.c $(CFLAGS) -ltoastyfs $(LFLAGS) -Iinc -L.
toastyfs_web$(EXT): libtoastyfs.a web/main.c web/chttp.c web/chttp.h
gcc -o $@ web/main.c web/chttp.c $(CFLAGS) -ltoastyfs $(LFLAGS) -Iinc -L.
%.o: %.c $(HFILES)
gcc -c -o $@ $< $(CFLAGS) -Iinc
+2429 -3171
View File
File diff suppressed because it is too large Load Diff
+929 -272
View File
File diff suppressed because it is too large Load Diff
+284 -259
View File
@@ -1,24 +1,37 @@
#include <stdio.h>
#include <stdbool.h>
#include <chttp.h>
#include <ToastyFS.h>
#define MAX_WAITING (1<<10)
#define MAX_PENDING (1<<9)
#include "chttp.h"
#ifdef _WIN32
#define POLL WSAPoll
#else
#define POLL poll
#endif
#define UNREACHABLE __builtin_trap()
typedef enum {
OPERATION_FREE,
OPERATION_CREATE_DIR,
OPERATION_CREATE_FILE,
OPERATION_DELETE,
OPERATION_READ_DIR,
OPERATION_READ_FILE,
OPERATION_WRITE,
} OperationType;
PROXIED_OPERATION_FREE,
PROXIED_OPERATION_CREATE_DIR,
PROXIED_OPERATION_CREATE_FILE,
PROXIED_OPERATION_DELETE,
PROXIED_OPERATION_READ_DIR,
PROXIED_OPERATION_READ_FILE,
PROXIED_OPERATION_WRITE,
} ProxiedOperationType;
typedef struct {
OperationType type;
ProxiedOperationType type;
// Don't write the content to the response
// when reading a file or directory.
bool head_only;
// Offset of the read/write
int offset;
// Length of the read/write
int length;
// Number of bytes read/written
int transferred;
@@ -27,291 +40,303 @@ typedef struct {
HTTP_ResponseBuilder builder;
ToastyHandle handle;
} Operation;
static int waiting_head;
static int num_waiting;
static int num_pending;
static Operation waiting[MAX_WAITING];
static Operation pending[MAX_PENDING];
} ProxiedOperation;
void worker(Worker *w)
#define MAX_PROXIED_OPERATIONS (1<<10)
static int find_unused_struct(ProxiedOperation *arr, int num)
{
for (;;) {
ToastyResult result;
int ret = toasty_wait_result(toasty, TOASTY_INVALID, &result, -1);
// TODO: check return value
// First, process completed requests. This frees up
// space for new ones.
//
// TODO: What if there was a WAKEUP request and no pending operations
// are present?
if (num == MAX_PROXIED_OPERATIONS)
return -1;
int i = 0;
while (pending[i].handle != result.handle) {
while (arr[i].type != PROXIED_OPERATION_FREE) {
i++;
assert(i < MAX_PENDING_OPERATIONS);
}
bool incomplete = false;
switch (pending[i].type) {
int cap;
int mincap;
char *dst;
ToastyString path;
case OPERATION_CREATE_DIR:
case OPERATION_CREATE_FILE:
if (result.type == TOASTY_RESULT_CREATE_SUCCESS) {
http_response_builder_status(pending[i].builder, 204); // TODO: What is the proper response to a CREATE request?
http_response_builder_done(pending[i].builder);
} else {
http_response_builder_undo(pending[i].builder);
http_response_builder_status(pending[i].builder, 500);
http_response_builder_done(pending[i].builder);
}
break;
case OPERATION_DELETE:
if (result.type == TOASTY_RESULT_DELETE_SUCCESS) {
http_response_builder_status(pending[i].builder, 204); // TODO: What is the proper response to a DELETE request?
http_response_builder_done(pending[i].builder);
} else {
http_response_builder_undo(pending[i].builder);
http_response_builder_status(pending[i].builder, 500);
http_response_builder_done(pending[i].builder);
}
break;
case OPERATION_READ_DIR:
// If the listing failed, abort the request
if (result.type != TOASTY_RESULT_READ_SUCCESS) {
http_response_builder_status(pending[i].builder, 500);
http_response_builder_done(pending[i].builder);
break;
}
http_response_builder_status(pending[i].builder, 500);
for (int i = 0; i < result.listing.count; i++)
http_response_builder_body(pending[i].builder, (HTTP_String) {
result.listing.items[i].name,
result.listing.items[i].name_len
});
http_response_builder_done(pending[i].builder);
toasty_free_listing(&result.listing);
break;
case OPERATION_READ_FILE:
// If the read failed, abort the request
if (result.type != TOASTY_RESULT_READ_SUCCESS) {
http_response_builder_bodyack(pending[i].builder, 0);
http_response_builder_undo(pending[i].builder);
http_response_builder_status(pending[i].builder, 500);
http_response_builder_done(pending[i].builder);
break;
}
// First, ACK the byte we just read, even if it's
// just 0 bytes (every bodybuf must by paired with
// a bodyack).
http_response_builder_bodyack(pending[i].builder, result.count);
pending[i].transferred += result.count;
// If we read 0 bytes, there is no more to read.
if (result.count == 0) {
http_response_builder_done(pending[i].builder);
break;
}
// There is more to read, so we need to start a
// new read.
// Make sure there is some free space in the buffer
mincap = 1<<10; // TODO: Choose based on overall file size
http_response_builder_bodycap(pending[i].builder, mincap);
// Get the location of that buffer
dst = http_response_builder_bodybuf(pending[i].builder, &cap);
if (dst == NULL) {
http_response_builder_done(pending[i].builder);
break;
}
// Begin the read. On error, abort.
path = (ToastyString) { pending[i].request->path.ptr, pending[i].request.path.len };
pending[i].handle = toasty_begin_read(toasty, path, off, dst, cap);
if (pending[i].handle == TOASTY_INVALID) {
http_response_builder_undo(pending[i].builder);
http_response_builder_status(pending[i].builder, 500);
http_response_builder_done(pending[i].builder);
break;
}
// Most of the time this switch will complete the pending
// operation, but not for reads.
incomplete = true;
break;
case OPERATION_WRITE:
if (pending[i].type != TOASTY_RESULT_WRITE_SUCCESS) {
http_response_builder_undo(pending[i].builder);
http_response_builder_status(pending[i].builder, 500);
http_response_builder_done(pending[i].builder);
break;
}
assert(pending[i].transferred == result.count);
http_response_builder_done(pending[i].builder);
break;
default:
assert(0); // TODO
}
if (!incomplete) {
// Free the pending structure
pending[i].type = OPERATION_FREE;
num_pending--;
}
// Now accept operations
for (Operation req; num_pending < MAX_PENDING_OPERATIONS
&& wait_queue_pop(&wait_queue, &req); ) {
int i = 0;
while (pending[i].type != PENDING_OPERATION_FREE) {
i++;
assert(i < MAX_PENDING_OPERATIONS);
}
pending[i] = req;
ToastyString path = { pending[i].request->path.ptr, pending[i].request.path.len };
ToastyString body = { pending[i].request->body.ptr, pending[i].request.body.len };
switch (pending[i].type) {
int cap;
int mincap;
char *dst;
uint32_t chunk_size;
case OPERATION_CREATE_DIR,
pending[i].handle = toasty_begin_create_dir(toasty, path);
break;
case OPERATION_CREATE_FILE,
chunk_size = 1<<10; // TODO: determine a better chunk size
pending[i].handle = toasty_begin_create_file(toasty, path, chunk_size);
break;
case OPERATION_DELETE,
pending[i].handle = toasty_begin_delete(toasty, path);
break;
case OPERATION_READ,
http_response_builder_status(pending[i].builder, 200); // TODO: Is statis 200 correct?
mincap = 1<<10; // TODO: do something smart to choose this
http_response_builder_bodycap(pending[i].builder, mincap);
dst = http_response_builder_bodybuf(pending[i].builder, &cap);
if (dst == NULL)
break;
pending[i].handle = toasty_begin_read(toasty, path, off, dst, cap);
pending[i].transferred = 0;
break;
case OPERATION_WRITE:
pending[i].handle = toasty_begin_write(toasty, body.ptr, body.len);
pending[i].transferred = 0;
break;
}
if (pending[i].handle == TOASTY_INVALID) {
http_response_builder_undo(pending[i].builder);
http_response_builder_status(pending[i].builder, 500);
http_response_builder_done(pending[i].builder);
pending[i].type = OPERATION_FREE;
}
num_pending++;
}
assert(i < MAX_PROXIED_OPERATIONS);
}
return i;
}
int main(void)
{
http_global_init();
ToastyString upstream_addr = TOASTY_STR("127.0.0.1");
uint16_t upstream_port = 9000;
HTTP_String addr = HTTP_STR("127.0.0.1");
uint16_t port = 8080;
HTTP_String local_addr = HTTP_STR("127.0.0.1");
uint16_t local_port = 8080;
ToastyString backend_addr = TOASTY_STR("127.0.0.1");
uint16_t backend_port = 9000;
HTTP_Server *server = http_server_init(addr, port);
if (server == NULL)
ToastyFS *toasty = toasty_connect(upstream_addr, upstream_port);
if (toasty == NULL)
return -1;
ToastyFS *toasty = toasty_connect(backend_addr, backend_port);
if (toasty == NULL)
HTTP_Server server;
if (http_server_init(&server) < 0)
return -1;
http_server_set_reuse_addr(&server, true);
http_server_set_trace_bytes(&server, true);
if (http_server_listen_tcp(&server, local_addr, local_port) < 0)
return -1;
int num_proxied = 0;
ProxiedOperation proxied[MAX_PROXIED_OPERATIONS];
for (;;) {
#define HTTP_SERVER_POLL_CAPACITY (HTTP_SERVER_CAPACITY+3)
#define TOASTY_POLL_CAPACITY (MAX_CONNS+1)
#define POLL_CAPACITY (HTTP_SERVER_POLL_CAPACITY + TOASTY_POLL_CAPACITY)
EventRegister reg;
void *ptrs[POLL_CAPACITY];
struct pollfd polled[POLL_CAPACITY];
void **http_ptrs = ptrs;
struct pollfd *http_polled = polled;
reg = (EventRegister) {
ptrs,
polled,
POLL_CAPACITY,
0
};
if (http_server_register_events(&server, &reg) < 0)
return -1;
int num_http_polled = reg.num_polled;
void **toasty_ptrs = ptrs + num_http_polled;
struct pollfd *toasty_polled = polled + num_http_polled;
int num_toasty_polled = toasty_process_events(toasty, toasty_ptrs, toasty_polled, 0);
if (num_toasty_polled < 0)
return -1;
int num_polled = num_http_polled + num_toasty_polled;
if (num_polled > 0)
POLL(polled, num_polled, -1);
// First, process toasty events so that we free space
// for incoming requests
if (toasty_process_events(toasty, toasty_ptrs, toasty_polled, num_toasty_polled) < 0)
return -1;
for (;;) {
HTTP_Request *request;
HTTP_ResponseBuilder builder;
int ret = http_server_wait(server, &req, &builder);
ToastyResult result;
int ret = toasty_get_result(toasty, TOASTY_INVALID, &result);
if (ret == 1)
break; // No completion
if (ret < 0)
return -1; // Error
// Completed
assert(ret == 0);
// Find the operation associated to this completion
int i = 0;
while (proxied[i].handle != result.handle) {
i++;
assert(i < MAX_PROXIED_OPERATIONS);
}
switch (proxied[i].type) {
case PROXIED_OPERATION_CREATE_DIR:
case PROXIED_OPERATION_CREATE_FILE:
{
if (result.type == TOASTY_RESULT_CREATE_SUCCESS) {
http_response_builder_status(proxied[i].builder, 201); // Created
http_response_builder_send(proxied[i].builder);
} else {
// TODO: Should differentiate between error conditions
http_response_builder_status(proxied[i].builder, 500); // Internal Server Error
http_response_builder_send(proxied[i].builder);
}
proxied[i].type = PROXIED_OPERATION_FREE;
num_proxied--;
}
break;
case PROXIED_OPERATION_DELETE:
{
if (result.type == TOASTY_RESULT_DELETE_SUCCESS) {
http_response_builder_status(proxied[i].builder, 204); // No Content
http_response_builder_send(proxied[i].builder);
} else {
// TODO: Should differentiate between error conditions
http_response_builder_status(proxied[i].builder, 500); // Internal Server Error
http_response_builder_send(proxied[i].builder);
}
proxied[i].type = PROXIED_OPERATION_FREE;
num_proxied--;
}
break;
case PROXIED_OPERATION_READ_DIR:
{
if (result.type == TOASTY_RESULT_LIST_SUCCESS) {
http_response_builder_status(proxied[i].builder, 200);
for (int i = 0; i < result.listing.count; i++)
http_response_builder_body(proxied[i].builder, (HTTP_String) {
result.listing.items[i].name,
result.listing.items[i].name_len
});
http_response_builder_send(proxied[i].builder);
toasty_free_listing(&result.listing);
} else {
// TODO: Should differentiate between error conditions
http_response_builder_status(proxied[i].builder, 500); // Internal Server Error
http_response_builder_send(proxied[i].builder);
}
proxied[i].type = PROXIED_OPERATION_FREE;
num_proxied--;
}
break;
case PROXIED_OPERATION_READ_FILE:
{
bool again = false;
if (result.type != TOASTY_RESULT_READ_SUCCESS) {
http_response_builder_bodyack(proxied[i].builder, 0);
http_response_builder_status(proxied[i].builder, 500);
http_response_builder_send(proxied[i].builder);
} else {
// First, ACK the byte we just read, even if it's
// just 0 bytes (every bodybuf must by paired with
// a bodyack).
proxied[i].transferred += result.count;
int ack = proxied[i].head_only ? 0 : result.count;
http_response_builder_bodyack(proxied[i].builder, ack);
// If we didn't reach the end of the file, start
// a new read.
if (result.count > 0) {
// Make sure there is some free space in the buffer
int mincap = 1<<10; // TODO: Choose based on overall file size
http_response_builder_bodycap(proxied[i].builder, mincap);
// Get the location of that buffer
int cap;
char *dst = http_response_builder_bodybuf(proxied[i].builder, &cap);
if (dst == NULL) {
assert(0); // TODO
}
proxied[i].handle = toasty_begin_read(toasty, path, off, dst, cap);
if (proxied[i].handle == TOASTY_INVALID) {
assert(0); // TODO
}
again = true;
}
}
if (!again) {
proxied[i].type = PROXIED_OPERATION_FREE;
num_proxied--;
}
}
break;
case PROXIED_OPERATION_WRITE:
{
if (result.type == TOASTY_RESULT_WRITE_SUCCESS) {
http_response_builder_status(proxied[i].builder, 201); // Created
http_response_builder_send(proxied[i].builder);
} else {
// TODO: Should differentiate between error conditions
http_response_builder_status(proxied[i].builder, 500); // Internal Server Error
http_response_builder_send(proxied[i].builder);
}
proxied[i].type = PROXIED_OPERATION_FREE;
num_proxied--;
}
break;
default:
UNREACHABLE;
break;
}
}
reg = (EventRegister) { http_ptrs, http_polled, HTTP_SERVER_POLL_CAPACITY, num_http_polled };
if (http_server_process_events(&server, &reg) < 0)
return -1;
ToastyString path = {
req->url.path.ptr,
req->url.path.len
};
Operation op;
op.type = OPERATION_FREE;
switch (req->method) {
HTTP_Request *request;
HTTP_ResponseBuilder builder;
if (http_server_next_request(&server, &request, &builder)) {
switch (request->method) {
case HTTP_METHOD_GET:
// TODO
http_response_builder_status(builder, 501); // Not Implemented
http_response_builder_send(builder);
break;
case HTTP_METHOD_HEAD:
// TODO
http_response_builder_status(builder, 501); // Not Implemented
http_response_builder_send(builder);
break;
case HTTP_METHOD_PUT:
// TODO
{
int i = find_unused_struct(proxied, num_proxied);
if (i < 0) {
http_response_builder_status(builder, 503); // Service Unavailable
http_response_builder_send(builder);
break;
}
case HTTP_METHOD_PATCH:
// TODO
ToastyHandle handle = toasty_begin_write(toasty, path, 0, request->body.ptr, request->body.len);
if (handle == TOASTY_INVALID) {
http_response_builder_status(builder, 500); // Internal Server Error
http_response_builder_send(builder);
break;
}
proxied[num_proxied].type = PROXIED_OPERATION_WRITE;
proxied[num_proxied].request = request;
proxied[num_proxied].builder = builder;
proxied[num_proxied].handle = handle;
num_proxied++;
}
break;
case HTTP_METHOD_DELETE:
// TODO
{
int i = find_unused_struct(proxied, num_proxied);
if (i < 0) {
http_response_builder_status(builder, 503); // Service Unavailable
http_response_builder_send(builder);
break;
}
ToastyHandle handle = toasty_begin_delete(toasty, path);
if (handle == TOASTY_INVALID) {
http_response_builder_status(builder, 500); // Internal Server Error
http_response_builder_send(builder);
break;
}
proxied[num_proxied].type = PROXIED_OPERATION_DELETE;
proxied[num_proxied].request = request;
proxied[num_proxied].builder = builder;
proxied[num_proxied].handle = handle;
num_proxied++;
}
break;
case HTTP_METHOD_OPTIONS:
http_response_builder_status(builder, 200);
http_response_builder_header(builder, HTTP_STR("Allow: GET, HEAD, PUT, PATCH, DELETE, OPTIONS"));
http_response_builder_done(builder);
http_response_builder_status(builder, 200); // OK
http_response_builder_header(builder, HTTP_STR("Allow: GET, HEAD, PUT, DELETE, OPTIONS"));
http_response_builder_send(builder);
break;
default:
http_response_builder_status(builder, 200); // TODO: use the status code for invalid methods
http_response_builder_header(builder, HTTP_STR("Allow: GET, HEAD, PUT, PATCH, DELETE, OPTIONS"));
http_response_builder_done(builder);
http_response_builder_status(builder, 405); // Method not allowed
http_response_builder_header(builder, HTTP_STR("Allow: GET, HEAD, PUT, DELETE, OPTIONS"));
http_response_builder_send(builder);
break;
}
if (op.type != OPERATION_FREE) {
assert(0); // TODO: Append to the queue
}
}
http_server_free(server);
http_global_free();
http_server_free(&server);
toasty_disconnect(toasty);
return 0;
}