commit 5b374ebc4599f18b3f150f6a964091f895bf005b Author: Sky Johnson Date: Mon Sep 15 12:57:54 2025 -0500 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b0b527 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/test +*.o diff --git a/app.c b/app.c new file mode 100644 index 0000000..ef51c7b --- /dev/null +++ b/app.c @@ -0,0 +1,318 @@ +#include "app.h" +#include "reactor/pool.h" +#include "reactor/epoll.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEFAULT_MAX_CONNS 10000 +#define DEFAULT_MAX_EVENTS 4096 + +// Default 404 handler +static void default_not_found(context_t *ctx) { + // send_text(ctx, 404, "404 Not Found"); +} + +// Accept handler for new connections +static void accept_handler(runtime_t *rt, int fd, uint32_t events, void *data) { + app_t *a = (app_t*)data; + + // Accept multiple connections in a loop for edge-triggered mode + // Limit to prevent starvation of other events + int accepted = 0; + const int max_accept = 64; + + while (accepted < max_accept) { + int ret = conn_accept(a->pool, fd); + if (ret < 0) break; + accepted++; + } +} + +// Global app pointer (temporary solution) +static app_t *g_app = NULL; + +// Wrapper to convert between handler types +static void handler_wrapper(conn_t *c) { + app_handler_t app_handler = (app_handler_t)c->user_data; + if (app_handler) { + context_t *ctx = context_new(c); + if (ctx) { + app_handler(ctx); + context_free(ctx); + } + } +} + +// Request router +static void route_request(conn_t *c) { + app_t *a = g_app; // Use global for now + if (!a || !a->router) return; + + // Use the router to find handler + char **params = NULL; + char **names = NULL; + int count = 0; + handler_t handler = router_lookup(a->router, c->method, c->path, ¶ms, &names, &count); + + if (handler) { + // The handler is actually an app_handler_t cast to handler_t + // Create context and call it + context_t *ctx = context_new(c); + if (ctx) { + // Store params in context (we'd need to add this to context) + // For now, just call the handler + ((app_handler_t)handler)(ctx); + context_free(ctx); + } + } else { + // Create a context and call not_found + context_t *ctx = context_new(c); + if (ctx) { + a->not_found(ctx); + context_free(ctx); + } + } +} + +// app_new creates a new web application. +app_t* app_new(void) { + app_t *a = calloc(1, sizeof(app_t)); + if (!a) return NULL; + + // Default configuration + a->max_conns = DEFAULT_MAX_CONNS; + a->read_buf_size = 8192; + a->write_buf_size = 8192; + a->arena_size = 4096; + a->not_found = default_not_found; + + // Create router + a->router = router_new(); + if (!a->router) { + free(a); + return NULL; + } + + // Store global reference (temporary solution) + g_app = a; + + return a; +} + +// app_free destroys the application. +void app_free(app_t *a) { + if (!a) return; + + // Free router + if (a->router) { + router_free(a->router); + } + + // Close listener + if (a->listen_fd > 0) { + runtime_del(a->rt, a->listen_fd); + close(a->listen_fd); + } + + // Free pool and runtime + pool_free(a->pool); + runtime_free(a->rt); + free(a); +} + +// app_listen sets up the listening socket. +int app_listen(app_t *a, int port) { + // Create runtime and pool if not already done + if (!a->rt) { + a->rt = runtime_new(DEFAULT_MAX_EVENTS); + if (!a->rt) return -1; + } + + if (!a->pool) { + a->pool = pool_new(a->rt, a->max_conns); + if (!a->pool) return -1; + + // Configure pool + a->pool->read_buf_size = a->read_buf_size; + a->pool->write_buf_size = a->write_buf_size; + a->pool->arena_size = a->arena_size; + + // Set router as request handler + a->pool->on_request = route_request; + } + + // Create socket + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) return -1; + + // Allow reuse + int opt = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + // Enable SO_REUSEPORT for better load distribution across CPU cores + setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)); + + // Disable Nagle's algorithm on listener + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); + + // Bind + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_port = htons(port), + .sin_addr.s_addr = INADDR_ANY + }; + + if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + close(fd); + return -1; + } + + // Listen with larger backlog for better performance + if (listen(fd, 4096) < 0) { + close(fd); + return -1; + } + + // Make non-blocking + set_nonblocking(fd); + + // Add to epoll + runtime_add(a->rt, fd, EPOLLIN | EPOLLET, accept_handler, a); + + a->listen_fd = fd; + a->port = port; + + printf("Server listening on :%d\n", port); + return 0; +} + +// app_run starts the event loop. +int app_run(app_t *a) { + if (!a->rt || a->listen_fd <= 0) { + return -1; + } + + // Override pool callback with router + a->pool->on_request = route_request; + + return runtime_run(a->rt); +} + +// app_route registers a route. +void app_route(app_t *a, const char *method, const char *path, app_handler_t handler) { + if (!a || !a->router) return; + + // For now, we need to bridge between app_handler_t and handler_t + // The router expects handler_t (conn_t*) but we have app_handler_t (context_t*) + // We'll need to wrap it somehow - for now just cast (not ideal) + router_add(a->router, method, path, (handler_t)handler); +} + +// Convenience route methods +void app_get(app_t *a, const char *path, app_handler_t handler) { + app_route(a, "GET", path, handler); +} + +void app_post(app_t *a, const char *path, app_handler_t handler) { + app_route(a, "POST", path, handler); +} + +void app_put(app_t *a, const char *path, app_handler_t handler) { + app_route(a, "PUT", path, handler); +} + +void app_delete(app_t *a, const char *path, app_handler_t handler) { + app_route(a, "DELETE", path, handler); +} + +// Configuration setters +void app_set_max_conns(app_t *a, int max) { + a->max_conns = max; +} + +void app_set_buffer_sizes(app_t *a, size_t read_size, size_t write_size) { + a->read_buf_size = read_size; + a->write_buf_size = write_size; +} + +void app_set_arena_size(app_t *a, size_t size) { + a->arena_size = size; +} + +void app_set_not_found(app_t *a, app_handler_t handler) { + a->not_found = handler; +} + +// Helper response functions +static void send_text(conn_t *c, int status, const char *text) { + conn_write_status(c, status); + conn_write_header(c, "Content-Type", "text/plain"); + conn_write_body(c, text, strlen(text)); +} + +void send_json(conn_t *c, int status, const char *json) { + conn_write_status(c, status); + conn_write_header(c, "Content-Type", "application/json"); + conn_write_body(c, json, strlen(json)); +} + +void send_html(conn_t *c, int status, const char *html) { + conn_write_status(c, status); + conn_write_header(c, "Content-Type", "text/html"); + conn_write_body(c, html, strlen(html)); +} + +void send_file(conn_t *c, const char *path) { + // Simple file serving (no caching, no range support) + FILE *f = fopen(path, "rb"); + if (!f) { + send_text(c, 404, "File not found"); + return; + } + + // Get file size + fseek(f, 0, SEEK_END); + size_t size = ftell(f); + fseek(f, 0, SEEK_SET); + + // Determine content type from extension + const char *content_type = "application/octet-stream"; + const char *ext = strrchr(path, '.'); + if (ext) { + if (strcmp(ext, ".html") == 0) content_type = "text/html"; + else if (strcmp(ext, ".css") == 0) content_type = "text/css"; + else if (strcmp(ext, ".js") == 0) content_type = "application/javascript"; + else if (strcmp(ext, ".json") == 0) content_type = "application/json"; + else if (strcmp(ext, ".png") == 0) content_type = "image/png"; + else if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0) content_type = "image/jpeg"; + else if (strcmp(ext, ".gif") == 0) content_type = "image/gif"; + else if (strcmp(ext, ".svg") == 0) content_type = "image/svg+xml"; + } + + // Send headers + conn_write_status(c, 200); + conn_write_header(c, "Content-Type", content_type); + + // For small files, read and send directly + if (size < c->wsize - c->wlen - 100) { + char *buf = malloc(size); + if (buf) { + fread(buf, 1, size, f); + conn_write_body(c, buf, size); + free(buf); + } + } else { + // File too large for buffer + conn_write_status(c, 500); + conn_end_response(c); + } + + fclose(f); +} diff --git a/app.h b/app.h new file mode 100644 index 0000000..2f93eb8 --- /dev/null +++ b/app.h @@ -0,0 +1,51 @@ +#ifndef APP_H +#define APP_H + +#include "context.h" +#include "router.h" +#include "reactor/pool.h" +#include "reactor/epoll.h" + +typedef struct app app_t; +typedef void (*app_handler_t)(context_t *ctx); + +// Web application +struct app { + runtime_t *rt; + pool_t *pool; + router_t *router; + int listen_fd; + int port; + + // Configuration + int max_conns; + size_t read_buf_size; + size_t write_buf_size; + size_t arena_size; + + // Default handlers + app_handler_t not_found; + app_handler_t error_handler; +}; + +// App lifecycle +app_t* app_new(void); +void app_free(app_t *a); +int app_listen(app_t *a, int port); +int app_run(app_t *a); + +// Route registration +void app_get(app_t *a, const char *path, app_handler_t handler); +void app_post(app_t *a, const char *path, app_handler_t handler); +void app_put(app_t *a, const char *path, app_handler_t handler); +void app_delete(app_t *a, const char *path, app_handler_t handler); +void app_route(app_t *a, const char *method, const char *path, app_handler_t handler); + +// Configuration +void app_set_max_conns(app_t *a, int max); +void app_set_buffer_sizes(app_t *a, size_t read_size, size_t write_size); +void app_set_arena_size(app_t *a, size_t size); +void app_set_not_found(app_t *a, app_handler_t handler); +void app_set_error(app_t *a, app_handler_t handler); + +#endif // APP_H diff --git a/context.c b/context.c new file mode 100644 index 0000000..864b698 --- /dev/null +++ b/context.c @@ -0,0 +1,145 @@ +#include "context.h" + +#include +#include + +// context_new creates a new context for a connection. +context_t* context_new(conn_t *conn) { + // Use connection's arena for all allocations + arena_t *arena = conn->arena; + + context_t *ctx = arena_alloc(arena, sizeof(context_t)); + if (!ctx) return NULL; + + ctx->conn = conn; + ctx->arena = arena; + ctx->data = NULL; + + // Create request + ctx->request = request_new(arena); + if (!ctx->request) return NULL; + + // Parse request from connection buffer + if (request_parse(ctx->request, conn->rbuf, conn->rlen) < 0) { + return NULL; + } + + // Create response + ctx->response = response_new(); + if (!ctx->response) return NULL; + + return ctx; +} + +// context_free frees the context. +void context_free(context_t *ctx) { + if (!ctx) return; + + // Response is malloced separately + response_free(ctx->response); + + // Everything else is in arena and will be freed with connection +} + +// context_set stores data in context. +void context_set(context_t *ctx, const char *key, void *value) { + context_data_t *d = arena_alloc(ctx->arena, sizeof(context_data_t)); + if (!d) return; + + d->key = arena_strdup(ctx->arena, key, strlen(key)); + d->value = value; + d->next = ctx->data; + ctx->data = d; +} + +// context_get retrieves data from context. +void* context_get(context_t *ctx, const char *key) { + context_data_t *d = ctx->data; + while (d) { + if (strcmp(d->key, key) == 0) { + return d->value; + } + d = d->next; + } + return NULL; +} + +// Request helpers +const char* ctx_param(context_t *ctx, const char *name) { + return request_param(ctx->request, name); +} + +const char* ctx_query(context_t *ctx, const char *name) { + return request_query(ctx->request, name); +} + +const char* ctx_header(context_t *ctx, const char *name) { + return request_header(ctx->request, name); +} + +const char* ctx_cookie(context_t *ctx, const char *name) { + return request_cookie(ctx->request, name); +} + +const char* ctx_body(context_t *ctx) { + return request_body(ctx->request); +} + +// Response helpers +void ctx_status(context_t *ctx, int code) { + response_status(ctx->response, code); +} + +void ctx_header_set(context_t *ctx, const char *name, const char *value) { + response_header(ctx->response, name, value); +} + +// ctx_text sends plain text response. +void ctx_text(context_t *ctx, int status, const char *text) { + response_text(ctx->response, status, text); + ctx_send(ctx); +} + +// ctx_html sends HTML response. +void ctx_html(context_t *ctx, int status, const char *html) { + response_html(ctx->response, status, html); + ctx_send(ctx); +} + +// ctx_json sends JSON response. +void ctx_json(context_t *ctx, int status, const char *json) { + response_json(ctx->response, status, json); + ctx_send(ctx); +} + +// ctx_redirect sends redirect response. +void ctx_redirect(context_t *ctx, const char *url) { + response_redirect(ctx->response, url, 302); + ctx_send(ctx); +} + +// ctx_error sends error response. +void ctx_error(context_t *ctx, int status, const char *message) { + // Check if client expects JSON + if (request_expects_json(ctx->request)) { + char json[256]; + snprintf(json, sizeof(json), "{\"error\":\"%s\"}", + message ? message : "Unknown error"); + response_json(ctx->response, status, json); + } else { + response_text(ctx->response, status, + message ? message : "Error"); + } + ctx_send(ctx); +} + +// ctx_send sends the response to client. +int ctx_send(context_t *ctx) { + conn_t *c = ctx->conn; + + // Serialize response to connection write buffer + c->wlen = response_serialize(ctx->response, c->wbuf, c->wsize); + c->wpos = 0; + + return 0; +} diff --git a/context.h b/context.h new file mode 100644 index 0000000..accb17d --- /dev/null +++ b/context.h @@ -0,0 +1,58 @@ +#ifndef CONTEXT_H +#define CONTEXT_H + +#include "request.h" +#include "response.h" +#include "reactor/conn.h" + +typedef struct context context_t; +typedef struct context_data context_data_t; + +// Key-value storage for context +struct context_data { + char *key; + void *value; + context_data_t *next; +}; + +// Request context +struct context { + request_t *request; + response_t *response; + conn_t *conn; + arena_t *arena; + + // User data storage + context_data_t *data; +}; + +// Context lifecycle +context_t* context_new(conn_t *conn); +void context_free(context_t *ctx); + +// Data storage +void context_set(context_t *ctx, const char *key, void *value); +void* context_get(context_t *ctx, const char *key); + +// Request helpers +const char* ctx_param(context_t *ctx, const char *name); +const char* ctx_query(context_t *ctx, const char *name); +const char* ctx_header(context_t *ctx, const char *name); +const char* ctx_cookie(context_t *ctx, const char *name); +const char* ctx_body(context_t *ctx); + +// Response helpers +void ctx_status(context_t *ctx, int code); +void ctx_header_set(context_t *ctx, const char *name, const char *value); + +// Send responses +void ctx_text(context_t *ctx, int status, const char *text); +void ctx_html(context_t *ctx, int status, const char *html); +void ctx_json(context_t *ctx, int status, const char *json); +void ctx_redirect(context_t *ctx, const char *url); +void ctx_error(context_t *ctx, int status, const char *message); + +// Send response to client +int ctx_send(context_t *ctx); + +#endif // CONTEXT_H diff --git a/reactor/arena.c b/reactor/arena.c new file mode 100644 index 0000000..2e31bc5 --- /dev/null +++ b/reactor/arena.c @@ -0,0 +1,66 @@ +#include "arena.h" + +#include +#include + +// arena_new creates a new arena with the specified size. +arena_t* arena_new(size_t size) { + arena_t *a = malloc(sizeof(arena_t)); + if (!a) return NULL; + + a->buf = malloc(size); + if (!a->buf) { + free(a); + return NULL; + } + + a->size = size; + a->used = 0; + a->next = NULL; + return a; +} + +// arena_free destroys the arena and its buffer. +void arena_free(arena_t *a) { + if (!a) return; + + // Free chained arenas if any + arena_t *next; + while (a) { + next = a->next; + free(a->buf); + free(a); + a = next; + } +} + +// arena_alloc allocates memory from the arena. +void* arena_alloc(arena_t *a, size_t size) { + // Align to 8 bytes for proper alignment + size = (size + 7) & ~7; + + if (a->used + size > a->size) { + // Could implement arena chaining here + // For now, just fail + return NULL; + } + + void *ptr = a->buf + a->used; + a->used += size; + return ptr; +} + +// arena_strdup duplicates a string into the arena. +char* arena_strdup(arena_t *a, const char *s, size_t len) { + char *copy = arena_alloc(a, len + 1); + if (!copy) return NULL; + memcpy(copy, s, len); + copy[len] = '\0'; + return copy; +} + +// arena_reset resets the arena for reuse. +void arena_reset(arena_t *a) { + a->used = 0; + // Could reset chained arenas here if implemented +} diff --git a/reactor/arena.h b/reactor/arena.h new file mode 100644 index 0000000..10d9184 --- /dev/null +++ b/reactor/arena.h @@ -0,0 +1,23 @@ +#ifndef ARENA_H +#define ARENA_H + +#include + +typedef struct arena arena_t; + +// Arena allocator for request-scoped allocations +struct arena { + char *buf; // memory buffer + size_t size; // total size + size_t used; // bytes used + arena_t *next; // for chaining arenas (future) +}; + +// Arena operations +arena_t* arena_new(size_t size); +void arena_free(arena_t *a); +void* arena_alloc(arena_t *a, size_t size); +char* arena_strdup(arena_t *a, const char *s, size_t len); +void arena_reset(arena_t *a); + +#endif // ARENA_H diff --git a/reactor/conn.c b/reactor/conn.c new file mode 100644 index 0000000..e096bf7 --- /dev/null +++ b/reactor/conn.c @@ -0,0 +1,299 @@ +#include "conn.h" +#include "pool.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +// Status text lookup +static const char* status_text(int code) { + switch (code) { + case 200: return "OK"; + case 201: return "Created"; + case 204: return "No Content"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 304: return "Not Modified"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 500: return "Internal Server Error"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + default: return "Unknown"; + } +} + +// conn_new creates a new connection. +conn_t* conn_new(pool_t *pool, size_t rbuf_size, size_t wbuf_size, size_t arena_size) { + conn_t *c = calloc(1, sizeof(conn_t)); + if (!c) return NULL; + + c->pool = pool; + c->state = CONN_IDLE; + + c->rbuf = malloc(rbuf_size); + c->rsize = rbuf_size; + + c->wbuf = malloc(wbuf_size); + c->wsize = wbuf_size; + + c->arena = arena_new(arena_size); + + if (!c->rbuf || !c->wbuf || !c->arena) { + conn_free(c); + return NULL; + } + + return c; +} + +// conn_free destroys a connection. +void conn_free(conn_t *c) { + if (!c) return; + if (c->fd > 0) close(c->fd); + free(c->rbuf); + free(c->wbuf); + arena_free(c->arena); + free(c); +} + +// conn_reset clears connection state for reuse. +void conn_reset(conn_t *c) { + // Preserve fd but reset buffers + c->rlen = 0; + c->rpos = 0; + c->wlen = 0; + c->wpos = 0; + + c->method = NULL; + c->path = NULL; + c->version = NULL; + c->headers = NULL; + c->body = NULL; + c->body_len = 0; + c->status_code = 0; + + arena_reset(c->arena); +} + +// Simple HTTP request line parser +static int parse_request_line(conn_t *c) { + char *p = c->rbuf + c->rpos; + char *end = c->rbuf + c->rlen; + char *line_end = memchr(p, '\n', end - p); + + if (!line_end) return 0; // need more data + + // Find method + char *method_end = memchr(p, ' ', line_end - p); + if (!method_end) return -1; // bad request + + c->method = arena_strdup(c->arena, p, method_end - p); + p = method_end + 1; + + // Find path + char *path_end = memchr(p, ' ', line_end - p); + if (!path_end) return -1; + + c->path = arena_strdup(c->arena, p, path_end - p); + p = path_end + 1; + + // Find version + char *version_end = line_end; + if (version_end > p && *(version_end - 1) == '\r') version_end--; + + c->version = arena_strdup(c->arena, p, version_end - p); + + // Move past the line + c->rpos = line_end - c->rbuf + 1; + + // For now, skip headers and assume no body + // Find blank line + p = c->rbuf + c->rpos; + while (p < end - 1) { + if (*p == '\n' && (p == c->rbuf + c->rpos || *(p-1) == '\r' || *(p-1) == '\n')) { + c->rpos = p - c->rbuf + 1; + return 1; // request complete + } + p++; + } + + return 0; // need more data +} + +// conn_read_handler handles read events. +void conn_read_handler(runtime_t *rt, int fd, uint32_t events, void *data) { + conn_t *c = (conn_t*)data; + pool_t *p = c->pool; + + if (events & (EPOLLHUP | EPOLLERR)) { + conn_close(c); + return; + } + + // Read all available data in a loop (edge-triggered) + while (1) { + ssize_t n = read(fd, c->rbuf + c->rlen, c->rsize - c->rlen); + if (n <= 0) { + if (n < 0 && errno == EAGAIN) break; + if (n == 0 || (n < 0 && errno != EAGAIN)) { + conn_close(c); + return; + } + } + c->rlen += n; + + // Buffer full, process what we have + if (c->rlen >= c->rsize) break; + } + + // Try to parse request - handle multiple requests in buffer + while (c->rpos < c->rlen) { + int status = parse_request_line(c); + if (status < 0) { + // Bad request + conn_write_status(c, 400); + conn_end_response(c); + c->state = CONN_WRITING; + runtime_mod(rt, fd, EPOLLOUT | EPOLLET, conn_write_handler, c); + break; + } else if (status > 0) { + // Request complete - call handler from pool + pool_handle_request(p, c); + c->state = CONN_WRITING; + runtime_mod(rt, fd, EPOLLOUT | EPOLLET, conn_write_handler, c); + break; + } else { + // Need more data + break; + } + } +} + +// conn_write_handler handles write events. +void conn_write_handler(runtime_t *rt, int fd, uint32_t events, void *data) { + conn_t *c = (conn_t*)data; + + if (events & (EPOLLHUP | EPOLLERR)) { + conn_close(c); + return; + } + + // Write buffered data + while (c->wpos < c->wlen) { + ssize_t n = write(fd, c->wbuf + c->wpos, c->wlen - c->wpos); + if (n < 0) { + if (errno == EAGAIN) return; + conn_close(c); + return; + } + c->wpos += n; + } + + // Response complete - reset for next request (keep-alive) + conn_reset(c); + c->state = CONN_READING; + runtime_mod(rt, fd, EPOLLIN | EPOLLET, conn_read_handler, c); +} + +// conn_accept accepts a new connection. +int conn_accept(pool_t *p, int listen_fd) { + struct sockaddr_in addr; + socklen_t addrlen = sizeof(addr); + + int fd = accept(listen_fd, (struct sockaddr*)&addr, &addrlen); + if (fd < 0) return -1; + + // Get connection from pool + conn_t *c = pool_get(p); + if (!c) { + close(fd); + return -1; + } + + // Setup connection + c->fd = fd; + set_nonblocking(fd); + + // Disable Nagle's algorithm for lower latency + int flag = 1; + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag)); + + // Add to epoll + runtime_add(pool_runtime(p), fd, EPOLLIN | EPOLLET, conn_read_handler, c); + + return 0; +} + +// conn_close closes a connection and returns it to the pool. +void conn_close(conn_t *c) { + if (c->fd > 0) { + runtime_del(pool_runtime(c->pool), c->fd); + close(c->fd); + c->fd = 0; + } + + pool_handle_close(c->pool, c); + pool_put(c->pool, c); +} + +// conn_write_status writes the HTTP status line. +int conn_write_status(conn_t *c, int code) { + c->status_code = code; + int n = snprintf(c->wbuf + c->wlen, c->wsize - c->wlen, + "HTTP/1.1 %d %s\r\n", code, status_text(code)); + if (n > 0) c->wlen += n; + return n; +} + +// conn_write_header writes an HTTP header. +int conn_write_header(conn_t *c, const char *name, const char *value) { + int n = snprintf(c->wbuf + c->wlen, c->wsize - c->wlen, + "%s: %s\r\n", name, value); + if (n > 0) c->wlen += n; + return n; +} + +// conn_write_body writes the response body. +int conn_write_body(conn_t *c, const char *data, size_t len) { + // Reserve space for headers and body upfront + size_t header_space = 64; // Enough for Content-Length header + size_t total_needed = header_space + len + 2; // +2 for \r\n + + if (c->wlen + total_needed > c->wsize) { + // Not enough space - truncate body + len = c->wsize - c->wlen - header_space - 2; + if (len <= 0) return -1; + } + + // Write Content-Length header directly + int n = snprintf(c->wbuf + c->wlen, header_space, + "Content-Length: %zu\r\n\r\n", len); + c->wlen += n; + + // Write body directly + memcpy(c->wbuf + c->wlen, data, len); + c->wlen += len; + + return len; +} + +// conn_end_response finalizes the response. +int conn_end_response(conn_t *c) { + // If no body was written, just end headers + if (c->status_code && c->wlen > 0) { + if (c->wlen + 2 < c->wsize) { + c->wbuf[c->wlen++] = '\r'; + c->wbuf[c->wlen++] = '\n'; + } + } + return 0; +} diff --git a/reactor/conn.h b/reactor/conn.h new file mode 100644 index 0000000..5169c52 --- /dev/null +++ b/reactor/conn.h @@ -0,0 +1,72 @@ +#ifndef CONN_H +#define CONN_H + +#include +#include +#include "arena.h" +#include "epoll.h" + +typedef struct conn conn_t; +typedef struct pool pool_t; // forward declaration + +// Connection states +typedef enum { + CONN_IDLE, // in free list + CONN_READING, // reading request + CONN_WRITING, // writing response + CONN_CLOSING // pending close +} conn_state_t; + +// HTTP connection +struct conn { + int fd; // socket file descriptor + conn_state_t state; // connection state + + // Read buffer + char *rbuf; // read buffer + size_t rsize; // read buffer size + size_t rlen; // bytes in read buffer + size_t rpos; // parse position + + // Write buffer + char *wbuf; // write buffer + size_t wsize; // write buffer size + size_t wlen; // bytes in write buffer + size_t wpos; // bytes written + + // Request data + arena_t *arena; // per-request arena + char *method; // GET, POST, etc + char *path; // /path/to/resource + char *version; // HTTP/1.1 + void *headers; // header storage (TBD) + char *body; // request body + size_t body_len; // body length + + // Response building + int status_code; // 200, 404, etc + + // Pool management + pool_t *pool; // owning pool + conn_t *next; // for free list + void *user_data; // app-specific data +}; + +// Connection operations +conn_t* conn_new(pool_t *pool, size_t rbuf_size, size_t wbuf_size, size_t arena_size); +void conn_free(conn_t *c); +void conn_reset(conn_t *c); +void conn_close(conn_t *c); +int conn_accept(pool_t *p, int listen_fd); + +// Internal handlers +void conn_read_handler(runtime_t *rt, int fd, uint32_t events, void *data); +void conn_write_handler(runtime_t *rt, int fd, uint32_t events, void *data); + +// Response building +int conn_write_status(conn_t *c, int code); +int conn_write_header(conn_t *c, const char *name, const char *value); +int conn_write_body(conn_t *c, const char *data, size_t len); +int conn_end_response(conn_t *c); + +#endif // CONN_H diff --git a/reactor/epoll.c b/reactor/epoll.c new file mode 100644 index 0000000..95144f2 --- /dev/null +++ b/reactor/epoll.c @@ -0,0 +1,203 @@ +#include "epoll.h" + +#include +#include +#include +#include +#include + +// fd_data holds per-file-descriptor data for O(1) dispatch +typedef struct fd_data { + handler_fn handler; // callback for this FD + void *user_data; // arbitrary user data passed to handler +} fd_data_t; + +// runtime manages the event loop +struct runtime { + int epfd; // epoll file descriptor + int max_events; // max events per epoll_wait + struct epoll_event *events; // buffer for epoll_wait results + fd_data_t *handlers; // array indexed by FD + int max_fd; // current size of handlers array + bool running; // controls event loop +}; + +// runtime_new creates a new event loop runtime. +// max_events controls batching - higher values mean fewer syscalls but more latency. +// Typical values are 128-1024. +runtime_t* runtime_new(int max_events) { + // Allocate runtime structure with all fields zeroed + runtime_t *rt = calloc(1, sizeof(runtime_t)); + if (!rt) return NULL; + + // Create epoll instance with CLOEXEC for exec() safety + rt->epfd = epoll_create1(EPOLL_CLOEXEC); + if (rt->epfd < 0) { + free(rt); + return NULL; + } + + // Allocate event buffer for epoll_wait results + rt->max_events = max_events; + rt->events = calloc(max_events, sizeof(struct epoll_event)); + if (!rt->events) { + close(rt->epfd); + free(rt); + return NULL; + } + + // Initialize handler array with reasonable initial size + // This will grow dynamically as needed + rt->max_fd = 1024; + rt->handlers = calloc(rt->max_fd, sizeof(fd_data_t)); + if (!rt->handlers) { + free(rt->events); + close(rt->epfd); + free(rt); + return NULL; + } + + rt->running = false; + return rt; +} + +// runtime_free destroys a runtime and frees all resources. +// Does NOT close user file descriptors - caller is responsible for those. +void runtime_free(runtime_t *rt) { + if (!rt) return; + close(rt->epfd); + free(rt->events); + free(rt->handlers); + free(rt); +} + +// ensure_capacity grows the handlers array if needed. +// The handlers array is indexed by FD for O(1) lookup. +// Growth is done in chunks of 1024 to minimize reallocations. +static int ensure_capacity(runtime_t *rt, int fd) { + if (fd >= rt->max_fd) { + // Grow by 1024 FDs at a time to reduce realloc frequency + int new_max = fd + 1024; + fd_data_t *new_handlers = realloc(rt->handlers, new_max * sizeof(fd_data_t)); + if (!new_handlers) return -1; + + // Zero out the new portion for NULL handler checks + memset(new_handlers + rt->max_fd, 0, (new_max - rt->max_fd) * sizeof(fd_data_t)); + rt->handlers = new_handlers; + rt->max_fd = new_max; + } + return 0; +} + +// runtime_add adds a file descriptor to the event loop. +// Common event flags: +// - EPOLLIN: Ready for reading +// - EPOLLOUT: Ready for writing +// - EPOLLET: Edge-triggered mode (recommended for performance) +// - EPOLLONESHOT: Disable after one event (useful for thread pools) +int runtime_add(runtime_t *rt, int fd, uint32_t events, handler_fn handler, void *data) { + // Make sure our handler array is big enough + if (ensure_capacity(rt, fd) < 0) return -1; + + // Configure epoll_event - store FD for easy retrieval + struct epoll_event ev = { + .events = events, + .data.fd = fd + }; + + // Register with epoll + if (epoll_ctl(rt->epfd, EPOLL_CTL_ADD, fd, &ev) < 0) { + return -1; + } + + // Store handler and user data for O(1) dispatch + rt->handlers[fd].handler = handler; + rt->handlers[fd].user_data = data; + return 0; +} + +// runtime_mod modifies events and/or handler for an existing FD. +// Useful for switching between read/write modes or changing handlers. +int runtime_mod(runtime_t *rt, int fd, uint32_t events, handler_fn handler, void *data) { + if (ensure_capacity(rt, fd) < 0) return -1; + + struct epoll_event ev = { + .events = events, + .data.fd = fd + }; + + // Update epoll registration + if (epoll_ctl(rt->epfd, EPOLL_CTL_MOD, fd, &ev) < 0) { + return -1; + } + + // Update handler storage + rt->handlers[fd].handler = handler; + rt->handlers[fd].user_data = data; + return 0; +} + +// runtime_del removes a file descriptor from the event loop. +// Does NOT close the FD - caller is responsible for that. +int runtime_del(runtime_t *rt, int fd) { + // Remove from epoll (NULL event allowed on Linux >= 2.6.9) + if (epoll_ctl(rt->epfd, EPOLL_CTL_DEL, fd, NULL) < 0) { + return -1; + } + + // Clear handler storage if FD is in range + if (fd < rt->max_fd) { + rt->handlers[fd].handler = NULL; + rt->handlers[fd].user_data = NULL; + } + return 0; +} + +// runtime_run starts the event loop. +// Blocks and dispatches handlers as events occur until runtime_stop() is called. +// Handlers receive: runtime, fd, events mask, and their user data. +int runtime_run(runtime_t *rt) { + rt->running = true; + + while (rt->running) { + // Wait for events. -1 timeout means block indefinitely + int n = epoll_wait(rt->epfd, rt->events, rt->max_events, -1); + + if (n < 0) { + // EINTR happens on signals - just retry + if (errno == EINTR) continue; + return -1; + } + + // Process all ready file descriptors + for (int i = 0; i < n; i++) { + int fd = rt->events[i].data.fd; + uint32_t events = rt->events[i].events; + + // Bounds check and NULL check before dispatch + // FDs can be removed by handlers, so we must check + if (fd < rt->max_fd && rt->handlers[fd].handler) { + rt->handlers[fd].handler(rt, fd, events, rt->handlers[fd].user_data); + } + } + } + + return 0; +} + +// runtime_stop signals the event loop to stop. +// Can be called from signal handlers or event handlers for clean shutdown. +void runtime_stop(runtime_t *rt) { + rt->running = false; +} + +// set_nonblocking makes a file descriptor non-blocking. +// Essential for epoll servers - blocking I/O would freeze the entire event loop. +int set_nonblocking(int fd) { + // Get current flags + int flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) return -1; + + // Add O_NONBLOCK flag + return fcntl(fd, F_SETFL, flags | O_NONBLOCK); +} diff --git a/reactor/epoll.h b/reactor/epoll.h new file mode 100644 index 0000000..2537f66 --- /dev/null +++ b/reactor/epoll.h @@ -0,0 +1,25 @@ +#ifndef EPOLL_H +#define EPOLL_H + +#include +#include +#include + +typedef struct runtime runtime_t; +typedef void (*handler_fn)(runtime_t *rt, int fd, uint32_t events, void *data); + +// Core runtime functions +runtime_t* runtime_new(int max_events); +void runtime_free(runtime_t *rt); +int runtime_run(runtime_t *rt); +void runtime_stop(runtime_t *rt); + +// File descriptor management +int runtime_add(runtime_t *rt, int fd, uint32_t events, handler_fn handler, void *data); +int runtime_mod(runtime_t *rt, int fd, uint32_t events, handler_fn handler, void *data); +int runtime_del(runtime_t *rt, int fd); + +// Utility functions +int set_nonblocking(int fd); + +#endif // EPOLL_H diff --git a/reactor/pool.c b/reactor/pool.c new file mode 100644 index 0000000..aade3bf --- /dev/null +++ b/reactor/pool.c @@ -0,0 +1,108 @@ +#include "pool.h" + +#include +#include + +#define DEFAULT_READ_BUF 8192 +#define DEFAULT_WRITE_BUF 8192 +#define DEFAULT_ARENA 4096 + +// pool_new creates a new connection pool. +pool_t* pool_new(runtime_t *rt, int max_conns) { + pool_t *p = calloc(1, sizeof(pool_t)); + if (!p) return NULL; + + p->rt = rt; + p->max_conns = max_conns; + p->read_buf_size = DEFAULT_READ_BUF; + p->write_buf_size = DEFAULT_WRITE_BUF; + p->arena_size = DEFAULT_ARENA; + + // Allocate connection array + p->conns = calloc(max_conns, sizeof(conn_t)); + if (!p->conns) { + free(p); + return NULL; + } + + // Initialize each connection + for (int i = 0; i < max_conns; i++) { + conn_t *c = &p->conns[i]; + c->pool = p; + c->state = CONN_IDLE; + + c->rbuf = malloc(p->read_buf_size); + c->rsize = p->read_buf_size; + + c->wbuf = malloc(p->write_buf_size); + c->wsize = p->write_buf_size; + + c->arena = arena_new(p->arena_size); + + if (!c->rbuf || !c->wbuf || !c->arena) { + // Cleanup on failure + pool_free(p); + return NULL; + } + + // Add to free list + c->next = p->free_list; + p->free_list = c; + } + + return p; +} + +// pool_free destroys the pool and all connections. +void pool_free(pool_t *p) { + if (!p) return; + + for (int i = 0; i < p->max_conns; i++) { + conn_t *c = &p->conns[i]; + if (c->fd > 0) close(c->fd); + free(c->rbuf); + free(c->wbuf); + arena_free(c->arena); + } + + free(p->conns); + free(p); +} + +// pool_get retrieves a connection from the free list. +conn_t* pool_get(pool_t *p) { + if (!p->free_list) return NULL; + + conn_t *c = p->free_list; + p->free_list = c->next; + c->next = NULL; + c->state = CONN_READING; + return c; +} + +// pool_put returns a connection to the free list. +void pool_put(pool_t *p, conn_t *c) { + conn_reset(c); + c->state = CONN_IDLE; + c->next = p->free_list; + p->free_list = c; +} + +// pool_runtime returns the pool's runtime. +runtime_t* pool_runtime(pool_t *p) { + return p->rt; +} + +// pool_handle_request calls the request handler if set. +void pool_handle_request(pool_t *p, conn_t *c) { + if (p->on_request) { + p->on_request(c); + } +} + +// pool_handle_close calls the close handler if set. +void pool_handle_close(pool_t *p, conn_t *c) { + if (p->on_close) { + p->on_close(c); + } +} diff --git a/reactor/pool.h b/reactor/pool.h new file mode 100644 index 0000000..3383fea --- /dev/null +++ b/reactor/pool.h @@ -0,0 +1,37 @@ +#ifndef POOL_H +#define POOL_H + +#include "conn.h" +#include "epoll.h" + +// Connection pool +struct pool { + conn_t *conns; // array of connections + int max_conns; // pool size + conn_t *free_list; // available connections + runtime_t *rt; // epoll runtime + + // Buffer configuration + size_t read_buf_size; // per-conn read buffer + size_t write_buf_size; // per-conn write buffer + size_t arena_size; // per-request arena size + + // Callbacks + void (*on_request)(conn_t *c); // request complete + void (*on_close)(conn_t *c); // connection closed +}; + +// Pool management +pool_t* pool_new(runtime_t *rt, int max_conns); +void pool_free(pool_t *p); +conn_t* pool_get(pool_t *p); +void pool_put(pool_t *p, conn_t *c); + +// Accessors +runtime_t* pool_runtime(pool_t *p); + +// Internal handlers +void pool_handle_request(pool_t *p, conn_t *c); +void pool_handle_close(pool_t *p, conn_t *c); + +#endif // POOL_H diff --git a/request.c b/request.c new file mode 100644 index 0000000..3a83cfc --- /dev/null +++ b/request.c @@ -0,0 +1,325 @@ +#include "request.h" + +#include +#include +#include + +// request_new creates a new request. +request_t* request_new(arena_t *arena) { + request_t *req = arena_alloc(arena, sizeof(request_t)); + if (!req) return NULL; + + memset(req, 0, sizeof(request_t)); + req->arena = arena; + return req; +} + +// parse_request_line parses the first line of HTTP request. +static int parse_request_line(request_t *req, const char *line, size_t len) { + const char *p = line; + const char *end = line + len; + + // Find method + const char *method_end = memchr(p, ' ', end - p); + if (!method_end) return -1; + + req->method = arena_strdup(req->arena, p, method_end - p); + p = method_end + 1; + + // Find path (may include query string) + const char *path_end = memchr(p, ' ', end - p); + if (!path_end) return -1; + + // Check for query string + const char *query = memchr(p, '?', path_end - p); + if (query) { + req->path = arena_strdup(req->arena, p, query - p); + req->query_string = arena_strdup(req->arena, query + 1, path_end - query - 1); + } else { + req->path = arena_strdup(req->arena, p, path_end - p); + } + + p = path_end + 1; + + // Find version + const char *version_end = end; + if (version_end > p && *(version_end - 1) == '\r') version_end--; + + req->version = arena_strdup(req->arena, p, version_end - p); + + return 0; +} + +// add_header adds a header to the request. +static void add_header(request_t *req, const char *name, size_t name_len, + const char *value, size_t value_len) { + header_t *h = arena_alloc(req->arena, sizeof(header_t)); + if (!h) return; + + h->name = arena_strdup(req->arena, name, name_len); + h->value = arena_strdup(req->arena, value, value_len); + h->next = req->headers; + req->headers = h; + req->header_count++; +} + +// request_parse parses an HTTP request. +int request_parse(request_t *req, const char *buf, size_t len) { + const char *p = buf; + const char *end = buf + len; + + // Find end of request line + const char *line_end = memchr(p, '\n', end - p); + if (!line_end) return -1; + + // Parse request line + if (parse_request_line(req, p, line_end - p) < 0) return -1; + + p = line_end + 1; + + // Parse headers + while (p < end) { + // Check for end of headers + if (*p == '\r' && p + 1 < end && *(p + 1) == '\n') { + p += 2; + break; + } else if (*p == '\n') { + p++; + break; + } + + // Find header name + const char *colon = memchr(p, ':', end - p); + if (!colon) break; + + const char *name_end = colon; + while (name_end > p && isspace(*(name_end - 1))) name_end--; + + // Find header value + const char *value_start = colon + 1; + while (value_start < end && isspace(*value_start)) value_start++; + + const char *value_end = memchr(value_start, '\n', end - value_start); + if (!value_end) value_end = end; + + if (value_end > value_start && *(value_end - 1) == '\r') value_end--; + + // Add header + add_header(req, p, name_end - p, value_start, value_end - value_start); + + p = value_end; + if (p < end && *p == '\r') p++; + if (p < end && *p == '\n') p++; + } + + // Store body + if (p < end) { + req->body_len = end - p; + req->body = arena_strdup(req->arena, p, req->body_len); + } + + return 0; +} + +// request_header returns a header value. +const char* request_header(request_t *req, const char *name) { + header_t *h = req->headers; + while (h) { + if (strcasecmp(h->name, name) == 0) { + return h->value; + } + h = h->next; + } + return NULL; +} + +// request_has_header checks if header exists. +int request_has_header(request_t *req, const char *name) { + return request_header(req, name) != NULL; +} + +// request_set_params sets route parameters. +void request_set_params(request_t *req, char **names, char **values, int count) { + req->param_names = names; + req->param_values = values; + req->param_count = count; +} + +// request_param returns a route parameter. +const char* request_param(request_t *req, const char *name) { + for (int i = 0; i < req->param_count; i++) { + if (req->param_names[i] && strcmp(req->param_names[i], name) == 0) { + return req->param_values[i]; + } + } + return NULL; +} + +// parse_url_encoded parses application/x-www-form-urlencoded data. +static void parse_url_encoded(request_t *req, const char *data, size_t len, + char ***keys, char ***values, int *count) { + if (!data || len == 0) return; + + // Count parameters + int n = 1; + for (size_t i = 0; i < len; i++) { + if (data[i] == '&') n++; + } + + *keys = arena_alloc(req->arena, n * sizeof(char*)); + *values = arena_alloc(req->arena, n * sizeof(char*)); + *count = 0; + + const char *p = data; + const char *end = data + len; + + while (p < end) { + // Find key + const char *eq = memchr(p, '=', end - p); + const char *amp = memchr(p, '&', end - p); + + if (!amp) amp = end; + + if (eq && eq < amp) { + // Key=value pair + (*keys)[*count] = arena_strdup(req->arena, p, eq - p); + (*values)[*count] = arena_strdup(req->arena, eq + 1, amp - eq - 1); + (*count)++; + } else { + // Key only + (*keys)[*count] = arena_strdup(req->arena, p, amp - p); + (*values)[*count] = ""; + (*count)++; + } + + p = amp + 1; + } +} + +// request_parse_query parses query parameters. +int request_parse_query(request_t *req) { + if (req->query_count > 0) return 0; // already parsed + + parse_url_encoded(req, req->query_string, + req->query_string ? strlen(req->query_string) : 0, + &req->query_keys, &req->query_values, &req->query_count); + return 0; +} + +// request_query returns a query parameter. +const char* request_query(request_t *req, const char *name) { + request_parse_query(req); + + for (int i = 0; i < req->query_count; i++) { + if (strcmp(req->query_keys[i], name) == 0) { + return req->query_values[i]; + } + } + return NULL; +} + +// request_parse_cookies parses cookies from Cookie header. +int request_parse_cookies(request_t *req) { + if (req->cookie_count > 0) return 0; // already parsed + + const char *cookies = request_header(req, "Cookie"); + if (!cookies) return 0; + + // Count cookies + int n = 1; + for (const char *p = cookies; *p; p++) { + if (*p == ';') n++; + } + + req->cookie_names = arena_alloc(req->arena, n * sizeof(char*)); + req->cookie_values = arena_alloc(req->arena, n * sizeof(char*)); + req->cookie_count = 0; + + const char *p = cookies; + while (*p) { + // Skip whitespace + while (*p && isspace(*p)) p++; + if (!*p) break; + + // Find name + const char *eq = strchr(p, '='); + const char *semi = strchr(p, ';'); + + if (!semi) semi = p + strlen(p); + + if (eq && eq < semi) { + req->cookie_names[req->cookie_count] = arena_strdup(req->arena, p, eq - p); + req->cookie_values[req->cookie_count] = arena_strdup(req->arena, eq + 1, semi - eq - 1); + req->cookie_count++; + } + + p = semi; + if (*p == ';') p++; + } + + return 0; +} + +// request_cookie returns a cookie value. +const char* request_cookie(request_t *req, const char *name) { + request_parse_cookies(req); + + for (int i = 0; i < req->cookie_count; i++) { + if (strcmp(req->cookie_names[i], name) == 0) { + return req->cookie_values[i]; + } + } + return NULL; +} + +// request_body returns the request body. +const char* request_body(request_t *req) { + return req->body; +} + +// request_body_len returns the body length. +size_t request_body_len(request_t *req) { + return req->body_len; +} + +// request_content_type returns the content type. +const char* request_content_type(request_t *req) { + const char *ct = request_header(req, "Content-Type"); + if (!ct) return NULL; + + // Return content type without charset + static char type[128]; + const char *semi = strchr(ct, ';'); + if (semi) { + size_t len = semi - ct; + if (len >= sizeof(type)) len = sizeof(type) - 1; + memcpy(type, ct, len); + type[len] = '\0'; + return type; + } + + return ct; +} + +// request_is_json checks if request has JSON content. +int request_is_json(request_t *req) { + const char *ct = request_content_type(req); + return ct && strcmp(ct, "application/json") == 0; +} + +// request_is_form checks if request has form data. +int request_is_form(request_t *req) { + const char *ct = request_content_type(req); + return ct && strcmp(ct, "application/x-www-form-urlencoded") == 0; +} + +// request_expects_json checks if client expects JSON response. +int request_expects_json(request_t *req) { + const char *accept = request_header(req, "Accept"); + if (accept && strstr(accept, "application/json")) return 1; + + const char *xhr = request_header(req, "X-Requested-With"); + if (xhr && strcmp(xhr, "XMLHttpRequest") == 0) return 1; + + return 0; +} diff --git a/request.h b/request.h new file mode 100644 index 0000000..66b9345 --- /dev/null +++ b/request.h @@ -0,0 +1,82 @@ +#ifndef REQUEST_H +#define REQUEST_H + +#include +#include "reactor/arena.h" + +typedef struct request request_t; +typedef struct header header_t; + +// HTTP header +struct header { + char *name; + char *value; + header_t *next; +}; + +// HTTP request +struct request { + // Request line + char *method; + char *path; + char *version; + char *query_string; + + // Headers + header_t *headers; + int header_count; + + // Body + char *body; + size_t body_len; + + // Parsed data + char **param_names; + char **param_values; + int param_count; + + // Query parameters (parsed on demand) + char **query_keys; + char **query_values; + int query_count; + + // Cookies (parsed on demand) + char **cookie_names; + char **cookie_values; + int cookie_count; + + // Arena for allocations + arena_t *arena; +}; + +// Request creation and parsing +request_t* request_new(arena_t *arena); +int request_parse(request_t *req, const char *buf, size_t len); + +// Header access +const char* request_header(request_t *req, const char *name); +int request_has_header(request_t *req, const char *name); + +// Parameter access (from router) +void request_set_params(request_t *req, char **names, char **values, int count); +const char* request_param(request_t *req, const char *name); + +// Query parameter access +const char* request_query(request_t *req, const char *name); +int request_parse_query(request_t *req); + +// Cookie access +const char* request_cookie(request_t *req, const char *name); +int request_parse_cookies(request_t *req); + +// Body access +const char* request_body(request_t *req); +size_t request_body_len(request_t *req); + +// Utility functions +int request_is_json(request_t *req); +int request_is_form(request_t *req); +int request_expects_json(request_t *req); +const char* request_content_type(request_t *req); + +#endif // REQUEST_H diff --git a/response.c b/response.c new file mode 100644 index 0000000..973f9db --- /dev/null +++ b/response.c @@ -0,0 +1,232 @@ +#include "response.h" + +#include +#include +#include + +// Status text lookup +static const char* get_status_text(int code) { + switch (code) { + case 200: return "OK"; + case 201: return "Created"; + case 204: return "No Content"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 304: return "Not Modified"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 500: return "Internal Server Error"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + default: return "Unknown"; + } +} + +// response_new creates a new response. +response_t* response_new(void) { + response_t *res = calloc(1, sizeof(response_t)); + if (!res) return NULL; + + res->status_code = 200; + res->body_cap = 8192; + res->body = malloc(res->body_cap); + + return res; +} + +// response_free destroys a response. +void response_free(response_t *res) { + if (!res) return; + + response_header_t *h = res->headers; + while (h) { + response_header_t *next = h->next; + free(h->name); + free(h->value); + free(h); + h = next; + } + + free(res->body); + free(res); +} + +// response_reset clears the response. +void response_reset(response_t *res) { + res->status_code = 200; + res->status_text = NULL; + + // Clear headers + response_header_t *h = res->headers; + while (h) { + response_header_t *next = h->next; + free(h->name); + free(h->value); + free(h); + h = next; + } + res->headers = NULL; + + res->body_len = 0; + res->headers_sent = 0; +} + +// response_status sets the status code. +response_t* response_status(response_t *res, int code) { + res->status_code = code; + return res; +} + +// response_header adds a header. +response_t* response_header(response_t *res, const char *name, const char *value) { + response_header_t *h = malloc(sizeof(response_header_t)); + if (!h) return res; + + h->name = strdup(name); + h->value = strdup(value); + h->next = res->headers; + res->headers = h; + + return res; +} + +// response_content_type sets Content-Type header. +response_t* response_content_type(response_t *res, const char *type) { + return response_header(res, "Content-Type", type); +} + +// response_content_length sets Content-Length header. +response_t* response_content_length(response_t *res, size_t len) { + char buf[32]; + snprintf(buf, sizeof(buf), "%zu", len); + return response_header(res, "Content-Length", buf); +} + +// ensure_capacity ensures body buffer has enough space. +static int ensure_capacity(response_t *res, size_t needed) { + if (res->body_len + needed > res->body_cap) { + size_t new_cap = res->body_cap * 2; + while (new_cap < res->body_len + needed) new_cap *= 2; + + char *new_body = realloc(res->body, new_cap); + if (!new_body) return -1; + + res->body = new_body; + res->body_cap = new_cap; + } + return 0; +} + +// response_body sets the body. +response_t* response_body(response_t *res, const char *data, size_t len) { + if (ensure_capacity(res, len) < 0) return res; + + memcpy(res->body, data, len); + res->body_len = len; + + return res; +} + +// response_write appends to body. +response_t* response_write(response_t *res, const char *data, size_t len) { + if (ensure_capacity(res, len) < 0) return res; + + memcpy(res->body + res->body_len, data, len); + res->body_len += len; + + return res; +} + +// response_text sends plain text. +response_t* response_text(response_t *res, int status, const char *text) { + response_status(res, status); + response_content_type(res, "text/plain"); + response_body(res, text, strlen(text)); + return res; +} + +// response_html sends HTML. +response_t* response_html(response_t *res, int status, const char *html) { + response_status(res, status); + response_content_type(res, "text/html; charset=utf-8"); + response_body(res, html, strlen(html)); + return res; +} + +// response_json sends JSON. +response_t* response_json(response_t *res, int status, const char *json) { + response_status(res, status); + response_content_type(res, "application/json"); + response_body(res, json, strlen(json)); + return res; +} + +// response_redirect sends a redirect. +response_t* response_redirect(response_t *res, const char *url, int status) { + if (status == 0) status = 302; + response_status(res, status); + response_header(res, "Location", url); + return res; +} + +// response_cookie sets a cookie. +response_t* response_cookie(response_t *res, const char *name, const char *value, + const char *path, int max_age, int http_only, int secure) { + char cookie[1024]; + int len = snprintf(cookie, sizeof(cookie), "%s=%s", name, value); + + if (path) { + len += snprintf(cookie + len, sizeof(cookie) - len, "; Path=%s", path); + } + + if (max_age > 0) { + len += snprintf(cookie + len, sizeof(cookie) - len, "; Max-Age=%d", max_age); + } + + if (http_only) { + len += snprintf(cookie + len, sizeof(cookie) - len, "; HttpOnly"); + } + + if (secure) { + len += snprintf(cookie + len, sizeof(cookie) - len, "; Secure"); + } + + len += snprintf(cookie + len, sizeof(cookie) - len, "; SameSite=Lax"); + + return response_header(res, "Set-Cookie", cookie); +} + +// response_serialize writes response to buffer. +int response_serialize(response_t *res, char *buf, size_t size) { + int len = 0; + + // Status line + len = snprintf(buf, size, "HTTP/1.1 %d %s\r\n", + res->status_code, get_status_text(res->status_code)); + + // Headers + response_header_t *h = res->headers; + while (h) { + len += snprintf(buf + len, size - len, "%s: %s\r\n", h->name, h->value); + h = h->next; + } + + // Content-Length if body exists + if (res->body_len > 0) { + len += snprintf(buf + len, size - len, "Content-Length: %zu\r\n", res->body_len); + } + + // End headers + len += snprintf(buf + len, size - len, "\r\n"); + + // Body + if (res->body_len > 0 && len + res->body_len < size) { + memcpy(buf + len, res->body, res->body_len); + len += res->body_len; + } + + return len; +} diff --git a/response.h b/response.h new file mode 100644 index 0000000..5dc8622 --- /dev/null +++ b/response.h @@ -0,0 +1,60 @@ +#ifndef RESPONSE_H +#define RESPONSE_H + +#include + +typedef struct response response_t; +typedef struct response_header response_header_t; + +// Response header +struct response_header { + char *name; + char *value; + response_header_t *next; +}; + +// HTTP response +struct response { + int status_code; + char *status_text; + + response_header_t *headers; + + char *body; + size_t body_len; + size_t body_cap; + + int headers_sent; +}; + +// Response creation +response_t* response_new(void); +void response_free(response_t *res); +void response_reset(response_t *res); + +// Status +response_t* response_status(response_t *res, int code); + +// Headers +response_t* response_header(response_t *res, const char *name, const char *value); +response_t* response_content_type(response_t *res, const char *type); +response_t* response_content_length(response_t *res, size_t len); + +// Body +response_t* response_body(response_t *res, const char *data, size_t len); +response_t* response_write(response_t *res, const char *data, size_t len); + +// Convenience methods +response_t* response_text(response_t *res, int status, const char *text); +response_t* response_html(response_t *res, int status, const char *html); +response_t* response_json(response_t *res, int status, const char *json); +response_t* response_redirect(response_t *res, const char *url, int status); + +// Cookie +response_t* response_cookie(response_t *res, const char *name, const char *value, + const char *path, int max_age, int http_only, int secure); + +// Serialize to buffer +int response_serialize(response_t *res, char *buf, size_t size); + +#endif // RESPONSE_H diff --git a/router.c b/router.c new file mode 100644 index 0000000..ac6cefe --- /dev/null +++ b/router.c @@ -0,0 +1,336 @@ +#include "router.h" +#include +#include +#include + +#define INITIAL_CHILDREN 4 +#define INITIAL_PARAMS 8 + +// node_new creates a new router node. +static node_t* node_new(void) { + node_t *n = calloc(1, sizeof(node_t)); + if (!n) return NULL; + + n->children = malloc(INITIAL_CHILDREN * sizeof(node_t*)); + n->child_cap = INITIAL_CHILDREN; + return n; +} + +// node_free recursively frees a node and its children. +static void node_free(node_t *n) { + if (!n) return; + + free(n->segment); + + for (int i = 0; i < n->child_count; i++) { + node_free(n->children[i]); + } + free(n->children); + + for (int i = 0; i < n->param_count; i++) { + free(n->param_names[i]); + } + free(n->param_names); + + free(n); +} + +// node_add_child adds a child node. +static void node_add_child(node_t *parent, node_t *child) { + if (parent->child_count >= parent->child_cap) { + parent->child_cap *= 2; + parent->children = realloc(parent->children, parent->child_cap * sizeof(node_t*)); + } + parent->children[parent->child_count++] = child; +} + +// read_segment extracts the next path segment. +static int read_segment(const char *path, int start, char **segment, int *end) { + if (start >= strlen(path)) { + *segment = NULL; + *end = start; + return 0; + } + + // Skip leading slash + if (path[start] == '/') start++; + if (start >= strlen(path)) { + *segment = NULL; + *end = start; + return 0; + } + + // Find end of segment + int pos = start; + while (path[pos] && path[pos] != '/') pos++; + + // Extract segment + int len = pos - start; + *segment = malloc(len + 1); + memcpy(*segment, path + start, len); + (*segment)[len] = '\0'; + + *end = pos; + return path[pos] != '\0'; // has more segments? +} + +// is_dynamic checks if segment is a dynamic parameter. +static int is_dynamic(const char *seg) { + return seg && seg[0] == ':'; +} + +// is_wildcard checks if segment is a wildcard. +static int is_wildcard(const char *seg) { + return seg && seg[0] == '*'; +} + +// extract_param_name gets the parameter name from :name or *name. +static char* extract_param_name(const char *seg) { + if (!seg || strlen(seg) < 2) return NULL; + if (seg[0] == ':' || seg[0] == '*') { + return strdup(seg + 1); + } + return NULL; +} + +// router_new creates a new router. +router_t* router_new(void) { + router_t *r = calloc(1, sizeof(router_t)); + if (!r) return NULL; + + r->get = node_new(); + r->post = node_new(); + r->put = node_new(); + r->patch = node_new(); + r->delete = node_new(); + + r->param_cap = INITIAL_PARAMS; + r->param_values = malloc(r->param_cap * sizeof(char*)); + r->param_names = malloc(r->param_cap * sizeof(char*)); + + return r; +} + +// router_free destroys the router. +void router_free(router_t *r) { + if (!r) return; + + node_free(r->get); + node_free(r->post); + node_free(r->put); + node_free(r->patch); + node_free(r->delete); + + free(r->param_values); + free(r->param_names); + free(r); +} + +// get_method_node returns the root node for a method. +static node_t* get_method_node(router_t *r, const char *method) { + if (strcmp(method, "GET") == 0) return r->get; + if (strcmp(method, "POST") == 0) return r->post; + if (strcmp(method, "PUT") == 0) return r->put; + if (strcmp(method, "PATCH") == 0) return r->patch; + if (strcmp(method, "DELETE") == 0) return r->delete; + return NULL; +} + +// router_add registers a route. +int router_add(router_t *r, const char *method, const char *path, handler_t handler) { + node_t *root = get_method_node(r, method); + if (!root) return -1; + + // Handle root path + if (strcmp(path, "/") == 0) { + root->handler = handler; + return 0; + } + + node_t *current = root; + int pos = 0; + char **params = NULL; + int param_count = 0; + + while (1) { + char *seg; + int end; + int has_more = read_segment(path, pos, &seg, &end); + + if (!seg) break; + + // Check for dynamic/wildcard + int is_dyn = is_dynamic(seg); + int is_wc = is_wildcard(seg); + + if (is_wc && has_more) { + free(seg); + free(params); + return -1; // wildcard must be last + } + + // Track parameter names + if (is_dyn || is_wc) { + params = realloc(params, (param_count + 1) * sizeof(char*)); + params[param_count++] = extract_param_name(seg); + } + + // Find or create child node + node_t *child = NULL; + for (int i = 0; i < current->child_count; i++) { + if (strcmp(current->children[i]->segment, seg) == 0) { + child = current->children[i]; + break; + } + } + + if (!child) { + child = node_new(); + child->segment = seg; + child->is_dynamic = is_dyn; + child->is_wildcard = is_wc; + node_add_child(current, child); + } else { + free(seg); + } + + current = child; + pos = end; + } + + // Set handler and params + current->handler = handler; + current->param_names = params; + current->param_count = param_count; + + return 0; +} + +// Convenience methods +int router_get(router_t *r, const char *path, handler_t handler) { + return router_add(r, "GET", path, handler); +} + +int router_post(router_t *r, const char *path, handler_t handler) { + return router_add(r, "POST", path, handler); +} + +int router_put(router_t *r, const char *path, handler_t handler) { + return router_add(r, "PUT", path, handler); +} + +int router_delete(router_t *r, const char *path, handler_t handler) { + return router_add(r, "DELETE", path, handler); +} + +// match_node recursively matches a path against the tree. +static handler_t match_node(node_t *n, const char *path, int start, + char **param_values, char **param_names, + int *param_idx, int max_params) { + // Check wildcards first + for (int i = 0; i < n->child_count; i++) { + if (n->children[i]->is_wildcard) { + // Capture rest of path + const char *rest = path + start; + if (*rest == '/') rest++; + + if (*param_idx < max_params) { + param_values[*param_idx] = (char*)rest; + param_names[*param_idx] = n->children[i]->param_names[0]; + (*param_idx)++; + } + + return n->children[i]->handler; + } + } + + // Extract current segment + char *seg; + int end; + int has_more = read_segment(path, start, &seg, &end); + + if (!seg) { + // End of path + return n->handler; + } + + // Try exact match first + for (int i = 0; i < n->child_count; i++) { + node_t *child = n->children[i]; + + if (!child->is_dynamic && !child->is_wildcard && + strcmp(child->segment, seg) == 0) { + handler_t h = match_node(child, path, end, param_values, + param_names, param_idx, max_params); + free(seg); + return h; + } + } + + // Try dynamic segments + for (int i = 0; i < n->child_count; i++) { + node_t *child = n->children[i]; + + if (child->is_dynamic) { + // Capture parameter value + if (*param_idx < max_params) { + param_values[*param_idx] = seg; // Don't free seg here + param_names[*param_idx] = child->param_names[0]; + (*param_idx)++; + } + + handler_t h = match_node(child, path, end, param_values, + param_names, param_idx, max_params); + if (h) return h; + + // Backtrack + if (*param_idx > 0) (*param_idx)--; + } + } + + free(seg); + return NULL; +} + +// router_lookup finds a matching route. +handler_t router_lookup(router_t *r, const char *method, const char *path, + char ***params, char ***names, int *count) { + node_t *root = get_method_node(r, method); + if (!root) return NULL; + + // Handle root path + if (strcmp(path, "/") == 0) { + *count = 0; + return root->handler; + } + + // Reset parameter buffers + memset(r->param_values, 0, r->param_cap * sizeof(char*)); + memset(r->param_names, 0, r->param_cap * sizeof(char*)); + + int param_idx = 0; + handler_t h = match_node(root, path, 0, r->param_values, + r->param_names, ¶m_idx, r->param_cap); + + if (h) { + *params = r->param_values; + *names = r->param_names; + *count = param_idx; + } + + return h; +} + +// router_param gets a parameter by name. +const char* router_param(conn_t *c, const char *name) { + // This would need to be implemented with connection context + // For now, return NULL + return NULL; +} + +// router_param_idx gets a parameter by index. +const char* router_param_idx(conn_t *c, int index) { + // This would need to be implemented with connection context + // For now, return NULL + return NULL; +} diff --git a/router.h b/router.h new file mode 100644 index 0000000..8fc3f8b --- /dev/null +++ b/router.h @@ -0,0 +1,55 @@ +#ifndef ROUTER_H +#define ROUTER_H + +#include "reactor/conn.h" + +typedef struct router router_t; +typedef struct node node_t; +typedef void (*handler_t)(conn_t *c); + +// Router node for tree-based matching +struct node { + char *segment; // path segment + handler_t handler; // handler for this path + node_t **children; // child nodes + int child_count; // number of children + int child_cap; // capacity of children array + int is_dynamic; // :param + int is_wildcard; // *path + char **param_names; // parameter names for this route + int param_count; // number of parameters +}; + +// HTTP router +struct router { + node_t *get; + node_t *post; + node_t *put; + node_t *patch; + node_t *delete; + + // Parameter extraction buffer + char **param_values; + char **param_names; + int param_cap; +}; + +// Router operations +router_t* router_new(void); +void router_free(router_t *r); + +// Route registration +int router_add(router_t *r, const char *method, const char *path, handler_t handler); +int router_get(router_t *r, const char *path, handler_t handler); +int router_post(router_t *r, const char *path, handler_t handler); +int router_put(router_t *r, const char *path, handler_t handler); +int router_delete(router_t *r, const char *path, handler_t handler); + +// Route lookup +handler_t router_lookup(router_t *r, const char *method, const char *path, char ***params, char ***names, int *count); + +// Parameter extraction from matched route +const char* router_param(conn_t *c, const char *name); +const char* router_param_idx(conn_t *c, int index); + +#endif // ROUTER_H