tinyhttp.zig -> tinyhttp.c

This commit is contained in:
judah 2026-02-20 15:58:11 -07:00
parent c04c142e79
commit 170b6cc494
3 changed files with 298 additions and 121 deletions

20
do.ps1
View file

@ -234,11 +234,21 @@ switch ($command) {
}
{($_ -eq "run-wasm") -or ($_ -eq "rw")} {
&"./do.ps1" build-cross wasm32-emscripten;
$cmd = "$ZIG run " + (Join-Path $DEPS_DIR "tinyhttp.zig");
Write-Host $cmd;
Invoke-Expression $cmd | Write-Host;
$server_in = (Join-Path $DEPS_DIR "tinyhttp.c");
$server_out = (Join-Path $OUT_DIR ($EXE_NAME + "-server" + (Get-Ext $HOST_OS)));
if (-not (Test-Path $server_out)) {
$cmd = "$ZIG cc $server_in -o $server_out";
if ($IsWindows) {
$cmd += " -lws2_32";
}
Write-Host $cmd;
Invoke-Expression $cmd | Write-Host;
}
Invoke-Expression $server_out | Write-Host;
}
{($_ -eq "clean") -or ($_ -eq "c")} {
Write-Host ":: Cleaning up build artifacts...";

283
thirdparty/tinyhttp.c vendored Normal file
View file

@ -0,0 +1,283 @@
/*
* Portable static HTTP file server serves files from ./out/ on port 8080.
*
* Build:
* Linux/macOS: cc -O2 -o server server.c
* Windows MSVC: cl /O2 server.c ws2_32.lib
* Windows MinGW: gcc -O2 -o server.exe server.c -lws2_32
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
typedef SOCKET socket_t;
#define CLOSESOCKET closesocket
#define ISVALIDSOCK(s) ((s) != INVALID_SOCKET)
#define STAT_FUNC _stat
#define STAT_ST struct _stat
#else
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
typedef int socket_t;
#define CLOSESOCKET close
#define ISVALIDSOCK(s) ((s) >= 0)
#define INVALID_SOCKET (-1)
#define STAT_FUNC stat
#define STAT_ST struct stat
#endif
#define LISTEN_PORT 8080
#define REQ_BUF_SIZE 4096
#define FILE_BUF_SIZE 8192
#define HDR_BUF_SIZE 512
#define PATH_BUF_SIZE 512
/* ------------------------------------------------------------------ */
/* MIME type lookup */
/* ------------------------------------------------------------------ */
typedef struct {
const char *ext;
const char *mime;
} mime_entry_t;
static const mime_entry_t mime_map[] = {
{ ".html", "text/html;charset=utf-8" },
{ ".css", "text/css;charset=utf-8" },
{ ".js", "application/javascript;charset=utf-8" },
{ ".json", "application/json" },
{ ".png", "image/png" },
{ ".jpg", "image/jpeg" },
{ ".jpeg", "image/jpeg" },
{ ".gif", "image/gif" },
{ ".svg", "image/svg+xml" },
{ ".ico", "image/x-icon" },
{ ".woff", "font/woff" },
{ ".woff2", "font/woff2" },
{ ".ttf", "font/ttf" },
{ ".wasm", "application/wasm" },
{ ".pdf", "application/pdf" },
{ ".txt", "text/plain;charset=utf-8" },
{ ".xml", "application/xml" },
{ NULL, NULL }
};
static const char *mime_from_path(const char *path)
{
const char *dot = strrchr(path, '.');
if (dot) {
for (const mime_entry_t *e = mime_map; e->ext; ++e) {
if (strcmp(dot, e->ext) == 0)
return e->mime;
}
}
return "application/octet-stream";
}
/* ------------------------------------------------------------------ */
/* Send an HTTP error response */
/* ------------------------------------------------------------------ */
static void send_error(socket_t sock, const char *status)
{
char hdr[256];
int len = (int)strlen(status);
int n = snprintf(hdr, sizeof(hdr),
"HTTP/1.1 %s\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: %d\r\n"
"Connection: close\r\n"
"\r\n",
status, len);
if (n > 0 && n < (int)sizeof(hdr)) {
send(sock, hdr, n, 0);
send(sock, status, len, 0);
}
}
/* ------------------------------------------------------------------ */
/* Handle one accepted connection */
/* ------------------------------------------------------------------ */
static void handle_request(socket_t sock)
{
char req_buf[REQ_BUF_SIZE];
int n = (int)recv(sock, req_buf, sizeof(req_buf) - 1, 0);
if (n <= 0) return;
req_buf[n] = '\0';
/* ---- Parse "GET /path HTTP/1.x\r\n..." ---- */
if (strncmp(req_buf, "GET ", 4) != 0) {
send_error(sock, "405 Method Not Allowed");
return;
}
char *path_start = req_buf + 4;
char *path_end = strchr(path_start, ' ');
if (!path_end) {
send_error(sock, "400 Bad Request");
return;
}
*path_end = '\0';
const char *path = path_start;
/* Default document */
if (strcmp(path, "/") == 0)
path = "p2601.html";
/* Strip leading '/' */
while (*path == '/')
++path;
/* Reject suspicious paths */
if (*path == '\0' ||
strstr(path, "..") != NULL ||
strchr(path, '\\') != NULL)
{
send_error(sock, "403 Forbidden");
return;
}
/* ---- Build filesystem path: out/<rel> ---- */
char filepath[PATH_BUF_SIZE];
int pn = snprintf(filepath, sizeof(filepath), "out/%s", path);
if (pn < 0 || pn >= (int)sizeof(filepath)) {
send_error(sock, "414 URI Too Long");
return;
}
/* ---- Open and stat the file ---- */
FILE *fp = fopen(filepath, "rb");
if (!fp) {
send_error(sock, "404 Not Found");
return;
}
STAT_ST st;
if (STAT_FUNC(filepath, &st) != 0) {
fclose(fp);
send_error(sock, "500 Internal Server Error");
return;
}
long long file_size = (long long)st.st_size;
const char *mime = mime_from_path(path);
/* ---- Send response header ---- */
char hdr_buf[HDR_BUF_SIZE];
int hdr_len = snprintf(hdr_buf, sizeof(hdr_buf),
"HTTP/1.1 200 OK\r\n"
"Content-Type: %s\r\n"
"Content-Length: %lld\r\n"
"Connection: close\r\n"
"\r\n",
mime, file_size);
if (hdr_len < 0 || hdr_len >= (int)sizeof(hdr_buf)) {
fclose(fp);
return;
}
if (send(sock, hdr_buf, hdr_len, 0) <= 0) {
fclose(fp);
return;
}
/* ---- Stream the file body in chunks ---- */
char file_buf[FILE_BUF_SIZE];
size_t bytes;
while ((bytes = fread(file_buf, 1, sizeof(file_buf), fp)) > 0) {
size_t sent = 0;
while (sent < bytes) {
int s = send(sock, file_buf + sent, (int)(bytes - sent), 0);
if (s <= 0) { fclose(fp); return; }
sent += (size_t)s;
}
}
fclose(fp);
}
/* ------------------------------------------------------------------ */
/* main — bind, listen, accept loop */
/* ------------------------------------------------------------------ */
int main(void)
{
#ifdef _WIN32
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
fprintf(stderr, "WSAStartup failed\n");
return 1;
}
#endif
socket_t srv = socket(AF_INET, SOCK_STREAM, 0);
if (!ISVALIDSOCK(srv)) {
perror("socket");
return 1;
}
/* SO_REUSEADDR */
int opt = 1;
#ifdef _WIN32
setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, (const char *)&opt, sizeof(opt));
#else
setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
#endif
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(LISTEN_PORT);
if (bind(srv, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
perror("bind");
CLOSESOCKET(srv);
return 1;
}
if (listen(srv, SOMAXCONN) != 0) {
perror("listen");
CLOSESOCKET(srv);
return 1;
}
fprintf(stderr, ":: Listening at http://localhost:%d\n", LISTEN_PORT);
for (;;) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
#ifdef _WIN32
socket_t client = accept(srv, (struct sockaddr *)&client_addr, (int *)&client_len);
#else
socket_t client = accept(srv, (struct sockaddr *)&client_addr, &client_len);
#endif
if (!ISVALIDSOCK(client))
continue;
handle_request(client);
CLOSESOCKET(client);
}
/* unreachable, but tidy */
CLOSESOCKET(srv);
#ifdef _WIN32
WSACleanup();
#endif
return 0;
}

View file

@ -1,116 +0,0 @@
const std = @import("std");
pub fn main() !void {
const addr = try std.net.Address.parseIp("0.0.0.0", 8080);
var server = try addr.listen(.{ .reuse_address = true });
defer server.deinit();
std.debug.print(":: Listening at http://localhost:8080\n", .{});
while (true) {
const conn = server.accept() catch continue;
defer conn.stream.close();
handle_request(conn.stream) catch continue;
}
}
fn handle_request(stream: std.net.Stream) !void {
var req_buf: [4096]u8 = undefined;
const n = try stream.read(&req_buf);
if (n == 0) return error.Empty;
const request = req_buf[0..n];
// Parse "GET /path HTTP/1.x\r\n..."
const path = blk: {
if (!std.mem.startsWith(u8, request, "GET ")) return send_error(stream, "405 Method Not Allowed");
const rest = request[4..];
const end = std.mem.indexOfScalar(u8, rest, ' ') orelse return error.BadRequest;
break :blk rest[0..end];
};
const raw = if (std.mem.eql(u8, path, "/")) "/p2601.html" else path;
// Strip leading '/' and reject anything suspicious
const rel = std.mem.trimLeft(u8, raw, "/");
if (rel.len == 0 or
std.mem.indexOf(u8, rel, "..") != null or
std.mem.indexOfScalar(u8, rel, '\\') != null or
rel[0] == '/')
{
return send_error(stream, "403 Forbidden");
}
// Open from ./out/
const dir = std.fs.cwd().openDir("out", .{}) catch return send_error(stream, "500 Internal Server Error");
const file = dir.openFile(rel, .{}) catch return send_error(stream, "404 Not Found");
defer file.close();
const stat = try file.stat();
const size = stat.size;
const mime = mime_from_path(rel);
// Send header
var hdr_buf: [512]u8 = undefined;
const header = std.fmt.bufPrint(&hdr_buf,
"HTTP/1.1 200 OK\r\n" ++
"Content-Type: {s}\r\n" ++
"Content-Length: {d}\r\n" ++
"Connection: close\r\n" ++
"\r\n",
.{ mime, size },
) catch return error.HeaderTooLarge;
stream.writeAll(header) catch return;
// Stream the file in chunks
var buf: [8192]u8 = undefined;
while (true) {
const bytes = file.read(&buf) catch return;
if (bytes == 0) break;
stream.writeAll(buf[0..bytes]) catch return;
}
}
fn send_error(stream: std.net.Stream, comptime status: []const u8) error{SendFailed} {
const body = status;
var hdr_buf: [256]u8 = undefined;
const header = std.fmt.bufPrint(&hdr_buf,
"HTTP/1.1 " ++ status ++ "\r\n" ++
"Content-Type: text/plain\r\n" ++
"Content-Length: {d}\r\n" ++
"Connection: close\r\n" ++
"\r\n",
.{body.len},
) catch return error.SendFailed;
stream.writeAll(header) catch {};
stream.writeAll(body) catch {};
return error.SendFailed;
}
fn mime_from_path(path: []const u8) []const u8 {
const ext = std.fs.path.extension(path);
const map = .{
.{ ".html", "text/html;charset=utf-8" },
.{ ".css", "text/css;charset=utf-8" },
.{ ".js", "application/javascript;charset=utf-8" },
.{ ".json", "application/json" },
.{ ".png", "image/png" },
.{ ".jpg", "image/jpeg" },
.{ ".jpeg", "image/jpeg" },
.{ ".gif", "image/gif" },
.{ ".svg", "image/svg+xml" },
.{ ".ico", "image/x-icon" },
.{ ".woff", "font/woff" },
.{ ".woff2", "font/woff2" },
.{ ".ttf", "font/ttf" },
.{ ".wasm", "application/wasm" },
.{ ".pdf", "application/pdf" },
.{ ".txt", "text/plain;charset=utf-8" },
.{ ".xml", "application/xml" },
};
inline for (map) |entry| {
if (std.mem.eql(u8, ext, entry[0])) return entry[1];
}
return "application/octet-stream";
}