// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // httpServe.c - serve a folder over HTTP on 127.0.0.1, for the web gates' // browser to load a built web program from (scripts/lib/webGate.sh). // // httpServe PORT DIR // // GET and HEAD of the files under DIR, each with its Content-Type (a .wasm // is application/wasm, which the browser's streaming compile needs). Of a // request's path the query and fragment are dropped, %XX is decoded, and // "." and ".." segments are resolved within DIR, never above it. A folder // is its index.html, and a folder's URL without its trailing slash is // redirected to the one with it. No file // is 404 (a folder without an index too: there are no listings), a request // that cannot be read is 400, and a method other than GET or HEAD is 501. // // Each connection is served by a child process, so a browser's parallel // requests are answered together; each answers one request and closes, as // HTTP/1.0 does. Says where it serves once listening, and runs until // stopped; exit 1 when the port cannot be had or DIR is no folder, 2 on bad // arguments. #define _DEFAULT_SOURCE #include #include #include #include #include #include #include #include #include #include #include "gateArg.h" #include "gateNet.h" #include "toolFile.h" #define TOOL_NAME "httpServe" #define ARG_COUNT 3 #define ARG_PORT 1 #define ARG_DIR 2 #define LISTEN_BACKLOG 64 // A request's head (its line and headers) holds at most this; the files // go out this much at a time; a client quiet this long is dropped. #define HEADER_MAX 16384u #define SEND_CHUNK 65536u #define CLIENT_SECONDS 30 #define PATH_MAX_BYTES 4096u #define RESPONSE_HEAD_MAX 512u #define HEX_BASE 16 #define HEX_LETTER 10 #define HEADER_END "\r\n\r\n" #define HEADER_END_BARE "\n\n" #define DEFAULT_TYPE "application/octet-stream" #define ERROR_TYPE "text/plain" #define INDEX_FILE "index.html" #define STATUS_OK 200 #define STATUS_MOVED 301 #define STATUS_BAD_REQUEST 400 #define STATUS_NOT_FOUND 404 #define STATUS_NO_METHOD 501 // A file name extension and the type it is served as. typedef struct TypeT { const char *extension; const char *type; } TypeT; static const char *contentType(const char *path); static int hexValue(char c); static bool resolve(const char *root, const char *target, char *path, size_t cap, bool *slash); static void sendError(int conn, int status, const char *message, bool head); static void sendFile(int conn, const char *path, bool head); static void sendRedirect(int conn, const char *target); static void serve(int conn, const char *root); static const TypeT kTypes[] = { {".html", "text/html"}, {".htm", "text/html"}, {".js", "text/javascript"}, {".mjs", "text/javascript"}, {".wasm", "application/wasm"}, {".data", "application/octet-stream"}, {".css", "text/css"}, {".png", "image/png"}, {".json", "application/json"}, {".txt", "text/plain"}, {".ico", "image/vnd.microsoft.icon"}, {".svg", "image/svg+xml"}, {".jpg", "image/jpeg"}, {".gif", "image/gif"}, {".wav", "audio/x-wav"} }; // The type a file is served as, by its name's extension (any case). static const char *contentType(const char *path) { const char *dot; size_t i; dot = strrchr(path, '.'); if (dot != NULL && strchr(dot, '/') == NULL) { for (i = 0; i < sizeof(kTypes) / sizeof(kTypes[0]); i++) { if (strcasecmp(dot, kTypes[i].extension) == 0) { return kTypes[i].type; } } } return DEFAULT_TYPE; } // A hex digit's value, or -1. static int hexValue(char c) { if (c >= '0' && c <= '9') { return c - '0'; } if (c >= 'a' && c <= 'f') { return c - 'a' + HEX_LETTER; } if (c >= 'A' && c <= 'F') { return c - 'A' + HEX_LETTER; } return -1; } // The file a request target names, under root, in path; slash says the // target's path ended with one. False when it can name none (too long, or // a NUL in it). static bool resolve(const char *root, const char *target, char *path, size_t cap, bool *slash) { char decoded[HEADER_MAX]; char *segment; char *cut; size_t rootLen; size_t len; size_t i; int high; int low; // The path alone, %XX decoded (a bad escape stays as it is). len = 0u; for (i = 0; target[i] != '\0' && target[i] != '?' && target[i] != '#' && len + 1u < sizeof(decoded); i++) { high = target[i] == '%' ? hexValue(target[i + 1u]) : -1; low = high >= 0 ? hexValue(target[i + 2u]) : -1; if (low >= 0) { decoded[len++] = (char)(high * HEX_BASE + low); i += 2u; } else { decoded[len++] = target[i]; } } decoded[len] = '\0'; *slash = len > 0u && target[i - 1u] == '/'; if (strlen(decoded) != len) { return false; } // Segment by segment onto root: none empty or ".", and ".." takes off // the one before it, never root's own. rootLen = strlen(root); if (rootLen >= cap) { return false; } memcpy(path, root, rootLen + 1u); for (segment = strtok(decoded, "/"); segment != NULL; segment = strtok(NULL, "/")) { if (strcmp(segment, ".") == 0) { continue; } if (strcmp(segment, "..") == 0) { cut = strrchr(path + rootLen, '/'); if (cut != NULL) { *cut = '\0'; } continue; } len = strlen(path); if (len + 1u + strlen(segment) >= cap) { return false; } path[len] = '/'; memcpy(path + len + 1u, segment, strlen(segment) + 1u); } return true; } // An error: its status line and the same as the body (the head alone, for // HEAD). static void sendError(int conn, int status, const char *message, bool head) { char top[RESPONSE_HEAD_MAX]; char body[RESPONSE_HEAD_MAX]; int bodyLen; int topLen; bodyLen = snprintf(body, sizeof(body), "%d %s\n", status, message); topLen = snprintf(top, sizeof(top), "HTTP/1.0 %d %s\r\nContent-Type: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n", status, message, ERROR_TYPE, bodyLen); if (gateNetSendAll(conn, top, (size_t)topLen) && !head) { gateNetSendAll(conn, body, (size_t)bodyLen); } } // A file, whole (its head alone, for HEAD); 404 when it cannot be opened. static void sendFile(int conn, const char *path, bool head) { struct stat info; uint8_t chunk[SEND_CHUNK]; char top[RESPONSE_HEAD_MAX]; ssize_t got; int topLen; int fd; fd = open(path, O_RDONLY); if (fd < 0 || fstat(fd, &info) != 0 || !S_ISREG(info.st_mode)) { sendError(conn, STATUS_NOT_FOUND, "File not found", head); if (fd >= 0) { close(fd); } return; } topLen = snprintf(top, sizeof(top), "HTTP/1.0 %d OK\r\nContent-Type: %s\r\nContent-Length: %lld\r\nConnection: close\r\n\r\n", STATUS_OK, contentType(path), (long long)info.st_size); if (gateNetSendAll(conn, top, (size_t)topLen) && !head) { while ((got = read(fd, chunk, sizeof(chunk))) > 0 && gateNetSendAll(conn, chunk, (size_t)got)) { // Chunk by chunk to the end, or until the client goes. } } close(fd); } // A folder's URL without its trailing slash: to the one with it (the // query and fragment kept). static void sendRedirect(int conn, const char *target) { char top[RESPONSE_HEAD_MAX + HEADER_MAX]; size_t pathLen; int topLen; pathLen = strcspn(target, "?#"); topLen = snprintf(top, sizeof(top), "HTTP/1.0 %d Moved Permanently\r\nLocation: %.*s/%s\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", STATUS_MOVED, (int)pathLen, target, target + pathLen); gateNetSendAll(conn, top, (size_t)topLen); } // One request on conn, answered. static void serve(int conn, const char *root) { struct stat info; char request[HEADER_MAX + 1u]; char path[PATH_MAX_BYTES]; char index[PATH_MAX_BYTES]; const char *method; const char *target; const char *version; size_t len; ssize_t got; bool head; bool slash; len = 0u; for (;;) { request[len] = '\0'; if (strstr(request, HEADER_END) != NULL || strstr(request, HEADER_END_BARE) != NULL) { break; } if (len == HEADER_MAX) { sendError(conn, STATUS_BAD_REQUEST, "Request too long", false); return; } got = read(conn, request + len, HEADER_MAX - len); if (got <= 0) { return; } len += (size_t)got; } // The request line alone: its method, target, and version. request[strcspn(request, "\r\n")] = '\0'; method = strtok(request, " "); target = strtok(NULL, " "); version = strtok(NULL, " "); if (method == NULL || target == NULL || version == NULL || strtok(NULL, " ") != NULL) { sendError(conn, STATUS_BAD_REQUEST, "Bad request syntax", false); return; } head = strcmp(method, "HEAD") == 0; if (!head && strcmp(method, "GET") != 0) { sendError(conn, STATUS_NO_METHOD, "Unsupported method", false); return; } if (!resolve(root, target, path, sizeof(path), &slash)) { sendError(conn, STATUS_NOT_FOUND, "File not found", head); return; } if (stat(path, &info) != 0 || !S_ISDIR(info.st_mode)) { if (slash) { sendError(conn, STATUS_NOT_FOUND, "File not found", head); } else { sendFile(conn, path, head); } return; } if (!slash) { sendRedirect(conn, target); return; } snprintf(index, sizeof(index), "%s/%s", path, INDEX_FILE); sendFile(conn, index, head); } int main(int argc, char **argv) { struct timeval timeout; struct stat info; const char *root; uint16_t port; pid_t child; int listener; int conn; gateNetStart(TOOL_NAME); if (argc != ARG_COUNT) { fprintf(stderr, "usage: %s PORT DIR\n", TOOL_NAME); return GATE_USAGE_EXIT; } port = gateArgPort(argv[ARG_PORT]); root = argv[ARG_DIR]; if (stat(root, &info) != 0 || !S_ISDIR(info.st_mode)) { toolFail("%s is no folder", root); } listener = gateNetListen(port, LISTEN_BACKLOG); printf("%s: serving %s at http://%s:%u/\n", TOOL_NAME, root, GATE_NET_HOST, (unsigned)port); // Children are reaped by the system; a client quiet too long is dropped. signal(SIGCHLD, SIG_IGN); timeout.tv_sec = CLIENT_SECONDS; timeout.tv_usec = 0; for (;;) { conn = accept(listener, NULL, NULL); if (conn < 0) { continue; } setsockopt(conn, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); setsockopt(conn, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); child = fork(); if (child == 0) { close(listener); serve(conn, root); close(conn); _exit(0); } // Without a process to spare, serve it here. if (child < 0) { serve(conn, root); } close(conn); } }