calog/libs/calogHttpd.c

806 lines
28 KiB
C

// calogHttpd.c -- calog polyglot HTTP server (see calogHttpd.h). Each server owns an acceptor
// thread; per request it parses the message, finds the matching route, invokes that route's handler
// (a CalogFnT, so the call marshals to the handler's owning context thread and runs there), and
// writes the response. Routes are guarded by a mutex; re-registering replaces the handler live.
// v1: HTTP/1.1, Connection: close, one request in flight at a time. No TLS or WebSocket yet.
#define _GNU_SOURCE
#include "calogHttpd.h"
#include "calogHandle.h"
#include "calogInternal.h"
#include "calogPlatform.h"
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#define HTTPD_TYPE_SERVER 1u
#define HTTPD_HEADER_MAX (64 * 1024)
#define HTTPD_BODY_MAX (16 * 1024 * 1024)
typedef struct HttpdBufT {
char *data;
size_t len;
size_t cap;
} HttpdBufT;
typedef struct RouteT {
char *method; // uppercase, or "*" for any
char *path; // exact match (query stripped)
CalogFnT *handler;
struct RouteT *next;
} RouteT;
typedef struct ServerT {
CalogSocketT listenFd;
pthread_t acceptor;
bool acceptorStarted;
pthread_mutex_t routesMutex;
RouteT *routes;
_Atomic bool stop;
int64_t maxBody;
} ServerT;
typedef struct HttpdLibT {
CalogHandleTableT *handles;
int32_t refCount;
} HttpdLibT;
typedef struct HttpdNativeT {
const char *name;
CalogNativeFnT fn;
} HttpdNativeT;
static pthread_mutex_t gHttpdMutex = PTHREAD_MUTEX_INITIALIZER;
static HttpdLibT *gHttpdLib = NULL;
static void *httpdAcceptor(void *arg);
static int32_t httpdBufAppend(HttpdBufT *buffer, const void *bytes, size_t length);
static void httpdCloser(uint32_t type, void *resource);
static const char *httpdFindHeader(const char *headers, size_t headerLen, const char *name, size_t *valueLen);
static CalogFnT *httpdMatchRoute(ServerT *server, const char *method, const char *path);
static int32_t httpdListen(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t httpdParseRequest(ServerT *server, const char *buffer, size_t headerLen, const char *body, size_t bodyLen, CalogValueT *out);
static bool httpdReadRequest(ServerT *server, CalogSocketT fd, HttpdBufT *buffer, size_t *headerLen, size_t *bodyLen);
static const char *httpdReason(int64_t status);
static int32_t httpdRoute(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void httpdServe(ServerT *server, CalogSocketT fd);
static int32_t httpdStop(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t httpdUnroute(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void httpdWriteResponse(CalogSocketT fd, const CalogValueT *response);
static void httpdWriteStatus(HttpdBufT *out, int64_t status, int64_t bodyLen);
static const HttpdNativeT gHttpdNatives[] = {
{ "httpdListen", httpdListen },
{ "httpdRoute", httpdRoute },
{ "httpdUnroute", httpdUnroute },
{ "httpdStop", httpdStop },
};
int32_t calogHttpdRegister(CalogT *calog) {
HttpdLibT *lib;
int32_t status;
size_t index;
pthread_mutex_lock(&gHttpdMutex);
if (gHttpdLib == NULL) {
HttpdLibT *created;
if (calogPlatformNetInit() != 0) {
pthread_mutex_unlock(&gHttpdMutex);
return calogErrUnsupportedE;
}
created = (HttpdLibT *)calloc(1, sizeof(*created));
if (created == NULL) {
calogPlatformNetShutdown();
pthread_mutex_unlock(&gHttpdMutex);
return calogErrOomE;
}
created->handles = calogHandleTableCreate();
if (created->handles == NULL) {
free(created);
calogPlatformNetShutdown();
pthread_mutex_unlock(&gHttpdMutex);
return calogErrOomE;
}
gHttpdLib = created;
}
gHttpdLib->refCount++;
lib = gHttpdLib;
pthread_mutex_unlock(&gHttpdMutex);
status = calogOkE;
for (index = 0; index < sizeof(gHttpdNatives) / sizeof(gHttpdNatives[0]); index++) {
status = calogRegisterInline(calog, gHttpdNatives[index].name, gHttpdNatives[index].fn, lib);
if (status != calogOkE) {
break;
}
}
if (status != calogOkE) {
calogHttpdShutdown();
return status;
}
return calogAtDestroy(calog, calogHttpdShutdown, calogDestroyBeforeContextsE);
}
void calogHttpdShutdown(void) {
pthread_mutex_lock(&gHttpdMutex);
if (gHttpdLib == NULL) {
pthread_mutex_unlock(&gHttpdMutex);
return;
}
gHttpdLib->refCount--;
if (gHttpdLib->refCount <= 0) {
calogHandleTableDestroy(gHttpdLib->handles, httpdCloser);
calogPlatformNetShutdown();
free(gHttpdLib);
gHttpdLib = NULL;
}
pthread_mutex_unlock(&gHttpdMutex);
}
// The acceptor thread: accept a connection, serve it to completion, repeat, until stopped. The
// listen socket is non-blocking and gated by poll() with a short timeout, so the loop notices `stop`
// promptly -- closing the socket from another thread does NOT reliably wake a thread in accept().
static void *httpdAcceptor(void *arg) {
ServerT *server;
server = (ServerT *)arg;
while (!atomic_load(&server->stop)) {
struct pollfd pfd;
CalogSocketT client;
pfd.fd = server->listenFd;
pfd.events = POLLIN;
pfd.revents = 0;
if (calogPoll(&pfd, 1, 200) <= 0) {
continue; // timeout or error -> re-check stop
}
client = accept(server->listenFd, NULL, NULL);
if (client == CALOG_INVALID_SOCKET) {
continue;
}
httpdServe(server, client);
calogSockClose(client);
}
return NULL;
}
static int32_t httpdBufAppend(HttpdBufT *buffer, const void *bytes, size_t length) {
if (buffer->len + length > buffer->cap) {
size_t wanted;
char *grown;
wanted = buffer->cap ? buffer->cap * 2 : 4096;
while (wanted < buffer->len + length) {
wanted *= 2;
}
grown = (char *)realloc(buffer->data, wanted);
if (grown == NULL) {
return calogErrOomE;
}
buffer->data = grown;
buffer->cap = wanted;
}
memcpy(buffer->data + buffer->len, bytes, length);
buffer->len += length;
return calogOkE;
}
static void httpdCloser(uint32_t type, void *resource) {
ServerT *server;
RouteT *route;
if (type != HTTPD_TYPE_SERVER) {
return;
}
server = (ServerT *)resource;
atomic_store(&server->stop, true);
if (server->acceptorStarted) {
pthread_join(server->acceptor, NULL); // exits within one poll timeout of stop
}
if (server->listenFd != CALOG_INVALID_SOCKET) {
calogSockClose(server->listenFd);
server->listenFd = CALOG_INVALID_SOCKET;
}
route = server->routes;
while (route != NULL) {
RouteT *next;
next = route->next;
calogFnRelease(route->handler);
free(route->method);
free(route->path);
free(route);
route = next;
}
pthread_mutex_destroy(&server->routesMutex);
free(server);
}
// Case-insensitive lookup of a header value within the raw header block (NUL-free scan bounded by
// headerLen). Returns a pointer to the value (trimmed of leading spaces) + its length, or NULL.
static const char *httpdFindHeader(const char *headers, size_t headerLen, const char *name, size_t *valueLen) {
size_t nameLen;
size_t i;
nameLen = strlen(name);
for (i = 0; i + nameLen + 1 < headerLen; i++) {
if ((i == 0 || headers[i - 1] == '\n') && strncasecmp(headers + i, name, nameLen) == 0 && headers[i + nameLen] == ':') {
size_t v;
size_t end;
v = i + nameLen + 1;
while (v < headerLen && (headers[v] == ' ' || headers[v] == '\t')) {
v++;
}
end = v;
while (end < headerLen && headers[end] != '\r' && headers[end] != '\n') {
end++;
}
*valueLen = end - v;
return headers + v;
}
}
*valueLen = 0;
return NULL;
}
static int32_t httpdListen(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
HttpdLibT *lib;
ServerT *server;
struct addrinfo hints;
struct addrinfo *res;
struct addrinfo *rp;
CalogSocketT fd;
char portBuffer[8];
int64_t port;
int64_t handle;
int yes;
lib = (HttpdLibT *)userData;
calogValueNil(result);
if (argCount < 1 || argCount > 2 || args[0].type != calogIntE) {
return calogFail(result, calogErrArgE, "httpdListen expects (port [, opts])");
}
if (argCount == 2 && args[1].type != calogAggE) {
return calogFail(result, calogErrArgE, "httpdListen: opts must be a map");
}
port = args[0].as.i;
if (port < 0 || port > 65535) {
return calogFail(result, calogErrArgE, "httpdListen: port out of range");
}
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
snprintf(portBuffer, sizeof(portBuffer), "%u", (unsigned int)port);
if (getaddrinfo(NULL, portBuffer, &hints, &res) != 0) {
return calogFail(result, calogErrArgE, "httpdListen: could not resolve the bind address");
}
fd = CALOG_INVALID_SOCKET;
yes = 1;
for (rp = res; rp != NULL; rp = rp->ai_next) {
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (fd == CALOG_INVALID_SOCKET) {
continue;
}
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char *)&yes, sizeof(yes));
if (bind(fd, rp->ai_addr, (socklen_t)rp->ai_addrlen) == 0 && listen(fd, 64) == 0) {
break;
}
calogSockClose(fd);
fd = CALOG_INVALID_SOCKET;
}
freeaddrinfo(res);
if (fd == CALOG_INVALID_SOCKET) {
return calogFail(result, calogErrArgE, "httpdListen: could not bind/listen on the port");
}
server = (ServerT *)calloc(1, sizeof(*server));
if (server == NULL) {
calogSockClose(fd);
return calogFail(result, calogErrOomE, "httpdListen: out of memory");
}
calogSockSetNonblock(fd, 1); // non-blocking + poll-gated accept, so the acceptor can be stopped
server->listenFd = fd;
server->maxBody = HTTPD_BODY_MAX;
pthread_mutex_init(&server->routesMutex, NULL);
handle = calogHandleAdd(lib->handles, HTTPD_TYPE_SERVER, server);
if (handle == 0) {
calogSockClose(fd);
pthread_mutex_destroy(&server->routesMutex);
free(server);
return calogFail(result, calogErrOomE, "httpdListen: out of memory");
}
if (pthread_create(&server->acceptor, NULL, httpdAcceptor, server) != 0) {
calogHandleRemove(lib->handles, handle, HTTPD_TYPE_SERVER);
httpdCloser(HTTPD_TYPE_SERVER, server);
return calogFail(result, calogErrUnsupportedE, "httpdListen: could not start the acceptor thread");
}
server->acceptorStarted = true;
calogValueInt(result, handle);
return calogOkE;
}
// Find the handler for method+path (RETAINED, so it survives a concurrent unroute), or NULL.
static CalogFnT *httpdMatchRoute(ServerT *server, const char *method, const char *path) {
RouteT *route;
CalogFnT *handler;
handler = NULL;
pthread_mutex_lock(&server->routesMutex);
for (route = server->routes; route != NULL; route = route->next) {
if ((strcmp(route->method, "*") == 0 || strcmp(route->method, method) == 0) && strcmp(route->path, path) == 0) {
handler = route->handler;
calogFnRetain(handler);
break;
}
}
pthread_mutex_unlock(&server->routesMutex);
return handler;
}
// Parse the request line + headers + body into a request map { method, path, query, headers, body }.
static int32_t httpdParseRequest(ServerT *server, const char *buffer, size_t headerLen, const char *body, size_t bodyLen, CalogValueT *out) {
CalogAggT *map;
CalogAggT *headers;
const char *p;
const char *lineEnd;
const char *methodEnd;
const char *target;
const char *targetEnd;
const char *query;
size_t i;
int32_t status;
(void)server;
calogValueNil(out);
// Request line: METHOD SP TARGET SP HTTP/x.y
p = buffer;
lineEnd = memchr(buffer, '\n', headerLen);
if (lineEnd == NULL) {
return calogErrArgE;
}
methodEnd = memchr(p, ' ', (size_t)(lineEnd - p));
if (methodEnd == NULL) {
return calogErrArgE;
}
target = methodEnd + 1;
targetEnd = memchr(target, ' ', (size_t)(lineEnd - target));
if (targetEnd == NULL) {
targetEnd = lineEnd;
}
query = memchr(target, '?', (size_t)(targetEnd - target));
status = calogAggCreate(&map, calogMapE);
if (status != calogOkE) {
return status;
}
status = calogMapSetStr(map, "method", p, (int64_t)(methodEnd - p));
if (status == calogOkE) {
const char *pathEnd;
pathEnd = query != NULL ? query : targetEnd;
status = calogMapSetStr(map, "path", target, (int64_t)(pathEnd - target));
}
if (status == calogOkE) {
status = calogMapSetStr(map, "query", query != NULL ? query + 1 : "", query != NULL ? (int64_t)(targetEnd - query - 1) : 0);
}
if (status == calogOkE) {
status = calogMapSetStr(map, "body", bodyLen > 0 ? body : "", (int64_t)bodyLen);
}
if (status != calogOkE) {
calogAggFree(map);
return status;
}
// Headers: each "Name: Value" line after the request line; names lowercased into a map.
status = calogAggCreate(&headers, calogMapE);
if (status != calogOkE) {
calogAggFree(map);
return status;
}
i = (size_t)(lineEnd - buffer) + 1;
while (i < headerLen) {
const char *colon;
const char *nl;
size_t lineLen;
char nameLower[128];
size_t nameLen;
size_t v;
size_t valueEnd;
size_t k;
nl = memchr(buffer + i, '\n', headerLen - i);
lineLen = nl != NULL ? (size_t)(nl - (buffer + i)) : (headerLen - i);
if (lineLen == 0 || (lineLen == 1 && buffer[i] == '\r')) {
break;
}
colon = memchr(buffer + i, ':', lineLen);
if (colon != NULL) {
nameLen = (size_t)(colon - (buffer + i));
if (nameLen >= sizeof(nameLower)) {
nameLen = sizeof(nameLower) - 1;
}
for (k = 0; k < nameLen; k++) {
char c;
c = buffer[i + k];
nameLower[k] = (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c;
}
nameLower[nameLen] = '\0';
v = (size_t)(colon - buffer) + 1;
while (v < i + lineLen && (buffer[v] == ' ' || buffer[v] == '\t')) {
v++;
}
valueEnd = i + lineLen;
while (valueEnd > v && (buffer[valueEnd - 1] == '\r' || buffer[valueEnd - 1] == ' ')) {
valueEnd--;
}
status = calogMapSetStr(headers, nameLower, buffer + v, (int64_t)(valueEnd - v));
if (status != calogOkE) {
calogAggFree(headers);
calogAggFree(map);
return status;
}
}
if (nl == NULL) {
break;
}
i += lineLen + 1;
}
{
CalogValueT headersKey;
CalogValueT headersValue;
calogValueAgg(&headersValue, headers);
if (calogValueString(&headersKey, "headers", 7) != calogOkE) {
calogValueFree(&headersValue);
calogAggFree(map);
return calogErrOomE;
}
status = calogAggSet(map, &headersKey, &headersValue);
if (status != calogOkE) {
calogAggFree(map);
return status;
}
}
calogValueAgg(out, map);
return calogOkE;
}
// Read the request headers (until CRLFCRLF) then the body (per Content-Length). Returns false on a
// closed/oversized/malformed request.
static bool httpdReadRequest(ServerT *server, CalogSocketT fd, HttpdBufT *buffer, size_t *headerLen, size_t *bodyLen) {
const char *marker;
const char *value;
size_t valueLen;
size_t headerEnd;
int64_t contentLength;
size_t have;
marker = NULL;
while (buffer->len < HTTPD_HEADER_MAX) {
char chunk[8192];
ssize_t got;
got = recv(fd, chunk, sizeof(chunk), 0);
if (got <= 0) {
return false;
}
if (httpdBufAppend(buffer, chunk, (size_t)got) != calogOkE) {
return false;
}
marker = memmem(buffer->data, buffer->len, "\r\n\r\n", 4);
if (marker != NULL) {
break;
}
}
if (marker == NULL) {
return false;
}
headerEnd = (size_t)(marker - buffer->data) + 4;
*headerLen = headerEnd;
contentLength = 0;
value = httpdFindHeader(buffer->data, headerEnd, "content-length", &valueLen);
if (value != NULL) {
char tmp[24];
if (valueLen >= sizeof(tmp)) {
return false;
}
memcpy(tmp, value, valueLen);
tmp[valueLen] = '\0';
contentLength = strtoll(tmp, NULL, 10);
if (contentLength < 0 || contentLength > server->maxBody) {
return false;
}
}
have = buffer->len - headerEnd; // body bytes already read past the header
while ((int64_t)have < contentLength) {
char chunk[8192];
ssize_t got;
got = recv(fd, chunk, sizeof(chunk), 0);
if (got <= 0) {
return false;
}
if (httpdBufAppend(buffer, chunk, (size_t)got) != calogOkE) {
return false;
}
have += (size_t)got;
}
*bodyLen = (size_t)contentLength;
return true;
}
static const char *httpdReason(int64_t status) {
switch (status) {
case 200: return "OK";
case 201: return "Created";
case 204: return "No Content";
case 301: return "Moved Permanently";
case 302: return "Found";
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";
default: return "Status";
}
}
static int32_t httpdRoute(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
HttpdLibT *lib;
ServerT *server;
RouteT *route;
RouteT *existing;
char *method;
char *path;
size_t i;
lib = (HttpdLibT *)userData;
calogValueNil(result);
if (argCount != 4 || args[0].type != calogIntE || args[1].type != calogStringE || args[2].type != calogStringE || args[3].type != calogFnE) {
return calogFail(result, calogErrArgE, "httpdRoute expects (server, method, path, handler)");
}
server = (ServerT *)calogHandleGet(lib->handles, args[0].as.i, HTTPD_TYPE_SERVER);
if (server == NULL) {
return calogFail(result, calogErrArgE, "httpdRoute: invalid server handle");
}
method = strdup(args[1].as.s.bytes);
path = strdup(args[2].as.s.bytes);
if (method == NULL || path == NULL) {
free(method);
free(path);
return calogFail(result, calogErrOomE, "httpdRoute: out of memory");
}
for (i = 0; method[i] != '\0'; i++) {
if (method[i] >= 'a' && method[i] <= 'z') {
method[i] = (char)(method[i] - 'a' + 'A');
}
}
pthread_mutex_lock(&server->routesMutex);
// Re-registering the same method+path replaces the handler live (hot reload).
for (existing = server->routes; existing != NULL; existing = existing->next) {
if (strcmp(existing->method, method) == 0 && strcmp(existing->path, path) == 0) {
calogFnRelease(existing->handler);
calogFnRetain(args[3].as.fn);
existing->handler = args[3].as.fn;
pthread_mutex_unlock(&server->routesMutex);
free(method);
free(path);
return calogOkE;
}
}
route = (RouteT *)calloc(1, sizeof(*route));
if (route == NULL) {
pthread_mutex_unlock(&server->routesMutex);
free(method);
free(path);
return calogFail(result, calogErrOomE, "httpdRoute: out of memory");
}
calogFnRetain(args[3].as.fn);
route->method = method;
route->path = path;
route->handler = args[3].as.fn;
route->next = server->routes;
server->routes = route;
pthread_mutex_unlock(&server->routesMutex);
return calogOkE;
}
// Serve one connection: read + parse the request, invoke the matching handler on its owning context
// thread, write the response.
static void httpdServe(ServerT *server, CalogSocketT fd) {
HttpdBufT buffer;
CalogValueT request;
CalogValueT response;
CalogFnT *handler;
CalogValueT *method;
CalogValueT *path;
size_t headerLen;
size_t bodyLen;
CalogValueT methodKey;
CalogValueT pathKey;
memset(&buffer, 0, sizeof(buffer));
if (!httpdReadRequest(server, fd, &buffer, &headerLen, &bodyLen)) {
free(buffer.data);
return;
}
if (httpdParseRequest(server, buffer.data, headerLen, buffer.data + headerLen, bodyLen, &request) != calogOkE) {
free(buffer.data);
return;
}
free(buffer.data);
// Look up the handler by the parsed method + path.
method = NULL;
path = NULL;
if (calogValueString(&methodKey, "method", 6) == calogOkE) {
method = calogAggGet(request.as.agg, &methodKey);
calogValueFree(&methodKey);
}
if (calogValueString(&pathKey, "path", 4) == calogOkE) {
path = calogAggGet(request.as.agg, &pathKey);
calogValueFree(&pathKey);
}
handler = (method != NULL && path != NULL) ? httpdMatchRoute(server, method->as.s.bytes, path->as.s.bytes) : NULL;
if (handler == NULL) {
static const char notFound[] = "HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\nConnection: close\r\n\r\nNot Found";
send(fd, notFound, sizeof(notFound) - 1, CALOG_MSG_NOSIGNAL);
calogValueFree(&request);
return;
}
calogValueNil(&response);
if (calogFnInvoke(handler, &request, 1, &response) != calogOkE) {
static const char err[] = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 21\r\nConnection: close\r\n\r\nInternal Server Error";
send(fd, err, sizeof(err) - 1, CALOG_MSG_NOSIGNAL);
} else {
httpdWriteResponse(fd, &response);
}
calogFnRelease(handler);
calogValueFree(&response);
calogValueFree(&request);
}
static int32_t httpdStop(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
HttpdLibT *lib;
ServerT *server;
lib = (HttpdLibT *)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogIntE) {
return calogFail(result, calogErrArgE, "httpdStop expects (server)");
}
server = (ServerT *)calogHandleRemove(lib->handles, args[0].as.i, HTTPD_TYPE_SERVER);
if (server == NULL) {
return calogFail(result, calogErrArgE, "httpdStop: invalid server handle");
}
httpdCloser(HTTPD_TYPE_SERVER, server);
return calogOkE;
}
static int32_t httpdUnroute(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
HttpdLibT *lib;
ServerT *server;
RouteT **link;
char method[16];
size_t i;
lib = (HttpdLibT *)userData;
calogValueNil(result);
if (argCount != 3 || args[0].type != calogIntE || args[1].type != calogStringE || args[2].type != calogStringE) {
return calogFail(result, calogErrArgE, "httpdUnroute expects (server, method, path)");
}
server = (ServerT *)calogHandleGet(lib->handles, args[0].as.i, HTTPD_TYPE_SERVER);
if (server == NULL) {
return calogFail(result, calogErrArgE, "httpdUnroute: invalid server handle");
}
snprintf(method, sizeof(method), "%s", args[1].as.s.bytes);
for (i = 0; method[i] != '\0'; i++) {
if (method[i] >= 'a' && method[i] <= 'z') {
method[i] = (char)(method[i] - 'a' + 'A');
}
}
pthread_mutex_lock(&server->routesMutex);
for (link = &server->routes; *link != NULL; link = &(*link)->next) {
RouteT *route;
route = *link;
if (strcmp(route->method, method) == 0 && strcmp(route->path, args[2].as.s.bytes) == 0) {
*link = route->next;
calogFnRelease(route->handler);
free(route->method);
free(route->path);
free(route);
break;
}
}
pthread_mutex_unlock(&server->routesMutex);
return calogOkE;
}
// Turn a handler's return value into an HTTP response and write it. nil -> 204; a string -> 200 with
// that body; a map -> { status (default 200), headers (map), body (string) }.
static void httpdWriteResponse(CalogSocketT fd, const CalogValueT *response) {
HttpdBufT out;
const char *body;
int64_t bodyLen;
int64_t status;
memset(&out, 0, sizeof(out));
body = "";
bodyLen = 0;
status = 200;
if (response->type == calogNilE) {
status = 204;
} else if (response->type == calogStringE) {
body = response->as.s.bytes;
bodyLen = response->as.s.length;
} else if (response->type == calogAggE && calogAggIsKeyed(response->as.agg)) {
CalogValueT key;
CalogValueT *field;
if (calogValueString(&key, "status", 6) == calogOkE) {
field = calogAggGet(response->as.agg, &key);
calogValueFree(&key);
if (field != NULL && field->type == calogIntE) {
status = field->as.i;
}
}
if (calogValueString(&key, "body", 4) == calogOkE) {
field = calogAggGet(response->as.agg, &key);
calogValueFree(&key);
if (field != NULL && field->type == calogStringE) {
body = field->as.s.bytes;
bodyLen = field->as.s.length;
}
}
httpdWriteStatus(&out, status, bodyLen);
// Custom headers, if any (Content-Length + Connection are set by httpdWriteStatus).
if (calogValueString(&key, "headers", 7) == calogOkE) {
field = calogAggGet(response->as.agg, &key);
calogValueFree(&key);
if (field != NULL && field->type == calogAggE && calogAggIsKeyed(field->as.agg)) {
int64_t h;
for (h = 0; h < field->as.agg->pairCount; h++) {
if (field->as.agg->pairs[h].key.type == calogStringE && field->as.agg->pairs[h].value.type == calogStringE) {
httpdBufAppend(&out, field->as.agg->pairs[h].key.as.s.bytes, (size_t)field->as.agg->pairs[h].key.as.s.length);
httpdBufAppend(&out, ": ", 2);
httpdBufAppend(&out, field->as.agg->pairs[h].value.as.s.bytes, (size_t)field->as.agg->pairs[h].value.as.s.length);
httpdBufAppend(&out, "\r\n", 2);
}
}
}
}
httpdBufAppend(&out, "\r\n", 2);
httpdBufAppend(&out, body, (size_t)bodyLen);
send(fd, out.data, out.len, CALOG_MSG_NOSIGNAL);
free(out.data);
return;
}
httpdWriteStatus(&out, status, bodyLen);
httpdBufAppend(&out, "\r\n", 2);
httpdBufAppend(&out, body, (size_t)bodyLen);
send(fd, out.data, out.len, CALOG_MSG_NOSIGNAL);
free(out.data);
}
// Write the status line + the always-present Content-Length and Connection: close headers.
static void httpdWriteStatus(HttpdBufT *out, int64_t status, int64_t bodyLen) {
char header[128];
int n;
n = snprintf(header, sizeof(header), "HTTP/1.1 %lld %s\r\nContent-Length: %lld\r\nConnection: close\r\n", (long long)status, httpdReason(status), (long long)bodyLen);
if (n > 0) {
httpdBufAppend(out, header, (size_t)n);
}
}