lander/lnm/src/http/lnm_http_loop_process.c

392 lines
12 KiB
C

#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include "lnm/http/consts.h"
#include "lnm/http/loop.h"
#include "lnm/http/loop_internal.h"
#include "lnm/http/req.h"
#include "lnm/log.h"
#include "lnm/loop.h"
#include "lnm/loop_internal.h"
static const char *section = "http";
/* static const lnm_http_loop_state lnm_http_loop_state_first_req =
* lnm_http_loop_state_parse_req; */
static const lnm_http_loop_state lnm_http_loop_state_first_res =
lnm_http_loop_state_add_headers;
void lnm_http_loop_process_parse_req(lnm_http_conn *conn) {
lnm_http_loop_ctx *ctx = conn->ctx;
lnm_http_parse_err res = lnm_http_req_parse(
&ctx->req, &conn->r.buf[conn->r.read], conn->r.size - conn->r.read);
switch (res) {
case lnm_http_parse_err_ok:
conn->r.read += ctx->req.buf.len;
ctx->state = lnm_http_loop_state_route;
lnm_linfo(section, "%s %.*s HTTP/1.%i",
lnm_http_method_names[ctx->req.method], (int)ctx->req.path.len,
ctx->req.buf.s + ctx->req.path.o, ctx->req.minor_version);
break;
case lnm_http_parse_err_incomplete:
// If the request is already the size of the read buffer, we close the
// request. Otherwise, we wait for anything read
if (conn->r.size - conn->r.read == LNM_LOOP_BUF_SIZE) {
lnm_linfo(section, "Received request larger than buffer (%i bytes)",
LNM_LOOP_BUF_SIZE);
conn->state = lnm_loop_state_end;
}
break;
case lnm_http_parse_err_invalid:
lnm_linfo(section, "%s", "Received invalid request");
conn->state = lnm_loop_state_end;
break;
case lnm_http_parse_err_unknown_method:
ctx->res.status = lnm_http_status_method_not_implemented;
ctx->state = lnm_http_loop_state_first_res;
break;
}
}
void lnm_http_loop_process_route(lnm_http_conn *conn) {
lnm_http_loop_ctx *ctx = conn->ctx;
lnm_http_loop_gctx *gctx = ctx->g;
// 0: no match
// 1: matched route, but not method
// 2: fully matched route
int match_level = 0;
lnm_http_route *route;
for (size_t i = 0; i < gctx->routes.len && match_level < 3; i++) {
route = gctx->routes.arr[i];
bool matched_path = false;
switch (route->type) {
case lnm_http_route_type_literal:
matched_path = strncmp(route->route.s, ctx->req.buf.s + ctx->req.path.o,
ctx->req.path.len) == 0;
break;
case lnm_http_route_type_regex:
matched_path =
regexec(route->route.regex, ctx->req.buf.s + ctx->req.path.o,
LNM_HTTP_MAX_REGEX_GROUPS, ctx->req.path.groups, 0) == 0;
break;
}
// GET routes also automatically route HEAD requests
bool matched_method = route->method == ctx->req.method ||
(route->method == lnm_http_method_get &&
ctx->req.method == lnm_http_method_head);
int new_match_level = 2 * matched_path + matched_method;
// Remember the previous match levels so we can return the correct status
// message
match_level = match_level < new_match_level ? new_match_level : match_level;
}
switch (match_level) {
case 0:
case 1:
ctx->res.status = lnm_http_status_not_found;
ctx->state = lnm_http_loop_state_first_res;
break;
case 2:
ctx->res.status = lnm_http_status_method_not_allowed;
ctx->state = lnm_http_loop_state_first_res;
break;
case 3:
ctx->route = route;
ctx->cur_step = route->step;
ctx->state = lnm_http_loop_state_parse_headers;
break;
}
}
void lnm_http_loop_process_parse_headers(lnm_http_conn *conn) {
lnm_http_loop_ctx *ctx = conn->ctx;
lnm_http_req *req = &ctx->req;
const char *value;
size_t value_len;
if (lnm_http_req_header_get(&value, &value_len, req,
lnm_http_header_content_length) == lnm_err_ok) {
req->body.expected_len = lnm_atoi(value, value_len);
}
ctx->state = lnm_http_loop_state_steps;
}
void lnm_http_loop_process_steps(lnm_http_conn *conn) {
lnm_http_loop_ctx *ctx = conn->ctx;
lnm_http_step *step = NULL;
// Loop until we either:
// - reach the end of the chain of steps, indicated by NULL
// - have a step that's waiting for I/O
while ((ctx->cur_step != NULL) && (step != ctx->cur_step)) {
step = ctx->cur_step;
switch (step->fn(conn)) {
case lnm_http_step_err_done:
ctx->cur_step = ctx->cur_step->next;
break;
case lnm_http_step_err_io_needed:
break;
case lnm_http_step_err_close:
conn->state = lnm_loop_state_end;
break;
case lnm_http_step_err_res:
ctx->state = lnm_http_loop_state_first_res;
break;
}
}
if (ctx->cur_step == NULL) {
ctx->state = lnm_http_loop_state_add_headers;
}
}
void lnm_http_loop_state_process_add_headers(lnm_http_conn *conn) {
lnm_http_loop_ctx *ctx = conn->ctx;
lnm_http_res *res = &ctx->res;
uint64_t digits = lnm_digits(res->body.len);
char *buf = malloc(digits + 1);
if (buf == NULL) {
conn->state = lnm_loop_state_end;
return;
}
sprintf(buf, "%lu", res->body.len);
lnm_http_res_add_header_len(res, lnm_http_header_content_length, buf, digits,
true);
if (ctx->g->server != NULL) {
lnm_http_res_add_header(res, lnm_http_header_server, (char *)ctx->g->server,
false);
}
if (res->status == 0) {
res->status = lnm_http_status_ok;
}
lnm_linfo(section, "%i %s", res->status,
lnm_http_status_names[res->status / 100 - 1][res->status % 100]);
ctx->state = lnm_http_loop_state_write_status_line;
}
// This function is intentionally written inefficiently for now, as it will most
// likely only have to run once for each response
void lnm_http_loop_process_write_status_line(lnm_http_conn *conn) {
lnm_http_loop_ctx *ctx = conn->ctx;
lnm_http_res *res = &ctx->res;
const char *response_type_name =
lnm_http_status_names[res->status / 100 - 1][res->status % 100];
// First we calculate the size of the start of the header
size_t buf_size =
snprintf(NULL, 0, "HTTP/1.1 %i %s\n", res->status, response_type_name);
char buf[buf_size + 1];
sprintf(buf, "HTTP/1.1 %i %s\n", res->status, response_type_name);
size_t to_write =
LNM_MIN(buf_size - res->written, LNM_LOOP_BUF_SIZE - conn->w.size);
memcpy(&conn->w.buf[conn->w.size], &buf[res->written], to_write);
conn->w.size += to_write;
res->written += to_write;
if (res->written == buf_size) {
res->written = 0;
ctx->state = lnm_http_loop_state_write_headers;
}
}
void lnm_http_loop_process_write_headers(lnm_http_conn *conn) {
lnm_http_loop_ctx *ctx = conn->ctx;
lnm_http_res *res = &ctx->res;
lnm_http_res_header *header;
// Loop as long as we can still write new data and have headers to write
while ((conn->w.size < LNM_LOOP_BUF_SIZE) &&
((header = res->headers.current) != NULL)) {
size_t buf_len = header->name.len + 2 + header->value.len + 1;
// Here, we also constantly calculate the entire buffer as we assume each
// header will be written in one go
char buf[buf_len];
memcpy(buf, header->name.s, header->name.len);
memcpy(&buf[header->name.len + 2], header->value.s, header->value.len);
buf[header->name.len] = ':';
buf[header->name.len + 1] = ' ';
buf[buf_len - 1] = '\n';
size_t to_write =
LNM_MIN(buf_len - res->written, LNM_LOOP_BUF_SIZE - conn->w.size);
memcpy(&conn->w.buf[conn->w.size], &buf[res->written], to_write);
conn->w.size += to_write;
res->written += to_write;
if (res->written == buf_len) {
res->written = 0;
res->headers.current = res->headers.current->next;
}
}
// The headers should end with an additional newline. If there's no space left
// in the write buffer, we don't switch states so we can re-try this write
// later
if ((res->headers.current == NULL) && (conn->w.size < LNM_LOOP_BUF_SIZE)) {
conn->w.buf[conn->w.size] = '\n';
conn->w.size++;
// HEAD requests function exactly the same as GET requests, except that they
// skip the body writing part
ctx->state =
ctx->req.method != lnm_http_method_head && ctx->res.body.len > 0
? lnm_http_loop_state_write_body
: lnm_http_loop_state_finish;
}
}
void lnm_http_loop_process_write_body(lnm_http_conn *conn) {
lnm_http_loop_ctx *ctx = conn->ctx;
lnm_http_res *res = &ctx->res;
size_t to_write =
LNM_MIN(res->body.len - res->written, LNM_LOOP_BUF_SIZE - conn->w.size);
size_t written = 0;
switch (res->body.type) {
case lnm_http_res_body_type_buf:
memcpy(&conn->w.buf[conn->w.size], &res->body.data.buf[res->written],
to_write);
written = to_write;
break;
case lnm_http_res_body_type_file:
written = fread(&conn->w.buf[conn->w.size], 1, to_write, res->body.data.f);
if ((written == 0) && (ferror(res->body.data.f) != 0)) {
ctx->state = lnm_http_loop_state_finish;
}
break;
case lnm_http_res_body_type_fn:
if (res->body.data.fn(&written, &conn->w.buf[conn->w.size], conn,
res->written, to_write) != lnm_err_ok) {
ctx->state = lnm_http_loop_state_finish;
}
break;
}
conn->w.size += written;
res->written += written;
if (res->written == res->body.len) {
ctx->state = lnm_http_loop_state_finish;
}
}
void lnm_http_loop_process_finish(lnm_http_conn *conn) {
// First we ensure the write buffer is fully flushed
if (conn->w.size > 0) {
return;
}
lnm_http_loop_ctx *ctx = conn->ctx;
lnm_http_loop_ctx_reset(ctx);
ctx->state = lnm_http_loop_state_parse_req;
}
void (*process_fns[])(lnm_http_conn *conn) = {
lnm_http_loop_process_parse_req,
lnm_http_loop_process_route,
lnm_http_loop_process_parse_headers,
lnm_http_loop_process_steps,
lnm_http_loop_state_process_add_headers,
lnm_http_loop_process_write_status_line,
lnm_http_loop_process_write_headers,
lnm_http_loop_process_write_body,
lnm_http_loop_process_finish,
};
lnm_loop_state state_map[] = {
// parse_req
lnm_loop_state_req,
// route
lnm_loop_state_req,
// parse_headers
lnm_loop_state_req,
// steps
lnm_loop_state_req,
// add_headers
lnm_loop_state_req,
// write_status_line
lnm_loop_state_res,
// write_headers
lnm_loop_state_res,
// write_body
lnm_loop_state_res,
// finish
lnm_loop_state_res,
};
void lnm_http_loop_process(lnm_http_conn *conn) {
lnm_http_loop_ctx *ctx = conn->ctx;
lnm_http_loop_state http_loop_state;
lnm_loop_state loop_state = conn->state;
// We stop processing if:
// - the event loop state has been explicitely changed inside the executed
// step, as we need to switch to the other I/O loop
// - the event loop state needs to be changed because the next step should be
// run in another event loop state
// - the process fn returned without changing the HTTP loop state, indicating
// it's waiting for I/O
do {
http_loop_state = ctx->state;
process_fns[http_loop_state](conn);
} while ((conn->state == state_map[ctx->state]) &&
(http_loop_state != ctx->state));
// Check required to prevent overwriting manually set event loop state
conn->state = conn->state == loop_state ? state_map[ctx->state] : conn->state;
// We move the request to a dedicated buffer if the read buffer needs to be
// reused
if ((conn->state == lnm_loop_state_req) && (conn->state == loop_state) &&
(!ctx->req.buf.owned) && (ctx->req.buf.len > 0)) {
char *buf = malloc(ctx->req.buf.len);
if (buf == NULL) {
lnm_lerror(section,
"Failed to allocate request buffer; closing connection %i",
conn->fd);
conn->state = lnm_loop_state_end;
} else {
memcpy(buf, ctx->req.buf.s, ctx->req.buf.len);
ctx->req.buf.s = buf;
ctx->req.buf.owned = true;
lnm_ldebug(section, "Allocated request buffer for connection %i",
conn->fd);
}
}
}