feat(http): add custom processing to responses using response steps

lsm
Jef Roosens 2023-11-02 10:27:34 +01:00
parent 6d74c8c550
commit afd18d3a37
Signed by: Jef Roosens
GPG Key ID: B75D4F293C7052DB
7 changed files with 128 additions and 66 deletions

View File

@ -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) ## [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) ## [0.1.0](https://git.rustybever.be/Chewing_Bever/lander/src/tag/0.1.0)

View File

@ -43,7 +43,7 @@ libtrie:
liblsm: liblsm:
$(MAKE) -C lsm $(MAKE) -C lsm
.PHONY: bin .PHONY: $(BIN)
$(BIN): libtrie liblsm $(OBJS) $(BIN): libtrie liblsm $(OBJS)
$(CC) -o $@ $(OBJS) $(_LDFLAGS) $(CC) -o $@ $(OBJS) $(_LDFLAGS)
@ -58,11 +58,17 @@ $(BUILD_DIR)/$(THIRDPARTY_DIR)/%.c.o: $(THIRDPARTY_DIR)/%.c
# =====TESTING===== # =====TESTING=====
.PHONY: run .PHONY: run
run: bin run: $(BIN)
LANDER_API_KEY=test \ LANDER_API_KEY=test \
LANDER_DATA_DIR=data \ LANDER_DATA_DIR=data \
'$(BUILD_DIR)/$(BIN_FILENAME)' '$(BUILD_DIR)/$(BIN_FILENAME)'
.PHONY: valgrind
valgrind: $(BIN)
LANDER_API_KEY=test \
LANDER_DATA_DIR=data \
valgrind '$(BUILD_DIR)/$(BIN_FILENAME)'
.PHONY: test .PHONY: test
test: $(TARGETS_TEST) test: $(TARGETS_TEST)

View File

@ -27,9 +27,10 @@ typedef enum http_route_type {
* Function describing a step in a route's processing. * Function describing a step in a route's processing.
* *
* @param conn connection to process * @param conn connection to process
* @return whether the processing can immediately advance to the next step. A * @return whether processing can proceed to the next step without performing
* step should return false if it's e.g. waiting for I/O, and can therefore not * I/O first. For a request step, `false` means more data needs to be read
* finish its task in the current cycle of the event loop. * 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); typedef bool (*step)(event_loop_conn *conn);
@ -44,6 +45,7 @@ typedef struct http_route {
// starting the http loop // starting the http loop
regex_t *regex; regex_t *regex;
step steps[HTTP_LOOP_MAX_STEPS]; step steps[HTTP_LOOP_MAX_STEPS];
step steps_res[HTTP_LOOP_MAX_STEPS];
} http_route; } http_route;
/** /**
@ -116,14 +118,6 @@ typedef struct event_loop http_loop;
*/ */
bool http_loop_handle_request(event_loop_conn *conn); 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. * 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); 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. * 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); 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. * Initialize a new http loop.
* *

View File

@ -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_init = (void *(*)(void *))http_loop_ctx_init;
el->ctx_free = (void (*)(void *))http_loop_ctx_free; el->ctx_free = (void (*)(void *))http_loop_ctx_free;
el->handle_data = http_loop_handle_request; 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(); http_loop_gctx *gctx = http_loop_gctx_init();
gctx->c = custom_gctx; gctx->c = custom_gctx;

View File

@ -138,13 +138,11 @@ void http_loop_process_request(event_loop_conn *conn) {
ctx->current_step++; ctx->current_step++;
} }
if (conn->state != event_loop_conn_state_req) { // Request processing can stop early by switching the connection state
return; // Either way, we reset the step counter as it will be used by the response
} // steps
if ((conn->state != event_loop_conn_state_req) ||
// If we've reached the end of the list of step functions, we report the (ctx->route->steps[ctx->current_step] == NULL)) {
// request as finished by clearing its route ctx->current_step = 0;
if (ctx->route->steps[ctx->current_step] == NULL) {
http_loop_ctx_reset(ctx);
} }
} }

View File

@ -48,7 +48,7 @@ void http_loop_init_header(http_response *res) {
res->head_len = buf_size + 1; 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; http_response *res = &((http_loop_ctx *)conn->ctx)->res;
// Create head response // Create head response
@ -56,43 +56,68 @@ void http_loop_write_response(event_loop_conn *conn) {
http_loop_init_header(res); http_loop_init_header(res);
} }
// The final iteration marks the end of the response, after which we reset the // Step has finished its work
// context so a next request can be processed if (res->head_written == res->head_len) {
if (res->head_written == res->head_len && return true;
res->body.expected_len == res->body.len) {
http_loop_ctx_reset(conn->ctx);
conn->state = event_loop_conn_state_req;
return;
} }
if (res->head_written < res->head_len) { size_t bytes_to_write = MIN(res->head_len - res->head_written,
size_t bytes_to_write = MIN(res->head_len - res->head_written, EVENT_LOOP_BUFFER_SIZE - conn->wbuf_size);
EVENT_LOOP_BUFFER_SIZE - conn->wbuf_size); memcpy(&conn->wbuf[conn->wbuf_size], &res->head[res->head_written],
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); bytes_to_write);
conn->wbuf_size += 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) { return false;
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) { void http_loop_handle_response(event_loop_conn *conn) {
case http_body_buf: http_loop_ctx *ctx = conn->ctx;
memcpy(&conn->wbuf[conn->wbuf_size], &(res->body.buf)[res->body.len],
bytes_to_write); while ((conn->state == event_loop_conn_state_res) &&
conn->wbuf_size += bytes_to_write; (ctx->route->steps_res[ctx->current_step] != NULL) &&
res->body.len += bytes_to_write; ctx->route->steps_res[ctx->current_step](conn)) {
break; ctx->current_step++;
case http_body_file: }
bytes_written = fread(&conn->wbuf[conn->wbuf_size], sizeof(uint8_t),
bytes_to_write, res->body.file); // Response processing can stop early be switching the connection state
conn->wbuf_size += bytes_written; // After response processing has finished its work, we reset the context to
res->body.len += bytes_written; // prepare for a new request
break; 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;
} }
} }

View File

@ -6,21 +6,33 @@ http_route lander_routes[] = {
{.type = http_route_literal, {.type = http_route_literal,
.method = http_get, .method = http_get,
.path = "/", .path = "/",
.steps = {lander_get_index, NULL}}, .steps = {lander_get_index, NULL},
{.type = http_route_regex, .steps_res = {http_loop_step_write_header, http_loop_step_write_body,
.method = http_get, NULL}},
.path = "^/([^/]+)$", {
.steps = {lander_get_entry, NULL}}, .type = http_route_regex,
{.type = http_route_regex, .method = http_get,
.method = http_post, .path = "^/([^/]+)$",
.path = "^/s(l?)/([^/]*)$", .steps = {lander_get_entry, NULL},
.steps = {http_loop_step_auth, http_loop_step_body_to_buf, .steps_res = {http_loop_step_write_header, http_loop_step_write_body,
lander_post_redirect, NULL}}, 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, {.type = http_route_regex,
.method = http_post, .method = http_post,
.path = "^/p(l?)/([^/]*)$", .path = "^/p(l?)/([^/]*)$",
.steps = {http_loop_step_auth, lander_post_paste, .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)); } void *lander_gctx_init() { return calloc(1, sizeof(lander_gctx)); }