Web/request.c
2025-09-15 12:57:54 -05:00

326 lines
8.2 KiB
C

#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;
}