diff options
| author | user <user@clank> | 2026-09-22 17:12:52 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-09-22 17:12:52 +0200 |
| commit | 30e67fe0f4e66e2161ca61f870f8947d50287995 (patch) | |
| tree | a04189f34687a4d600903100feca18aedc5d1604 /usr/httpd.c | |
| parent | net: passive open (listen/accept) + own sockets + close promptly (diff) | |
| download | gbos-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.c | 277 |
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 */ + } +} |
