// testHttpd.c -- the polyglot HTTP server. A Lua context and a JavaScript context each stand up a // server with route handlers written in their own language; the test drives real HTTP requests over // a socket and checks the responses, a 404, a response map (custom status), and HOT RELOAD // (re-registering a route on a live server swaps the handler). #define _POSIX_C_SOURCE 200809L #include "calog.h" #include "calogHttpd.h" #include #include #include #include #include #include #include #include #include #define LUA_PORT 38900 #define JS_PORT 38901 #define PUMP_LIMIT 4000 static CalogT *calog = NULL; static _Atomic int32_t readyCount = 0; static int32_t testsRun = 0; static int32_t testsFailed = 0; #define CHECK(cond, msg) checkImpl((cond), (msg), __LINE__) static void checkImpl(bool condition, const char *message, int32_t line); static bool httpGet(int port, const char *path, int *statusOut, char *body, size_t bodyCap); static int32_t nativeReady(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static void pumpForReady(int32_t target); static void checkImpl(bool condition, const char *message, int32_t line) { testsRun++; if (!condition) { testsFailed++; printf("FAIL testHttpd.c:%d %s\n", line, message); } } // Minimal HTTP/1.1 client: connect, GET path, parse status code + body. static bool httpGet(int port, const char *path, int *statusOut, char *body, size_t bodyCap) { struct sockaddr_in addr; char request[256]; char response[8192]; char *bodyStart; ssize_t total; ssize_t got; int fd; int requestLen; fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { return false; } memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons((uint16_t)port); inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { close(fd); return false; } requestLen = snprintf(request, sizeof(request), "GET %s HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", path); if (send(fd, request, (size_t)requestLen, 0) != requestLen) { close(fd); return false; } total = 0; while ((got = recv(fd, response + total, sizeof(response) - 1 - (size_t)total, 0)) > 0) { total += got; if ((size_t)total >= sizeof(response) - 1) { break; } } close(fd); if (total <= 0) { return false; } response[total] = '\0'; *statusOut = 0; if (strncmp(response, "HTTP/1.1 ", 9) == 0) { *statusOut = atoi(response + 9); } bodyStart = strstr(response, "\r\n\r\n"); body[0] = '\0'; if (bodyStart != NULL) { snprintf(body, bodyCap, "%s", bodyStart + 4); } return true; } static int32_t nativeReady(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)args; (void)argCount; (void)userData; atomic_fetch_add(&readyCount, 1); calogValueNil(result); return calogOkE; } static void pumpForReady(int32_t target) { struct timespec ts = { 0, 500000 }; int32_t i; for (i = 0; i < PUMP_LIMIT; i++) { calogPump(calog); if (atomic_load(&readyCount) >= target) { return; } nanosleep(&ts, NULL); } } int main(void) { CalogContextT *lua; CalogContextT *js; char body[8192]; int status; calog = calogCreate(); if (calog == NULL) { printf("calog create failed\n"); return 1; } calogHttpdRegister(calog); calogRegister(calog, "ready", nativeReady, NULL); // Lua server: a plain-string route, a response-map route (custom status), and a 404 for the rest. lua = calogContextOpen(calog, &calogLuaEngine); calogContextEval(lua, "srv = httpdListen(38900)\n" "httpdRoute(srv, 'GET', '/hi', function(req) return 'hello from lua path=' .. req.path end)\n" "httpdRoute(srv, 'GET', '/made', function(req) return { status = 201, body = 'created' } end)\n" "ready()"); pumpForReady(1); // JavaScript server on another port -- a handler written in a different language. js = calogContextOpen(calog, &calogJsEngine); calogContextEval(js, "var s = httpdListen(38901);\n" "httpdRoute(s, 'GET', '/hi', function(req) { return 'hello from js'; });\n" "ready();"); pumpForReady(2); CHECK(httpGet(LUA_PORT, "/hi", &status, body, sizeof(body)) && status == 200 && strstr(body, "hello from lua") != NULL && strstr(body, "path=/hi") != NULL, "lua route serves + sees the request path"); CHECK(httpGet(JS_PORT, "/hi", &status, body, sizeof(body)) && status == 200 && strstr(body, "hello from js") != NULL, "js route serves on its own server"); CHECK(httpGet(LUA_PORT, "/made", &status, body, sizeof(body)) && status == 201 && strcmp(body, "created") == 0, "response map sets a custom status"); CHECK(httpGet(LUA_PORT, "/nope", &status, body, sizeof(body)) && status == 404, "an unmatched route is 404"); // Hot reload: re-register /hi on the LIVE lua server with a new handler. atomic_store(&readyCount, 0); calogContextEval(lua, "httpdRoute(srv, 'GET', '/hi', function(req) return 'reloaded' end); ready()"); pumpForReady(1); CHECK(httpGet(LUA_PORT, "/hi", &status, body, sizeof(body)) && status == 200 && strcmp(body, "reloaded") == 0, "re-registering a route hot-swaps the handler"); calogDestroy(calog); printf("\n%d checks, %d failed\n", testsRun, testsFailed); fflush(stdout); return testsFailed == 0 ? 0 : 1; }