summaryrefslogtreecommitdiffstats
path: root/test/http_server.c
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:52:43 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:52:43 -0300
commit42d8c12d2bd88462df8c3aac9449abbf98400084 (patch)
tree564b07e68aed22c3aa4a066b822d731328fb4e97 /test/http_server.c
downloadfortran-42d8c12d2bd88462df8c3aac9449abbf98400084.tar.gz
Initial import
Diffstat (limited to 'test/http_server.c')
-rw-r--r--test/http_server.c461
1 files changed, 461 insertions, 0 deletions
diff --git a/test/http_server.c b/test/http_server.c
new file mode 100644
index 0000000..623f958
--- /dev/null
+++ b/test/http_server.c
@@ -0,0 +1,461 @@
+/*
+ * A throwaway HTTP/1.1 server on a random loopback port.
+ *
+ * Fortran has no sockets, so the suite's server is C and is compiled and
+ * linked alongside the tests. Nothing is mocked: the client under test takes
+ * the same path it takes in production, libcurl included. It doubles as the
+ * test that base_url works, since without it none of the suite would run.
+ */
+#include <arpa/inet.h>
+#include <errno.h>
+#include <netinet/in.h>
+#include <poll.h>
+#include <pthread.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#define TS_MAX_ROUTES 32
+#define TS_MAX_REPLIES 8
+#define TS_MAX_CALLS 4096
+#define TS_REQUEST_MAX 262144
+
+typedef struct {
+ long status;
+ char *body;
+ char *headers;
+} ts_reply;
+
+typedef struct {
+ char *path;
+ ts_reply replies[TS_MAX_REPLIES];
+ size_t count;
+ size_t served;
+} ts_route;
+
+typedef struct {
+ int listen_fd;
+ int port;
+ pthread_t thread;
+ pthread_mutex_t lock;
+ int stopping;
+ ts_route routes[TS_MAX_ROUTES];
+ size_t route_count;
+ ts_route fallback;
+ char *calls[TS_MAX_CALLS];
+ size_t call_count;
+} ts_server;
+
+static char *ts_dup(const char *text)
+{
+ size_t len = (text != NULL) ? strlen(text) : 0;
+ char *copy = malloc(len + 1);
+
+ if (copy == NULL)
+ return NULL;
+ if (len > 0)
+ memcpy(copy, text, len);
+ copy[len] = '\0';
+ return copy;
+}
+
+static void route_clear(ts_route *route)
+{
+ size_t i;
+
+ for (i = 0; i < route->count; i++) {
+ free(route->replies[i].body);
+ free(route->replies[i].headers);
+ }
+ route->count = 0;
+ route->served = 0;
+}
+
+static ts_route *route_for(ts_server *server, const char *path, int create)
+{
+ size_t i;
+
+ for (i = 0; i < server->route_count; i++)
+ if (strcmp(server->routes[i].path, path) == 0)
+ return &server->routes[i];
+ if (!create || server->route_count >= TS_MAX_ROUTES)
+ return NULL;
+ server->routes[server->route_count].path = ts_dup(path);
+ return &server->routes[server->route_count++];
+}
+
+static void route_push(ts_route *route, long status, const char *body,
+ const char *headers)
+{
+ if (route == NULL || route->count >= TS_MAX_REPLIES)
+ return;
+ route->replies[route->count].status = status;
+ route->replies[route->count].body = ts_dup(body);
+ route->replies[route->count].headers = ts_dup(headers);
+ route->count++;
+}
+
+static const char *reason_phrase(long status)
+{
+ switch (status) {
+ case 200:
+ return "OK";
+ case 400:
+ return "Bad Request";
+ case 404:
+ return "Not Found";
+ case 429:
+ return "Too Many Requests";
+ case 500:
+ return "Internal Server Error";
+ case 502:
+ return "Bad Gateway";
+ case 503:
+ return "Service Unavailable";
+ case 504:
+ return "Gateway Timeout";
+ default:
+ return "Status";
+ }
+}
+
+static int read_request_path(int fd, char *out, size_t out_size)
+{
+ static char buffer[TS_REQUEST_MAX];
+ size_t used = 0;
+ const char *start;
+ const char *space;
+ size_t len;
+
+ for (;;) {
+ ssize_t got;
+ struct pollfd pfd;
+
+ pfd.fd = fd;
+ pfd.events = POLLIN;
+ pfd.revents = 0;
+ if (poll(&pfd, 1, 5000) <= 0)
+ return 0;
+ got = read(fd, buffer + used, sizeof(buffer) - used - 1);
+ if (got <= 0)
+ return 0;
+ used += (size_t)got;
+ buffer[used] = '\0';
+ if (strstr(buffer, "\r\n\r\n") != NULL)
+ break;
+ if (used + 1 >= sizeof(buffer))
+ return 0;
+ }
+
+ start = strchr(buffer, ' ');
+ if (start == NULL)
+ return 0;
+ start++;
+ space = strchr(start, ' ');
+ if (space == NULL)
+ return 0;
+ len = (size_t)(space - start);
+ if (len + 1 > out_size)
+ return 0;
+ memcpy(out, start, len);
+ out[len] = '\0';
+ return 1;
+}
+
+static void write_all(int fd, const char *data, size_t len)
+{
+ size_t sent = 0;
+
+ while (sent < len) {
+ ssize_t wrote = write(fd, data + sent, len - sent);
+ if (wrote <= 0) {
+ if (wrote < 0 && errno == EINTR)
+ continue;
+ return;
+ }
+ sent += (size_t)wrote;
+ }
+}
+
+static void serve(ts_server *server, int fd)
+{
+ static char path[TS_REQUEST_MAX];
+ char head[1024];
+ ts_route *route;
+ long status;
+ char *body;
+ char *headers;
+ size_t index;
+ int head_len;
+
+ if (!read_request_path(fd, path, sizeof(path)))
+ return;
+
+ pthread_mutex_lock(&server->lock);
+ if (server->call_count < TS_MAX_CALLS)
+ server->calls[server->call_count++] = ts_dup(path);
+
+ route = route_for(server, path, 0);
+ if (route == NULL || route->count == 0)
+ route = &server->fallback;
+
+ if (route->count == 0) {
+ pthread_mutex_unlock(&server->lock);
+ head_len = snprintf(head, sizeof(head),
+ "HTTP/1.1 404 Not Found\r\n"
+ "Content-Length: 0\r\n"
+ "Connection: close\r\n\r\n");
+ if (head_len > 0)
+ write_all(fd, head, (size_t)head_len);
+ return;
+ }
+
+ index = route->served;
+ if (index >= route->count)
+ index = route->count - 1;
+ route->served++;
+
+ status = route->replies[index].status;
+ body = ts_dup(route->replies[index].body);
+ headers = ts_dup(route->replies[index].headers);
+ pthread_mutex_unlock(&server->lock);
+
+ head_len = snprintf(head, sizeof(head),
+ "HTTP/1.1 %ld %s\r\n"
+ "Content-Type: application/octet-stream\r\n"
+ "Content-Length: %zu\r\n"
+ "Connection: close\r\n"
+ "%s%s"
+ "\r\n",
+ status, reason_phrase(status),
+ body != NULL ? strlen(body) : 0,
+ (headers != NULL && headers[0] != '\0') ? headers : "",
+ (headers != NULL && headers[0] != '\0') ? "\r\n" : "");
+ if (head_len > 0)
+ write_all(fd, head, (size_t)head_len);
+ if (body != NULL)
+ write_all(fd, body, strlen(body));
+ free(body);
+ free(headers);
+}
+
+static void *accept_loop(void *argument)
+{
+ ts_server *server = argument;
+
+ for (;;) {
+ struct pollfd pfd;
+ int fd;
+
+ pthread_mutex_lock(&server->lock);
+ if (server->stopping) {
+ pthread_mutex_unlock(&server->lock);
+ return NULL;
+ }
+ pthread_mutex_unlock(&server->lock);
+
+ pfd.fd = server->listen_fd;
+ pfd.events = POLLIN;
+ pfd.revents = 0;
+ if (poll(&pfd, 1, 20) <= 0)
+ continue;
+
+ fd = accept(server->listen_fd, NULL, NULL);
+ if (fd < 0)
+ continue;
+ serve(server, fd);
+ shutdown(fd, SHUT_WR);
+ close(fd);
+ }
+}
+
+void *ts_start(void)
+{
+ struct sockaddr_in address;
+ socklen_t length = sizeof(address);
+ ts_server *server = calloc(1, sizeof(*server));
+ int reuse = 1;
+
+ if (server == NULL)
+ return NULL;
+ pthread_mutex_init(&server->lock, NULL);
+
+ server->listen_fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (server->listen_fd < 0) {
+ free(server);
+ return NULL;
+ }
+ setsockopt(server->listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse,
+ sizeof(reuse));
+
+ memset(&address, 0, sizeof(address));
+ address.sin_family = AF_INET;
+ address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ address.sin_port = 0;
+
+ if (bind(server->listen_fd, (struct sockaddr *)&address,
+ sizeof(address)) != 0 ||
+ listen(server->listen_fd, 64) != 0 ||
+ getsockname(server->listen_fd, (struct sockaddr *)&address,
+ &length) != 0) {
+ close(server->listen_fd);
+ free(server);
+ return NULL;
+ }
+
+ server->port = ntohs(address.sin_port);
+
+ if (pthread_create(&server->thread, NULL, accept_loop, server) != 0) {
+ close(server->listen_fd);
+ free(server);
+ return NULL;
+ }
+ return server;
+}
+
+void ts_stop(void *handle)
+{
+ ts_server *server = handle;
+ size_t i;
+
+ if (server == NULL)
+ return;
+ pthread_mutex_lock(&server->lock);
+ server->stopping = 1;
+ pthread_mutex_unlock(&server->lock);
+ pthread_join(server->thread, NULL);
+ close(server->listen_fd);
+
+ for (i = 0; i < server->route_count; i++) {
+ route_clear(&server->routes[i]);
+ free(server->routes[i].path);
+ }
+ route_clear(&server->fallback);
+ for (i = 0; i < server->call_count; i++)
+ free(server->calls[i]);
+ pthread_mutex_destroy(&server->lock);
+ free(server);
+}
+
+int ts_port(void *handle)
+{
+ ts_server *server = handle;
+
+ return (server != NULL) ? server->port : 0;
+}
+
+void ts_reset(void *handle)
+{
+ ts_server *server = handle;
+ size_t i;
+
+ if (server == NULL)
+ return;
+ pthread_mutex_lock(&server->lock);
+ for (i = 0; i < server->route_count; i++) {
+ route_clear(&server->routes[i]);
+ free(server->routes[i].path);
+ server->routes[i].path = NULL;
+ }
+ server->route_count = 0;
+ route_clear(&server->fallback);
+ for (i = 0; i < server->call_count; i++)
+ free(server->calls[i]);
+ server->call_count = 0;
+ pthread_mutex_unlock(&server->lock);
+}
+
+void ts_respond(void *handle, const char *path, long status, const char *body,
+ const char *headers)
+{
+ ts_server *server = handle;
+ ts_route *route;
+
+ pthread_mutex_lock(&server->lock);
+ route = route_for(server, path, 1);
+ if (route != NULL) {
+ route_clear(route);
+ route_push(route, status, body, headers);
+ }
+ pthread_mutex_unlock(&server->lock);
+}
+
+void ts_respond_more(void *handle, const char *path, long status,
+ const char *body, const char *headers)
+{
+ ts_server *server = handle;
+
+ pthread_mutex_lock(&server->lock);
+ route_push(route_for(server, path, 1), status, body, headers);
+ pthread_mutex_unlock(&server->lock);
+}
+
+void ts_respond_default(void *handle, long status, const char *body,
+ const char *headers)
+{
+ ts_server *server = handle;
+
+ pthread_mutex_lock(&server->lock);
+ route_clear(&server->fallback);
+ route_push(&server->fallback, status, body, headers);
+ pthread_mutex_unlock(&server->lock);
+}
+
+size_t ts_count(void *handle, const char *prefix)
+{
+ ts_server *server = handle;
+ size_t total = 0;
+ size_t len = strlen(prefix);
+ size_t i;
+
+ pthread_mutex_lock(&server->lock);
+ for (i = 0; i < server->call_count; i++)
+ if (server->calls[i] != NULL &&
+ strncmp(server->calls[i], prefix, len) == 0)
+ total++;
+ pthread_mutex_unlock(&server->lock);
+ return total;
+}
+
+size_t ts_total(void *handle)
+{
+ ts_server *server = handle;
+ size_t total;
+
+ pthread_mutex_lock(&server->lock);
+ total = server->call_count;
+ pthread_mutex_unlock(&server->lock);
+ return total;
+}
+
+size_t ts_path_length(void *handle, size_t index)
+{
+ ts_server *server = handle;
+ size_t len = 0;
+
+ pthread_mutex_lock(&server->lock);
+ if (index < server->call_count && server->calls[index] != NULL)
+ len = strlen(server->calls[index]);
+ pthread_mutex_unlock(&server->lock);
+ return len;
+}
+
+void ts_path_copy(void *handle, size_t index, char *out, size_t out_size)
+{
+ ts_server *server = handle;
+
+ if (out_size == 0)
+ return;
+ out[0] = '\0';
+ pthread_mutex_lock(&server->lock);
+ if (index < server->call_count && server->calls[index] != NULL) {
+ size_t len = strlen(server->calls[index]);
+ if (len >= out_size)
+ len = out_size - 1;
+ memcpy(out, server->calls[index], len);
+ out[len] = '\0';
+ }
+ pthread_mutex_unlock(&server->lock);
+}