From f13054ee327c044a675f6c701e8ef40c78e214a3 Mon Sep 17 00:00:00 2001 From: Judah Caruso Date: Thu, 19 Feb 2026 23:40:33 -0700 Subject: [PATCH] add itty bitty http server for running wasm --- thirdparty/tinyhttp.zig | 116 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 thirdparty/tinyhttp.zig diff --git a/thirdparty/tinyhttp.zig b/thirdparty/tinyhttp.zig new file mode 100644 index 0000000..c90bf66 --- /dev/null +++ b/thirdparty/tinyhttp.zig @@ -0,0 +1,116 @@ +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"; +}