aboutsummaryrefslogtreecommitdiffstats
path: root/usr/httpd.c
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-22 17:12:52 +0200
committeruser <user@clank>2026-09-22 17:12:52 +0200
commit30e67fe0f4e66e2161ca61f870f8947d50287995 (patch)
treea04189f34687a4d600903100feca18aedc5d1604 /usr/httpd.c
parentnet: passive open (listen/accept) + own sockets + close promptly (diff)
downloadgbos-30e67fe0f4e66e2161ca61f870f8947d50287995.tar.gz
gbos-30e67fe0f4e66e2161ca61f870f8947d50287995.tar.xz
gbos-30e67fe0f4e66e2161ca61f870f8947d50287995.zip
usr: httpd - the Game Boy serves its filesystem over HTTP
/# httpd & host$ curl http://10.0.0.2/ -> a listing of the cart RAM host$ curl http://10.0.0.2/readme -> the file, from battery-backed SRAM GET only. "/" serves /index.html if present, else a generated listing; any directory lists with every entry linked, so you can click around a 1997 handheld's disk from a browser. Content type from the extension. No Content-Length: HTTP/1.0 + Connection: close delimits the body by closing, which suits a stack with one in-flight segment. All output goes through one 200-byte buffer, so each net_send is a full segment instead of one tiny segment per string. It drains the whole request, not just the request line. That is not politeness: leaving the rest unread means those segments are never acknowledged, so the client retransmits headers for a minute, never sends its FIN, and its dead connection keeps arriving at port 80 long after we re-listen. One curl with 1.4 KB of headers stalled every following request for 30-120s. It deliberately does NOT read the keyboard either. Backgrounded - the useful way to run it - every key it polled would be a key stolen from the shell's prompt. It is a daemon: kill(1) stops it, and the kernel reclaims its socket. Verified with curl: index, byte-exact file bodies, nested paths, directory listings, 404, 501, a 1.4 KB request spanning segments, browser user-agents, three parallel clients (0.1/1.1/2.1s), the shell staying usable alongside it, and six kill/restart cycles - more than there are sockets.
Diffstat (limited to 'usr/httpd.c')
-rw-r--r--usr/httpd.c277
1 files changed, 277 insertions, 0 deletions
diff --git a/usr/httpd.c b/usr/httpd.c
new file mode 100644
index 0000000..335e824
--- /dev/null
+++ b/usr/httpd.c
@@ -0,0 +1,277 @@
+#include "sock.h"
+/* httpd [PORT] - serve the Game Boy's filesystem over HTTP. Default port 80.
+ *
+ * /# dhcp (or let boot do it)
+ * /# httpd & background it and keep using the shell
+ * host$ curl http://10.0.0.2/ -> a directory listing of the cart RAM
+ * /# ps ; kill 4 stop it (the kernel frees its socket)
+ *
+ * Deliberately does NOT read the keyboard: backgrounded (which is the useful
+ * way to run it) any key it polled would be a key stolen from the shell's own
+ * prompt. It is a daemon; kill(1) stops it.
+ *
+ * GET only. "/" serves /index.html if it exists, else a generated listing;
+ * any directory lists; anything else is served from the filesystem with a
+ * content type guessed from the extension. No Content-Length: HTTP/1.0 with
+ * Connection: close delimits the body by closing, which is also all a stack
+ * with one in-flight segment wants to promise.
+ *
+ * One connection at a time: sockets demux on local port alone, so the
+ * listener IS the connection (see net_op_listen). Serve, close, re-listen.
+ *
+ * All output goes through obuf, so every net_send() is one ~MSS segment
+ * rather than one tiny segment per string.
+ *
+ * Statics are fine here (unlike grep/more/cp) because main() copies the
+ * command line off $A000 before touching any of them - sock.h's _nr already
+ * sits there. Same trick as ping/irc.
+ */
+
+#define PORT_DEF 80
+#define BUFSZ 208 /* one full segment in */
+#define OBUFSZ 200 /* one full segment out (TCP_MSS) */
+#define REQSZ 120 /* the request line is all we parse */
+#define PATHSZ 48
+
+static unsigned char sock;
+static unsigned char rbuf[BUFSZ];
+static char obuf[OBUFSZ];
+static unsigned char olen;
+static char req[REQSZ];
+static unsigned char reqlen;
+static char path[PATHSZ];
+
+/* ---- buffered output ------------------------------------------------------ */
+static void oflush(void) {
+ if (olen) {
+ net_send(sock, obuf, olen);
+ olen = 0;
+ }
+}
+static void oputc(char c) {
+ if (olen >= OBUFSZ) oflush();
+ obuf[olen++] = c;
+}
+static void oputs(const char *s) {
+ while (*s) oputc(*s++);
+}
+
+/* ---- request parsing ------------------------------------------------------ */
+/* Read the request line, then KEEP READING to the blank line that ends the
+ headers - only the first line is kept.
+
+ Draining is not politeness, it is required. Stopping at the request line
+ leaves the rest of a browser-sized request unread, so our window never
+ opens and those segments are never acknowledged: the client retransmits
+ headers for a minute, never sends its FIN, and its dead connection keeps
+ arriving at port 80 long after we re-listen. One curl with 1.4 KB of
+ headers was enough to stall every following request for 30-120s. */
+static unsigned char read_request(void) {
+ unsigned char idle = 0, n, i, c, nlrun = 0, got_line = 0;
+ unsigned int total = 0;
+ reqlen = 0;
+ req[0] = 0;
+ for (;;) {
+ n = net_recv_nb(sock, rbuf, BUFSZ);
+ if (n == 0) return reqlen; /* peer closed */
+ if (n == 0xFE || n == 0xFF) {
+ if (++idle >= 40) return reqlen; /* ~2s of silence */
+ msleep(50);
+ continue;
+ }
+ idle = 0;
+ total += n;
+ for (i = 0; i < n; i++) {
+ c = rbuf[i];
+ if (c == '\r') continue;
+ if (c == '\n') {
+ if (!got_line) { /* first line: that's the one */
+ req[reqlen] = 0;
+ got_line = 1;
+ }
+ if (++nlrun >= 2) return reqlen; /* blank line: headers done */
+ continue;
+ }
+ nlrun = 0;
+ if (!got_line && reqlen < REQSZ - 1) req[reqlen++] = (char)c;
+ }
+ if (total > 4000) return reqlen; /* don't read a body forever */
+ }
+}
+
+/* "GET /path HTTP/1.0" -> path[]; query strings are dropped, %xx is not
+ decoded (gbos filenames are 15 chars of plain text). */
+static void parse_path(void) {
+ unsigned char i = 0, j = 0;
+ while (req[i] && req[i] != ' ') i++; /* skip the method */
+ while (req[i] == ' ') i++;
+ while (req[i] && req[i] != ' ' && req[i] != '?' && j < PATHSZ - 1) path[j++] = req[i++];
+ path[j] = 0;
+ if (!path[0]) {
+ path[0] = '/';
+ path[1] = 0;
+ }
+}
+
+static unsigned char is_get(void) {
+ return (unsigned char)(req[0] == 'G' && req[1] == 'E' && req[2] == 'T');
+}
+
+/* content type from the extension (everything unknown is text/plain, which
+ browsers will happily render) */
+static const char *ctype_for(const char *p) {
+ unsigned char i = 0, dot = 0;
+ while (p[i]) {
+ if (p[i] == '.') dot = (unsigned char)(i + 1);
+ i++;
+ }
+ if (!dot) return "text/plain";
+ if (p[dot] == 'h' && p[dot + 1] == 't' && p[dot + 2] == 'm') return "text/html";
+ if (p[dot] == 'c' && p[dot + 1] == 's' && p[dot + 2] == 's') return "text/css";
+ if (p[dot] == 'j' && p[dot + 1] == 's') return "text/javascript";
+ return "text/plain";
+}
+
+/* ---- responses ------------------------------------------------------------ */
+static void hdr(const char *status, const char *ctype) {
+ oputs("HTTP/1.0 ");
+ oputs(status);
+ oputs("\r\nServer: gbos\r\nContent-Type: ");
+ oputs(ctype);
+ oputs("\r\nConnection: close\r\n\r\n");
+}
+
+static void logline(const char *code) {
+ puts(code);
+ putc(' ');
+ puts(path);
+ nl();
+}
+
+static void serve_error(const char *status, const char *body) {
+ hdr(status, "text/plain");
+ oputs(body);
+ oputs("\r\n");
+ oflush();
+ logline(status);
+}
+
+static void serve_fd(unsigned char fd, const char *ctype) {
+ int ch;
+ hdr("200 OK", ctype);
+ while ((ch = fgetc(fd)) >= 0) oputc((char)ch);
+ close(fd);
+ oflush();
+ logline("200");
+}
+
+/* a generated index: every entry linked, so you can click around the cart RAM
+ from a browser on the other end of the link cable */
+static void serve_dir(const char *dir) {
+ char name[16];
+ unsigned char i, root;
+ if (opendir(dir)) {
+ serve_error("404 Not Found", "no such directory");
+ return;
+ }
+ root = (unsigned char)(dir[0] == '/' && dir[1] == 0);
+ hdr("200 OK", "text/html");
+ oputs("<html><head><title>gbos:");
+ oputs(dir);
+ oputs("</title></head><body><h1>gbos:");
+ oputs(dir);
+ oputs("</h1><ul>");
+ for (i = 0; flist(i, name); i++) {
+ name[15] = 0;
+ oputs("<li><a href=\"");
+ oputs(dir);
+ if (!root) oputc('/');
+ oputs(name);
+ oputs("\">");
+ oputs(name);
+ oputs("</a></li>");
+ }
+ oputs("</ul><hr><i>served from battery-backed cart RAM by a Game Boy "
+ "Color</i></body></html>");
+ oflush();
+ logline("200");
+}
+
+static void handle(void) {
+ unsigned char fd;
+ olen = 0;
+ if (!read_request()) return; /* connected, then said nothing */
+ parse_path();
+ if (!is_get()) {
+ serve_error("501 Not Implemented", "GET only");
+ return;
+ }
+ if (path[0] == '/' && path[1] == 0) { /* "/" -> index.html, else listing */
+ fd = open("/index.html", O_READ);
+ if (fd <= 3) {
+ serve_fd(fd, "text/html");
+ return;
+ }
+ serve_dir("/");
+ return;
+ }
+ fd = open(path, O_READ);
+ if (fd == EISDIR) {
+ serve_dir(path);
+ return;
+ }
+ if (fd > 3) {
+ serve_error("404 Not Found", "no such file");
+ return;
+ }
+ serve_fd(fd, ctype_for(path));
+}
+
+/* atou() is 8-bit; ports are not */
+static unsigned int atou16(const char *s) {
+ unsigned int n = 0;
+ while (*s >= '0' && *s <= '9') {
+ n = n * 10 + (unsigned char)(*s - '0');
+ s++;
+ }
+ return n;
+}
+
+void main(void) {
+ char args[16];
+ unsigned int port;
+ unsigned char i;
+
+ { /* copy the command line before any static (incl. _nr) lands on $A000 */
+ char *a = getargs();
+ for (i = 0; a[i] && i < 15; i++) args[i] = a[i];
+ args[i] = 0;
+ }
+ olen = 0;
+ port = (args[0] >= '0' && args[0] <= '9') ? atou16(args) : PORT_DEF;
+
+ puts("httpd: listening on port ");
+ putu(port);
+ nl();
+ puts(" (run it with & and kill it to stop)");
+ nl();
+
+ for (;;) {
+ sock = net_socket(SOCK_TCP);
+ if (sock == 0xFF) {
+ puts("httpd: no socket");
+ nl();
+ sexit(1);
+ }
+ if (net_listen(sock, port) == 0xFF) {
+ puts("httpd: listen failed");
+ nl();
+ net_close(sock);
+ sexit(1);
+ }
+ while (net_accept(sock) != 0) /* one RX pump per call */
+ msleep(20);
+ handle();
+ net_close(sock); /* FIN: this is what ends the body */
+ }
+}