#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 lsock; /* the listener, held for our lifetime */ static unsigned char sock; /* the client being served right now */ 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("