// testHttpdLua.c -- the httpd-as-a-script (examples/httpd.lua). A Lua context loads the script (which // implements HTTP/1.1 + routing + keep-alive + WebSocket purely in Lua over calog's tcp* transport and // crypto natives) and serves on a port; this test drives real requests over a loopback socket: routing, // a 404, a response-map custom status, HTTP/1.1 keep-alive (two requests on one connection), and a full // WebSocket handshake + echo. There is no C HTTP code under test -- the point is that the protocol // lives in the script. #define _GNU_SOURCE // strcasestr #include "calog.h" #include "calogCrypto.h" #include "calogNet.h" #include #include #include #include #include #include #include #include #include #include #define PORT 38910 #define HTTPS_PORT 38911 #define PUMP_LIMIT 4000 static CalogT *calog = NULL; static _Atomic bool serving = true; static _Atomic bool isReady = false; 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 genCert(const char *certPath, const char *keyPath); static int clientConnect(int port); static bool httpsGet(int port, const char *path, int *statusOut, char *body, size_t bodyCap); static char *loadScript(const char *path); static int32_t nativeKeepServing(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReady(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static bool readResponse(int fd, int *statusOut, char *body, size_t bodyCap); static void checkImpl(bool condition, const char *message, int32_t line) { testsRun++; if (!condition) { testsFailed++; printf("FAIL testHttpdLua.c:%d %s\n", line, message); } } static int clientConnect(int port) { struct sockaddr_in addr; int fd; fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { return -1; } 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 -1; } return fd; } // Read up to one full HTTP response (headers + Content-Length body) from fd. Parses the status and // copies the body out. Returns false on a closed/short read. Consumes EXACTLY one response (headers // byte-by-byte to the CRLFCRLF, then precisely Content-Length body bytes) so a following pipelined // response is left in the socket for the next call -- a bulk recv could coalesce two responses and // make the second read block forever. static bool readResponse(int fd, int *statusOut, char *body, size_t bodyCap) { char response[8192]; char *clHeader; int total; int headerLen; int contentLen; int bodyRead; total = 0; headerLen = -1; while (total < (int)sizeof(response) - 1) { ssize_t got; got = recv(fd, response + total, 1, 0); if (got <= 0) { break; } total += (int)got; if (total >= 4 && memcmp(response + total - 4, "\r\n\r\n", 4) == 0) { headerLen = total; break; } } if (headerLen < 0) { return false; } response[total] = '\0'; contentLen = 0; clHeader = strcasestr(response, "Content-Length:"); if (clHeader != NULL) { contentLen = atoi(clHeader + 15); } bodyRead = 0; while (bodyRead < contentLen && headerLen + bodyRead < (int)sizeof(response) - 1) { ssize_t got; got = recv(fd, response + headerLen + bodyRead, 1, 0); if (got <= 0) { break; } bodyRead += (int)got; } *statusOut = 0; if (strncmp(response, "HTTP/1.1 ", 9) == 0) { *statusOut = atoi(response + 9); } response[headerLen + bodyRead] = '\0'; snprintf(body, bodyCap, "%s", response + headerLen); return true; } static char *loadScript(const char *path) { FILE *f; char *buffer; long size; size_t got; f = fopen(path, "rb"); if (f == NULL) { return NULL; } fseek(f, 0, SEEK_END); size = ftell(f); fseek(f, 0, SEEK_SET); if (size < 0) { fclose(f); return NULL; } buffer = (char *)malloc((size_t)size + 1); if (buffer == NULL) { fclose(f); return NULL; } got = fread(buffer, 1, (size_t)size, f); buffer[got] = '\0'; fclose(f); return buffer; } // Generate a throwaway self-signed cert + key via the system openssl CLI, for the HTTPS test. static bool genCert(const char *certPath, const char *keyPath) { char cmd[512]; snprintf(cmd, sizeof(cmd), "openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 1 -nodes -subj /CN=localhost >/dev/null 2>&1", keyPath, certPath); return system(cmd) == 0; } // Minimal HTTPS client: connect, TLS handshake (no cert verification -- self-signed test cert), GET // the path, parse the status + body. Exercises the tcp transport's tls option end to end. static bool httpsGet(int port, const char *path, int *statusOut, char *body, size_t bodyCap) { SSL_CTX *ctx; SSL *ssl; char request[256]; char response[8192]; char *bodyStart; int fd; int total; int requestLen; bool ok; ctx = SSL_CTX_new(TLS_client_method()); if (ctx == NULL) { return false; } fd = clientConnect(port); if (fd < 0) { SSL_CTX_free(ctx); return false; } ssl = SSL_new(ctx); if (ssl == NULL) { close(fd); SSL_CTX_free(ctx); return false; } SSL_set_fd(ssl, fd); ok = SSL_connect(ssl) == 1; if (ok) { requestLen = snprintf(request, sizeof(request), "GET %s HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", path); SSL_write(ssl, request, requestLen); total = 0; for (;;) { int got; got = SSL_read(ssl, response + total, (int)sizeof(response) - 1 - total); if (got <= 0) { break; } total += got; if (total >= (int)sizeof(response) - 1) { break; } } response[total > 0 ? total : 0] = '\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); } ok = total > 0; } if (ssl != NULL) { SSL_shutdown(ssl); SSL_free(ssl); } close(fd); SSL_CTX_free(ctx); return ok; } static int32_t nativeKeepServing(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)args; (void)argCount; (void)userData; calogValueBool(result, atomic_load(&serving)); return calogOkE; } static int32_t nativeReady(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)args; (void)argCount; (void)userData; atomic_store(&isReady, true); calogValueNil(result); return calogOkE; } int main(void) { CalogContextT *lua; CalogContextT *tls; char *script; char *source; char body[8192]; const char *setup; int i; int status; int fd; calog = calogCreate(); if (calog == NULL) { printf("calog create failed\n"); return 1; } calogNetRegister(calog); calogCryptoRegister(calog); calogRegisterInline(calog, "keepServing", nativeKeepServing, NULL); calogRegisterInline(calog, "ready", nativeReady, NULL); script = loadScript("examples/httpd.lua"); if (script == NULL) { printf("could not load examples/httpd.lua\n"); return 1; } // Wrap the module (it ends in `return httpd`) so it binds to a global, then register routes and // serve. keepServing lets this test stop the accept loop; ready fires once the socket is bound. setup = "\nlocal s = httpd.new()\n" "s:route('GET', '/hi', function(req) return 'hello ' .. req.path end)\n" "s:route('GET', '/made', function(req) return { status = 201, body = 'created' } end)\n" "s:route('GET', '/boom', function(req) return nil .. 'x' end)\n" /* runtime error -> 500 */ "s:websocket('/ws', function(msg) return 'echo: ' .. msg.message end)\n" "s:serve(38910, { keep = keepServing, onReady = ready })\n"; source = (char *)malloc(strlen(script) + strlen(setup) + 64); if (source == NULL) { return 1; } sprintf(source, "httpd = (function()\n%s\nend)()\n%s", script, setup); free(script); lua = calogContextOpen(calog, &calogLuaEngine); calogContextEval(lua, source); free(source); // Wait until the script reports the socket is listening (it runs on its own context thread). for (i = 0; i < PUMP_LIMIT && !atomic_load(&isReady); i++) { struct timespec ts = { 0, 500000 }; calogPump(calog); nanosleep(&ts, NULL); } CHECK(atomic_load(&isReady), "the script httpd bound and is listening"); // --- plain HTTP routing (Connection: close) --- fd = clientConnect(PORT); if (fd >= 0) { const char *req = "GET /hi HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n"; send(fd, req, strlen(req), 0); CHECK(readResponse(fd, &status, body, sizeof(body)) && status == 200 && strcmp(body, "hello /hi") == 0, "GET route serves and sees the path"); close(fd); } else { CHECK(false, "connect for GET /hi"); } fd = clientConnect(PORT); if (fd >= 0) { const char *req = "GET /made HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n"; send(fd, req, strlen(req), 0); CHECK(readResponse(fd, &status, body, sizeof(body)) && status == 201 && strcmp(body, "created") == 0, "response map sets a custom status"); close(fd); } fd = clientConnect(PORT); if (fd >= 0) { const char *req = "GET /nope HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n"; send(fd, req, strlen(req), 0); CHECK(readResponse(fd, &status, body, sizeof(body)) && status == 404, "an unmatched route is 404"); close(fd); } // --- HTTP/1.1 keep-alive: two requests on ONE connection --- fd = clientConnect(PORT); if (fd >= 0) { const char *req = "GET /hi HTTP/1.1\r\nHost: x\r\nConnection: keep-alive\r\n\r\n"; bool first; bool second; send(fd, req, strlen(req), 0); first = readResponse(fd, &status, body, sizeof(body)) && status == 200 && strcmp(body, "hello /hi") == 0; send(fd, req, strlen(req), 0); second = readResponse(fd, &status, body, sizeof(body)) && status == 200 && strcmp(body, "hello /hi") == 0; CHECK(first && second, "keep-alive serves two requests on one connection"); close(fd); } // --- WebSocket: handshake (RFC 6455 sample key -> known accept) + a masked echo round-trip --- fd = clientConnect(PORT); if (fd >= 0) { const char *req = "GET /ws HTTP/1.1\r\nHost: x\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n"; unsigned char frame[10]; unsigned char reply[64]; char handshake[1024]; ssize_t got; ssize_t rgot; send(fd, req, strlen(req), 0); got = recv(fd, handshake, sizeof(handshake) - 1, 0); handshake[got > 0 ? got : 0] = '\0'; CHECK(got > 0 && strstr(handshake, "101 Switching Protocols") != NULL && strstr(handshake, "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=") != NULL, "WebSocket handshake returns the RFC 6455 accept value"); // One masked text frame carrying "ping": FIN|text, MASK|len=4, mask 01020304, "ping" XOR mask. frame[0] = 0x81; frame[1] = 0x80 | 4; frame[2] = 0x01; frame[3] = 0x02; frame[4] = 0x03; frame[5] = 0x04; frame[6] = (unsigned char)('p' ^ 0x01); frame[7] = (unsigned char)('i' ^ 0x02); frame[8] = (unsigned char)('n' ^ 0x03); frame[9] = (unsigned char)('g' ^ 0x04); send(fd, frame, sizeof(frame), 0); rgot = recv(fd, reply, sizeof(reply), 0); // Server frame: 0x81, len=10 (unmasked), "echo: ping". CHECK(rgot >= 12 && reply[0] == 0x81 && reply[1] == 10 && memcmp(reply + 2, "echo: ping", 10) == 0, "WebSocket echoes a masked text frame back through the Lua handler"); close(fd); } // --- WebSocket FRAGMENTATION: "hello" split across a text frame (FIN=0) + a continuation (FIN=1) --- fd = clientConnect(PORT); if (fd >= 0) { const char *req = "GET /ws HTTP/1.1\r\nHost: x\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n"; unsigned char frames[16]; unsigned char reply[64]; char handshake[512]; ssize_t rgot; send(fd, req, strlen(req), 0); recv(fd, handshake, sizeof(handshake) - 1, 0); // consume the 101 // Frame 1: opcode 0x1 (text) FIN=0, masked, len 3 "hel" ^ {1,2,3,4} frames[0] = 0x01; frames[1] = 0x83; frames[2] = 1; frames[3] = 2; frames[4] = 3; frames[5] = 4; frames[6] = (unsigned char)('h' ^ 1); frames[7] = (unsigned char)('e' ^ 2); frames[8] = (unsigned char)('l' ^ 3); // Frame 2: opcode 0x0 (continuation) FIN=1, masked, len 2 "lo" ^ {1,2,3,4} frames[9] = 0x80; frames[10] = 0x82; frames[11] = 1; frames[12] = 2; frames[13] = 3; frames[14] = 4; frames[15] = (unsigned char)('l' ^ 1); send(fd, frames, sizeof(frames), 0); send(fd, (const unsigned char[]){ (unsigned char)('o' ^ 2) }, 1, 0); // last masked byte rgot = recv(fd, reply, sizeof(reply), 0); // Server reply frame: 0x81, len 11, "echo: hello" -- the two fragments were reassembled. CHECK(rgot >= 13 && reply[0] == 0x81 && reply[1] == 11 && memcmp(reply + 2, "echo: hello", 11) == 0, "WebSocket reassembles a fragmented text message"); close(fd); } // --- A handler that ERRORS returns 500, and the server keeps serving afterward (pcall isolation) --- fd = clientConnect(PORT); if (fd >= 0) { const char *req = "GET /boom HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n"; send(fd, req, strlen(req), 0); CHECK(readResponse(fd, &status, body, sizeof(body)) && status == 500, "a handler runtime error becomes a 500"); close(fd); } // --- An abrupt RST mid-request must NOT kill the server (native I/O raises; handle's pcall catches) --- fd = clientConnect(PORT); if (fd >= 0) { struct linger sl; const char *partial = "GET /hi HTTP/1.1\r\nHost: x\r\n"; // no terminating CRLF -> server waits, then sees RST sl.l_onoff = 1; sl.l_linger = 0; // close() sends RST, not FIN setsockopt(fd, SOL_SOCKET, SO_LINGER, &sl, sizeof(sl)); send(fd, partial, strlen(partial), 0); close(fd); } { struct timespec ts = { 0, 20000000 }; nanosleep(&ts, NULL); // let the server process the RST } fd = clientConnect(PORT); if (fd >= 0) { const char *req = "GET /hi HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n"; send(fd, req, strlen(req), 0); CHECK(readResponse(fd, &status, body, sizeof(body)) && status == 200 && strcmp(body, "hello /hi") == 0, "server survives a client RST mid-request and keeps serving"); close(fd); } // --- HTTP/1.1 PIPELINING: two requests in one write; the leftover buffer must carry the second --- fd = clientConnect(PORT); if (fd >= 0) { const char *reqs = "GET /hi HTTP/1.1\r\nHost: x\r\nConnection: keep-alive\r\n\r\n" "GET /made HTTP/1.1\r\nHost: x\r\nConnection: keep-alive\r\n\r\n"; bool one; bool two; send(fd, reqs, strlen(reqs), 0); one = readResponse(fd, &status, body, sizeof(body)) && status == 200 && strcmp(body, "hello /hi") == 0; two = readResponse(fd, &status, body, sizeof(body)) && status == 201 && strcmp(body, "created") == 0; CHECK(one && two, "pipelined second request (buffered past the first) is served, not lost"); close(fd); } // --- SMUGGLING: a Transfer-Encoding request must be REJECTED (its trailing bytes must not be // re-parsed as a smuggled pipelined request via the leftover buffer) --- fd = clientConnect(PORT); if (fd >= 0) { const char *req = "POST /made HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n" "0\r\n\r\nGET /made HTTP/1.1\r\nHost: x\r\n\r\n"; bool served; send(fd, req, strlen(req), 0); served = readResponse(fd, &status, body, sizeof(body)); CHECK(!served || status != 201, "Transfer-Encoding request rejected; the smuggled GET /made is not served"); close(fd); } // --- SMUGGLING: conflicting duplicate Content-Length must be REJECTED (CL.CL desync) --- fd = clientConnect(PORT); if (fd >= 0) { const char *req = "GET /hi HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\nContent-Length: 5\r\nConnection: close\r\n\r\nhello"; bool served; send(fd, req, strlen(req), 0); served = readResponse(fd, &status, body, sizeof(body)); CHECK(!served || status != 200, "conflicting duplicate Content-Length is rejected"); close(fd); } // --- HTTPS: a second context serves TLS with a generated self-signed cert (tcp tls option) --- tls = NULL; { const char *certPath = "/tmp/calogHttpdTest-cert.pem"; const char *keyPath = "/tmp/calogHttpdTest-key.pem"; if (genCert(certPath, keyPath)) { char *tlsScript; char *tlsSource; char tlsSetup[512]; atomic_store(&isReady, false); tlsScript = loadScript("examples/httpd.lua"); snprintf(tlsSetup, sizeof(tlsSetup), "\nlocal s = httpd.new()\n" "s:route('GET', '/hi', function(req) return 'secure ' .. req.path end)\n" "s:serve(38911, { tls = true, cert = '%s', key = '%s', keep = keepServing, onReady = ready })\n", certPath, keyPath); tlsSource = tlsScript != NULL ? (char *)malloc(strlen(tlsScript) + strlen(tlsSetup) + 64) : NULL; if (tlsSource != NULL) { sprintf(tlsSource, "httpd = (function()\n%s\nend)()\n%s", tlsScript, tlsSetup); tls = calogContextOpen(calog, &calogLuaEngine); calogContextEval(tls, tlsSource); for (i = 0; i < PUMP_LIMIT && !atomic_load(&isReady); i++) { struct timespec ts = { 0, 500000 }; calogPump(calog); nanosleep(&ts, NULL); } CHECK(httpsGet(HTTPS_PORT, "/hi", &status, body, sizeof(body)) && status == 200 && strcmp(body, "secure /hi") == 0, "HTTPS: the tcp transport's tls option serves an encrypted request"); // A bad handshake (plain HTTP to the TLS port) makes tcpAccept raise; the accept loop // must survive it. Fire one, then confirm the TLS server still serves. { int bad; bad = clientConnect(HTTPS_PORT); if (bad >= 0) { const char *probe = "GET / HTTP/1.1\r\nHost: x\r\n\r\n"; // not a TLS ClientHello send(bad, probe, strlen(probe), 0); close(bad); } { struct timespec ts = { 0, 20000000 }; nanosleep(&ts, NULL); } CHECK(httpsGet(HTTPS_PORT, "/hi", &status, body, sizeof(body)) && status == 200, "TLS server survives a bad/plain-HTTP handshake probe and keeps serving"); } } free(tlsScript); free(tlsSource); unlink(certPath); unlink(keyPath); } else { printf("SKIP: openssl CLI could not generate a test cert (HTTPS check skipped)\n"); } } // Stop the accept loops and let the context threads unwind before teardown. atomic_store(&serving, false); for (i = 0; i < PUMP_LIMIT; i++) { struct timespec ts = { 0, 500000 }; calogPump(calog); nanosleep(&ts, NULL); } calogContextClose(lua); if (tls != NULL) { calogContextClose(tls); } calogDestroy(calog); printf("\n%d checks, %d failed\n", testsRun, testsFailed); fflush(stdout); return testsFailed == 0 ? 0 : 1; }