/* * 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 #include #include #include #include #include #include #include #include #include #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); }