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")} {
|
||||
&"./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
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