359 lines
12 KiB
C
359 lines
12 KiB
C
// 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 <fcntl.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/time.h>
|
|
#include <unistd.h>
|
|
|
|
#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);
|
|
}
|
|
}
|