368 lines
11 KiB
C
368 lines
11 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;
|
|
const lnm_http_loop_gctx *gctx = ctx->g;
|
|
|
|
switch (lnm_http_router_route(&ctx->req.route_match, gctx->router,
|
|
ctx->req.method,
|
|
ctx->req.buf.s + ctx->req.path.o)) {
|
|
case lnm_http_route_err_match:
|
|
ctx->cur_step = ctx->req.route_match.route->step;
|
|
ctx->state = lnm_http_loop_state_parse_headers;
|
|
break;
|
|
case lnm_http_route_err_unknown_method:
|
|
ctx->res.status = lnm_http_status_method_not_allowed;
|
|
ctx->state = lnm_http_loop_state_first_res;
|
|
break;
|
|
case lnm_http_route_err_unknown_route:
|
|
ctx->res.status = lnm_http_status_not_found;
|
|
ctx->state = lnm_http_loop_state_first_res;
|
|
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;
|
|
|
|
if (step->blocking && (conn->state != lnm_loop_state_req_work)) {
|
|
conn->state = lnm_loop_state_req_work;
|
|
|
|
break;
|
|
}
|
|
|
|
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:
|
|
// Ensure steps that require more I/O are executed on the event loop
|
|
conn->state = lnm_loop_state_req_io;
|
|
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) {
|
|
conn->state = lnm_loop_state_res_io;
|
|
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_io,
|
|
// route
|
|
lnm_loop_state_req_io,
|
|
// parse_headers
|
|
lnm_loop_state_req_io,
|
|
// steps
|
|
lnm_loop_state_req_io,
|
|
// add_headers
|
|
lnm_loop_state_req_io,
|
|
// write_status_line
|
|
lnm_loop_state_res_io,
|
|
// write_headers
|
|
lnm_loop_state_res_io,
|
|
// write_body
|
|
lnm_loop_state_res_io,
|
|
// finish
|
|
lnm_loop_state_res_io,
|
|
};
|
|
|
|
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_io) && (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);
|
|
}
|
|
}
|
|
}
|