233 lines
7.8 KiB
C
233 lines
7.8 KiB
C
// testHttp.c -- exercises the HTTP client library. A tiny in-test HTTP/1.1 server runs on its
|
|
// own thread (bind an ephemeral 127.0.0.1 port, accept one connection, reply with a fixed
|
|
// Content-Length response), and a Lua context httpGet()s it and reports the status, body, and a
|
|
// lowercased header. Also covers the connect-refused and bad-scheme error paths. Kept to
|
|
// plain http:// so no TLS server is needed, though the binary still links OpenSSL because the
|
|
// library references it.
|
|
|
|
#define _GNU_SOURCE
|
|
|
|
#include "calog.h"
|
|
#include "calogHttp.h"
|
|
|
|
#include <arpa/inet.h>
|
|
#include <netinet/in.h>
|
|
#include <pthread.h>
|
|
#include <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
|
|
|
|
#define PUMP_LIMIT 4000
|
|
#define RESULT_SLOTS 12
|
|
|
|
static CalogT *calog = NULL;
|
|
static _Atomic int64_t results[RESULT_SLOTS];
|
|
static _Atomic int32_t doneCount = 0;
|
|
static _Atomic int32_t errorCount = 0;
|
|
static int32_t testsRun = 0;
|
|
static int32_t testsFailed = 0;
|
|
static int gListenFd = -1;
|
|
|
|
static int64_t asInt(const CalogValueT *value);
|
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
|
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static void onError(uint64_t contextId, const char *message, void *userData);
|
|
static void pumpUntilDone(int32_t target);
|
|
static void *serverThread(void *arg);
|
|
|
|
|
|
static int64_t asInt(const CalogValueT *value) {
|
|
if (value->type == calogIntE) {
|
|
return value->as.i;
|
|
}
|
|
return (int64_t)value->as.r;
|
|
}
|
|
|
|
|
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
|
|
testsRun++;
|
|
if (!condition) {
|
|
testsFailed++;
|
|
printf("FAIL %s:%d %s\n", file, line, message);
|
|
}
|
|
}
|
|
|
|
|
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)args;
|
|
(void)argCount;
|
|
(void)userData;
|
|
atomic_fetch_add(&doneCount, 1);
|
|
calogValueNil(result);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
int64_t tag;
|
|
|
|
(void)userData;
|
|
calogValueNil(result);
|
|
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
|
|
return calogFail(result, calogErrArgE, "report expects (tag, value)");
|
|
}
|
|
tag = asInt(&args[0]);
|
|
if (tag < 0 || tag >= RESULT_SLOTS) {
|
|
return calogFail(result, calogErrArgE, "report: tag out of range");
|
|
}
|
|
atomic_store(&results[tag], asInt(&args[1]));
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static void onError(uint64_t contextId, const char *message, void *userData) {
|
|
(void)contextId;
|
|
(void)userData;
|
|
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
|
|
atomic_fetch_add(&errorCount, 1);
|
|
}
|
|
|
|
|
|
static void pumpUntilDone(int32_t target) {
|
|
struct timespec ts = { 0, 500000 };
|
|
int i;
|
|
|
|
for (i = 0; i < PUMP_LIMIT; i++) {
|
|
calogPump(calog);
|
|
if (atomic_load(&doneCount) >= target) {
|
|
calogPump(calog);
|
|
return;
|
|
}
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
}
|
|
|
|
|
|
static void *serverThread(void *arg) {
|
|
static const char ok[] = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
|
|
// A hostile chunked response: a chunk size that saturates size_t. A correct client rejects
|
|
// it (a catchable error); a broken one loops forever in the dechunker.
|
|
static const char evil[] = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nffffffffffffffff\r\nx\r\n0\r\n\r\n";
|
|
char buf[2048];
|
|
int n;
|
|
|
|
(void)arg;
|
|
// Serve two connections in order: the well-formed response, then the hostile one. Each
|
|
// consumes (part of) the request, replies, and closes so the client's read-to-EOF ends.
|
|
for (n = 0; n < 2; n++) {
|
|
int cfd;
|
|
cfd = accept(gListenFd, NULL, NULL);
|
|
if (cfd < 0) {
|
|
break;
|
|
}
|
|
{
|
|
ssize_t received;
|
|
const char *response;
|
|
size_t length;
|
|
received = recv(cfd, buf, sizeof(buf), 0);
|
|
(void)received;
|
|
response = (n == 0) ? ok : evil;
|
|
length = (n == 0) ? (sizeof(ok) - 1) : (sizeof(evil) - 1);
|
|
(void)send(cfd, response, length, MSG_NOSIGNAL);
|
|
close(cfd);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
int main(void) {
|
|
CalogContextT *ctx;
|
|
pthread_t server;
|
|
struct sockaddr_in addr;
|
|
socklen_t addrLen;
|
|
char script[512];
|
|
int port;
|
|
int yes;
|
|
int32_t i;
|
|
|
|
calog = calogCreate();
|
|
if (calog == NULL) {
|
|
printf("calog create failed\n");
|
|
return 1;
|
|
}
|
|
calogSetErrorHandler(calog, onError, NULL);
|
|
calogRegister(calog, "report", nativeReport, NULL);
|
|
calogRegister(calog, "done", nativeDone, NULL);
|
|
calogHttpRegister(calog);
|
|
for (i = 0; i < RESULT_SLOTS; i++) {
|
|
atomic_store(&results[i], -1);
|
|
}
|
|
|
|
// In-test HTTP/1.1 server on an ephemeral loopback port.
|
|
yes = 1;
|
|
gListenFd = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (gListenFd < 0) {
|
|
printf("socket failed\n");
|
|
return 1;
|
|
}
|
|
setsockopt(gListenFd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
|
memset(&addr, 0, sizeof(addr));
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
addr.sin_port = 0;
|
|
if (bind(gListenFd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
|
|
printf("bind failed\n");
|
|
return 1;
|
|
}
|
|
if (listen(gListenFd, 1) != 0) {
|
|
printf("listen failed\n");
|
|
return 1;
|
|
}
|
|
addrLen = sizeof(addr);
|
|
if (getsockname(gListenFd, (struct sockaddr *)&addr, &addrLen) != 0) {
|
|
printf("getsockname failed\n");
|
|
return 1;
|
|
}
|
|
port = (int)ntohs(addr.sin_port);
|
|
pthread_create(&server, NULL, serverThread, NULL);
|
|
|
|
snprintf(script, sizeof(script),
|
|
"local r = httpGet('http://127.0.0.1:%d/')\n"
|
|
"report(1, r.status)\n"
|
|
"report(2, r.body == 'hello' and 1 or 0)\n"
|
|
"report(3, r.headers['content-length'] == '5' and 1 or 0)\n"
|
|
"local okc = pcall(function() return httpGet('http://127.0.0.1:%d/') end)\n"
|
|
"report(6, okc and 0 or 1)\n" // hostile chunk size -> catchable error, not a hang
|
|
"local ok = pcall(function() httpGet('http://127.0.0.1:1/') end)\n"
|
|
"report(4, ok and 0 or 1)\n"
|
|
"local ok2 = pcall(function() httpGet('ftp://example/') end)\n"
|
|
"report(5, ok2 and 0 or 1)\n"
|
|
"done()", port, port);
|
|
|
|
ctx = calogContextOpen(calog, &calogLuaEngine);
|
|
calogContextEval(ctx, script);
|
|
pumpUntilDone(1);
|
|
|
|
pthread_join(server, NULL);
|
|
close(gListenFd);
|
|
|
|
CHECK(atomic_load(&results[1]) == 200, "httpGet returns status 200 from the in-test server");
|
|
CHECK(atomic_load(&results[2]) == 1, "httpGet returns the response body verbatim");
|
|
CHECK(atomic_load(&results[3]) == 1, "response headers are keyed by lowercased name");
|
|
CHECK(atomic_load(&results[4]) == 1, "a refused connection raises a catchable error");
|
|
CHECK(atomic_load(&results[5]) == 1, "an unsupported URL scheme raises a catchable error");
|
|
CHECK(atomic_load(&results[6]) == 1, "a hostile oversized chunk size is rejected cleanly, not hung");
|
|
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
|
|
|
|
calogContextClose(ctx);
|
|
calogDestroy(calog);
|
|
|
|
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
|
|
fflush(stdout);
|
|
if (testsFailed != 0) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|