From afd18d3a37ee4285e77612b683a3b3ce8367dc3a Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 2 Nov 2023 10:27:34 +0100 Subject: [PATCH] feat(http): add custom processing to responses using response steps --- CHANGELOG.md | 6 ++- Makefile | 10 +++- include/http_loop.h | 39 ++++++++++----- src/http_loop/http_loop.c | 2 +- src/http_loop/http_loop_req.c | 14 +++--- src/http_loop/http_loop_res.c | 89 ++++++++++++++++++++++------------- src/lander/lander.c | 34 ++++++++----- 7 files changed, 128 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91784d4..3872f31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/lander/src/branch/dev) -* Fully decoupled HTTP loop functionnality +* HTTP Loop + * Fully decoupled functionality from Lander-specific code + * Users can now define custom global & request-local contexts + * Introduced "response steps", allowing custom code during the response + part of a request ## [0.1.0](https://git.rustybever.be/Chewing_Bever/lander/src/tag/0.1.0) diff --git a/Makefile b/Makefile index 1206b6c..1dc0dce 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ libtrie: liblsm: $(MAKE) -C lsm -.PHONY: bin +.PHONY: $(BIN) $(BIN): libtrie liblsm $(OBJS) $(CC) -o $@ $(OBJS) $(_LDFLAGS) @@ -58,11 +58,17 @@ $(BUILD_DIR)/$(THIRDPARTY_DIR)/%.c.o: $(THIRDPARTY_DIR)/%.c # =====TESTING===== .PHONY: run -run: bin +run: $(BIN) LANDER_API_KEY=test \ LANDER_DATA_DIR=data \ '$(BUILD_DIR)/$(BIN_FILENAME)' +.PHONY: valgrind +valgrind: $(BIN) + LANDER_API_KEY=test \ + LANDER_DATA_DIR=data \ + valgrind '$(BUILD_DIR)/$(BIN_FILENAME)' + .PHONY: test test: $(TARGETS_TEST) diff --git a/include/http_loop.h b/include/http_loop.h index 486ccbf..c25becf 100644 --- a/include/http_loop.h +++ b/include/http_loop.h @@ -27,9 +27,10 @@ typedef enum http_route_type { * Function describing a step in a route's processing. * * @param conn connection to process - * @return whether the processing can immediately advance to the next step. A - * step should return false if it's e.g. waiting for I/O, and can therefore not - * finish its task in the current cycle of the event loop. + * @return whether processing can proceed to the next step without performing + * I/O first. For a request step, `false` means more data needs to be read + * before the step can finish its processing. For response steps, `false` means + * there's new data in the write buffer that needs to be written. */ typedef bool (*step)(event_loop_conn *conn); @@ -44,6 +45,7 @@ typedef struct http_route { // starting the http loop regex_t *regex; step steps[HTTP_LOOP_MAX_STEPS]; + step steps_res[HTTP_LOOP_MAX_STEPS]; } http_route; /** @@ -116,14 +118,6 @@ typedef struct event_loop http_loop; */ bool http_loop_handle_request(event_loop_conn *conn); -/** - * Write the HTTP response to the file descriptor. This is the "write_data" - * function for the event loop. - * - * @param conn connection to process - */ -void http_loop_write_response(event_loop_conn *conn); - /** * Try to parse the incoming data as an HTTP request. * @@ -148,6 +142,14 @@ void http_loop_route_request(event_loop_conn *conn); */ void http_loop_process_request(event_loop_conn *conn); +/** + * Handles the response processing. This is the `write_data` function for the + * event loop. + * + * @param conn connection to process + */ +void http_loop_handle_response(event_loop_conn *conn); + /** * Request step that consumes the request body and stores it in a buffer. * @@ -180,6 +182,21 @@ bool http_loop_step_auth(event_loop_conn *conn); */ bool http_loop_step_switch_res(event_loop_conn *conn); +/** + * Write the HTTP header back to the connection. If `res->head` is not set, a + * header will be generated for you. + * + * @param conn connection to process + */ +bool http_loop_step_write_header(event_loop_conn *conn); + +/** + * Write the HTTP body back to the connection. + * + * @param conn connection to process + */ +bool http_loop_step_write_body(event_loop_conn *conn); + /** * Initialize a new http loop. * diff --git a/src/http_loop/http_loop.c b/src/http_loop/http_loop.c index 536f89d..dc76061 100644 --- a/src/http_loop/http_loop.c +++ b/src/http_loop/http_loop.c @@ -54,7 +54,7 @@ event_loop *http_loop_init(http_route *routes, size_t route_count, el->ctx_init = (void *(*)(void *))http_loop_ctx_init; el->ctx_free = (void (*)(void *))http_loop_ctx_free; el->handle_data = http_loop_handle_request; - el->write_data = http_loop_write_response; + el->write_data = http_loop_handle_response; http_loop_gctx *gctx = http_loop_gctx_init(); gctx->c = custom_gctx; diff --git a/src/http_loop/http_loop_req.c b/src/http_loop/http_loop_req.c index 09b0ed4..76e15d5 100644 --- a/src/http_loop/http_loop_req.c +++ b/src/http_loop/http_loop_req.c @@ -138,13 +138,11 @@ void http_loop_process_request(event_loop_conn *conn) { ctx->current_step++; } - if (conn->state != event_loop_conn_state_req) { - return; - } - - // If we've reached the end of the list of step functions, we report the - // request as finished by clearing its route - if (ctx->route->steps[ctx->current_step] == NULL) { - http_loop_ctx_reset(ctx); + // Request processing can stop early by switching the connection state + // Either way, we reset the step counter as it will be used by the response + // steps + if ((conn->state != event_loop_conn_state_req) || + (ctx->route->steps[ctx->current_step] == NULL)) { + ctx->current_step = 0; } } diff --git a/src/http_loop/http_loop_res.c b/src/http_loop/http_loop_res.c index d2ae029..7cbf9d3 100644 --- a/src/http_loop/http_loop_res.c +++ b/src/http_loop/http_loop_res.c @@ -48,7 +48,7 @@ void http_loop_init_header(http_response *res) { res->head_len = buf_size + 1; } -void http_loop_write_response(event_loop_conn *conn) { +bool http_loop_step_write_header(event_loop_conn *conn) { http_response *res = &((http_loop_ctx *)conn->ctx)->res; // Create head response @@ -56,43 +56,68 @@ void http_loop_write_response(event_loop_conn *conn) { http_loop_init_header(res); } - // The final iteration marks the end of the response, after which we reset the - // context so a next request can be processed - if (res->head_written == res->head_len && - res->body.expected_len == res->body.len) { - http_loop_ctx_reset(conn->ctx); - conn->state = event_loop_conn_state_req; - return; + // Step has finished its work + if (res->head_written == res->head_len) { + return true; } - if (res->head_written < res->head_len) { - size_t bytes_to_write = MIN(res->head_len - res->head_written, - EVENT_LOOP_BUFFER_SIZE - conn->wbuf_size); - memcpy(&conn->wbuf[conn->wbuf_size], &res->head[res->head_written], + size_t bytes_to_write = MIN(res->head_len - res->head_written, + EVENT_LOOP_BUFFER_SIZE - conn->wbuf_size); + memcpy(&conn->wbuf[conn->wbuf_size], &res->head[res->head_written], + bytes_to_write); + + conn->wbuf_size += bytes_to_write; + res->head_written += bytes_to_write; + + return false; +} + +bool http_loop_step_write_body(event_loop_conn *conn) { + http_response *res = &((http_loop_ctx *)conn->ctx)->res; + + if (res->body.expected_len == res->body.len) { + return true; + } + + size_t bytes_to_write = MIN(res->body.expected_len - res->body.len, + EVENT_LOOP_BUFFER_SIZE - conn->wbuf_size); + + size_t bytes_written; + + switch (res->body.type) { + case http_body_buf: + memcpy(&conn->wbuf[conn->wbuf_size], &(res->body.buf)[res->body.len], bytes_to_write); - conn->wbuf_size += bytes_to_write; - res->head_written += bytes_to_write; + res->body.len += bytes_to_write; + break; + case http_body_file: + bytes_written = fread(&conn->wbuf[conn->wbuf_size], sizeof(uint8_t), + bytes_to_write, res->body.file); + conn->wbuf_size += bytes_written; + res->body.len += bytes_written; + break; } - if (res->body.len < res->body.expected_len) { - size_t bytes_to_write = MIN(res->body.expected_len - res->body.len, - EVENT_LOOP_BUFFER_SIZE - conn->wbuf_size); - size_t bytes_written; + return false; +} - switch (res->body.type) { - case http_body_buf: - memcpy(&conn->wbuf[conn->wbuf_size], &(res->body.buf)[res->body.len], - bytes_to_write); - conn->wbuf_size += bytes_to_write; - res->body.len += bytes_to_write; - break; - case http_body_file: - bytes_written = fread(&conn->wbuf[conn->wbuf_size], sizeof(uint8_t), - bytes_to_write, res->body.file); - conn->wbuf_size += bytes_written; - res->body.len += bytes_written; - break; - } +void http_loop_handle_response(event_loop_conn *conn) { + http_loop_ctx *ctx = conn->ctx; + + while ((conn->state == event_loop_conn_state_res) && + (ctx->route->steps_res[ctx->current_step] != NULL) && + ctx->route->steps_res[ctx->current_step](conn)) { + ctx->current_step++; + } + + // Response processing can stop early be switching the connection state + // After response processing has finished its work, we reset the context to + // prepare for a new request + if ((conn->state != event_loop_conn_state_res) || + (ctx->route->steps_res[ctx->current_step] == NULL)) { + http_loop_ctx_reset(ctx); + + conn->state = event_loop_conn_state_req; } } diff --git a/src/lander/lander.c b/src/lander/lander.c index 9606326..5a86f76 100644 --- a/src/lander/lander.c +++ b/src/lander/lander.c @@ -6,21 +6,33 @@ http_route lander_routes[] = { {.type = http_route_literal, .method = http_get, .path = "/", - .steps = {lander_get_index, NULL}}, - {.type = http_route_regex, - .method = http_get, - .path = "^/([^/]+)$", - .steps = {lander_get_entry, NULL}}, - {.type = http_route_regex, - .method = http_post, - .path = "^/s(l?)/([^/]*)$", - .steps = {http_loop_step_auth, http_loop_step_body_to_buf, - lander_post_redirect, NULL}}, + .steps = {lander_get_index, NULL}, + .steps_res = {http_loop_step_write_header, http_loop_step_write_body, + NULL}}, + { + .type = http_route_regex, + .method = http_get, + .path = "^/([^/]+)$", + .steps = {lander_get_entry, NULL}, + .steps_res = {http_loop_step_write_header, http_loop_step_write_body, + NULL}, + }, + { + .type = http_route_regex, + .method = http_post, + .path = "^/s(l?)/([^/]*)$", + .steps = {http_loop_step_auth, http_loop_step_body_to_buf, + lander_post_redirect, NULL}, + .steps_res = {http_loop_step_write_header, http_loop_step_write_body, + NULL}, + }, {.type = http_route_regex, .method = http_post, .path = "^/p(l?)/([^/]*)$", .steps = {http_loop_step_auth, lander_post_paste, - http_loop_step_body_to_file, http_loop_step_switch_res, NULL}}, + http_loop_step_body_to_file, http_loop_step_switch_res, NULL}, + .steps_res = {http_loop_step_write_header, http_loop_step_write_body, + NULL}}, }; void *lander_gctx_init() { return calloc(1, sizeof(lander_gctx)); }