tinyhttp.zig -> tinyhttp.c
This commit is contained in:
parent
c04c142e79
commit
170b6cc494
3 changed files with 298 additions and 121 deletions
20
do.ps1
20
do.ps1
|
|
@ -234,11 +234,21 @@ switch ($command) {
|
||||||
}
|
}
|
||||||
{($_ -eq "run-wasm") -or ($_ -eq "rw")} {
|
{($_ -eq "run-wasm") -or ($_ -eq "rw")} {
|
||||||
&"./do.ps1" build-cross wasm32-emscripten;
|
&"./do.ps1" build-cross wasm32-emscripten;
|
||||||
|
|
||||||
$cmd = "$ZIG run " + (Join-Path $DEPS_DIR "tinyhttp.zig");
|
$server_in = (Join-Path $DEPS_DIR "tinyhttp.c");
|
||||||
Write-Host $cmd;
|
$server_out = (Join-Path $OUT_DIR ($EXE_NAME + "-server" + (Get-Ext $HOST_OS)));
|
||||||
|
|
||||||
Invoke-Expression $cmd | Write-Host;
|
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")} {
|
{($_ -eq "clean") -or ($_ -eq "c")} {
|
||||||
Write-Host ":: Cleaning up build artifacts...";
|
Write-Host ":: Cleaning up build artifacts...";
|
||||||
|
|
|
||||||
283
thirdparty/tinyhttp.c
vendored
Normal file
283
thirdparty/tinyhttp.c
vendored
Normal 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;
|
||||||
|
}
|
||||||
116
thirdparty/tinyhttp.zig
vendored
116
thirdparty/tinyhttp.zig
vendored
|
|
@ -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";
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue