806 lines
28 KiB
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);
|
|
}
|
|
}
|