// SPDX-License-Identifier: GPL-2.0-or-later // // 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), 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), "