aboutsummaryrefslogtreecommitdiffstats
path: root/usr/httpd.c
blob: e704e8cebc7746b44cc1638eafc897c00fefc1f8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#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("<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();

    lsock = net_socket(SOCK_TCP);
    if (lsock == 0xFF) {
        puts("httpd: no socket");
        nl();
        sexit(1);
    }
    if (net_listen(lsock, port) == 0xFF) {
        puts("httpd: listen failed (port in use?)");
        nl();
        net_close(lsock);
        sexit(1);
    }
    for (;;) {
        while ((sock = net_accept(lsock)) == 0xFE) /* one RX pump per call */
            msleep(20);
        handle();
        net_close(sock); /* FIN: this is what ends the body; listener stays */
    }
}