171 lines
5.8 KiB
C
171 lines
5.8 KiB
C
// 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 <arpa/inet.h>
|
|
#include <netinet/in.h>
|
|
#include <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#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;
|
|
}
|