huh/thirdparty/tinyhttp.c
2026-02-20 16:03:55 -07:00

274 lines
7.2 KiB
C

#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;
}