initial commit

This commit is contained in:
Sky Johnson 2025-09-15 12:57:54 -05:00
commit 5b374ebc45
19 changed files with 2497 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/test
*.o

318
app.c Normal file
View File

@ -0,0 +1,318 @@
#include "app.h"
#include "reactor/pool.h"
#include "reactor/epoll.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <fcntl.h>
#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, &params, &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);
}

51
app.h Normal file
View File

@ -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

145
context.c Normal file
View File

@ -0,0 +1,145 @@
#include "context.h"
#include <string.h>
#include <stdio.h>
// 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;
}

58
context.h Normal file
View File

@ -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

66
reactor/arena.c Normal file
View File

@ -0,0 +1,66 @@
#include "arena.h"
#include <stdlib.h>
#include <string.h>
// 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
}

23
reactor/arena.h Normal file
View File

@ -0,0 +1,23 @@
#ifndef ARENA_H
#define ARENA_H
#include <stddef.h>
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

299
reactor/conn.c Normal file
View File

@ -0,0 +1,299 @@
#include "conn.h"
#include "pool.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <errno.h>
#include <stdio.h>
// 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;
}

72
reactor/conn.h Normal file
View File

@ -0,0 +1,72 @@
#ifndef CONN_H
#define CONN_H
#include <stddef.h>
#include <stdint.h>
#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

203
reactor/epoll.c Normal file
View File

@ -0,0 +1,203 @@
#include "epoll.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
// 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);
}

25
reactor/epoll.h Normal file
View File

@ -0,0 +1,25 @@
#ifndef EPOLL_H
#define EPOLL_H
#include <sys/epoll.h>
#include <stdint.h>
#include <stdbool.h>
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

108
reactor/pool.c Normal file
View File

@ -0,0 +1,108 @@
#include "pool.h"
#include <stdlib.h>
#include <unistd.h>
#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);
}
}

37
reactor/pool.h Normal file
View File

@ -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

325
request.c Normal file
View File

@ -0,0 +1,325 @@
#include "request.h"
#include <string.h>
#include <stdio.h>
#include <ctype.h>
// 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;
}

82
request.h Normal file
View File

@ -0,0 +1,82 @@
#ifndef REQUEST_H
#define REQUEST_H
#include <stddef.h>
#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

232
response.c Normal file
View File

@ -0,0 +1,232 @@
#include "response.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
// 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;
}

60
response.h Normal file
View File

@ -0,0 +1,60 @@
#ifndef RESPONSE_H
#define RESPONSE_H
#include <stddef.h>
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

336
router.c Normal file
View File

@ -0,0 +1,336 @@
#include "router.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#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, &param_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;
}

55
router.h Normal file
View File

@ -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