// SPDX-License-Identifier: Apache-2.0 // // httpd -- serve a directory over HTTP. // // SchOS could fetch a page before it could serve one, which is a strange shape // for a system with a TCP stack, a filesystem and no web server. This is the // other half. // // httpd /srv on port 80, anywhere // httpd -r /tmp -p 8080 somewhere else, some other port // httpd -a 127.0.0.1 loopback only // // GET and HEAD, static files, `index.html` for a directory and a listing when // there is none. It is deliberately a *file server* and not a web server: // there is no CGI, no scripting, no configuration file, no virtual hosts, no // keep-alive and no TLS. Every one of those is a decision somebody should make // on purpose, and none of them is needed to answer the question this exists to // answer -- whether this machine can put a file on the network. // // **The path check is the only security-relevant code here**, and it is done // by rejecting rather than by normalising. See resolve() below. #include "types.h" #include "user.h" #include "fcntl.h" #include "stat.h" #include "socket.h" #include "posix.h" #define DEFAULT_PORT 80 #define DEFAULT_ROOT "/srv" static char root[128] = DEFAULT_ROOT; static int slen(const char *s) { int n = 0; while (s[n]) n++; return n; } static void puts_(const char *s) { write(1, (void *)s, slen(s)); } static void putn(int v) { char b[12]; int i = 0; if (v == 0) { write(1, "0", 1); return; } if (v < 0) { write(1, "-", 1); v = -v; } while (v > 0 && i < 11) { b[i++] = '0' + (v % 10); v /= 10; } while (i > 0) { i--; write(1, &b[i], 1); } } static int atoi_(const char *s) { int v = 0; while (*s >= '0' && *s <= '9') v = v * 10 + (*s++ - '0'); return v; } static int scat(char *dst, int at, int cap, const char *src) { while (*src && at < cap - 1) dst[at++] = *src++; dst[at] = 0; return at; } static int ucat(char *dst, int at, int cap, unsigned long long v) { char b[24]; int i = 0; if (v == 0) b[i++] = '0'; while (v > 0 && i < 23) { b[i++] = '0' + (int)(v % 10); v /= 10; } while (i > 0 && at < cap - 1) dst[at++] = b[--i]; dst[at] = 0; return at; } static unsigned int parse_ip(const char *s, int *ok) { unsigned int b[4] = { 0, 0, 0, 0 }; int i; *ok = 0; for (i = 0; i < 4; i++) { int any = 0; while (*s >= '0' && *s <= '9') { b[i] = b[i] * 10 + (*s++ - '0'); any = 1; } if (!any || b[i] > 255) return 0; if (i < 3) { if (*s != '.') return 0; s++; } } if (*s) return 0; *ok = 1; return b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24); } // --- what a file is called, and what that means ----------------------------- static const struct { const char *ext, *type; } mime[] = { { ".html", "text/html" }, { ".htm", "text/html" }, { ".css", "text/css" }, { ".js", "text/javascript" }, { ".json", "application/json" }, { ".txt", "text/plain" }, { ".md", "text/plain" }, { ".c", "text/plain" }, { ".h", "text/plain" }, { ".png", "image/png" }, { ".jpg", "image/jpeg" }, { ".jpeg", "image/jpeg" }, { ".gif", "image/gif" }, { ".svg", "image/svg+xml" }, { ".ico", "image/x-icon" }, { ".pdf", "application/pdf" }, { ".gz", "application/gzip" }, { ".tar", "application/x-tar" }, }; // Unknown types are octet-stream rather than text/plain. A browser handed the // wrong text type renders a binary as mojibake; handed octet-stream it offers // to save it, which is the behaviour you want for the file you did not think // of when writing this table. static const char * type_of(const char *path) { int n = slen(path), i, j; for (i = 0; i < (int)(sizeof(mime) / sizeof(mime[0])); i++) { int e = slen(mime[i].ext); if (n < e) continue; for (j = 0; j < e; j++) { char a = path[n - e + j], b = mime[i].ext[j]; if (a >= 'A' && a <= 'Z') a += 32; if (a != b) break; } if (j == e) return mime[i].type; } return "application/octet-stream"; } // --- the path ---------------------------------------------------------------- static int hexval(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; } // Turn a request target into a path under `root`, or refuse. // // **Refusing rather than normalising** is the whole design. The tempting // version resolves `..` and collapses slashes and then checks the result, and // it is wrong in a way that is hard to see: every such normaliser has to agree // exactly with the kernel's, and when it does not, the difference is a file // outside the root. So no component may be `..`, full stop -- there is nothing // to get subtly wrong, and no legitimate request contains one. // // Percent-decoding happens first, because `%2e%2e` is `..` and a check that // runs before decoding does not see it. // // Returns 0 on success, -1 on a malformed target, -2 on one that escapes. static int resolve(const char *target, char *out, int cap) { char dec[512]; int i = 0, n = 0, at; // Decode, stopping at the query string -- there is nothing here that reads // one, and carrying it into a filename would look for a file named after // it. while (target[i] && target[i] != '?' && target[i] != '#' && n < (int)sizeof(dec) - 1) { if (target[i] == '%') { int h = hexval(target[i + 1]), l = h < 0 ? -1 : hexval(target[i + 2]); if (l < 0) return -1; dec[n++] = (char)((h << 4) | l); i += 3; } else { dec[n++] = target[i++]; } } dec[n] = 0; if (dec[0] != '/') return -1; // A NUL anywhere in the decoded target truncates the path for the kernel // but not for any length check above it. Nothing legitimate contains one. for (i = 0; i < n; i++) if (dec[i] == 0) return -1; // No component may be `..`, and none may be empty-with-a-dot. for (i = 0; i < n; i++) { if (dec[i] != '/') continue; if (dec[i + 1] == '.' && dec[i + 2] == '.' && (dec[i + 3] == '/' || dec[i + 3] == 0)) return -2; } at = scat(out, 0, cap, root); at = scat(out, at, cap, dec); if (at >= cap - 1) return -1; // Trailing slash off, so that stat() and the listing agree about the name. while (at > 1 && out[at - 1] == '/') out[--at] = 0; return 0; } // --- responses --------------------------------------------------------------- static void head(int fd, const char *status, const char *type, long long len) { char h[256]; int at = 0; at = scat(h, at, sizeof(h), "HTTP/1.0 "); at = scat(h, at, sizeof(h), status); at = scat(h, at, sizeof(h), "\r\nServer: SchOS httpd\r\nConnection: close\r\n" "Content-Type: "); at = scat(h, at, sizeof(h), type); if (len >= 0) { at = scat(h, at, sizeof(h), "\r\nContent-Length: "); at = ucat(h, at, sizeof(h), (unsigned long long)len); } at = scat(h, at, sizeof(h), "\r\n\r\n"); write(fd, h, at); } static void fail(int fd, const char *status, const char *msg) { char b[256]; int at = 0; at = scat(b, at, sizeof(b), ""); at = scat(b, at, sizeof(b), status); at = scat(b, at, sizeof(b), "

"); at = scat(b, at, sizeof(b), status); at = scat(b, at, sizeof(b), "

"); at = scat(b, at, sizeof(b), msg); at = scat(b, at, sizeof(b), "\n"); head(fd, status, "text/html", at); write(fd, b, at); } static void send_file(int fd, const char *path, int body) { char buf[4096]; struct stat st; int in, n; in = open((char *)path, O_RDONLY); if (in < 0) { fail(fd, "404 Not Found", "No such file."); return; } if (fstat(in, &st) < 0) { close(in); fail(fd, "500 Internal Server Error", "stat failed."); return; } head(fd, "200 OK", type_of(path), (long long)st.size); if (!body) { close(in); return; } while ((n = read(in, buf, sizeof(buf))) > 0) if (write(fd, buf, n) != n) break; // the client went away; not an error close(in); } // A directory with no index.html. Generated straight into the socket, so a // directory with ten thousand entries does not need ten thousand entries' // worth of memory -- which means no Content-Length, which is why every // response here is HTTP/1.0 with Connection: close. static void send_listing(int fd, const char *path, const char *target, int body) { char dbuf[2048], line[512]; int d, n, at; d = open((char *)path, O_RDONLY); if (d < 0) { fail(fd, "404 Not Found", "No such directory."); return; } head(fd, "200 OK", "text/html", -1); if (!body) { close(d); return; } at = scat(line, 0, sizeof(line), ""); at = scat(line, at, sizeof(line), target); at = scat(line, at, sizeof(line), "

"); at = scat(line, at, sizeof(line), target); at = scat(line, at, sizeof(line), "

\n", 6); close(d); } // --- one request ------------------------------------------------------------- static void serve(int fd) { char req[1024], path[640], target[512]; struct stat st; int n = 0, i, body = 1; // The request line is all this reads. Headers are skipped rather than // parsed: nothing here varies on one, and a parser for something nobody // consults is a parser that can only be wrong. while (n < (int)sizeof(req) - 1) { int r = read(fd, req + n, 1); if (r <= 0) return; if (req[n] == '\n') break; n++; } req[n] = 0; while (n > 0 && (req[n - 1] == '\r' || req[n - 1] == ' ')) req[--n] = 0; if (req[0] == 'G' && req[1] == 'E' && req[2] == 'T' && req[3] == ' ') i = 4, body = 1; else if (req[0] == 'H' && req[1] == 'E' && req[2] == 'A' && req[3] == 'D' && req[4] == ' ') i = 5, body = 0; else { fail(fd, "501 Not Implemented", "GET and HEAD only."); return; } { int j = 0; while (req[i] && req[i] != ' ' && j < (int)sizeof(target) - 1) target[j++] = req[i++]; target[j] = 0; } switch (resolve(target, path, sizeof(path))) { case -1: fail(fd, "400 Bad Request", "That is not a path this serves."); return; case -2: fail(fd, "403 Forbidden", "The path leaves the document root."); return; } puts_("httpd: "); puts_(body ? "GET " : "HEAD "); puts_(path); puts_("\n"); if (stat(path, &st) < 0) { fail(fd, "404 Not Found", "No such file."); return; } if (st.type == T_DIR) { char idx[700]; int at = scat(idx, 0, sizeof(idx), path); at = scat(idx, at, sizeof(idx), "/index.html"); if (stat(idx, &st) == 0 && st.type != T_DIR) send_file(fd, idx, body); else send_listing(fd, path, target, body); return; } send_file(fd, path, body); } int main(int argc, char *argv[]) { struct sockaddr_in sa; unsigned int addr = 0; // 0.0.0.0 int port = DEFAULT_PORT, lfd, i; for (i = 1; i < argc; i++) { if (argv[i][0] != '-' || argv[i][2] != 0 || i + 1 >= argc) goto usage; switch (argv[i][1]) { case 'p': port = atoi_(argv[++i]); break; case 'r': { int at = scat(root, 0, sizeof(root), argv[++i]); while (at > 1 && root[at - 1] == '/') root[--at] = 0; } break; case 'a': { int ok; addr = parse_ip(argv[++i], &ok); if (!ok) { puts_("httpd: -a wants an address\n"); return 1; } } break; default: usage: puts_("usage: httpd [-r root] [-p port] [-a address]\n" " serves " DEFAULT_ROOT " on port 80, from anywhere, by default.\n"); return 1; } } if (port <= 0 || port > 65535) { puts_("httpd: that is not a port\n"); return 1; } { struct stat st; if (stat(root, &st) < 0 || st.type != T_DIR) { puts_("httpd: no such directory: "); puts_(root); puts_("\n"); return 1; } } lfd = socket(AF_INET, SOCK_STREAM, 0); if (lfd < 0) { puts_("httpd: no socket\n"); return 1; } sa.sin_family = AF_INET; sa.sin_port = (unsigned short)(((port & 0xff) << 8) | ((port >> 8) & 0xff)); sa.sin_addr = addr; for (i = 0; i < 8; i++) sa.sin_zero[i] = 0; if (bind(lfd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { puts_("httpd: cannot bind port "); putn(port); puts_(port < 1024 ? " (ports below 1024 may be reserved)\n" : "\n"); return 1; } if (listen(lfd, 8) < 0) { puts_("httpd: cannot listen\n"); return 1; } puts_("httpd: serving "); puts_(root); puts_(" on port "); putn(port); puts_("\n"); for (;;) { int fd = accept(lfd, 0, 0); if (fd < 0) continue; // A child per connection, so that one client that stops reading cannot // stop the server. The parent reaps without blocking: a zombie per // request would fill the process table on a busy machine, and blocking // to reap would undo the fork. int pid = fork(); if (pid == 0) { close(lfd); serve(fd); close(fd); exit(); } close(fd); if (pid < 0) puts_("httpd: cannot fork; dropped a connection\n"); while (waitpid(-1, 0, WNOHANG) > 0) ; } }