diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:52:43 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:52:43 -0300 |
| commit | 42d8c12d2bd88462df8c3aac9449abbf98400084 (patch) | |
| tree | 564b07e68aed22c3aa4a066b822d731328fb4e97 /test/http_server.c | |
| download | fortran-42d8c12d2bd88462df8c3aac9449abbf98400084.tar.gz | |
Initial import
Diffstat (limited to 'test/http_server.c')
| -rw-r--r-- | test/http_server.c | 461 |
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); +} |