initial commit
This commit is contained in:
commit
5b374ebc45
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/test
|
||||||
|
*.o
|
||||||
318
app.c
Normal file
318
app.c
Normal 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, ¶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);
|
||||||
|
}
|
||||||
51
app.h
Normal file
51
app.h
Normal 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
145
context.c
Normal 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
58
context.h
Normal 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
66
reactor/arena.c
Normal 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
23
reactor/arena.h
Normal 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
299
reactor/conn.c
Normal 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
72
reactor/conn.h
Normal 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
203
reactor/epoll.c
Normal 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
25
reactor/epoll.h
Normal 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
108
reactor/pool.c
Normal 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
37
reactor/pool.h
Normal 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
325
request.c
Normal 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
82
request.h
Normal 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
232
response.c
Normal 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
60
response.h
Normal 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
336
router.c
Normal 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, ¶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;
|
||||||
|
}
|
||||||
55
router.h
Normal file
55
router.h
Normal 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
|
||||||
Loading…
x
Reference in New Issue
Block a user