cosmopolitan/tool/net/greenbean.c

1293 lines
38 KiB
C

/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│
╞══════════════════════════════════════════════════════════════════════════════╡
│ Copyright 2020 Justine Alexandra Roberts Tunney │
│ │
│ This program is free software; you can redistribute it and/or modify │
│ it under the terms of the GNU General Public License as published by │
│ the Free Software Foundation; version 2 of the License. │
│ │
│ This program is distributed in the hope that it will be useful, but │
│ WITHOUT ANY WARRANTY; without even the implied warranty of │
│ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU │
│ General Public License for more details. │
│ │
│ You should have received a copy of the GNU General Public License │
│ along with this program; if not, write to the Free Software │
│ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA │
│ 02110-1301 USA │
╚─────────────────────────────────────────────────────────────────────────────*/
#include "libc/alg/arraylist2.internal.h"
#include "libc/bits/bits.h"
#include "libc/bits/bswap.h"
#include "libc/bits/safemacros.internal.h"
#include "libc/calls/calls.h"
#include "libc/calls/struct/iovec.h"
#include "libc/calls/struct/itimerval.h"
#include "libc/calls/struct/stat.h"
#include "libc/calls/weirdtypes.h"
#include "libc/fmt/conv.h"
#include "libc/fmt/fmt.h"
#include "libc/fmt/itoa.h"
#include "libc/log/check.h"
#include "libc/log/log.h"
#include "libc/macros.h"
#include "libc/math.h"
#include "libc/mem/mem.h"
#include "libc/nexgen32e/crc32.h"
#include "libc/rand/rand.h"
#include "libc/runtime/gc.h"
#include "libc/runtime/runtime.h"
#include "libc/sock/epoll.h"
#include "libc/sock/sock.h"
#include "libc/stdio/stdio.h"
#include "libc/str/str.h"
#include "libc/str/undeflate.h"
#include "libc/sysv/consts/af.h"
#include "libc/sysv/consts/auxv.h"
#include "libc/sysv/consts/clock.h"
#include "libc/sysv/consts/epoll.h"
#include "libc/sysv/consts/ex.h"
#include "libc/sysv/consts/exit.h"
#include "libc/sysv/consts/f.h"
#include "libc/sysv/consts/fd.h"
#include "libc/sysv/consts/inaddr.h"
#include "libc/sysv/consts/ipproto.h"
#include "libc/sysv/consts/itimer.h"
#include "libc/sysv/consts/map.h"
#include "libc/sysv/consts/o.h"
#include "libc/sysv/consts/prot.h"
#include "libc/sysv/consts/sa.h"
#include "libc/sysv/consts/shut.h"
#include "libc/sysv/consts/sig.h"
#include "libc/sysv/consts/so.h"
#include "libc/sysv/consts/sock.h"
#include "libc/sysv/consts/sol.h"
#include "libc/sysv/consts/tcp.h"
#include "libc/sysv/errfuns.h"
#include "libc/time/struct/tm.h"
#include "libc/time/time.h"
#include "libc/x/x.h"
#include "libc/zip.h"
#include "libc/zipos/zipos.internal.h"
#include "net/http/http.h"
#include "third_party/getopt/getopt.h"
#include "third_party/zlib/zlib.h"
/**
* @fileoverview redbean.c modified for epoll tutorial
* This'll work on Linux and Windows but BSD is TODO
* TODO(jart): fix memory bug
*/
#define USAGE \
" [-hvdsm] [-p PORT]\n\
\n\
DESCRIPTION\n\
\n\
greenbean distributable static web server\n\
\n\
FLAGS\n\
\n\
-h help\n\
-v verbosity\n\
-d daemonize\n\
-s uniprocess\n\
-m log messages\n\
-c INT cache seconds\n\
-r /X=/Y redirect X to Y\n\
-l ADDR listen ip [default 0.0.0.0]\n\
-p PORT listen port [default 8080]\n\
-L PATH log file location\n\
-P PATH pid file location\n\
-U INT daemon set user id\n\
-G INT daemon set group id\n\
-B STR changes server header\n\
\n\
FEATURES\n\
\n\
- HTTP v1.1\n\
- Content-Encoding\n\
- Range / Content-Range\n\
- Last-Modified / If-Modified-Since\n\
\n\
USAGE\n\
\n\
This executable is also a ZIP file that contains static assets.\n\
\n\
unzip -vl greenbean.com # shows listing of zip contents\n\
\n\
Audio video content should not be compressed in your ZIP files.\n\
Uncompressed assets enable browsers to send Range HTTP request.\n\
On the other hand compressed assets are best for gzip encoding.\n\
\n\
zip greenbean.com index.html # adds file\n\
zip -0 greenbean.com video.mp4 # adds without compression\n\
\n\
Each connection uses a point in time snapshot of your ZIP file.\n\
If your ZIP is deleted then serving continues. If it's replaced\n\
then issuing SIGUSR1 (or SIGHUP if daemon) will reindex the zip\n\
for subsequent connections without interrupting active ones. If\n\
SIGINT or SIGTERM is issued then a graceful shutdown is started\n\
but if it's issued a second time, active connections are reset.\n\
\n"
#define HASH_LOAD_FACTOR /* 1. / */ 4
#define DEFAULT_PORT 8080
#define DEFAULT_SERVER "greenbean/0.1"
#define DEFAULT_CONTENT_TYPE "application/octet-stream"
#define DEFAULT_PATH "/tool/net/redbean.html"
#define FAVICON "tool/net/redbean.ico"
#define STPCPY(p, s) mempcpy(p, s, strlen(s))
#define AppendHeaderName(p, s) STPCPY(STPCPY(p, s), ": ")
struct Client {
uint32_t poison;
struct Client *prev;
struct Client *next;
int fd;
int iovlen;
bool close;
uint32_t insize;
uint32_t addrsize;
uint8_t gzip_footer[8];
struct sockaddr_in addr;
long double startrequest;
long double startconnection;
struct HttpRequest req;
struct iovec iov[8];
char addrstr[32];
char outbuf[1500];
char inbuf[2048];
};
static const struct itimerval kHeartbeat = {
{0, 500000},
{0, 500000},
};
static const uint8_t kGzipHeader[] = {
0x1F, // MAGNUM
0x8B, // MAGNUM
0x08, // CM: DEFLATE
0x00, // FLG: NONE
0x00, // MTIME: NONE
0x00, //
0x00, //
0x00, //
0x00, // XFL
kZipOsUnix, // OS
};
static const struct ContentTypeExtension {
unsigned char ext[8];
const char *mime;
} kContentTypeExtension[] = {
{"S", "text/plain"}, //
{"bmp", "image/x-ms-bmp"}, //
{"c", "text/plain"}, //
{"cc", "text/plain"}, //
{"css", "text/css"}, //
{"csv", "text/csv"}, //
{"gif", "image/gif"}, //
{"h", "text/plain"}, //
{"html", "text/html"}, //
{"i", "text/plain"}, //
{"ico", "image/vnd.microsoft.icon"}, //
{"jpeg", "image/jpeg"}, //
{"jpg", "image/jpeg"}, //
{"js", "application/javascript"}, //
{"json", "application/json"}, //
{"m4a", "audio/mpeg"}, //
{"mp2", "audio/mpeg"}, //
{"mp3", "audio/mpeg"}, //
{"mp4", "video/mp4"}, //
{"mpg", "video/mpeg"}, //
{"otf", "font/otf"}, //
{"pdf", "application/pdf"}, //
{"png", "image/png"}, //
{"png", "image/png"}, //
{"png", "image/png"}, //
{"png", "image/png"}, //
{"s", "text/plain"}, //
{"svg", "image/svg+xml"}, //
{"tiff", "image/tiff"}, //
{"ttf", "font/ttf"}, //
{"txt", "text/plain"}, //
{"wav", "audio/x-wav"}, //
{"woff", "font/woff"}, //
{"woff2", "font/woff2"}, //
{"xml", "application/xml"}, //
{"zip", "application/zip"}, //
};
static struct Redirects {
size_t i, n;
struct Redirect {
const char *path;
size_t pathlen;
const char *dest;
} * p;
} redirects;
static struct Assets {
uint32_t n;
struct Asset {
uint32_t hash;
uint32_t cf;
int64_t lastmodified;
char *lastmodifiedstr;
} * p;
} assets;
static bool killed;
static bool notimer;
static bool heartbeat;
static bool daemonize;
static bool terminated;
static bool uniprocess;
static bool legacyhttp;
static bool invalidated;
static bool logmessages;
static int epfd;
static int gmtoff;
static int server;
static int daemonuid;
static int daemongid;
static int cacheseconds;
static void *zmap;
static uint8_t *zbase;
static uint8_t *zcdir;
static size_t zmapsize;
static const char *pidpath;
static const char *logpath;
static int64_t programtime;
static struct Client *clients;
static const char *programfile;
static const char *serverheader;
static long double nowish;
static struct sockaddr_in serveraddr;
static char currentdate[32];
static char serveraddrstr[32];
static void OnHup(void) {
invalidated = true;
}
static void OnAlarm(void) {
heartbeat = true;
}
static void OnTerminate(void) {
if (terminated) {
killed = true;
} else {
terminated = true;
}
}
static void AddRedirect(const char *arg) {
const char *p;
struct Redirect r;
CHECK_NOTNULL((p = strchr(arg, '=')));
CHECK_GT(p - arg, 0);
r.path = arg;
r.pathlen = p - arg;
r.dest = strdup(p + 1);
APPEND(&redirects.p, &redirects.i, &redirects.n, &r);
}
static int CompareRedirects(const struct Redirect *a,
const struct Redirect *b) {
return strcmp(a->path, b->path);
}
static void SortRedirects(void) {
qsort(redirects.p, redirects.i, sizeof(struct Redirect),
(void *)CompareRedirects);
}
static const char *LookupRedirect(const char *path, size_t n) {
int c, m, l, r, z;
l = 0;
r = redirects.i - 1;
while (l <= r) {
m = (l + r) >> 1;
c = memcmp(redirects.p[m].path, path, MIN(redirects.p[m].pathlen, n));
if (c < 0) {
l = m + 1;
} else if (c > 0) {
r = m - 1;
} else if (redirects.p[m].pathlen < n) {
l = m + 1;
} else if (redirects.p[m].pathlen > n) {
r = m - 1;
} else {
return redirects.p[m].dest;
}
}
return NULL;
}
static int CompareInts(const uint64_t x, uint64_t y) {
return x > y ? 1 : x < y ? -1 : 0;
}
static const char *LookupContentType(uint64_t ext) {
int c, m, l, r;
l = 0;
r = ARRAYLEN(kContentTypeExtension) - 1;
while (l <= r) {
m = (l + r) >> 1;
c = CompareInts(READ64BE(kContentTypeExtension[m].ext), ext);
if (c < 0) {
l = m + 1;
} else if (c > 0) {
r = m - 1;
} else {
return kContentTypeExtension[m].mime;
}
}
return DEFAULT_CONTENT_TYPE;
}
static const char *GetContentType(const char *path, size_t n) {
size_t i;
uint64_t x;
const char *p;
if ((p = memrchr(path, '.', n))) {
for (x = 0, i = n; i-- > p + 1 - path;) {
x <<= 8;
x |= path[i] & 0xFF;
}
return LookupContentType(bswap_64(x));
} else {
return DEFAULT_CONTENT_TYPE;
}
}
static wontreturn void PrintUsage(FILE *f, int rc) {
fprintf(f, "SYNOPSIS\n\n %s%s", program_invocation_name, USAGE);
exit(rc);
}
static void DescribeAddress(char buf[32], const struct sockaddr_in *addr) {
char *p = buf;
const uint8_t *ip4 = (const uint8_t *)&addr->sin_addr.s_addr;
p += uint64toarray_radix10(ip4[0], p), *p++ = '.';
p += uint64toarray_radix10(ip4[1], p), *p++ = '.';
p += uint64toarray_radix10(ip4[2], p), *p++ = '.';
p += uint64toarray_radix10(ip4[3], p), *p++ = ':';
p += uint64toarray_radix10(ntohs(addr->sin_port), p);
*p = '\0';
}
void GetOpts(int argc, char *argv[]) {
int opt;
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(DEFAULT_PORT);
serveraddr.sin_addr.s_addr = INADDR_ANY;
while ((opt = getopt(argc, argv, "hduvml:p:w:r:c:L:P:U:G:B:")) != -1) {
switch (opt) {
case 'v':
g_loglevel++;
break;
case 'd':
daemonize = true;
break;
case 'u':
uniprocess = true;
break;
case 'm':
logmessages = true;
break;
case 'r':
AddRedirect(optarg);
break;
case 'c':
cacheseconds = atoi(optarg);
break;
case 'p':
CHECK_NE(0xFFFF, (serveraddr.sin_port = htons(parseport(optarg))));
break;
case 'l':
CHECK_EQ(1, inet_pton(AF_INET, optarg, &serveraddr.sin_addr));
break;
case 'B':
serverheader = optarg;
break;
case 'L':
logpath = optarg;
break;
case 'P':
pidpath = optarg;
break;
case 'U':
daemonuid = atoi(optarg);
break;
case 'G':
daemonuid = atoi(optarg);
break;
case 'h':
PrintUsage(stdout, EXIT_SUCCESS);
default:
PrintUsage(stderr, EX_USAGE);
}
}
SortRedirects();
if (logpath) {
CHECK_NOTNULL(freopen(logpath, "a", stderr));
}
}
static void Daemonize(void) {
char ibuf[21];
int i, fd, pid;
for (i = 0; i < 128; ++i) close(i);
xsigaction(SIGHUP, OnHup, 0, 0, 0);
CHECK_NE(-1, (pid = fork()));
if (pid > 0) exit(0);
if (pid == -1) return;
CHECK_NE(-1, setsid());
CHECK_NE(-1, (pid = fork()));
if (pid > 0) _exit(0);
LOGIFNEG1(umask(0));
if (pidpath) {
CHECK_NE(-1, (fd = open(pidpath, O_CREAT | O_EXCL | O_WRONLY, 0644)));
CHECK_NE(-1, write(fd, ibuf, uint64toarray_radix10(getpid(), ibuf)));
LOGIFNEG1(close(fd));
}
if (!logpath) logpath = "/dev/null";
CHECK_NOTNULL(freopen("/dev/null", "r", stdin));
CHECK_NOTNULL(freopen(logpath, "a", stdout));
CHECK_NOTNULL(freopen(logpath, "a", stderr));
}
static int CompareHeaderValue(struct Client *c, int h, const char *s) {
return strncmp(s, c->inbuf + c->req.headers[h].a,
c->req.headers[h].b - c->req.headers[h].a);
}
static size_t GetIovSize(struct iovec *iov, int iovlen) {
int i;
size_t size;
for (size = i = 0; i < iovlen; ++i) {
DCHECK_NOTNULL(iov[i].iov_base);
size += iov[i].iov_len;
}
return size;
}
static ssize_t WriteSome(int fd, struct iovec *iov, int iovlen) {
ssize_t rc;
size_t wrote, n;
if ((rc = writev(fd, iov, iovlen)) != -1) {
wrote = n = rc;
do {
if (n >= iov->iov_len) {
iov->iov_len = 0;
n -= iov->iov_len;
++iov;
--iovlen;
} else {
iov->iov_base = (char *)iov->iov_base + n;
iov->iov_len -= n;
n = 0;
}
} while (n);
return wrote;
} else if (errno == EINTR) {
return 0;
} else {
return -1;
}
}
static uint32_t Hash(const void *data, size_t size) {
uint32_t h;
h = crc32c(0, data, size);
if (!h) h = 1;
return h;
}
static bool HasHeader(struct Client *c, int h) {
return c->req.headers[h].b > c->req.headers[h].a;
}
int64_t GetGmtOffset(void) {
int64_t t;
struct tm tm;
t = nowl();
localtime_r(&t, &tm);
return tm.tm_gmtoff;
}
static int64_t LocoTimeToZulu(int64_t x) {
return x - gmtoff;
}
static int64_t GetLastModifiedZip(uint8_t *cfile) {
uint8_t *p, *pe;
for (p = ZIP_CFILE_EXTRA(cfile), pe = p + ZIP_CFILE_EXTRASIZE(cfile); p < pe;
p += ZIP_EXTRA_SIZE(p)) {
if (ZIP_EXTRA_HEADERID(p) == kZipExtraNtfs) {
return LocoTimeToZulu(READ64LE(ZIP_EXTRA_CONTENT(p) + 8) /
HECTONANOSECONDS -
MODERNITYSECONDS);
} else if (ZIP_EXTRA_HEADERID(p) == kZipExtraExtendedTimestamp) {
return READ32LE(ZIP_EXTRA_CONTENT(p) + 1);
}
}
return LocoTimeToZulu(DosDateTimeToUnix(ZIP_CFILE_LASTMODIFIEDDATE(cfile),
ZIP_CFILE_LASTMODIFIEDTIME(cfile)));
}
static bool IsCompressed(struct Asset *a) {
return ZIP_CFILE_COMPRESSIONMETHOD(zbase + a->cf) == kZipCompressionDeflate;
}
static int GetHttpVersion(struct Client *c) {
return ParseHttpVersion(c->inbuf + c->req.version.a,
c->req.version.b - c->req.version.a);
}
static bool IsNotModified(struct Client *c, struct Asset *a) {
if (!HasHeader(c, kHttpIfModifiedSince)) return false;
return a->lastmodified >=
ParseHttpDateTime(c->inbuf + c->req.headers[kHttpIfModifiedSince].a,
c->req.headers[kHttpIfModifiedSince].b -
c->req.headers[kHttpIfModifiedSince].a);
}
static char *FormatUnixHttpDateTime(char *s, int64_t t) {
struct tm tm;
gmtime_r(&t, &tm);
FormatHttpDateTime(s, &tm);
return s;
}
static void FreeAssetsIndex(struct Asset *p, size_t n) {
int i;
if (p) {
for (i = 0; i < n; ++i) {
free(p[i].lastmodifiedstr);
}
free(p);
}
}
static bool IndexAssets(const uint8_t *base, const uint8_t *cdir) {
bool ok;
int64_t lm;
struct Asset *p;
uint32_t i, n, m, cf, step, hash;
DCHECK_GE(HASH_LOAD_FACTOR, 2);
n = ZIP_CDIR_RECORDS(cdir);
m = roundup2pow(MAX(1, n) * HASH_LOAD_FACTOR);
p = calloc(m, sizeof(struct Asset));
ok = ZIP_CDIR_MAGIC(cdir) == kZipCdirHdrMagic;
if (p && ok) {
for (cf = ZIP_CDIR_OFFSET(cdir); n--; cf += ZIP_CFILE_HDRSIZE(base + cf)) {
if (ZIP_CFILE_MAGIC(base + cf) == kZipCfileHdrMagic) {
hash = Hash(ZIP_CFILE_NAME(base + cf), ZIP_CFILE_NAMESIZE(base + cf));
step = 0;
do {
i = (hash + (step * (step + 1)) >> 1) & (m - 1);
++step;
} while (p[i].hash);
lm = GetLastModifiedZip(base + cf);
p[i].hash = hash;
p[i].cf = cf;
p[i].lastmodified = lm;
p[i].lastmodifiedstr = FormatUnixHttpDateTime(xmalloc(30), lm);
} else {
WARNF("corrupt zip central directory entry");
ok = false;
break;
}
}
} else {
WARNF("corrupt zip central directory");
}
if (ok) {
FreeAssetsIndex(assets.p, assets.n);
assets.p = p;
assets.n = m;
} else {
FreeAssetsIndex(p, m);
}
return ok;
}
static bool OpenZip(const char *path) {
int fd;
bool ok;
void *map;
struct stat st;
const uint8_t *cdir;
if (zmap) {
LOGIFNEG1(munmap(zmap, zmapsize));
}
if (!zmap && ZIP_CDIR_MAGIC(__zip_end) == kZipCdirHdrMagic) {
if (IndexAssets(_base, __zip_end)) {
ok = true;
zbase = _base;
zcdir = __zip_end;
} else {
ok = false;
}
} else {
fd = -1;
map = MAP_FAILED;
if ((fd = open(path, O_RDONLY)) != -1 && fstat(fd, &st) != -1 &&
st.st_size &&
(map = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0)) &&
(cdir = zipfindcentraldir(zmap, zmapsize)) && IndexAssets(map, cdir)) {
ok = true;
zmap = map;
zbase = map;
zcdir = cdir;
map = MAP_FAILED;
zmapsize = st.st_size;
} else {
ok = false;
}
if (map != MAP_FAILED) LOGIFNEG1(munmap(map, st.st_size));
if (fd != -1) LOGIFNEG1(close(fd));
}
return ok;
}
static struct Asset *FindAsset(const char *path, size_t pathlen) {
uint32_t i, step, hash;
if (pathlen && path[0] == '/') ++path, --pathlen;
hash = Hash(path, pathlen);
for (step = 0;; ++step) {
i = (hash + (step * (step + 1)) >> 1) & (assets.n - 1);
if (!assets.p[i].hash) return NULL;
if (hash == assets.p[i].hash &&
pathlen == ZIP_CFILE_NAMESIZE(zbase + assets.p[i].cf) &&
memcmp(path, ZIP_CFILE_NAME(zbase + assets.p[i].cf), pathlen) == 0) {
return &assets.p[i];
}
}
}
static struct Asset *FindFile(const char *path, size_t pathlen) {
char *p, *buf;
struct Asset *asset;
if ((asset = FindAsset(path, pathlen))) return asset;
if (pathlen == 12 && memcmp(path, "/favicon.ico", 12) == 0) {
return FindAsset(FAVICON, strlen(FAVICON));
} else {
return NULL;
}
}
static void *AddRange(char *content, long start, long length) {
intptr_t mend, mstart;
if (!__builtin_add_overflow((intptr_t)content, start, &mstart) ||
!__builtin_add_overflow(mstart, length, &mend) ||
((intptr_t)zbase <= mstart && mstart <= (intptr_t)zbase + zmapsize) ||
((intptr_t)zbase <= mend && mend <= (intptr_t)zbase + zmapsize)) {
return (void *)mstart;
} else {
abort();
}
}
static bool IsConnectionClose(struct Client *c) {
int n;
char *p;
p = c->inbuf + c->req.headers[kHttpConnection].a;
n = c->req.headers[kHttpConnection].b - c->req.headers[kHttpConnection].a;
return n == 5 && memcmp(p, "close", 5) == 0;
}
static char *AppendCrlf(char *p) {
return STPCPY(p, "\r\n");
}
#define AppendStatus(p, c, s) AppendStatus(p, c, s, sizeof(s) - 1)
static char *(AppendStatus)(char *p, int c, const char *s, size_t n) {
if (legacyhttp) {
p = STPCPY(p, "HTTP/1.0 ");
} else {
p = STPCPY(p, "HTTP/1.1 ");
}
p += uint64toarray_radix10(c, p);
*p++ = ' ';
p = mempcpy(p, s, n);
return AppendCrlf(p);
}
static void UpdateCurrentDate(long double now) {
int64_t t;
struct tm tm;
t = nowish = now;
gmtime_r(&t, &tm);
FormatHttpDateTime(currentdate, &tm);
}
static char *AppendDate(char *p) {
p = AppendHeaderName(p, "Date");
p = mempcpy(p, currentdate, 29);
return AppendCrlf(p);
}
static char *AppendLastModified(char *p, const char *s) {
p = AppendHeaderName(p, "Last-Modified");
p = mempcpy(p, s, 29);
return AppendCrlf(p);
}
static char *AppendServer(char *p) {
const char *s;
if (*(s = firstnonnull(serverheader, DEFAULT_SERVER))) {
p = AppendHeaderName(p, "Server");
p = stpcpy(p, s);
p = AppendCrlf(p);
}
return p;
}
static char *AppendConnectionClose(char *p) {
p = AppendHeaderName(p, "Connection");
p = STPCPY(p, "close");
return AppendCrlf(p);
}
static char *AppendAcceptRangesBytes(char *p) {
p = AppendHeaderName(p, "Accept-Ranges");
p = STPCPY(p, "bytes");
return AppendCrlf(p);
}
static char *AppendNosniff(char *p) {
p = AppendHeaderName(p, "X-Content-Type-Options");
p = STPCPY(p, "nosniff");
return AppendCrlf(p);
}
static char *AppendContentType(char *p, const char *ct) {
p = AppendHeaderName(p, "Content-Type");
p = stpcpy(p, ct);
if (startswith(ct, "text/")) {
p = STPCPY(p, "; charset=utf-8");
}
return AppendCrlf(p);
}
static char *AppendContentTypeTextPlain(char *p) {
return AppendContentType(p, "text/plain");
}
static char *AppendExpires(char *p, int64_t t) {
struct tm tm;
gmtime_r(&t, &tm);
p = AppendHeaderName(p, "Expires");
p = FormatHttpDateTime(p, &tm);
return AppendCrlf(p);
}
static char *AppendVaryContentEncoding(char *p) {
p = AppendHeaderName(p, "Vary");
p = STPCPY(p, "Accept-Encoding");
return AppendCrlf(p);
}
static char *AppendCache(char *p) {
int x;
x = cacheseconds;
if (!x) return p;
x = MAX(0, x);
p = AppendHeaderName(p, "Cache-Control");
p = STPCPY(p, "max-age=");
p += uint64toarray_radix10(x, p);
if (x) p = STPCPY(p, ", public");
p = AppendCrlf(p);
return AppendExpires(p, nowish + cacheseconds);
}
static char *AppendContentLength(char *p, size_t n) {
p = AppendHeaderName(p, "Content-Length");
p += uint64toarray_radix10(n, p);
return AppendCrlf(p);
}
static char *AppendContentRange(char *p, long rangestart, long rangelength,
long contentlength) {
long endrange;
CHECK_GE(rangestart + rangelength, rangestart);
CHECK_LE(rangestart + rangelength, contentlength);
if (__builtin_add_overflow(rangestart, rangelength, &endrange)) abort();
p = AppendHeaderName(p, "Content-Range");
p = STPCPY(p, "bytes ");
p += uint64toarray_radix10(rangestart, p);
*p++ = '-';
p += uint64toarray_radix10(endrange, p);
*p++ = '/';
p += uint64toarray_radix10(contentlength, p);
return AppendCrlf(p);
}
static char *AppendContentEncodingGzip(char *p) {
p = AppendHeaderName(p, "Content-Encoding");
p = STPCPY(p, "gzip");
return AppendCrlf(p);
}
static char *AppendRedirect(struct Client *c, char *p, const char *s) {
VERBOSEF("%s %s %.*s redirect %s", c->addrstr, kHttpMethod[c->req.method],
c->req.uri.b - c->req.uri.a, c->inbuf + c->req.uri.a, s);
p = AppendStatus(p, 302, "Temporary Redirect");
p = AppendHeaderName(p, "Location");
p = STPCPY(p, s);
return AppendCrlf(p);
}
static bool InflateTiny(uint8_t *outbuf, size_t outsize, const uint8_t *inbuf,
size_t insize) {
struct DeflateState ds;
return undeflate(outbuf, outsize, inbuf, insize, &ds) != -1;
}
static bool InflateZlib(uint8_t *outbuf, size_t outsize, const uint8_t *inbuf,
size_t insize) {
bool ok;
z_stream zs;
ok = false;
zs.next_in = inbuf;
zs.avail_in = insize;
zs.total_in = insize;
zs.next_out = outbuf;
zs.avail_out = outsize;
zs.total_out = outsize;
zs.zfree = Z_NULL;
zs.zalloc = Z_NULL;
if (inflateInit2(&zs, -MAX_WBITS) == Z_OK) {
switch (inflate(&zs, Z_NO_FLUSH)) {
case Z_STREAM_END:
ok = true;
break;
case Z_MEM_ERROR:
WARNF("Z_MEM_ERROR");
break;
case Z_DATA_ERROR:
WARNF("Z_DATA_ERROR");
break;
case Z_NEED_DICT:
WARNF("Z_NEED_DICT");
break;
default:
abort();
}
inflateEnd(&zs);
}
return ok;
}
static bool Inflate(uint8_t *outbuf, size_t outsize, const uint8_t *inbuf,
size_t insize) {
if (IsTiny()) {
return InflateTiny(outbuf, outsize, inbuf, insize);
} else {
return InflateZlib(outbuf, outsize, inbuf, insize);
}
}
static void LogRequestLatency(struct Client *c) {
long double now = nowl();
LOGF("%s latency req %,16ldns conn %,16ldns", c->addrstr,
(long)((now - c->startrequest) * 1e9),
(long)((now - c->startconnection) * 1e9));
}
bool HandleRequest(struct Client *c) {
char *p;
int msgsize;
bool gzipped;
void *content;
size_t pathlen;
struct Asset *a;
unsigned version;
const char *path, *location;
long lf, contentlength, actualcontentlength, rangestart, rangelength;
p = c->outbuf;
content = "";
gzipped = false;
contentlength = -1;
c->close = false;
if ((msgsize = ParseHttpRequest(&c->req, c->inbuf, c->insize)) != -1) {
if (!msgsize) return false;
if (logmessages) {
LOGF("%s received %,d byte message\n%.*s", c->addrstr, c->req.length,
c->req.length - 4, c->inbuf);
}
version = GetHttpVersion(c);
if (version < 101) c->close = true, legacyhttp = true;
if (version <= 101) {
if (IsConnectionClose(c)) c->close = true;
path = c->inbuf + c->req.uri.a;
pathlen = c->req.uri.b - c->req.uri.a;
if (c->req.method == kHttpGet || c->req.method == kHttpHead) {
if (ParseContentLength(&c->req, c->inbuf)) c->close = true;
VERBOSEF(
"%s %s %.*s referer %.*s", c->addrstr, kHttpMethod[c->req.method],
pathlen, path,
c->req.headers[kHttpReferer].b - c->req.headers[kHttpReferer].a,
c->inbuf + c->req.headers[kHttpReferer].a);
if ((location = LookupRedirect(path, pathlen))) {
p = AppendRedirect(c, p, DEFAULT_PATH);
} else if ((a = FindFile(path, pathlen))) {
if (IsNotModified(c, a)) {
VERBOSEF("%s %s %.*s not modified", c->addrstr,
kHttpMethod[c->req.method], pathlen, path);
p = AppendStatus(p, 304, "Not Modified");
} else {
lf = ZIP_CFILE_OFFSET(zbase + a->cf);
content = ZIP_LFILE_CONTENT(zbase + lf);
contentlength = ZIP_CFILE_COMPRESSEDSIZE(zbase + a->cf);
if (IsCompressed(a)) {
if (memmem(c->inbuf + c->req.headers[kHttpAcceptEncoding].a,
c->req.headers[kHttpAcceptEncoding].b -
c->req.headers[kHttpAcceptEncoding].a,
"gzip", 4)) {
gzipped = true;
memcpy(c->gzip_footer + 0, zbase + a->cf + kZipCfileOffsetCrc32,
4);
memcpy(c->gzip_footer + 4,
zbase + a->cf + kZipCfileOffsetUncompressedsize, 4);
p = AppendStatus(p, 200, "OK");
p = AppendContentEncodingGzip(p);
} else if (Inflate(
(content = gc(xmalloc(
ZIP_CFILE_UNCOMPRESSEDSIZE(zbase + a->cf)))),
(contentlength =
ZIP_CFILE_UNCOMPRESSEDSIZE(zbase + a->cf)),
ZIP_LFILE_CONTENT(zbase + lf),
ZIP_CFILE_COMPRESSEDSIZE(zbase + a->cf))) {
p = AppendStatus(p, 200, "OK");
} else {
WARNF("%s %s %.*s internal server error", c->addrstr,
kHttpMethod[c->req.method], pathlen, path);
p = AppendStatus(p, 500, "Internal Server Error");
content = "Internal Server Error\r\n";
contentlength = -1;
}
} else if (HasHeader(c, kHttpRange)) {
if (ParseHttpRange(c->inbuf + c->req.headers[kHttpRange].a,
c->req.headers[kHttpRange].b -
c->req.headers[kHttpRange].a,
contentlength, &rangestart, &rangelength)) {
p = AppendStatus(p, 206, "Partial Content");
p = AppendContentRange(p, rangestart, rangelength,
contentlength);
content = AddRange(content, rangestart, rangelength);
contentlength = rangelength;
} else {
WARNF(
"%s %s %.*s bad range %`'.*s", c->addrstr,
kHttpMethod[c->req.method], pathlen, path,
c->req.headers[kHttpRange].b - c->req.headers[kHttpRange].a,
c->inbuf + c->req.headers[kHttpRange].a);
p = AppendStatus(p, 416, "Range Not Satisfiable");
p = AppendContentRange(p, rangestart, rangelength,
contentlength);
content = "";
contentlength = 0;
}
} else {
p = AppendStatus(p, 200, "OK");
}
}
p = AppendLastModified(p, a->lastmodifiedstr);
p = AppendContentType(p, GetContentType(path, pathlen));
p = AppendCache(p);
if (!IsCompressed(a)) {
p = AppendAcceptRangesBytes(p);
} else {
p = AppendVaryContentEncoding(p);
}
} else {
WARNF("%s %s %.*s not found", c->addrstr, kHttpMethod[c->req.method],
pathlen, path);
p = AppendStatus(p, 404, "Not Found");
p = AppendContentTypeTextPlain(p);
content = "Not Found\r\n";
}
} else {
WARNF("%s %s %.*s method not allowed", c->addrstr,
kHttpMethod[c->req.method], pathlen, path);
p = AppendStatus(p, 405, "method not allowed");
p = AppendContentTypeTextPlain(p);
content = "Method Not Allowed\r\n";
c->close = true;
}
} else {
WARNF("%s http version not supported %`'.*s", c->addrstr,
c->req.version.b - c->req.version.a, c->inbuf + c->req.version.a);
p = AppendStatus(p, 505, "HTTP Version Not Supported");
p = AppendContentTypeTextPlain(p);
content = "HTTP Version Not Supported\r\n";
c->close = true;
}
} else {
WARNF("%s parse error %s", c->addrstr, strerror(errno));
p = AppendStatus(p, 400, "Bad Request");
p = AppendContentTypeTextPlain(p);
content = "Bad Request\r\n";
c->close = true;
}
p = AppendDate(p);
p = AppendNosniff(p);
p = AppendServer(p);
if (c->close) p = AppendConnectionClose(p);
if (contentlength == -1) contentlength = strlen(content);
actualcontentlength = contentlength;
if (gzipped) {
actualcontentlength += sizeof(kGzipHeader) + sizeof(c->gzip_footer);
}
p = AppendContentLength(p, actualcontentlength);
p = AppendCrlf(p);
if (logmessages) {
LOGF("%s sending %,d byte message\n%.*s", c->addrstr, p - c->outbuf,
p - c->outbuf - 4, c->outbuf);
}
CHECK_LT(p, c->outbuf + sizeof(c->outbuf));
c->iovlen = 0;
c->iov[c->iovlen].iov_base = c->outbuf;
c->iov[c->iovlen].iov_len = p - c->outbuf;
++c->iovlen;
if (c->req.method != kHttpHead) {
if (gzipped) {
c->iov[c->iovlen].iov_base = kGzipHeader;
c->iov[c->iovlen].iov_len = sizeof(kGzipHeader);
++c->iovlen;
}
c->iov[c->iovlen].iov_base = content;
c->iov[c->iovlen].iov_len = contentlength;
++c->iovlen;
if (gzipped) {
c->iov[c->iovlen].iov_base = c->gzip_footer;
c->iov[c->iovlen].iov_len = sizeof(c->gzip_footer);
++c->iovlen;
}
}
/* LogRequestLatency(); */
if (WriteSome(c->fd, c->iov, c->iovlen) == -1) {
VERBOSEF("%s send error %s", c->addrstr, strerror(errno));
c->close = true;
c->insize = 0;
c->iovlen = 0;
}
if (!c->close) {
memcpy(c->inbuf, c->inbuf + msgsize, c->insize - msgsize);
c->insize -= msgsize;
}
return true;
}
static void DeleteClient(struct Client *c) {
DEBUGF("%s close", c->addrstr);
CHECK_NE(-1, epoll_ctl(epfd, EPOLL_CTL_DEL, c->fd, NULL));
LOGIFNEG1(close(c->fd));
if (c->next) {
c->next->prev = c->prev;
}
if (c->prev) {
c->prev->next = c->next;
} else {
clients = c->next;
}
memset(c, 0, sizeof(*c));
free(c);
}
static void ProcessInbuf(struct Client *c) {
long double now;
for (;;) {
c->startrequest = now = nowl();
if (now - nowish > 1) UpdateCurrentDate(now);
if (HandleRequest(c)) {
if (GetIovSize(c->iov, c->iovlen)) {
CHECK_NE(-1, epoll_ctl(epfd, EPOLL_CTL_MOD, c->fd,
&(struct epoll_event){EPOLLOUT, {c}}));
} else {
if (c->close && !c->insize) {
DeleteClient(c);
} else if (c->insize) {
continue;
}
}
} else if (c->insize == sizeof(c->inbuf)) {
WARNF("%s http message too big", c->addrstr);
DeleteClient(c);
}
break;
}
}
static void ProcessRead(struct Client *c) {
char *p;
ssize_t rc;
rc = read(c->fd, c->inbuf + c->insize, sizeof(c->inbuf) - c->insize);
if (rc != -1) {
if (rc) {
c->insize += (size_t)rc;
ProcessInbuf(c);
} else {
DeleteClient(c);
}
} else if (errno != EINTR) {
if (errno == ECONNRESET) {
DEBUGF("%s reset", c->addrstr);
} else {
WARNF("%s recv error %s", c->addrstr, strerror(errno));
}
DeleteClient(c);
}
}
static void ProcessWrite(struct Client *c) {
if (WriteSome(c->fd, c->iov, c->iovlen) != -1) {
if (!GetIovSize(c->iov, c->iovlen)) {
if (!c->close) {
CHECK_NE(-1, epoll_ctl(epfd, EPOLL_CTL_MOD, c->fd,
&(struct epoll_event){EPOLLIN, {c}}));
ProcessInbuf(c);
} else {
DeleteClient(c);
}
}
} else {
VERBOSEF("%s send error %s", c->addrstr, strerror(errno));
DeleteClient(c);
}
}
static void ProcessConnect(void) {
int fd;
struct Client *c;
c = xmalloc(sizeof(struct Client));
c->addrsize = sizeof(c->addr);
if ((fd = accept(server, &c->addr, &c->addrsize)) != -1) {
c->fd = fd;
c->prev = NULL;
c->next = clients;
c->insize = 0;
c->iovlen = 0;
c->close = false;
c->startconnection = nowl();
c->startrequest = c->startconnection;
DescribeAddress(c->addrstr, &c->addr);
CHECK_NE(-1, epoll_ctl(epfd, EPOLL_CTL_ADD, c->fd,
&(struct epoll_event){EPOLLIN, {c}}));
if (clients) clients->prev = c;
clients = c;
DEBUGF("%s accept", c->addrstr);
} else if (errno != EINTR) {
FATALF("%s accept error %s", serveraddrstr, strerror(errno));
free(c);
}
}
static void HandleSignals(void) {
if (invalidated) {
if (OpenZip(programfile)) {
LOGF("%s reindexed zip", serveraddrstr);
} else {
WARNF("%s reindexing failed", serveraddrstr);
}
invalidated = false;
}
if (heartbeat | notimer) {
UpdateCurrentDate(nowl());
heartbeat = false;
}
}
static void ProcessEvents(void) {
int i, j, n;
struct epoll_event events[16];
for (;;) {
if (terminated) {
while (clients) {
if (!clients->insize) {
DeleteClient(clients);
} else {
break;
}
}
if (!clients) break;
}
if ((n = epoll_wait(epfd, events, ARRAYLEN(events), 200)) != -1) {
for (i = 0; i < n; ++i) {
if (!events[i].data.ptr) {
CHECK(events[i].events & EPOLLIN);
ProcessConnect();
} else if (events[i].events & (EPOLLIN | EPOLLERR | EPOLLHUP)) {
ProcessRead(events[i].data.ptr);
} else if (events[i].events & EPOLLOUT) {
ProcessWrite(events[i].data.ptr);
}
HandleSignals();
}
} else {
CHECK_EQ(EINTR, errno);
HandleSignals();
}
}
}
static void TuneServerSocket(void) {
int yes = 1;
LOGIFNEG1(setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)));
LOGIFNEG1(setsockopt(server, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes)));
LOGIFNEG1(setsockopt(server, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof(yes)));
LOGIFNEG1(setsockopt(server, IPPROTO_TCP, TCP_FASTOPEN, &yes, sizeof(yes)));
LOGIFNEG1(setsockopt(server, IPPROTO_TCP, TCP_QUICKACK, &yes, sizeof(yes)));
}
void GreenBean(void) {
gmtoff = GetGmtOffset();
programfile = (const char *)getauxval(AT_EXECFN);
CHECK(OpenZip(programfile));
xsigaction(SIGINT, OnTerminate, 0, 0, 0);
xsigaction(SIGHUP, OnTerminate, 0, 0, 0);
xsigaction(SIGTERM, OnTerminate, 0, 0, 0);
xsigaction(SIGCHLD, SIG_IGN, 0, 0, 0);
xsigaction(SIGPIPE, SIG_IGN, 0, 0, 0);
xsigaction(SIGUSR1, OnHup, 0, 0, 0);
xsigaction(SIGALRM, OnAlarm, 0, 0, 0);
if (setitimer(ITIMER_REAL, &kHeartbeat, NULL) == -1) notimer = true;
CHECK_NE(-1, (epfd = epoll_create1(0)));
CHECK_NE(-1, (server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)));
TuneServerSocket();
CHECK_NE(-1, bind(server, &serveraddr, sizeof(serveraddr)));
CHECK_NE(-1, listen(server, 10));
CHECK_NE(-1, epoll_ctl(epfd, EPOLL_CTL_ADD, server,
&(struct epoll_event){EPOLLIN}));
DescribeAddress(serveraddrstr, &serveraddr);
if (daemonize) Daemonize();
VERBOSEF("%s listen", serveraddrstr);
heartbeat = true;
ProcessEvents();
CHECK_NE(-1, epoll_ctl(epfd, EPOLL_CTL_DEL, server, NULL));
VERBOSEF("%s terminated", serveraddrstr);
LOGIFNEG1(close(server));
LOGIFNEG1(close(epfd));
}
int main(int argc, char *argv[]) {
showcrashreports();
GetOpts(argc, argv);
GreenBean();
return 0;
}