diff --git a/backend/.clang-format b/backend/.clang-format index c4323f6..a56cbd0 100644 --- a/backend/.clang-format +++ b/backend/.clang-format @@ -6,7 +6,6 @@ IndentCaseLabels: true InsertNewlineAtEOF: true AllowShortFunctionsOnASingleLine: None -AlignAfterOpenBracket: AlwaysBreak BinPackArguments: false AllowAllArgumentsOnNextLine: true diff --git a/backend/src/http/client.c b/backend/src/http/client.c new file mode 100644 index 0000000..c5966e9 --- /dev/null +++ b/backend/src/http/client.c @@ -0,0 +1,247 @@ +#include "client.h" +#include "../str_util.h" +#include "packet.h" +#include +#include +#include +#include +#include +#include + +static inline int parse_request_header(Client* client, Request* request); +static inline int recieve_request_body(Client* client, Request* request); +static inline int next_line(Client* client, StrSlice* slice); +static inline int next_char(Client* client, char* ch); +static inline int next_u8(Client* client, uint8_t* ch); + +Client* http_client_new(ClientConnection connection) +{ + Client* client = malloc(sizeof(Client) + CLIENT_BUFFER_SIZE); + *client = (Client) { + .connection = connection, + .buffer_i = 0, + .buffer_size = 0, + }; + return client; +} + +void http_client_free(Client* client) +{ + free(client); +} + +int http_client_next(Client* client, Request* request) +{ + if (parse_request_header(client, request) != 0) + return -1; + + if (request->method == Method_POST) { + if (!http_request_has_header(request, "Content-Length")) { + fprintf(stderr, + "error: POST request has no body and/or Content-Length " + "header\n"); + return -1; + } + if (recieve_request_body(client, request) != 0) + return -1; + } + return 0; +} + +static inline int recieve_request_body(Client* client, Request* request) +{ + const char* length_val = http_request_get_header(request, "Content-Length"); + size_t length = strtoul(length_val, NULL, 10); + + uint8_t* body = calloc(length + 1, sizeof(uint8_t)); + for (size_t i = 0; i < length; ++i) { + uint8_t ch; + if (next_u8(client, &ch) != 0) + return -1; + body[i] = ch; + } + + request->body = body; + request->body_size = length; + + return 0; +} + +static inline int parse_request_header(Client* client, Request* request) +{ + + StrSlice req_line; + if (next_line(client, &req_line) != 0) + return -1; + StrSplitter req_line_splitter + = str_splitter(req_line.ptr, req_line.len, " "); + StrSlice method_str = str_split_next(&req_line_splitter); + StrSlice uri_str = str_split_next(&req_line_splitter); + StrSlice version_str = str_split_next(&req_line_splitter); + + if (strncmp(version_str.ptr, "HTTP/1.1", 8) != 0) { + fprintf(stderr, + "error: unrecognized http version '%.*s'\n", + (int)version_str.len, + version_str.ptr); + return -1; + } + + if (method_str.len >= 8) { + fprintf(stderr, "error: malformed http method\n"); + return -1; + } + char normalized_method[8] = ""; + for (size_t i = 0; i < method_str.len; ++i) { + normalized_method[i] = (char)toupper(method_str.ptr[i]); + } + + struct MethodStrMapEntry { + const char* key; + Method val; + }; + const struct MethodStrMapEntry method_str_map[] = { + { "GET", Method_GET }, + { "POST", Method_POST }, + }; + const size_t method_str_map_size + = sizeof(method_str_map) / sizeof(method_str_map[0]); + + Method method; + bool method_found = false; + for (size_t i = 0; i < method_str_map_size; ++i) { + if (strncmp(normalized_method, method_str_map[i].key, method_str.len) + == 0) { + method = method_str_map[i].val; + method_found = true; + break; + } + } + if (!method_found) { + fprintf(stderr, + "error: unrecognized http method '%.*s'\n", + (int)method_str.len, + method_str.ptr); + return -1; + } + + if (uri_str.len >= MAX_PATH_LEN + 1) { + fprintf(stderr, "error: path too long\n"); + return -1; + } + + size_t path_len = 0; + while (path_len < uri_str.len && uri_str.ptr[path_len] != '?' + && uri_str.ptr[path_len] != '#') { + path_len += 1; + } + + char* path = calloc(path_len + 1, sizeof(char)); + strncpy(path, uri_str.ptr, path_len); + path[path_len] = '\0'; + + char* query = NULL; + if (path_len < uri_str.len) { + size_t query_len = 0; + while (path_len + query_len < uri_str.len + && uri_str.ptr[path_len + query_len] != '#') { + query_len += 1; + } + query = calloc(query_len + 1, sizeof(char)); + strncpy(query, &uri_str.ptr[path_len], query_len); + query[query_len] = '\0'; + } + + HeaderVec headers; + header_vec_construct(&headers); + + while (headers.size < MAX_HEADERS_LEN) { + StrSlice line; + if (next_line(client, &line) != 0) + return -1; + if (line.len == 0) { + break; + } + + size_t key_len = 0; + while (key_len < line.len && line.ptr[key_len] != ':') { + key_len += 1; + } + if (key_len == 0 || key_len > MAX_HEADER_KEY_LEN) { + fprintf(stderr, "error: header key too long\n"); + return -1; + } + size_t value_begin = key_len + 1; + while (value_begin < line.len && line.ptr[value_begin] == ' ') { + value_begin += 1; + } + size_t value_len = line.len - value_begin; + if (value_len == 0 || value_len > MAX_HEADER_VALUE_LEN) { + fprintf(stderr, "error: header value too long, %ld\n", value_len); + return -1; + } + + char* key = calloc(key_len + 1, sizeof(char)); + strncpy(key, line.ptr, key_len); + + char* value = calloc(value_len + 1, sizeof(char)); + strncpy(value, &line.ptr[value_begin], value_len); + + header_vec_push(&headers, (Header) { key, value }); + } + + *request = (Request) { + method, + path, + query, + headers, + .body = NULL, + .body_size = 0, + }; + return 0; +} + +static inline int next_line(Client* client, StrSlice* slice) +{ + size_t begin = client->buffer_i; + size_t len = 0; + char last = '\0'; + char ch; + if (next_char(client, &ch) != 0) + return -1; + len += 1; + while (!(last == '\r' && ch == '\n')) { + last = ch; + if (next_char(client, &ch) != 0) + return -1; + len += 1; + } + *slice = (StrSlice) { + .ptr = (const char*)&client->buffer[begin], + .len = len - 2, + }; + return 0; +} + +static inline int next_char(Client* client, char* ch) +{ + return next_u8(client, (uint8_t*)ch); +} + +static inline int next_u8(Client* client, uint8_t* ch) +{ + if (client->buffer_i >= client->buffer_size) { + ssize_t bytes_received = recv( + client->connection.file, client->buffer, CLIENT_BUFFER_SIZE, 0); + if (bytes_received == -1) { + fprintf(stderr, + "error: could not receive from client: %s\n", + strerror(errno)); + return -1; + } + client->buffer_i = 0; + client->buffer_size = (size_t)bytes_received; + } + *ch = client->buffer[client->buffer_i++]; + return 0; +} diff --git a/backend/src/http/client.h b/backend/src/http/client.h new file mode 100644 index 0000000..b0cabfc --- /dev/null +++ b/backend/src/http/client.h @@ -0,0 +1,20 @@ +#pragma once + +#include "client_connection.h" +#include "request.h" +#include + +#define CLIENT_BUFFER_SIZE 8192 + +typedef struct { + ClientConnection connection; + size_t buffer_i; + size_t buffer_size; + uint8_t buffer[]; +} Client; + +Client* http_client_new(ClientConnection connection); +void http_client_free(Client* client); + +// Returns not 0 on error +int http_client_next(Client* client, Request* request); diff --git a/backend/src/http/client_connection.h b/backend/src/http/client_connection.h new file mode 100644 index 0000000..0bd419a --- /dev/null +++ b/backend/src/http/client_connection.h @@ -0,0 +1,15 @@ +#pragma once + +#include "../collection.h" +#include +#include + +typedef struct sockaddr SockAddr; +typedef struct sockaddr_in SockAddrIn; + +typedef struct { + int file; + SockAddrIn addr; +} ClientConnection; + +DEFINE_STATIC_QUEUE(ClientConnection, ClientQueue, client_queue) diff --git a/backend/src/http/packet.h b/backend/src/http/packet.h new file mode 100644 index 0000000..f0fbe25 --- /dev/null +++ b/backend/src/http/packet.h @@ -0,0 +1,24 @@ +#pragma once + +#include "../collection.h" +#include + +#define MAX_HEADER_BUFFER_SIZE 65536 + +#define MAX_PATH_LEN 128 - 1 +#define MAX_QUERY_LEN 128 - 1 +#define MAX_HEADERS_LEN 32 +#define MAX_HEADER_KEY_LEN 32 - 1 +#define MAX_HEADER_VALUE_LEN 512 - 1 + +typedef enum { + Method_GET, + Method_POST, +} Method; + +typedef struct { + char* key; + char* value; +} Header; + +DEFINE_VEC(Header, HeaderVec, header_vec, 8) diff --git a/backend/src/http/request.c b/backend/src/http/request.c new file mode 100644 index 0000000..c5b2796 --- /dev/null +++ b/backend/src/http/request.c @@ -0,0 +1,178 @@ +#include "request.h" +#include "../str_util.h" +#include "packet.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int http_request_parse( + Request* req, size_t* body_idx, const char* const buf, size_t buf_size) +{ + StrSplitter splitter = str_splitter(buf, buf_size, "\r\n"); + + StrSlice req_line = str_split_next(&splitter); + StrSplitter req_line_splitter + = str_splitter(req_line.ptr, req_line.len, " "); + StrSlice method_str = str_split_next(&req_line_splitter); + StrSlice uri_str = str_split_next(&req_line_splitter); + StrSlice version_str = str_split_next(&req_line_splitter); + + if (strncmp(version_str.ptr, "HTTP/1.1", 8) != 0) { + fprintf(stderr, + "error: unrecognized http version '%.*s'\n", + (int)version_str.len, + version_str.ptr); + return -1; + } + + if (method_str.len >= 8) { + fprintf(stderr, "error: malformed http method\n"); + return -1; + } + char normalized_method[8] = ""; + for (size_t i = 0; i < method_str.len; ++i) { + normalized_method[i] = (char)toupper(method_str.ptr[i]); + } + + Method method; + if (strncmp(normalized_method, "GET", method_str.len) == 0) { + method = Method_GET; + } else if (strncmp(normalized_method, "POST", method_str.len) == 0) { + method = Method_POST; + } else { + fprintf(stderr, + "error: unrecognized http method '%.*s'\n", + (int)method_str.len, + method_str.ptr); + return -1; + } + + if (uri_str.len >= MAX_PATH_LEN + 1) { + fprintf(stderr, "error: path too long\n"); + return -1; + } + + size_t path_len = 0; + while (path_len < uri_str.len && uri_str.ptr[path_len] != '?' + && uri_str.ptr[path_len] != '#') { + path_len += 1; + } + + char* path = calloc(path_len + 1, sizeof(char)); + strncpy(path, uri_str.ptr, path_len); + path[path_len] = '\0'; + + char* query = NULL; + if (path_len < uri_str.len) { + size_t query_len = 0; + while (path_len + query_len < uri_str.len + && uri_str.ptr[path_len + query_len] != '#') { + query_len += 1; + } + query = calloc(query_len + 1, sizeof(char)); + strncpy(query, &uri_str.ptr[path_len], query_len); + query[query_len] = '\0'; + } + + HeaderVec headers; + header_vec_construct(&headers); + + while (headers.size < MAX_HEADERS_LEN) { + StrSlice line = str_split_next(&splitter); + if (line.len == 0) { + *body_idx = (size_t)&line.ptr[2] - (size_t)buf; + break; + } + + size_t key_len = 0; + while (key_len < line.len && line.ptr[key_len] != ':') { + key_len += 1; + } + if (key_len == 0 || key_len > MAX_HEADER_KEY_LEN) { + fprintf(stderr, "error: header key too long\n"); + return -1; + } + size_t value_begin = key_len + 1; + while (value_begin < line.len && line.ptr[value_begin] == ' ') { + value_begin += 1; + } + size_t value_len = line.len - value_begin; + if (value_len == 0 || value_len > MAX_HEADER_VALUE_LEN) { + fprintf(stderr, "error: header value too long, %ld\n", value_len); + return -1; + } + + char* key = calloc(key_len + 1, sizeof(char)); + strncpy(key, line.ptr, key_len); + + char* value = calloc(value_len + 1, sizeof(char)); + strncpy(value, &line.ptr[value_begin], value_len); + + header_vec_push(&headers, (Header) { key, value }); + } + + *req = (Request) { + method, + path, + query, + headers, + .body = NULL, + .body_size = 0, + }; + return 0; +} + +void http_request_destroy(Request* req) +{ + free(req->path); + for (size_t i = 0; i < req->headers.size; ++i) { + free(req->headers.data[i].key); + free(req->headers.data[i].value); + } + header_vec_destroy(&req->headers); + if (req->body) + free(req->body); +} + +static inline int strcmp_lower(const char* a, const char* b) +{ + size_t i = 0; + for (; a[i] != '\0' && b[i] != '\0'; ++i) { + if (tolower(a[i]) != tolower(b[i])) { + return 1; + } + } + if (a[i] != b[i]) { + return 1; + } + return 0; +} + +bool http_request_has_header(const Request* req, const char* key) +{ + for (size_t i = 0; i < req->headers.size; ++i) { + if (strcmp_lower(key, req->headers.data[i].key) == 0) { + return true; + } + } + return false; +} + +const char* http_request_get_header(const Request* req, const char* key) +{ + for (size_t i = 0; i < req->headers.size; ++i) { + if (strcmp_lower(key, req->headers.data[i].key) == 0) { + return req->headers.data[i].value; + } + } + return NULL; +} diff --git a/backend/src/http/request.h b/backend/src/http/request.h new file mode 100644 index 0000000..ef6f317 --- /dev/null +++ b/backend/src/http/request.h @@ -0,0 +1,20 @@ +#pragma once + +#include "packet.h" +#include + +typedef struct { + Method method; + char* path; + char* query; + HeaderVec headers; + uint8_t* body; + size_t body_size; +} Request; + +/// On error, returns -1. +int http_request_parse( + Request* req, size_t* body_idx, const char* const buf, size_t buf_size); +void http_request_destroy(Request* req); +bool http_request_has_header(const Request* req, const char* key); +const char* http_request_get_header(const Request* req, const char* key); diff --git a/backend/src/http/server.c b/backend/src/http/server.c index 4df7330..e284ff6 100644 --- a/backend/src/http/server.c +++ b/backend/src/http/server.c @@ -1,7 +1,7 @@ #include "server.h" #include "../http.h" #include "../str_util.h" -#include +#include #include #include #include @@ -17,37 +17,46 @@ HttpServer* http_server_new(HttpServerOpts opts) { - int server_fd = socket(AF_INET, SOCK_STREAM, 0); - if (server_fd == -1) { - fprintf(stderr, "error: could not initialize socket\n"); + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd == -1) { + fprintf(stderr, + "error: could not initialize socket: %s\n", + strerror(errno)); return NULL; } - SockAddrIn server_addr; - server_addr.sin_family = AF_INET; - server_addr.sin_addr.s_addr = INADDR_ANY; - server_addr.sin_port = htons(opts.port); + SockAddrIn addr; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(opts.port); int reuse = 1; - setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); - - int res = bind(server_fd, (SockAddr*)&server_addr, sizeof(server_addr)); + int res = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); if (res != 0) { - fprintf(stderr, "error: could not bind socket\n"); + fprintf(stderr, + "error: could not set socket options: %s\n", + strerror(errno)); return NULL; } - res = listen(server_fd, 16); + res = bind(fd, (SockAddr*)&addr, sizeof(addr)); if (res != 0) { - fprintf(stderr, "error: could not listen on socket\n"); + fprintf(stderr, "error: could not bind socket: %s\n", strerror(errno)); + return NULL; + } + + res = listen(fd, 16); + if (res != 0) { + fprintf( + stderr, "error: could not listen on socket: %s\n", strerror(errno)); return NULL; } HttpServer* server = malloc(sizeof(HttpServer)); *server = (HttpServer) { - .file = server_fd, - .addr = server_addr, - // .ctx = {}, + .fd = fd, + .addr = addr, + .ctx = (WorkerCtx) { 0 }, .workers = malloc(sizeof(Worker) * opts.workers_amount), .workers_size = opts.workers_amount, .handlers = { 0 }, @@ -55,9 +64,9 @@ HttpServer* http_server_new(HttpServerOpts opts) .user_ctx = NULL, }; - worker_ctx_construct(&server->ctx, server); + http_worker_ctx_construct(&server->ctx, server); for (size_t i = 0; i < opts.workers_amount; ++i) { - worker_construct(&server->workers[i], &server->ctx); + http_worker_construct(&server->workers[i], &server->ctx); } handler_vec_construct(&server->handlers); @@ -66,11 +75,11 @@ HttpServer* http_server_new(HttpServerOpts opts) void http_server_free(HttpServer* server) { - close(server->file); + close(server->fd); for (size_t i = 0; i < server->workers_size; ++i) { - worker_destroy(&server->workers[i]); + http_worker_destroy(&server->workers[i]); } - worker_ctx_destroy(&server->ctx); + http_worker_ctx_destroy(&server->ctx); handler_vec_destroy(&server->handlers); free(server); } @@ -83,13 +92,13 @@ int http_server_listen(HttpServer* server) SockAddrIn client_addr; socklen_t addr_size = sizeof(client_addr); - int res = accept(server->file, (SockAddr*)&client_addr, &addr_size); + int res = accept(server->fd, (SockAddr*)&client_addr, &addr_size); if (res == -1) { fprintf(stderr, "error: could not accept\n"); return -1; } - Client req = { .file = res, client_addr }; + ClientConnection req = { .file = res, client_addr }; pthread_mutex_lock(&ctx->mutex); res = client_queue_push(&ctx->req_queue, req); @@ -138,12 +147,12 @@ const char* http_ctx_req_path(HttpCtx* ctx) bool http_ctx_req_headers_has(HttpCtx* ctx, const char* key) { - return request_has_header(ctx->req, key); + return http_request_has_header(ctx->req, key); } const char* http_ctx_req_headers_get(HttpCtx* ctx, const char* key) { - return request_get_header(ctx->req, key); + return http_request_get_header(ctx->req, key); } const char* http_ctx_req_body(HttpCtx* ctx) @@ -170,8 +179,7 @@ void http_ctx_respond(HttpCtx* ctx, int status, const char* body) string_construct(&res); char first_line[32]; - snprintf( - first_line, + snprintf(first_line, 32 - 1, "HTTP/1.1 %d %s\r\n", status, @@ -195,347 +203,3 @@ void http_ctx_respond(HttpCtx* ctx, int status, const char* body) string_destroy(&res); } - -// -// -// - -static inline void -worker_ctx_construct(WorkerCtx* ctx, const HttpServer* server) -{ - ctx->server = server; - pthread_mutex_init(&ctx->mutex, NULL); - pthread_cond_init(&ctx->cond, NULL); - client_queue_construct(&ctx->req_queue, 8192); -} - -static inline void worker_ctx_destroy(WorkerCtx* ctx) -{ - pthread_mutex_destroy(&ctx->mutex); - pthread_cond_destroy(&ctx->cond); - client_queue_destroy(&ctx->req_queue); -} - -static inline int worker_construct(Worker* worker, WorkerCtx* ctx) -{ - *worker = (Worker) { - // .thread = {}, - .ctx = ctx, - }; - - pthread_create(&worker->thread, NULL, worker_thread_fn, worker); - return 0; -} - -static inline void worker_destroy(Worker* worker) -{ - if (worker->thread != 0) { - pthread_cancel(worker->thread); - - // a bit ugly, but who cares? - pthread_cond_broadcast(&worker->ctx->cond); - - pthread_join(worker->thread, NULL); - } -} - -static inline void* worker_thread_fn(void* data) -{ - Worker* worker = data; - worker_listen(worker); - return NULL; -} - -static inline void worker_listen(Worker* worker) -{ - WorkerCtx* ctx = worker->ctx; - while (true) { - pthread_testcancel(); - - pthread_mutex_lock(&ctx->mutex); - - if (client_queue_size(&ctx->req_queue) > 0) { - Client req; - client_queue_pop(&ctx->req_queue, &req); - pthread_mutex_unlock(&ctx->mutex); - - worker_handle_request(worker, &req); - continue; - } - - pthread_cond_wait(&ctx->cond, &ctx->mutex); - - pthread_mutex_unlock(&ctx->mutex); - } -} - -static inline void worker_handle_request(Worker* worker, Client* client) -{ - (void)worker; - - uint8_t* buffer = calloc(MAX_HEADER_BUFFER_SIZE, sizeof(char)); - ssize_t bytes_received - = recv(client->file, buffer, MAX_HEADER_BUFFER_SIZE * sizeof(char), 0); - - if (bytes_received == -1) { - fprintf(stderr, "error: could not receive request\n"); - goto l0_return; - } - - size_t header_end = 0; - for (ssize_t i = 0; i < bytes_received - 3; ++i) { - if (memcmp((char*)&buffer[i], "\r\n\r\n", 4)) { - header_end = (size_t)i + 5; - } - } - if (header_end == 0) { - fprintf( - stderr, - "error: header too big, exceeded %d bytes\n", - MAX_HEADER_BUFFER_SIZE); - goto l0_return; - } - - Request req; - size_t body_idx; - int res = parse_request_header(&req, &body_idx, (char*)buffer, header_end); - if (res != 0) { - fprintf(stderr, "error: failed to parse header\n"); - goto l0_return; - } - - char* body = NULL; - if (req.method == Method_POST) { - if (!request_has_header(&req, "Content-Length")) { - fprintf( - stderr, - "error: POST request has no body and/or Content-Length " - "header\n"); - goto l1_return; - } - const char* length_val = request_get_header(&req, "Content-Length"); - size_t length = strtoul(length_val, NULL, 10); - body = calloc(length + 1, sizeof(char)); - strncpy(body, (char*)&buffer[body_idx], length); - body[length] = '\0'; - - // HACK - // TODO: We should treat the input as a stream rather than a block. - // Look at either @camper0008's stream example or other HTTP-server - // implementations. - size_t defacto_length = strlen(body); - int attempts = 0; - const int arbitrary_max_attempts = 10; - while (defacto_length < length && attempts < arbitrary_max_attempts) { - attempts += 1; - - uint8_t buffer[8192]; - ssize_t recieved = recv(client->file, buffer, 8192, 0); - - if (recieved == -1) { - fprintf(stderr, "error: could not receive request body\n"); - goto l1_return; - } - - strncpy(&body[defacto_length], (char*)buffer, (size_t)recieved); - defacto_length += (size_t)recieved; - body[defacto_length] = '\0'; - } - if (defacto_length < length) { - fprintf(stderr, "error: could not receive entire request body\n"); - goto l1_return; - } - } - - HttpCtx handler_ctx = { - .client = client, - .req = &req, - .req_body = body, - .res_headers = { 0 }, - .user_ctx = worker->ctx->server->user_ctx, - }; - - header_vec_construct(&handler_ctx.res_headers); - - bool been_handled = false; - - for (size_t i = 0; i < worker->ctx->server->handlers.size; ++i) { - Handler* handler = &worker->ctx->server->handlers.data[i]; - if (handler->method != req.method) - continue; - if (strcmp(handler->path, req.path) != 0) - continue; - handler->handler(&handler_ctx); - been_handled = true; - break; - } - - if (!been_handled && worker->ctx->server->not_found_handler != NULL) { - worker->ctx->server->not_found_handler(&handler_ctx); - } - -l1_return: - header_vec_destroy(&handler_ctx.res_headers); - request_destroy(&req); - if (body) - free(body); -l0_return: - free(buffer); - close(client->file); -} - -static inline int parse_request_header( - Request* req, size_t* body_idx, const char* const buf, size_t buf_size) -{ - StrSplitter splitter = str_split(buf, buf_size, "\r\n"); - - StrSlice req_line = strsplit_next(&splitter); - StrSplitter req_line_splitter = str_split(req_line.ptr, req_line.len, " "); - StrSlice method_str = strsplit_next(&req_line_splitter); - StrSlice uri_str = strsplit_next(&req_line_splitter); - StrSlice version_str = strsplit_next(&req_line_splitter); - - if (strncmp(version_str.ptr, "HTTP/1.1", 8) != 0) { - fprintf( - stderr, - "error: unrecognized http version '%.*s'\n", - (int)version_str.len, - version_str.ptr); - return -1; - } - - if (method_str.len >= 8) { - fprintf(stderr, "error: malformed http method\n"); - return -1; - } - char normalized_method[8] = ""; - for (size_t i = 0; i < method_str.len; ++i) { - normalized_method[i] = (char)toupper(method_str.ptr[i]); - } - - Method method; - if (strncmp(normalized_method, "GET", method_str.len) == 0) { - method = Method_GET; - } else if (strncmp(normalized_method, "POST", method_str.len) == 0) { - method = Method_POST; - } else { - fprintf( - stderr, - "error: unrecognized http method '%.*s'\n", - (int)method_str.len, - method_str.ptr); - return -1; - } - - if (uri_str.len >= MAX_PATH_LEN + 1) { - fprintf(stderr, "error: path too long\n"); - return -1; - } - - size_t path_len = 0; - while (path_len < uri_str.len && uri_str.ptr[path_len] != '?' - && uri_str.ptr[path_len] != '#') { - path_len += 1; - } - - char* path = calloc(path_len + 1, sizeof(char)); - strncpy(path, uri_str.ptr, path_len); - path[path_len] = '\0'; - - char* query = NULL; - if (path_len < uri_str.len) { - size_t query_len = 0; - while (path_len + query_len < uri_str.len - && uri_str.ptr[path_len + query_len] != '#') { - query_len += 1; - } - query = calloc(query_len + 1, sizeof(char)); - strncpy(query, &uri_str.ptr[path_len], query_len); - query[query_len] = '\0'; - } - - HeaderVec headers; - header_vec_construct(&headers); - - while (headers.size < MAX_HEADERS_LEN) { - StrSlice line = strsplit_next(&splitter); - if (line.len == 0) { - *body_idx = (size_t)&line.ptr[2] - (size_t)buf; - break; - } - - size_t key_len = 0; - while (key_len < line.len && line.ptr[key_len] != ':') { - key_len += 1; - } - if (key_len == 0 || key_len > MAX_HEADER_KEY_LEN) { - fprintf(stderr, "error: header key too long\n"); - return -1; - } - size_t value_begin = key_len + 1; - while (value_begin < line.len && line.ptr[value_begin] == ' ') { - value_begin += 1; - } - size_t value_len = line.len - value_begin; - if (value_len == 0 || value_len > MAX_HEADER_VALUE_LEN) { - fprintf(stderr, "error: header value too long, %ld\n", value_len); - return -1; - } - - char* key = calloc(key_len + 1, sizeof(char)); - strncpy(key, line.ptr, key_len); - - char* value = calloc(value_len + 1, sizeof(char)); - strncpy(value, &line.ptr[value_begin], value_len); - - header_vec_push(&headers, (Header) { key, value }); - } - - *req = (Request) { method, path, query, headers }; - return 0; -} - -static inline void request_destroy(Request* req) -{ - free(req->path); - for (size_t i = 0; i < req->headers.size; ++i) { - free(req->headers.data[i].key); - free(req->headers.data[i].value); - } - header_vec_destroy(&req->headers); -} - -static inline int strcmp_lower(const char* a, const char* b) -{ - size_t i = 0; - for (; a[i] != '\0' && b[i] != '\0'; ++i) { - if (tolower(a[i]) != tolower(b[i])) { - return 1; - } - } - if (a[i] != b[i]) { - return 1; - } - return 0; -} - -static inline bool request_has_header(const Request* req, const char* key) -{ - for (size_t i = 0; i < req->headers.size; ++i) { - if (strcmp_lower(key, req->headers.data[i].key) == 0) { - return true; - } - } - return false; -} - -static inline const char* -request_get_header(const Request* req, const char* key) -{ - for (size_t i = 0; i < req->headers.size; ++i) { - if (strcmp_lower(key, req->headers.data[i].key) == 0) { - return req->headers.data[i].value; - } - } - return NULL; -} diff --git a/backend/src/http/server.h b/backend/src/http/server.h index 0a6221b..08d13c8 100644 --- a/backend/src/http/server.h +++ b/backend/src/http/server.h @@ -2,6 +2,10 @@ #include "../collection.h" #include "../http.h" +#include "client_connection.h" +#include "packet.h" +#include "request.h" +#include "worker.h" #include #include #include @@ -11,75 +15,6 @@ #include #include -typedef struct sockaddr SockAddr; -typedef struct sockaddr_in SockAddrIn; - -typedef struct { - int file; - SockAddrIn addr; -} Client; - -DEFINE_STATIC_QUEUE(Client, ClientQueue, client_queue) - -typedef struct { - const HttpServer* server; - pthread_mutex_t mutex; - pthread_cond_t cond; - ClientQueue req_queue; -} WorkerCtx; - -static inline void -worker_ctx_construct(WorkerCtx* ctx, const HttpServer* server); -static inline void worker_ctx_destroy(WorkerCtx* ctx); - -typedef struct { - pthread_t thread; - WorkerCtx* ctx; -} Worker; - -/// On ok, returns 0. -/// On error, returns -1; -static inline int worker_construct(Worker* worker, WorkerCtx* ctx); -static inline void worker_destroy(Worker* worker); -static inline void* worker_thread_fn(void* data); -static inline void worker_listen(Worker* worker); -static inline void worker_handle_request(Worker* worker, Client* req); - -#define MAX_HEADER_BUFFER_SIZE 65536 - -#define MAX_PATH_LEN 128 - 1 -#define MAX_QUERY_LEN 128 - 1 -#define MAX_HEADERS_LEN 32 -#define MAX_HEADER_KEY_LEN 32 - 1 -#define MAX_HEADER_VALUE_LEN 512 - 1 - -typedef enum { - Method_GET, - Method_POST, -} Method; - -typedef struct { - char* key; - char* value; -} Header; - -DEFINE_VEC(Header, HeaderVec, header_vec, 8) - -typedef struct { - Method method; - char* path; - char* query; - HeaderVec headers; -} Request; - -/// On error, returns -1. -static inline int parse_request_header( - Request* req, size_t* body_idx, const char* const buf, size_t buf_size); -static inline void request_destroy(Request* req); -static inline bool request_has_header(const Request* req, const char* key); -static inline const char* -request_get_header(const Request* req, const char* key); - typedef struct { const char* path; Method method; @@ -89,7 +24,7 @@ typedef struct { DEFINE_VEC(Handler, HandlerVec, handler_vec, 8) struct HttpServer { - int file; + int fd; SockAddrIn addr; WorkerCtx ctx; Worker* workers; @@ -100,7 +35,7 @@ struct HttpServer { }; struct HttpCtx { - Client* client; + ClientConnection* client; const Request* req; const char* req_body; HeaderVec res_headers; diff --git a/backend/src/http/worker.c b/backend/src/http/worker.c new file mode 100644 index 0000000..fc6f02b --- /dev/null +++ b/backend/src/http/worker.c @@ -0,0 +1,126 @@ +#include "worker.h" +#include "client.h" +#include "server.h" +#include +#include + +void http_worker_ctx_construct(WorkerCtx* ctx, const HttpServer* server) +{ + ctx->server = server; + pthread_mutex_init(&ctx->mutex, NULL); + pthread_cond_init(&ctx->cond, NULL); + client_queue_construct(&ctx->req_queue, 8192); +} + +void http_worker_ctx_destroy(WorkerCtx* ctx) +{ + pthread_mutex_destroy(&ctx->mutex); + pthread_cond_destroy(&ctx->cond); + client_queue_destroy(&ctx->req_queue); +} + +void http_worker_construct(Worker* worker, WorkerCtx* ctx) +{ + *worker = (Worker) { + .thread = (pthread_t) { 0 }, + .ctx = ctx, + }; + + pthread_create(&worker->thread, NULL, http_worker_thread_fn, worker); +} + +void http_worker_destroy(Worker* worker) +{ + if (worker->thread != 0) { + pthread_cancel(worker->thread); + + // a bit ugly, but who cares? + pthread_cond_broadcast(&worker->ctx->cond); + + pthread_join(worker->thread, NULL); + } +} + +void* http_worker_thread_fn(void* data) +{ + Worker* worker = data; + http_worker_listen(worker); + return NULL; +} + +void http_worker_listen(Worker* worker) +{ + WorkerCtx* ctx = worker->ctx; + while (true) { + pthread_testcancel(); + + pthread_mutex_lock(&ctx->mutex); + + if (client_queue_size(&ctx->req_queue) > 0) { + ClientConnection connection; + client_queue_pop(&ctx->req_queue, &connection); + pthread_mutex_unlock(&ctx->mutex); + + http_worker_handle_connection(worker, connection); + continue; + } + + pthread_cond_wait(&ctx->cond, &ctx->mutex); + + pthread_mutex_unlock(&ctx->mutex); + } +} + +void http_worker_handle_connection(Worker* worker, ClientConnection connection) +{ + (void)worker; + + Client* client = http_client_new(connection); + Request request; + + int res = http_client_next(client, &request); + if (res != 0) { + fprintf(stderr, + "warning: failed to parse request. sending 400 Bad Request " + "response\n"); + const char* res = "HTTP/1.1 400 Bad Request\r\n\r\n"; + ssize_t bytes_written = write(connection.file, res, strlen(res)); + if (bytes_written != (ssize_t)strlen(res)) { + fprintf(stderr, "error: could not send 400 Bad Request response\n"); + } + goto l0_return; + } + + HttpCtx handler_ctx = { + .client = &client->connection, + .req = &request, + .req_body = (char*)request.body, + .res_headers = { 0 }, + .user_ctx = worker->ctx->server->user_ctx, + }; + + header_vec_construct(&handler_ctx.res_headers); + + bool been_handled = false; + + for (size_t i = 0; i < worker->ctx->server->handlers.size; ++i) { + Handler* handler = &worker->ctx->server->handlers.data[i]; + if (handler->method != request.method) + continue; + if (strcmp(handler->path, request.path) != 0) + continue; + handler->handler(&handler_ctx); + been_handled = true; + break; + } + + if (!been_handled && worker->ctx->server->not_found_handler != NULL) { + worker->ctx->server->not_found_handler(&handler_ctx); + } + + header_vec_destroy(&handler_ctx.res_headers); + http_request_destroy(&request); +l0_return: + close(client->connection.file); + http_client_free(client); +} diff --git a/backend/src/http/worker.h b/backend/src/http/worker.h new file mode 100644 index 0000000..1cd2b98 --- /dev/null +++ b/backend/src/http/worker.h @@ -0,0 +1,26 @@ +#pragma once + +#include "../http.h" +#include "client_connection.h" +#include + +typedef struct { + const HttpServer* server; + pthread_mutex_t mutex; + pthread_cond_t cond; + ClientQueue req_queue; +} WorkerCtx; + +void http_worker_ctx_construct(WorkerCtx* ctx, const HttpServer* server); +void http_worker_ctx_destroy(WorkerCtx* ctx); + +typedef struct { + pthread_t thread; + WorkerCtx* ctx; +} Worker; + +void http_worker_construct(Worker* worker, WorkerCtx* ctx); +void http_worker_destroy(Worker* worker); +void* http_worker_thread_fn(void* data); +void http_worker_listen(Worker* worker); +void http_worker_handle_connection(Worker* worker, ClientConnection connection); diff --git a/backend/src/str_util.c b/backend/src/str_util.c index 1c2c9b7..5d855b7 100644 --- a/backend/src/str_util.c +++ b/backend/src/str_util.c @@ -14,7 +14,7 @@ char* str_dup(const char* str) return clone; } -StrSplitter str_split(const char* text, size_t text_len, const char* split) +StrSplitter str_splitter(const char* text, size_t text_len, const char* split) { return (StrSplitter) { .text = text, @@ -25,7 +25,7 @@ StrSplitter str_split(const char* text, size_t text_len, const char* split) }; } -StrSlice strsplit_next(StrSplitter* splitter) +StrSlice str_split_next(StrSplitter* splitter) { for (size_t i = splitter->i; i < splitter->text_len; ++i) { if (strncmp(&splitter->text[i], splitter->split, splitter->split_len) diff --git a/backend/src/str_util.h b/backend/src/str_util.h index 90ed380..6c7c3d8 100644 --- a/backend/src/str_util.h +++ b/backend/src/str_util.h @@ -20,8 +20,8 @@ typedef struct { size_t split_len; } StrSplitter; -StrSplitter str_split(const char* text, size_t text_len, const char* split); -StrSlice strsplit_next(StrSplitter* splitter); +StrSplitter str_splitter(const char* text, size_t text_len, const char* split); +StrSlice str_split_next(StrSplitter* splitter); DEFINE_VEC(char, String, string, 8)