toybox/toys/net/httpd.c
<<
>>
Prefs
   1/* httpd.c - Web server.
   2 *
   3 * Copyright 2022 Rob Landley <rob@landley.net>
   4 *
   5 * See https://www.ietf.org/rfc/rfc2616.txt
   6 *
   7 * TODO: multiple domains, https, actual inetd with ratelimit...
   8 * range, gzip, ETag (If-None-Match:, Last-Modified:), Date:
   9 * "Accept-Ranges: bytes"/"Range: bytes=xxx-[yyy]"
  10 * .htaccess (auth, forward)
  11 * optional conf file, error pages
  12 * -ifv -p [IP:]PORT -u [USER][:GRP] -c CFGFILE
  13 * cgi: SERVER_PORT SERVER_NAME REMOTE_ADDR REMOTE_HOST REQUEST_METHOD
  14
  15USE_HTTPD(NEWTOY(httpd, ">1", TOYFLAG_USR|TOYFLAG_BIN))
  16
  17config HTTPD
  18  bool "httpd"
  19  default y
  20  help
  21    usage: httpd [DIR]
  22
  23    Serve contents of directory as static web pages.
  24*/
  25
  26#include "toys.h"
  27
  28char *rfc1123(char *buf, time_t t)
  29{
  30  strftime(buf, 64, "%a, %d %b %Y %T GMT", gmtime(&t));
  31
  32  return buf;
  33}
  34
  35// Stop: header time.
  36void header_time(int stat, char *str, char *more)
  37{
  38  char buf[64];
  39
  40  xprintf("HTTP/1.1 %d %s\r\nServer: toybox httpd/%s\r\nDate: %s\r\n%s"
  41    "Connection: close\r\n\r\n", stat, str, TOYBOX_VERSION,
  42    rfc1123(buf, time(0)), more ? : "");
  43}
  44
  45void error_time(int stat, char *str)
  46{
  47  header_time(stat, str, 0);
  48  xprintf("<html><head><title>%d %s</title></head>"
  49    "<body><h3>%d %s</h3></body></html>", stat, str, stat, str);
  50}
  51
  52// She never told me...
  53char *mime(char *file)
  54{
  55  char *s = strrchr(file, '.'), *types[] = {
  56    "asc\0text/plain", "bin\0application/octet-stream", "bmp\0image/bmp",
  57    "cpio\0application/x-cpio", "css\0text/css", "doc\0application/msword",
  58    "dtd\0text/xml", "dvi\0application/x-dvi", "gif\0image/gif",
  59    "htm\0text/html", "html\0text/html", "jar\0applicat/x-java-archive",
  60    "jpeg\0image/jpeg", "jpg\0image/jpeg", "js\0application/x-javascript",
  61    "mp3\0audio/mpeg", "mp4\0video/mp4", "mpg\0video/mpeg",
  62    "ogg\0application/ogg", "pbm\0image/x-portable-bitmap",
  63    "pdf\0application/pdf", "png\0image/png",
  64    "ppt\0application/vnd.ms-powerpoint", "ps\0application/postscript",
  65    "rtf\0text/rtf", "sgml\0text/sgml", "svg\0image/svg+xml",
  66    "tar\0application/x-tar", "tex\0application/x-tex", "tiff\0image/tiff",
  67    "txt\0text/plain", "wav\0audio/x-wav", "xls\0application/vnd.ms-excel",
  68    "xml\0tet/xml", "zip\0application/zip"
  69  };
  70  int i;
  71
  72  strcpy(toybuf, "text/plain");
  73  if (s++) for (i = 0; i<ARRAY_LEN(types); i++) {
  74    if (strcasecmp(s, types[i])) continue;
  75    strcpy(toybuf, types[i]+strlen(types[i])+1);
  76    break;
  77  }
  78  if (!strncmp(toybuf, "text/", 5)) strcat(toybuf, "; charset=UTF-8");
  79
  80  return toybuf;
  81}
  82
  83static int isunder(char *file, char *dir)
  84{
  85  char *s1 = xabspath(dir, ABS_FILE), *s2 = xabspath(file, 0), *ss = s2;
  86  int rc = s1 && s2 && strstart(&ss, s1) && (!*ss || *ss=='/' || ss[-1]=='/');
  87
  88  free(s2);
  89  free(s1);
  90
  91  return rc;
  92}
  93
  94// Handle a connection on fd
  95void handle(int infd, int outfd)
  96{
  97  FILE *fp = fdopen(infd, "r");
  98  char *s = xgetline(fp), *ss, *esc, *path, *word[3];
  99  int i, fd;
 100
 101  // Split line into method/path/protocol
 102  for (i = 0, ss = s;;) {
 103    word[i++] = ss;
 104    while (*ss && !strchr(" \r\n", *ss)) ss++;
 105    while (*ss && strchr(" \r\n", *ss)) *(ss++) = 0;
 106    if (i==3) break;
 107    if (!*ss) return header_time(400, "Bad Request", 0);
 108  }
 109
 110  // Process additional http/1.1 lines
 111  while ((ss = xgetline(fp))) {
 112    i = *chomp(ss);
 113// TODO: any of
 114//User-Agent: Wget/1.20.1 (linux-gnu) - do we want to log anything?
 115//Accept: */* - 406 Too Snobbish
 116//Accept-Encoding: identity - we can gzip?
 117//Host: landley.net  - we could handle multiple domains?
 118//Connection: Keep-Alive - probably don't care
 119
 120    free(ss);
 121    if (!i) break;
 122  }
 123
 124  if (!strcasecmp(word[0], "get")) {
 125    struct stat st;
 126    if (*(ss = word[1])!='/') error_time(400, "Bad Request");
 127    while (*ss=='/') ss++;
 128    if (!*ss) ss = ".";
 129    else unescape_url(ss);
 130
 131    // TODO domain.com:/path/to/blah domain2.com:/path/to/that
 132    if (!isunder(ss, ".") || stat(ss, &st)) error_time(404, "Not Found");
 133    else if (-1 == (fd = open(ss, O_RDONLY))) error_time(403, "Forbidden");
 134    else if (!S_ISDIR(st.st_mode)) {
 135      char buf[64];
 136file:
 137      header_time(200, "Ok", ss = xmprintf("Content-Type: %s\r\n"
 138        "Content-Length: %lld\r\nLast-Modified: %s\r\n",
 139        mime(ss), (long long)st.st_size, rfc1123(buf, st.st_mtime)));
 140      free(ss);
 141      xsendfile(fd, outfd);
 142    } else if (ss[strlen(ss)-1]!='/' && strcmp(ss, ".")) {
 143      header_time(302, "Found", path = xmprintf("Location: %s/\r\n", word[1]));
 144      free(path);
 145    } else {
 146      DIR *dd;
 147      struct dirent *dir;
 148
 149      // Do we have an index.html?
 150      path = ss;
 151      ss = "index.html";
 152      path = xmprintf("%s%s", path, ss);
 153      if (!stat(path, &st) || !S_ISREG(st.st_mode)) i = -1;
 154      else if (-1 == (i = open(path, O_RDONLY))) error_time(403, "Forbidden");
 155      free(path);
 156      if (i != -1) {
 157        close(fd);
 158        fd = i;
 159
 160        goto file;
 161      }
 162
 163      // List directory contents
 164      header_time(200, "Ok", "Content-Type: text/html\r\n");
 165      dprintf(outfd, "<html><head><title>Index of %s</title></head>\n"
 166        "<body><h3>Index of %s</h3></body>\n", word[1], word[1]);
 167      for (dd = fdopendir(fd); (dir = readdir(dd));) {
 168        esc = escape_url(dir->d_name, "<>&\"");
 169        dprintf(outfd, "<a href=\"%s\">%s</a><br />\n", esc, esc);
 170        free(esc);
 171      }
 172      dprintf(outfd, "</body></html>\n");
 173    }
 174  } else error_time(501, "Not Implemented");
 175  free(s);
 176}
 177
 178void httpd_main(void)
 179{
 180  if (toys.optc && chdir(*toys.optargs))
 181    return error_time(500, "Internal Error");
 182  // inetd only at the moment
 183  handle(0, 1);
 184}
 185