// 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 #include #include #include #include #include #include #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); } }