mirror of
https://github.com/jart/cosmopolitan.git
synced 2025-01-31 19:43:32 +00:00
3c19b6e352
You can now pass `-D directory` to redbean which will serve assets from the local filesystem. This is useful for development since it allows us to skip needing to shut down the server and run InfoZIP when testing an iteration of a lua server page script. See #97
2278 lines
63 KiB
C
2278 lines
63 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 │
|
|
│ │
|
|
│ Permission to use, copy, modify, and/or distribute this software for │
|
|
│ any purpose with or without fee is hereby granted, provided that the │
|
|
│ above copyright notice and this permission notice appear in all copies. │
|
|
│ │
|
|
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │
|
|
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │
|
|
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │
|
|
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │
|
|
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │
|
|
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │
|
|
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
|
|
│ PERFORMANCE OF THIS SOFTWARE. │
|
|
╚─────────────────────────────────────────────────────────────────────────────*/
|
|
#include "libc/bits/bits.h"
|
|
#include "libc/bits/bswap.h"
|
|
#include "libc/bits/popcnt.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/dce.h"
|
|
#include "libc/errno.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.internal.h"
|
|
#include "libc/math.h"
|
|
#include "libc/mem/mem.h"
|
|
#include "libc/nexgen32e/bsf.h"
|
|
#include "libc/nexgen32e/bsr.h"
|
|
#include "libc/nexgen32e/crc32.h"
|
|
#include "libc/rand/rand.h"
|
|
#include "libc/runtime/runtime.h"
|
|
#include "libc/sock/sock.h"
|
|
#include "libc/stdio/stdio.h"
|
|
#include "libc/str/str.h"
|
|
#include "libc/str/thompike.h"
|
|
#include "libc/str/undeflate.h"
|
|
#include "libc/str/utf16.h"
|
|
#include "libc/sysv/consts/af.h"
|
|
#include "libc/sysv/consts/auxv.h"
|
|
#include "libc/sysv/consts/clock.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/consts/w.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/base64.h"
|
|
#include "net/http/escape.h"
|
|
#include "net/http/http.h"
|
|
#include "third_party/getopt/getopt.h"
|
|
#include "third_party/lua/lauxlib.h"
|
|
#include "third_party/lua/ltests.h"
|
|
#include "third_party/lua/lua.h"
|
|
#include "third_party/lua/lualib.h"
|
|
#include "third_party/zlib/zlib.h"
|
|
|
|
#define USAGE \
|
|
" [-hvdsm] [-p PORT] [-- SCRIPTARGS...]\n\
|
|
\n\
|
|
DESCRIPTION\n\
|
|
\n\
|
|
redbean - single-file distributable web server\n\
|
|
\n\
|
|
FLAGS\n\
|
|
\n\
|
|
-h help\n\
|
|
-v verbosity\n\
|
|
-d daemonize\n\
|
|
-u uniprocess\n\
|
|
-z print port\n\
|
|
-m log messages\n\
|
|
-b log message bodies\n\
|
|
-D DIR serve assets from directory\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\
|
|
- Lua v5.4\n\
|
|
- HTTP v0.9\n\
|
|
- HTTP v1.0\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 redbean.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 redbean.com index.html # adds file\n\
|
|
zip -0 redbean.com video.mp4 # adds without compression\n\
|
|
\n\
|
|
You can run redbean interactively in your terminal as follows:\n\
|
|
\n\
|
|
redbean.com -vv\n\
|
|
CTRL-C # 1x: graceful shutdown\n\
|
|
CTRL-C # 2x: forceful shutdown\n\
|
|
\n\
|
|
You can have redbean run as a daemon by doing the following:\n\
|
|
\n\
|
|
redbean.com -vv -d -L redbean.log -P redbean.pid\n\
|
|
kill -TERM $(cat redbean.pid) # 1x: graceful shutdown\n\
|
|
kill -TERM $(cat redbean.pid) # 2x: forceful shutdown\n\
|
|
\n\
|
|
redbean imposes a 32kb limit on requests to limit the memory of\n\
|
|
connection processes, which grow to whatever number your system\n\
|
|
limits and tcp stack configuration allow. If fork() should fail\n\
|
|
then redbean starts shutting idle connections down.\n\
|
|
\n\
|
|
Redirects emit a 307 response unless the location exists in\n\
|
|
the zip directory, in which case it transparently rewrites.\n\
|
|
\n\
|
|
\n"
|
|
|
|
#define HASH_LOAD_FACTOR /* 1. / */ 4
|
|
#define DEFAULT_PORT 8080
|
|
|
|
#define AppendHeaderName(p, s) stpcpy(stpcpy(p, s), ": ")
|
|
|
|
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 char kHexToInt[256] = {
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x20
|
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, // 0x30
|
|
0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x40
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x50
|
|
0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x60
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x70
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x80
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x90
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xa0
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xb0
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xc0
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xd0
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xe0
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xf0
|
|
};
|
|
|
|
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"}, //
|
|
{"markdown", "text/plain"}, //
|
|
{"md", "text/plain"}, //
|
|
{"mp2", "audio/mpeg"}, //
|
|
{"mp3", "audio/mpeg"}, //
|
|
{"mp4", "video/mp4"}, //
|
|
{"mpg", "video/mpeg"}, //
|
|
{"otf", "font/otf"}, //
|
|
{"pdf", "application/pdf"}, //
|
|
{"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"}, //
|
|
};
|
|
|
|
struct Buffer {
|
|
size_t n;
|
|
char *p;
|
|
};
|
|
|
|
struct Strings {
|
|
size_t n;
|
|
char **p;
|
|
};
|
|
|
|
struct Parser {
|
|
int i;
|
|
int c;
|
|
const char *data;
|
|
int size;
|
|
bool isform;
|
|
bool islatin1;
|
|
char *p;
|
|
char *q;
|
|
};
|
|
|
|
struct Params {
|
|
size_t n;
|
|
struct Param {
|
|
struct Buffer key;
|
|
struct Buffer val; // val.n may be SIZE_MAX
|
|
} * p;
|
|
};
|
|
|
|
struct Request {
|
|
struct Buffer path;
|
|
struct Params params;
|
|
struct Buffer fragment;
|
|
};
|
|
|
|
static struct Freelist {
|
|
size_t n;
|
|
void **p;
|
|
} freelist;
|
|
|
|
static struct Redirects {
|
|
size_t n;
|
|
struct Redirect {
|
|
const char *path;
|
|
size_t pathlen;
|
|
const char *location;
|
|
} * p;
|
|
} redirects;
|
|
|
|
static struct Assets {
|
|
uint32_t n;
|
|
struct Asset {
|
|
uint32_t lf;
|
|
uint32_t hash;
|
|
int64_t lastmodified;
|
|
char *lastmodifiedstr;
|
|
struct File {
|
|
char *path;
|
|
struct stat st;
|
|
} * file;
|
|
} * p;
|
|
} assets;
|
|
|
|
static bool killed;
|
|
static bool istext;
|
|
static bool zombied;
|
|
static bool gzipped;
|
|
static bool branded;
|
|
static bool meltdown;
|
|
static bool heartless;
|
|
static bool printport;
|
|
static bool heartbeat;
|
|
static bool daemonize;
|
|
static bool logbodies;
|
|
static bool terminated;
|
|
static bool uniprocess;
|
|
static bool invalidated;
|
|
static bool logmessages;
|
|
static bool checkedmethod;
|
|
static bool connectionclose;
|
|
static bool keyboardinterrupt;
|
|
|
|
static int frags;
|
|
static int gmtoff;
|
|
static int server;
|
|
static int client;
|
|
static int daemonuid;
|
|
static int daemongid;
|
|
static int statuscode;
|
|
static unsigned httpversion;
|
|
static uint32_t clientaddrsize;
|
|
|
|
static lua_State *L;
|
|
static size_t zsize;
|
|
static void *content;
|
|
static uint8_t *zdir;
|
|
static uint8_t *zmap;
|
|
static size_t hdrsize;
|
|
static size_t msgsize;
|
|
static size_t amtread;
|
|
static char *luaheaderp;
|
|
struct Strings stagedirs;
|
|
static const char *pidpath;
|
|
static const char *logpath;
|
|
static int64_t programtime;
|
|
static size_t contentlength;
|
|
static int64_t cacheseconds;
|
|
static uint8_t gzip_footer[8];
|
|
static const char *serverheader;
|
|
|
|
static struct Buffer inbuf;
|
|
static struct Buffer hdrbuf;
|
|
static struct Buffer outbuf;
|
|
static struct Request request;
|
|
|
|
static long double nowish;
|
|
static long double startread;
|
|
static long double startrequest;
|
|
static long double startconnection;
|
|
static struct sockaddr_in serveraddr;
|
|
static struct sockaddr_in clientaddr;
|
|
|
|
static struct HttpRequest msg;
|
|
static char currentdate[32];
|
|
static char clientaddrstr[32];
|
|
static char serveraddrstr[32];
|
|
|
|
static void OnChld(void) {
|
|
zombied = true;
|
|
}
|
|
|
|
static void OnAlrm(void) {
|
|
heartbeat = true;
|
|
}
|
|
|
|
static void OnUsr1(void) {
|
|
invalidated = true;
|
|
}
|
|
|
|
static void OnUsr2(void) {
|
|
meltdown = true;
|
|
}
|
|
|
|
static void OnTerm(void) {
|
|
if (!terminated) {
|
|
terminated = true;
|
|
} else {
|
|
killed = true;
|
|
}
|
|
}
|
|
|
|
static void OnInt(void) {
|
|
keyboardinterrupt = true;
|
|
OnTerm();
|
|
}
|
|
|
|
static void OnHup(void) {
|
|
if (daemonize) {
|
|
OnUsr1();
|
|
} else {
|
|
OnTerm();
|
|
}
|
|
}
|
|
|
|
static int CompareSlices(const char *a, size_t n, const char *b, size_t m) {
|
|
int c;
|
|
if ((c = memcmp(a, b, MIN(n, m)))) return c;
|
|
if (n < m) return -1;
|
|
if (n > m) return +1;
|
|
return 0;
|
|
}
|
|
|
|
static int CompareSlicesCase(const char *a, size_t n, const char *b, size_t m) {
|
|
int c;
|
|
if ((c = memcasecmp(a, b, MIN(n, m)))) return c;
|
|
if (n < m) return -1;
|
|
if (n > m) return +1;
|
|
return 0;
|
|
}
|
|
|
|
static long FindRedirect(const char *path, size_t n) {
|
|
int c, m, l, r, z;
|
|
l = 0;
|
|
r = redirects.n - 1;
|
|
while (l <= r) {
|
|
m = (l + r) >> 1;
|
|
c = CompareSlices(redirects.p[m].path, redirects.p[m].pathlen, path, n);
|
|
if (c < 0) {
|
|
l = m + 1;
|
|
} else if (c > 0) {
|
|
r = m - 1;
|
|
} else {
|
|
return m;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static void AddRedirect(const char *arg) {
|
|
long i, j;
|
|
const char *p;
|
|
struct Redirect r;
|
|
CHECK_NOTNULL((p = strchr(arg, '=')));
|
|
CHECK_GT(p - arg, 0);
|
|
r.path = arg;
|
|
r.pathlen = p - arg;
|
|
r.location = p + 1;
|
|
if ((i = FindRedirect(r.path, r.pathlen)) != -1) {
|
|
redirects.p[i] = r;
|
|
} else {
|
|
i = redirects.n;
|
|
redirects.p = xrealloc(redirects.p, (i + 1) * sizeof(*redirects.p));
|
|
for (j = i; j > 0; --j) {
|
|
if (CompareSlices(r.path, r.pathlen, redirects.p[j - 1].path,
|
|
redirects.p[j - 1].pathlen) < 0) {
|
|
redirects.p[j] = redirects.p[j - 1];
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
redirects.p[j] = r;
|
|
++redirects.n;
|
|
}
|
|
}
|
|
|
|
static int CompareInts(const uint64_t x, uint64_t y) {
|
|
return x > y ? 1 : x < y ? -1 : 0;
|
|
}
|
|
|
|
static const char *FindContentType(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 NULL;
|
|
}
|
|
|
|
static const char *GetContentType(const char *path, size_t n) {
|
|
size_t i;
|
|
uint64_t x;
|
|
const char *p, *r;
|
|
if ((p = memrchr(path, '.', n))) {
|
|
for (x = 0, i = n; i-- > p + 1 - path;) {
|
|
x <<= 8;
|
|
x |= path[i] & 0xFF;
|
|
}
|
|
if ((r = FindContentType(bswap_64(x)))) {
|
|
return r;
|
|
}
|
|
}
|
|
return "application/octet-stream";
|
|
}
|
|
|
|
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';
|
|
}
|
|
|
|
static void SetDefaults(void) {
|
|
cacheseconds = -1;
|
|
serverheader = "redbean/0.2";
|
|
serveraddr.sin_family = AF_INET;
|
|
serveraddr.sin_port = htons(DEFAULT_PORT);
|
|
serveraddr.sin_addr.s_addr = INADDR_ANY;
|
|
AddRedirect("/=/tool/net/redbean.html");
|
|
AddRedirect("/index.html=/tool/net/redbean.html");
|
|
AddRedirect("/favicon.ico=/tool/net/redbean.ico");
|
|
}
|
|
|
|
static wontreturn void PrintUsage(FILE *f, int rc) {
|
|
fprintf(f, "SYNOPSIS\n\n %s%s", program_invocation_name, USAGE);
|
|
exit(rc);
|
|
}
|
|
|
|
static char *RemoveTrailingSlashes(char *s) {
|
|
size_t n;
|
|
n = strlen(s);
|
|
while (n && (s[n - 1] == '/' || s[n - 1] == '\\')) s[--n] = '\0';
|
|
return s;
|
|
}
|
|
|
|
static void AddStagingDirectory(const char *dirpath) {
|
|
char *s;
|
|
s = RemoveTrailingSlashes(strdup(dirpath));
|
|
if (!isdirectory(s)) {
|
|
fprintf(stderr, "error: not a directory: %s\n", s);
|
|
exit(1);
|
|
}
|
|
stagedirs.p = xrealloc(stagedirs.p, ++stagedirs.n * sizeof(*stagedirs.p));
|
|
stagedirs.p[stagedirs.n - 1] = s;
|
|
}
|
|
|
|
static void GetOpts(int argc, char *argv[]) {
|
|
int opt;
|
|
while ((opt = getopt(argc, argv, "zhduvmbl:p:w:r:c:L:P:U:G:B:D:")) != -1) {
|
|
switch (opt) {
|
|
case 'v':
|
|
__log_level++;
|
|
break;
|
|
case 'd':
|
|
daemonize = true;
|
|
break;
|
|
case 'u':
|
|
uniprocess = true;
|
|
break;
|
|
case 'm':
|
|
logmessages = true;
|
|
break;
|
|
case 'b':
|
|
logbodies = true;
|
|
break;
|
|
case 'z':
|
|
printport = true;
|
|
break;
|
|
case 'r':
|
|
AddRedirect(optarg);
|
|
break;
|
|
case 'D':
|
|
AddStagingDirectory(optarg);
|
|
break;
|
|
case 'c':
|
|
cacheseconds = strtol(optarg, NULL, 0);
|
|
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 = emptytonull(EncodeHttpHeaderValue(optarg, -1, 0));
|
|
break;
|
|
case 'L':
|
|
logpath = optarg;
|
|
break;
|
|
case 'P':
|
|
pidpath = optarg;
|
|
break;
|
|
case 'U':
|
|
daemonuid = atoi(optarg);
|
|
break;
|
|
case 'G':
|
|
daemongid = atoi(optarg);
|
|
break;
|
|
case 'h':
|
|
PrintUsage(stdout, EXIT_SUCCESS);
|
|
default:
|
|
PrintUsage(stderr, EX_USAGE);
|
|
}
|
|
}
|
|
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);
|
|
if ((pid = fork()) > 0) exit(0);
|
|
setsid();
|
|
if ((pid = fork()) > 0) _exit(0);
|
|
umask(0);
|
|
if (pidpath) {
|
|
fd = open(pidpath, O_CREAT | O_EXCL | O_WRONLY, 0644);
|
|
write(fd, ibuf, uint64toarray_radix10(getpid(), ibuf));
|
|
close(fd);
|
|
}
|
|
if (!logpath) logpath = "/dev/null";
|
|
freopen("/dev/null", "r", stdin);
|
|
freopen(logpath, "a", stdout);
|
|
freopen(logpath, "a", stderr);
|
|
LOGIFNEG1(setuid(daemonuid));
|
|
LOGIFNEG1(setgid(daemongid));
|
|
}
|
|
|
|
static void OnWorkerExit(int pid, int ws) {
|
|
if (WIFEXITED(ws)) {
|
|
if (WEXITSTATUS(ws)) {
|
|
WARNF("worker %d exited with %d", pid, WEXITSTATUS(ws));
|
|
} else {
|
|
DEBUGF("worker %d exited", pid);
|
|
}
|
|
} else {
|
|
WARNF("worker %d terminated with %s", pid, strsignal(WTERMSIG(ws)));
|
|
}
|
|
}
|
|
|
|
static void WaitAll(void) {
|
|
int ws, pid;
|
|
for (;;) {
|
|
if ((pid = wait(&ws)) != -1) {
|
|
OnWorkerExit(pid, ws);
|
|
} else {
|
|
if (errno == ECHILD) break;
|
|
if (errno == EINTR) {
|
|
if (killed) {
|
|
WARNF("%s terminating harder", serveraddrstr);
|
|
LOGIFNEG1(kill(0, SIGTERM));
|
|
}
|
|
continue;
|
|
}
|
|
FATALF("%s wait error %s", serveraddrstr, strerror(errno));
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ReapZombies(void) {
|
|
int ws, pid;
|
|
zombied = false;
|
|
do {
|
|
if ((pid = waitpid(-1, &ws, WNOHANG)) != -1) {
|
|
if (pid) {
|
|
OnWorkerExit(pid, ws);
|
|
} else {
|
|
break;
|
|
}
|
|
} else {
|
|
if (errno == ECHILD) break;
|
|
if (errno == EINTR) continue;
|
|
FATALF("%s wait error %s", serveraddrstr, strerror(errno));
|
|
}
|
|
} while (!terminated);
|
|
}
|
|
|
|
static ssize_t WritevAll(int fd, struct iovec *iov, int iovlen) {
|
|
ssize_t rc;
|
|
size_t wrote;
|
|
do {
|
|
if ((rc = writev(fd, iov, iovlen)) != -1) {
|
|
wrote = rc;
|
|
do {
|
|
if (wrote >= iov->iov_len) {
|
|
wrote -= iov->iov_len;
|
|
++iov;
|
|
--iovlen;
|
|
} else {
|
|
iov->iov_base = (char *)iov->iov_base + wrote;
|
|
iov->iov_len -= wrote;
|
|
wrote = 0;
|
|
}
|
|
} while (wrote);
|
|
} else if (errno == EINTR) {
|
|
if (killed || (meltdown && nowl() - startread > 2)) {
|
|
return -1;
|
|
}
|
|
} else {
|
|
return -1;
|
|
}
|
|
} while (iovlen);
|
|
return 0;
|
|
}
|
|
|
|
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(int h) {
|
|
return msg.headers[h].b > msg.headers[h].a;
|
|
}
|
|
|
|
static int CompareHeader(int h, const char *s) {
|
|
return CompareSlices(s, strlen(s), inbuf.p + msg.headers[h].a,
|
|
msg.headers[h].b - msg.headers[h].a);
|
|
}
|
|
|
|
static bool HeaderEquals(int h, const char *s) {
|
|
return !CompareHeader(h, s);
|
|
}
|
|
|
|
static bool ClientAcceptsGzip(void) {
|
|
return httpversion >= 100 &&
|
|
!!memmem(inbuf.p + msg.headers[kHttpAcceptEncoding].a,
|
|
msg.headers[kHttpAcceptEncoding].b -
|
|
msg.headers[kHttpAcceptEncoding].a,
|
|
"gzip", 4);
|
|
}
|
|
|
|
static void UpdateCurrentDate(long double now) {
|
|
int64_t t;
|
|
struct tm tm;
|
|
t = nowish = now;
|
|
gmtime_r(&t, &tm);
|
|
FormatHttpDateTime(currentdate, &tm);
|
|
}
|
|
|
|
static 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(const uint8_t *cfile) {
|
|
const 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 !a->file &&
|
|
ZIP_LFILE_COMPRESSIONMETHOD(zmap + a->lf) == kZipCompressionDeflate;
|
|
}
|
|
|
|
static bool IsNotModified(struct Asset *a) {
|
|
if (httpversion < 100) return false;
|
|
if (!HasHeader(kHttpIfModifiedSince)) return false;
|
|
return a->lastmodified >=
|
|
ParseHttpDateTime(inbuf.p + msg.headers[kHttpIfModifiedSince].a,
|
|
msg.headers[kHttpIfModifiedSince].b -
|
|
msg.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 *FreeLater(void *p) {
|
|
if (p) {
|
|
freelist.p = xrealloc(freelist.p, ++freelist.n * sizeof(*freelist.p));
|
|
freelist.p[freelist.n - 1] = p;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
static void CollectGarbage(void) {
|
|
size_t i;
|
|
for (i = 0; i < freelist.n; ++i) free(freelist.p[i]);
|
|
freelist.n = 0;
|
|
free(request.params.p);
|
|
DestroyHttpRequest(&msg);
|
|
}
|
|
|
|
static void IndexAssets(void) {
|
|
int64_t lm;
|
|
struct Asset *p;
|
|
uint32_t i, n, m, cf, step, hash;
|
|
CHECK_GE(HASH_LOAD_FACTOR, 2);
|
|
n = ZIP_CDIR_RECORDS(zdir);
|
|
m = roundup2pow(MAX(1, n) * HASH_LOAD_FACTOR);
|
|
p = xcalloc(m, sizeof(struct Asset));
|
|
CHECK_EQ(kZipCdirHdrMagic, ZIP_CDIR_MAGIC(zdir));
|
|
for (cf = ZIP_CDIR_OFFSET(zdir); n--; cf += ZIP_CFILE_HDRSIZE(zmap + cf)) {
|
|
CHECK_EQ(kZipCfileHdrMagic, ZIP_CFILE_MAGIC(zmap + cf));
|
|
hash = Hash(ZIP_CFILE_NAME(zmap + cf), ZIP_CFILE_NAMESIZE(zmap + cf));
|
|
step = 0;
|
|
do {
|
|
i = (hash + (step * (step + 1)) >> 1) & (m - 1);
|
|
++step;
|
|
} while (p[i].hash);
|
|
lm = GetLastModifiedZip(zmap + cf);
|
|
p[i].hash = hash;
|
|
p[i].lf = ZIP_CFILE_OFFSET(zmap + cf);
|
|
p[i].lastmodified = lm;
|
|
p[i].lastmodifiedstr = FormatUnixHttpDateTime(xmalloc(30), lm);
|
|
}
|
|
assets.p = p;
|
|
assets.n = m;
|
|
}
|
|
|
|
static void OpenZip(const char *path) {
|
|
int fd;
|
|
uint8_t *p;
|
|
struct stat st;
|
|
CHECK_NE(-1, (fd = open(path, O_RDONLY)));
|
|
CHECK_NE(-1, fstat(fd, &st));
|
|
CHECK((zsize = st.st_size));
|
|
CHECK_NE(MAP_FAILED,
|
|
(zmap = mmap(NULL, zsize, PROT_READ, MAP_SHARED, fd, 0)));
|
|
CHECK_NOTNULL((zdir = zipfindcentraldir(zmap, zsize)));
|
|
if (endswith(path, ".com.dbg") && (p = memmem(zmap, zsize, "MZqFpD", 6))) {
|
|
zsize -= p - zmap;
|
|
zmap = p;
|
|
}
|
|
close(fd);
|
|
}
|
|
|
|
static struct Asset *FindAsset(const char *path, size_t pathlen) {
|
|
uint32_t i, step, hash;
|
|
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_LFILE_NAMESIZE(zmap + assets.p[i].lf) &&
|
|
memcmp(path, ZIP_LFILE_NAME(zmap + assets.p[i].lf), pathlen) == 0) {
|
|
return &assets.p[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
static struct Asset *LocateAssetZip(const char *path, size_t pathlen) {
|
|
char *path2;
|
|
struct Asset *a;
|
|
if (pathlen && path[0] == '/') ++path, --pathlen;
|
|
if (!(a = FindAsset(path, pathlen)) &&
|
|
(!pathlen || (pathlen && path[pathlen - 1] == '/'))) {
|
|
path2 = xmalloc(pathlen + 10);
|
|
memcpy(path2, path, pathlen);
|
|
memcpy(path2 + pathlen, "index.lua", 9);
|
|
if (!(a = FindAsset(path2, pathlen + 9))) {
|
|
memcpy(path2 + pathlen, "index.html", 10);
|
|
a = FindAsset(path2, pathlen + 10);
|
|
}
|
|
free(path2);
|
|
}
|
|
return a;
|
|
}
|
|
|
|
static struct Asset *LocateAssetFile(const char *path, size_t pathlen) {
|
|
size_t i;
|
|
char *path2;
|
|
struct Asset *a;
|
|
if (stagedirs.n) {
|
|
a = FreeLater(xcalloc(1, sizeof(struct Asset)));
|
|
a->file = FreeLater(xmalloc(sizeof(struct File)));
|
|
for (i = 0; i < stagedirs.n; ++i) {
|
|
if (stat((a->file->path = FreeLater(xasprintf(
|
|
"%s%.*s", stagedirs.p[i], request.path.n, request.path.p))),
|
|
&a->file->st) != -1 &&
|
|
(S_ISREG(a->file->st.st_mode) ||
|
|
(S_ISDIR(a->file->st.st_mode) &&
|
|
((stat((a->file->path = FreeLater(
|
|
xasprintf("%s%s", a->file->path, "index.lua"))),
|
|
&a->file->st) != -1 &&
|
|
S_ISREG(a->file->st.st_mode)) ||
|
|
(stat((a->file->path = FreeLater(
|
|
xasprintf("%s%s", a->file->path, "index.html"))),
|
|
&a->file->st) != -1 &&
|
|
S_ISREG(a->file->st.st_mode)))))) {
|
|
a->lastmodifiedstr = FormatUnixHttpDateTime(
|
|
FreeLater(xmalloc(30)),
|
|
(a->lastmodified = a->file->st.st_mtim.tv_sec));
|
|
return a;
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static struct Asset *LocateAsset(const char *path, size_t pathlen) {
|
|
struct Asset *a;
|
|
if (!(a = LocateAssetFile(path, pathlen))) {
|
|
a = LocateAssetZip(path, pathlen);
|
|
}
|
|
return a;
|
|
}
|
|
|
|
static void EmitParamKey(struct Parser *u, struct Params *h) {
|
|
h->p = xrealloc(h->p, ++h->n * sizeof(*h->p));
|
|
h->p[h->n - 1].key.p = u->q;
|
|
h->p[h->n - 1].key.n = u->p - u->q;
|
|
u->q = u->p;
|
|
}
|
|
|
|
static void EmitParamVal(struct Parser *u, struct Params *h, bool t) {
|
|
if (!t) {
|
|
if (u->p > u->q) {
|
|
EmitParamKey(u, h);
|
|
h->p[h->n - 1].val.p = NULL;
|
|
h->p[h->n - 1].val.n = SIZE_MAX;
|
|
}
|
|
} else {
|
|
h->p[h->n - 1].val.p = u->q;
|
|
h->p[h->n - 1].val.n = u->p - u->q;
|
|
u->q = u->p;
|
|
}
|
|
}
|
|
|
|
static void ParseLatin1(struct Parser *u) {
|
|
*u->p++ = 0300 | u->c >> 6;
|
|
*u->p++ = 0200 | u->c & 077;
|
|
}
|
|
|
|
static void ParseEscape(struct Parser *u) {
|
|
int a, b;
|
|
a = u->i < u->size ? u->data[u->i++] & 0xff : 0;
|
|
b = u->i < u->size ? u->data[u->i++] & 0xff : 0;
|
|
*u->p++ = kHexToInt[a] << 4 | kHexToInt[b];
|
|
}
|
|
|
|
static void ParsePath(struct Parser *u, struct Buffer *h) {
|
|
while (u->i < u->size) {
|
|
u->c = u->data[u->i++] & 0xff;
|
|
if (u->c == '#' || u->c == '?') {
|
|
break;
|
|
} else if (u->c == '%') {
|
|
ParseEscape(u);
|
|
} else if (u->c >= 0200 && u->islatin1) {
|
|
ParseLatin1(u);
|
|
} else {
|
|
*u->p++ = u->c;
|
|
}
|
|
}
|
|
h->p = u->q;
|
|
h->n = u->p - u->q;
|
|
u->q = u->p;
|
|
}
|
|
|
|
static void ParseParams(struct Parser *u, struct Params *h) {
|
|
bool t = false;
|
|
while (u->i < u->size) {
|
|
u->c = u->data[u->i++] & 0xff;
|
|
if (u->c == '#') {
|
|
break;
|
|
} else if (u->c == '%') {
|
|
ParseEscape(u);
|
|
} else if (u->c == '+') {
|
|
*u->p++ = u->isform ? ' ' : '+';
|
|
} else if (u->c == '&') {
|
|
EmitParamVal(u, h, t);
|
|
t = false;
|
|
} else if (u->c == '=') {
|
|
if (!t) {
|
|
if (u->p > u->q) {
|
|
EmitParamKey(u, h);
|
|
t = true;
|
|
}
|
|
} else {
|
|
*u->p++ = '=';
|
|
}
|
|
} else if (u->c >= 0200 && u->islatin1) {
|
|
ParseLatin1(u);
|
|
} else {
|
|
*u->p++ = u->c;
|
|
}
|
|
}
|
|
EmitParamVal(u, h, t);
|
|
}
|
|
|
|
static void ParseFragment(struct Parser *u, struct Buffer *h) {
|
|
while (u->i < u->size) {
|
|
u->c = u->data[u->i++] & 0xff;
|
|
if (u->c == '%') {
|
|
ParseEscape(u);
|
|
} else if (u->c >= 0200 && u->islatin1) {
|
|
ParseLatin1(u);
|
|
} else {
|
|
*u->p++ = u->c;
|
|
}
|
|
}
|
|
h->p = u->q;
|
|
h->n = u->p - u->q;
|
|
u->q = u->p;
|
|
}
|
|
|
|
static bool ParseRequestUri(void) {
|
|
struct Parser u;
|
|
u.i = 0;
|
|
u.c = 0;
|
|
u.isform = false;
|
|
u.islatin1 = true;
|
|
u.data = inbuf.p + msg.uri.a;
|
|
u.size = msg.uri.b - msg.uri.a;
|
|
memset(&request, 0, sizeof(request));
|
|
u.q = u.p = FreeLater(xmalloc(u.size * 2));
|
|
ParsePath(&u, &request.path);
|
|
if (u.c == '?') ParseParams(&u, &request.params);
|
|
if (u.c == '#') ParseFragment(&u, &request.fragment);
|
|
return u.i == u.size &&
|
|
IsAcceptableHttpRequestPath(request.path.p, request.path.n);
|
|
}
|
|
|
|
static void ParseFormParams(void) {
|
|
struct Parser u;
|
|
u.i = 0;
|
|
u.c = 0;
|
|
u.isform = true;
|
|
u.islatin1 = false;
|
|
u.data = inbuf.p + hdrsize;
|
|
u.size = msgsize - hdrsize;
|
|
u.q = u.p = FreeLater(xmalloc(u.size));
|
|
ParseParams(&u, &request.params);
|
|
}
|
|
|
|
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)zmap <= mstart && mstart <= (intptr_t)zmap + zsize) ||
|
|
((intptr_t)zmap <= mend && mend <= (intptr_t)zmap + zsize)) {
|
|
return (void *)mstart;
|
|
} else {
|
|
abort();
|
|
}
|
|
}
|
|
|
|
static bool IsConnectionClose(void) {
|
|
int n;
|
|
char *p;
|
|
p = inbuf.p + msg.headers[kHttpConnection].a;
|
|
n = msg.headers[kHttpConnection].b - msg.headers[kHttpConnection].a;
|
|
return n == 5 && memcmp(p, "close", 5) == 0;
|
|
}
|
|
|
|
static char *AppendCrlf(char *p) {
|
|
p[0] = '\r';
|
|
p[1] = '\n';
|
|
return p + 2;
|
|
}
|
|
|
|
static bool MustNotIncludeMessageBody(void) { /* RFC2616 § 4.4 */
|
|
return msg.method == kHttpHead || (100 <= statuscode && statuscode <= 199) ||
|
|
statuscode == 204 || statuscode == 304;
|
|
}
|
|
|
|
static char *SetStatus(int code, const char *reason) {
|
|
char *p;
|
|
statuscode = code;
|
|
p = hdrbuf.p;
|
|
p = stpcpy(p, "HTTP/1.");
|
|
*p++ = httpversion == 100 ? '0' : '1';
|
|
*p++ = ' ';
|
|
p += uint64toarray_radix10(code, p);
|
|
*p++ = ' ';
|
|
p = stpcpy(p, reason);
|
|
return AppendCrlf(p);
|
|
}
|
|
|
|
static char *AppendHeader(char *p, const char *k, const char *v) {
|
|
if (!v) return p;
|
|
return AppendCrlf(stpcpy(AppendHeaderName(p, k), v));
|
|
}
|
|
|
|
static char *AppendContentType(char *p, const char *ct) {
|
|
p = AppendHeaderName(p, "Content-Type");
|
|
p = stpcpy(p, ct);
|
|
if (startswith(ct, "text/") && !strchr(ct, ';')) {
|
|
p = stpcpy(p, "; charset=utf-8");
|
|
istext = true;
|
|
}
|
|
return AppendCrlf(p);
|
|
}
|
|
|
|
static char *ServeError(int code, const char *reason) {
|
|
char *p;
|
|
size_t reasonlen;
|
|
reasonlen = strlen(reason);
|
|
p = SetStatus(code, reason);
|
|
p = AppendContentType(p, "text/plain");
|
|
content = FreeLater(xmalloc(reasonlen + 3));
|
|
contentlength = reasonlen + 2;
|
|
stpcpy(stpcpy(content, reason), "\r\n");
|
|
WARNF("%s %s %`'.*s %d %s", clientaddrstr, kHttpMethod[msg.method],
|
|
request.path.n, request.path.p, code, reason);
|
|
return p;
|
|
}
|
|
|
|
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 *AppendCache(char *p, int64_t seconds) {
|
|
struct tm tm;
|
|
if (seconds < 0) return p;
|
|
p = AppendHeaderName(p, "Cache-Control");
|
|
p = stpcpy(p, "max-age=");
|
|
p += uint64toarray_radix10(seconds, p);
|
|
if (seconds) p = stpcpy(p, ", public");
|
|
p = AppendCrlf(p);
|
|
return AppendExpires(p, (int64_t)nowish + seconds);
|
|
}
|
|
|
|
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 bool Inflate(uint8_t *dp, size_t dn, const uint8_t *sp, size_t sn) {
|
|
bool ok;
|
|
z_stream zs;
|
|
ok = false;
|
|
zs.next_in = sp;
|
|
zs.avail_in = sn;
|
|
zs.total_in = sn;
|
|
zs.next_out = dp;
|
|
zs.avail_out = dn;
|
|
zs.total_out = dn;
|
|
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 void *Deflate(const void *data, size_t size, size_t *out_size) {
|
|
void *res;
|
|
z_stream zs;
|
|
CHECK_EQ(Z_OK, deflateInit2(memset(&zs, 0, sizeof(zs)), 4, Z_DEFLATED,
|
|
-MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY));
|
|
zs.next_in = data;
|
|
zs.avail_in = size;
|
|
zs.avail_out = compressBound(size);
|
|
zs.next_out = res = xmalloc(zs.avail_out);
|
|
CHECK_EQ(Z_STREAM_END, deflate(&zs, Z_FINISH));
|
|
CHECK_EQ(Z_OK, deflateEnd(&zs));
|
|
*out_size = zs.total_out;
|
|
return xrealloc(res, zs.total_out);
|
|
}
|
|
|
|
static void *LoadAsset(struct Asset *a, size_t *out_size) {
|
|
size_t size;
|
|
uint8_t *data;
|
|
if (a->file) return FreeLater(xslurp(a->file->path, out_size));
|
|
size = ZIP_LFILE_UNCOMPRESSEDSIZE(zmap + a->lf);
|
|
data = xmalloc(size + 1);
|
|
if (ZIP_LFILE_COMPRESSIONMETHOD(zmap + a->lf) == kZipCompressionDeflate) {
|
|
CHECK(Inflate(data, size, ZIP_LFILE_CONTENT(zmap + a->lf),
|
|
ZIP_LFILE_COMPRESSEDSIZE(zmap + a->lf)));
|
|
} else {
|
|
memcpy(data, ZIP_LFILE_CONTENT(zmap + a->lf), size);
|
|
}
|
|
data[size] = '\0';
|
|
if (out_size) *out_size = size;
|
|
return data;
|
|
}
|
|
|
|
static ssize_t Send(struct iovec *iov, int iovlen) {
|
|
ssize_t rc;
|
|
if ((rc = WritevAll(client, iov, iovlen)) == -1) {
|
|
if (errno == ECONNRESET) {
|
|
DEBUGF("%s send reset", clientaddrstr);
|
|
} else {
|
|
WARNF("%s send error %s", clientaddrstr, strerror(errno));
|
|
}
|
|
connectionclose = true;
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static char *ServeAsset(struct Asset *a, const char *path, size_t pathlen) {
|
|
char *p;
|
|
long rangestart, rangelength;
|
|
if (IsNotModified(a)) {
|
|
DEBUGF("%s %s %`'.*s not modified", clientaddrstr, kHttpMethod[msg.method],
|
|
pathlen, path);
|
|
p = SetStatus(304, "Not Modified");
|
|
} else {
|
|
if (!a->file) {
|
|
content = ZIP_LFILE_CONTENT(zmap + a->lf);
|
|
contentlength = ZIP_LFILE_COMPRESSEDSIZE(zmap + a->lf);
|
|
} else {
|
|
/* TODO(jart): Use sendfile(). */
|
|
content = FreeLater(xslurp(a->file->path, &contentlength));
|
|
}
|
|
if (IsCompressed(a)) {
|
|
if (ClientAcceptsGzip()) {
|
|
gzipped = true;
|
|
memcpy(gzip_footer + 0, zmap + a->lf + kZipLfileOffsetCrc32, 4);
|
|
memcpy(gzip_footer + 4, zmap + a->lf + kZipLfileOffsetUncompressedsize,
|
|
4);
|
|
p = SetStatus(200, "OK");
|
|
p = AppendHeader(p, "Content-Encoding", "gzip");
|
|
} else {
|
|
CHECK(Inflate(
|
|
(content =
|
|
FreeLater(xmalloc(ZIP_LFILE_UNCOMPRESSEDSIZE(zmap + a->lf)))),
|
|
(contentlength = ZIP_LFILE_UNCOMPRESSEDSIZE(zmap + a->lf)),
|
|
ZIP_LFILE_CONTENT(zmap + a->lf),
|
|
ZIP_LFILE_COMPRESSEDSIZE(zmap + a->lf)));
|
|
p = SetStatus(200, "OK");
|
|
}
|
|
} else if (httpversion >= 101 && HasHeader(kHttpRange)) {
|
|
if (ParseHttpRange(inbuf.p + msg.headers[kHttpRange].a,
|
|
msg.headers[kHttpRange].b - msg.headers[kHttpRange].a,
|
|
contentlength, &rangestart, &rangelength)) {
|
|
p = SetStatus(206, "Partial Content");
|
|
p = AppendContentRange(p, rangestart, rangelength, contentlength);
|
|
content = AddRange(content, rangestart, rangelength);
|
|
contentlength = rangelength;
|
|
} else {
|
|
WARNF("%s %s %`'.*s bad range %`'.*s", clientaddrstr,
|
|
kHttpMethod[msg.method], pathlen, path,
|
|
msg.headers[kHttpRange].b - msg.headers[kHttpRange].a,
|
|
inbuf.p + msg.headers[kHttpRange].a);
|
|
p = SetStatus(416, "Range Not Satisfiable");
|
|
p = AppendContentRange(p, rangestart, rangelength, contentlength);
|
|
content = "";
|
|
contentlength = 0;
|
|
}
|
|
} else {
|
|
p = SetStatus(200, "OK");
|
|
}
|
|
}
|
|
if (httpversion >= 100) {
|
|
p = AppendHeader(p, "Last-Modified", a->lastmodifiedstr);
|
|
p = AppendContentType(p, GetContentType(path, pathlen));
|
|
if (httpversion >= 101) {
|
|
p = AppendCache(p, cacheseconds);
|
|
if (!IsCompressed(a)) {
|
|
p = AppendHeader(p, "Accept-Ranges", "bytes");
|
|
} else {
|
|
p = AppendHeader(p, "Vary", "Accept-Encoding");
|
|
}
|
|
}
|
|
}
|
|
return p;
|
|
}
|
|
|
|
static int LuaServeAsset(lua_State *L) {
|
|
size_t pathlen;
|
|
struct Asset *a;
|
|
const char *path;
|
|
path = luaL_checklstring(L, 1, &pathlen);
|
|
if (!(a = LocateAsset(path, pathlen))) {
|
|
return luaL_argerror(L, 1, "not found");
|
|
}
|
|
luaheaderp = ServeAsset(a, path, pathlen);
|
|
return 0;
|
|
}
|
|
|
|
static int LuaRespond(lua_State *L, char *respond(int, const char *)) {
|
|
char *p;
|
|
int code;
|
|
size_t reasonlen;
|
|
const char *reason;
|
|
code = luaL_checkinteger(L, 1);
|
|
if (!(100 <= code && code <= 999)) {
|
|
return luaL_argerror(L, 1, "bad status code");
|
|
}
|
|
if (lua_isnoneornil(L, 2)) {
|
|
luaheaderp = respond(code, GetHttpReason(code));
|
|
} else {
|
|
reason = lua_tolstring(L, 2, &reasonlen);
|
|
if (reasonlen < 128 && (p = EncodeHttpHeaderValue(reason, reasonlen, 0))) {
|
|
luaheaderp = respond(code, p);
|
|
free(p);
|
|
} else {
|
|
return luaL_argerror(L, 2, "invalid");
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int LuaSetStatus(lua_State *L) {
|
|
return LuaRespond(L, SetStatus);
|
|
}
|
|
|
|
static int LuaServeError(lua_State *L) {
|
|
return LuaRespond(L, ServeError);
|
|
}
|
|
|
|
static int LuaLoadAsset(lua_State *L) {
|
|
char *data;
|
|
struct Asset *a;
|
|
const char *path;
|
|
size_t size, pathlen;
|
|
path = luaL_checklstring(L, 1, &pathlen);
|
|
if ((a = LocateAsset(path, pathlen))) {
|
|
data = LoadAsset(a, &size);
|
|
lua_pushlstring(L, data, size);
|
|
free(data);
|
|
} else {
|
|
lua_pushnil(L);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int LuaGetDate(lua_State *L) {
|
|
lua_pushinteger(L, nowish);
|
|
return 1;
|
|
}
|
|
|
|
static int LuaGetVersion(lua_State *L) {
|
|
lua_pushinteger(L, httpversion);
|
|
return 1;
|
|
}
|
|
|
|
static int LuaGetMethod(lua_State *L) {
|
|
lua_pushstring(L, kHttpMethod[msg.method]);
|
|
return 1;
|
|
}
|
|
|
|
static int LuaGetPath(lua_State *L) {
|
|
lua_pushlstring(L, request.path.p, request.path.n);
|
|
return 1;
|
|
}
|
|
|
|
static int LuaGetFragment(lua_State *L) {
|
|
lua_pushlstring(L, request.fragment.p, request.fragment.n);
|
|
return 1;
|
|
}
|
|
|
|
static void LuaPushLatin1(lua_State *L, const char *s, size_t n) {
|
|
char *t;
|
|
size_t m;
|
|
t = DecodeLatin1(s, n, &m);
|
|
lua_pushlstring(L, t, m);
|
|
free(t);
|
|
}
|
|
|
|
static int LuaGetUri(lua_State *L) {
|
|
LuaPushLatin1(L, inbuf.p + msg.uri.a, msg.uri.b - msg.uri.a);
|
|
return 1;
|
|
}
|
|
|
|
static int LuaFormatHttpDateTime(lua_State *L) {
|
|
char buf[30];
|
|
lua_pushstring(L, FormatUnixHttpDateTime(buf, luaL_checkinteger(L, 1)));
|
|
return 1;
|
|
}
|
|
|
|
static int LuaParseHttpDateTime(lua_State *L) {
|
|
size_t n;
|
|
const char *s;
|
|
s = luaL_checklstring(L, 1, &n);
|
|
lua_pushinteger(L, ParseHttpDateTime(s, n));
|
|
return 1;
|
|
}
|
|
|
|
static int LuaGetClientAddr(lua_State *L) {
|
|
lua_pushstring(L, clientaddrstr);
|
|
return 1;
|
|
}
|
|
|
|
static int LuaGetServerAddr(lua_State *L) {
|
|
lua_pushstring(L, serveraddrstr);
|
|
return 1;
|
|
}
|
|
|
|
static int LuaGetPayload(lua_State *L) {
|
|
lua_pushlstring(L, inbuf.p + hdrsize, msgsize - hdrsize);
|
|
return 1;
|
|
}
|
|
|
|
static int LuaGetHeader(lua_State *L) {
|
|
int h;
|
|
const char *key;
|
|
size_t i, keylen;
|
|
key = luaL_checklstring(L, 1, &keylen);
|
|
if ((h = GetHttpHeader(key, keylen)) != -1) {
|
|
LuaPushLatin1(L, inbuf.p + msg.headers[h].a,
|
|
msg.headers[h].b - msg.headers[h].a);
|
|
return 1;
|
|
}
|
|
for (i = 0; i < msg.xheaders.n; ++i) {
|
|
if (!CompareSlicesCase(key, keylen, inbuf.p + msg.xheaders.p[i].k.a,
|
|
msg.xheaders.p[i].k.b - msg.xheaders.p[i].k.a)) {
|
|
LuaPushLatin1(L, inbuf.p + msg.xheaders.p[i].v.a,
|
|
msg.xheaders.p[i].v.b - msg.xheaders.p[i].v.a);
|
|
return 1;
|
|
}
|
|
}
|
|
lua_pushstring(L, "");
|
|
return 1;
|
|
}
|
|
|
|
static int LuaGetHeaders(lua_State *L) {
|
|
size_t i;
|
|
char *name;
|
|
lua_newtable(L);
|
|
for (i = 0; i < kHttpHeadersMax; ++i) {
|
|
if (msg.headers[i].b - msg.headers[i].a) {
|
|
LuaPushLatin1(L, inbuf.p + msg.headers[i].a,
|
|
msg.headers[i].b - msg.headers[i].a);
|
|
lua_setfield(L, -2, GetHttpHeaderName(i));
|
|
}
|
|
}
|
|
for (i = 0; i < msg.xheaders.n; ++i) {
|
|
LuaPushLatin1(L, inbuf.p + msg.xheaders.p[i].v.a,
|
|
msg.xheaders.p[i].v.b - msg.xheaders.p[i].v.a);
|
|
lua_setfield(L, -2,
|
|
(name = DecodeLatin1(
|
|
inbuf.p + msg.xheaders.p[i].k.a,
|
|
msg.xheaders.p[i].k.b - msg.xheaders.p[i].k.a, 0)));
|
|
free(name);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int LuaSetHeader(lua_State *L) {
|
|
int h;
|
|
char *p;
|
|
ssize_t rc;
|
|
const char *key, *val, *eval;
|
|
size_t i, keylen, vallen, evallen;
|
|
key = luaL_checklstring(L, 1, &keylen);
|
|
val = luaL_checklstring(L, 2, &vallen);
|
|
if ((h = GetHttpHeader(key, keylen)) == -1) {
|
|
if (!IsValidHttpToken(key, keylen)) {
|
|
return luaL_argerror(L, 1, "invalid");
|
|
}
|
|
}
|
|
if (!(eval = EncodeHttpHeaderValue(val, vallen, &evallen))) {
|
|
return luaL_argerror(L, 2, "invalid");
|
|
}
|
|
if (!luaheaderp) {
|
|
p = SetStatus(200, "OK");
|
|
} else {
|
|
while (luaheaderp - hdrbuf.p + keylen + 2 + evallen + 2 + 512 > hdrbuf.n) {
|
|
hdrbuf.n += hdrbuf.n >> 1;
|
|
p = xrealloc(hdrbuf.p, hdrbuf.n);
|
|
luaheaderp = p + (luaheaderp - hdrbuf.p);
|
|
hdrbuf.p = p;
|
|
}
|
|
p = luaheaderp;
|
|
}
|
|
switch (h) {
|
|
case kHttpDate:
|
|
case kHttpContentRange:
|
|
case kHttpContentLength:
|
|
case kHttpContentEncoding:
|
|
return luaL_argerror(L, 1, "abstracted");
|
|
case kHttpConnection:
|
|
if (evallen != 5 || memcmp(eval, "close", 5)) {
|
|
return luaL_argerror(L, 2, "unsupported");
|
|
}
|
|
connectionclose = true;
|
|
break;
|
|
case kHttpContentType:
|
|
p = AppendContentType(p, eval);
|
|
break;
|
|
case kHttpServer:
|
|
branded = true;
|
|
p = AppendHeader(p, "Server", eval);
|
|
break;
|
|
default:
|
|
p = AppendHeader(p, key, eval);
|
|
break;
|
|
}
|
|
luaheaderp = p;
|
|
free(eval);
|
|
return 0;
|
|
}
|
|
|
|
static int LuaHasParam(lua_State *L) {
|
|
const char *key;
|
|
size_t i, keylen;
|
|
key = luaL_checklstring(L, 1, &keylen);
|
|
for (i = 0; i < request.params.n; ++i) {
|
|
if (request.params.p[i].key.n == keylen &&
|
|
!memcmp(request.params.p[i].key.p, key, keylen)) {
|
|
lua_pushboolean(L, true);
|
|
return 1;
|
|
}
|
|
}
|
|
lua_pushboolean(L, false);
|
|
return 1;
|
|
}
|
|
|
|
static int LuaGetParam(lua_State *L) {
|
|
const char *key;
|
|
size_t i, keylen;
|
|
key = luaL_checklstring(L, 1, &keylen);
|
|
for (i = 0; i < request.params.n; ++i) {
|
|
if (request.params.p[i].key.n == keylen &&
|
|
!memcmp(request.params.p[i].key.p, key, keylen)) {
|
|
if (request.params.p[i].val.n == SIZE_MAX) break;
|
|
lua_pushlstring(L, request.params.p[i].val.p, request.params.p[i].val.n);
|
|
return 1;
|
|
}
|
|
}
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
|
|
static int LuaGetParams(lua_State *L) {
|
|
size_t i;
|
|
lua_newtable(L);
|
|
for (i = 0; i < request.params.n; ++i) {
|
|
lua_newtable(L);
|
|
lua_pushlstring(L, request.params.p[i].key.p, request.params.p[i].key.n);
|
|
lua_seti(L, -2, 1);
|
|
if (request.params.p[i].val.n != SIZE_MAX) {
|
|
lua_pushlstring(L, request.params.p[i].val.p, request.params.p[i].val.n);
|
|
lua_seti(L, -2, 2);
|
|
}
|
|
lua_seti(L, -2, i + 1);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int LuaWrite(lua_State *L) {
|
|
int h;
|
|
size_t size;
|
|
const char *data;
|
|
data = luaL_checklstring(L, 1, &size);
|
|
outbuf.p = xrealloc(outbuf.p, outbuf.n + size);
|
|
memcpy(outbuf.p + outbuf.n, data, size);
|
|
outbuf.n += size;
|
|
return 0;
|
|
}
|
|
|
|
static int LuaEscaper(lua_State *L,
|
|
struct EscapeResult escape(const char *, size_t)) {
|
|
size_t size;
|
|
const char *data;
|
|
struct EscapeResult r;
|
|
data = luaL_checklstring(L, 1, &size);
|
|
r = escape(data, size);
|
|
lua_pushlstring(L, r.data, r.size);
|
|
free(r.data);
|
|
return 1;
|
|
}
|
|
|
|
static int LuaEscapeHtml(lua_State *L) {
|
|
return LuaEscaper(L, EscapeHtml);
|
|
}
|
|
|
|
static int LuaEscapeParam(lua_State *L) {
|
|
return LuaEscaper(L, EscapeUrlParam);
|
|
}
|
|
|
|
static int LuaEscapePath(lua_State *L) {
|
|
return LuaEscaper(L, EscapeUrlPath);
|
|
}
|
|
|
|
static int LuaEscapeSegment(lua_State *L) {
|
|
return LuaEscaper(L, EscapeUrlPathSegment);
|
|
}
|
|
|
|
static int LuaEscapeFragment(lua_State *L) {
|
|
return LuaEscaper(L, EscapeUrlFragment);
|
|
}
|
|
|
|
static int LuaEscapeLiteral(lua_State *L) {
|
|
return LuaEscaper(L, EscapeJsStringLiteral);
|
|
}
|
|
|
|
static int LuaEncodeBase64(lua_State *L) {
|
|
char *p;
|
|
size_t size, n;
|
|
const char *data;
|
|
data = luaL_checklstring(L, 1, &size);
|
|
p = EncodeBase64(data, size, &n);
|
|
lua_pushlstring(L, p, n);
|
|
free(p);
|
|
return 1;
|
|
}
|
|
|
|
static int LuaDecodeBase64(lua_State *L) {
|
|
char *p;
|
|
size_t size, n;
|
|
const char *data;
|
|
data = luaL_checklstring(L, 1, &size);
|
|
p = DecodeBase64(data, size, &n);
|
|
lua_pushlstring(L, p, n);
|
|
free(p);
|
|
return 1;
|
|
}
|
|
|
|
static int LuaPopcnt(lua_State *L) {
|
|
lua_pushinteger(L, popcnt(luaL_checkinteger(L, 1)));
|
|
return 1;
|
|
}
|
|
|
|
static int LuaBsr(lua_State *L) {
|
|
long x;
|
|
if ((x = luaL_checkinteger(L, 1))) {
|
|
lua_pushinteger(L, bsr(x));
|
|
return 1;
|
|
} else {
|
|
return luaL_argerror(L, 1, "zero");
|
|
}
|
|
}
|
|
|
|
static int LuaBsf(lua_State *L) {
|
|
long x;
|
|
if ((x = luaL_checkinteger(L, 1))) {
|
|
lua_pushinteger(L, bsf(x));
|
|
return 1;
|
|
} else {
|
|
return luaL_argerror(L, 1, "zero");
|
|
}
|
|
}
|
|
|
|
static int LuaCrc32(lua_State *L) {
|
|
long i;
|
|
size_t n;
|
|
const char *p;
|
|
i = luaL_checkinteger(L, 1);
|
|
p = luaL_checklstring(L, 2, &n);
|
|
lua_pushinteger(L, crc32_z(i, p, n));
|
|
return 1;
|
|
}
|
|
|
|
static int LuaCrc32c(lua_State *L) {
|
|
long i;
|
|
size_t n;
|
|
const char *p;
|
|
i = luaL_checkinteger(L, 1);
|
|
p = luaL_checklstring(L, 2, &n);
|
|
lua_pushinteger(L, crc32c(i, p, n));
|
|
return 1;
|
|
}
|
|
|
|
static void LuaRun(const char *path) {
|
|
struct Asset *a;
|
|
const char *code;
|
|
if ((a = LocateAsset(path, strlen(path)))) {
|
|
code = LoadAsset(a, NULL);
|
|
if (luaL_dostring(L, code) != LUA_OK) {
|
|
WARNF("%s %s", path, lua_tostring(L, -1));
|
|
}
|
|
free(code);
|
|
} else {
|
|
DEBUGF("%s not found", path);
|
|
}
|
|
}
|
|
|
|
static const luaL_Reg kLuaFuncs[] = {
|
|
{"DecodeBase64", LuaDecodeBase64}, //
|
|
{"EncodeBase64", LuaEncodeBase64}, //
|
|
{"EscapeFragment", LuaEscapeFragment}, //
|
|
{"EscapeHtml", LuaEscapeHtml}, //
|
|
{"EscapeLiteral", LuaEscapeLiteral}, //
|
|
{"EscapeParam", LuaEscapeParam}, //
|
|
{"EscapePath", LuaEscapePath}, //
|
|
{"EscapeSegment", LuaEscapeSegment}, //
|
|
{"FormatHttpDateTime", LuaFormatHttpDateTime}, //
|
|
{"GetClientAddr", LuaGetClientAddr}, //
|
|
{"GetDate", LuaGetDate}, //
|
|
{"GetFragment", LuaGetFragment}, //
|
|
{"GetHeader", LuaGetHeader}, //
|
|
{"GetHeaders", LuaGetHeaders}, //
|
|
{"GetMethod", LuaGetMethod}, //
|
|
{"GetParam", LuaGetParam}, //
|
|
{"GetParams", LuaGetParams}, //
|
|
{"GetPath", LuaGetPath}, //
|
|
{"GetPayload", LuaGetPayload}, //
|
|
{"GetServerAddr", LuaGetServerAddr}, //
|
|
{"GetUri", LuaGetUri}, //
|
|
{"GetVersion", LuaGetVersion}, //
|
|
{"HasParam", LuaHasParam}, //
|
|
{"LoadAsset", LuaLoadAsset}, //
|
|
{"ParseHttpDateTime", LuaParseHttpDateTime}, //
|
|
{"ServeAsset", LuaServeAsset}, //
|
|
{"ServeError", LuaServeError}, //
|
|
{"SetHeader", LuaSetHeader}, //
|
|
{"SetStatus", LuaSetStatus}, //
|
|
{"Write", LuaWrite}, //
|
|
{"bsf", LuaBsf}, //
|
|
{"bsr", LuaBsr}, //
|
|
{"crc32", LuaCrc32}, //
|
|
{"crc32c", LuaCrc32c}, //
|
|
{"popcnt", LuaPopcnt}, //
|
|
};
|
|
|
|
static void LuaSetArgv(void) {
|
|
size_t i;
|
|
lua_newtable(L);
|
|
for (i = optind; i < __argc; ++i) {
|
|
lua_pushstring(L, __argv[i]);
|
|
lua_seti(L, -2, i - optind + 1);
|
|
}
|
|
lua_setglobal(L, "argv");
|
|
}
|
|
|
|
static void LuaInit(void) {
|
|
size_t i;
|
|
L = luaL_newstate();
|
|
luaL_openlibs(L);
|
|
for (i = 0; i < ARRAYLEN(kLuaFuncs); ++i) {
|
|
lua_pushcfunction(L, kLuaFuncs[i].func);
|
|
lua_setglobal(L, kLuaFuncs[i].name);
|
|
}
|
|
LuaSetArgv();
|
|
LuaRun("/tool/net/.init.lua");
|
|
}
|
|
|
|
static void LuaReload(void) {
|
|
LuaRun("/tool/net/.reload.lua");
|
|
}
|
|
|
|
static char *ServeLua(struct Asset *a) {
|
|
char *p;
|
|
outbuf.n = 0;
|
|
luaheaderp = NULL;
|
|
if (luaL_dostring(L, FreeLater(LoadAsset(a, NULL))) == LUA_OK) {
|
|
if (!(p = luaheaderp)) {
|
|
p = SetStatus(200, "OK");
|
|
p = AppendContentType(p, "text/html");
|
|
}
|
|
if (outbuf.n) {
|
|
if (istext && outbuf.n >= 100 && ClientAcceptsGzip()) {
|
|
gzipped = true;
|
|
p = AppendHeader(p, "Content-Encoding", "gzip");
|
|
p = AppendHeader(p, "Vary", "Accept-Encoding");
|
|
WRITE32LE(gzip_footer + 0, crc32_z(0, outbuf.p, outbuf.n));
|
|
WRITE32LE(gzip_footer + 4, outbuf.n);
|
|
content = FreeLater(Deflate(outbuf.p, outbuf.n, &contentlength));
|
|
} else {
|
|
content = outbuf.p;
|
|
contentlength = outbuf.n;
|
|
}
|
|
}
|
|
return p;
|
|
} else {
|
|
WARNF("%s %s", clientaddrstr, lua_tostring(L, -1));
|
|
lua_pop(L, 1); /* remove message */
|
|
connectionclose = true;
|
|
return ServeError(500, "Internal Server Error");
|
|
}
|
|
}
|
|
|
|
static bool IsLua(struct Asset *a) {
|
|
if (a->file) return endswith(a->file->path, ".lua");
|
|
return ZIP_LFILE_NAMESIZE(zmap + a->lf) >= 4 &&
|
|
!memcmp(ZIP_LFILE_NAME(zmap + a->lf) +
|
|
ZIP_LFILE_NAMESIZE(zmap + a->lf) - 4,
|
|
".lua", 4);
|
|
}
|
|
|
|
static char *HandleAsset(struct Asset *a, const char *path, size_t pathlen) {
|
|
char *p;
|
|
if (IsLua(a)) {
|
|
p = ServeLua(a);
|
|
} else if (msg.method == kHttpGet || msg.method == kHttpHead) {
|
|
p = ServeAsset(a, path, pathlen);
|
|
p = AppendHeader(p, "X-Content-Type-Options", "nosniff");
|
|
} else {
|
|
p = ServeError(405, "Method Not Allowed");
|
|
}
|
|
return p;
|
|
}
|
|
|
|
static char *HandleRedirect(struct Redirect *r) {
|
|
char *p;
|
|
struct Asset *a;
|
|
if ((a = LocateAsset(r->location, strlen(r->location)))) {
|
|
DEBUGF("%s %s %`'.*s rewritten %`'s", clientaddrstr,
|
|
kHttpMethod[msg.method], request.path.n, request.path.p,
|
|
r->location);
|
|
p = HandleAsset(a, r->location, strlen(r->location));
|
|
} else if (httpversion == 9) {
|
|
p = ServeError(505, "HTTP Version Not Supported");
|
|
} else {
|
|
DEBUGF("%s %s %`'.*s redirecting %`'s", clientaddrstr,
|
|
kHttpMethod[msg.method], request.path.n, request.path.p,
|
|
r->location);
|
|
p = SetStatus(307, "Temporary Redirect");
|
|
p = AppendHeader(p, "Location",
|
|
FreeLater(EncodeHttpHeaderValue(r->location, -1, 0)));
|
|
}
|
|
return p;
|
|
}
|
|
|
|
static void LogMessage(const char *d, const char *s, size_t n) {
|
|
size_t n2, n3, n4;
|
|
char *s2, *s3, *s4;
|
|
if (!logmessages) return;
|
|
while (n && (s[n - 1] == '\r' || s[n - 1] == '\n')) --n;
|
|
if ((s2 = DecodeLatin1(s, n, &n2))) {
|
|
if ((s3 = VisualizeControlCodes(s2, n2, &n3))) {
|
|
if ((s4 = IndentLines(s3, n3, &n4, 1))) {
|
|
LOGF("%s %s %,ld byte message\n%.*s", clientaddrstr, d, n, n4, s4);
|
|
free(s4);
|
|
}
|
|
free(s3);
|
|
}
|
|
free(s2);
|
|
}
|
|
}
|
|
|
|
static void LogBody(const char *d, const char *s, size_t n) {
|
|
char *s2, *s3;
|
|
size_t n2, n3;
|
|
if (!n || !logbodies) return;
|
|
while (n && (s[n - 1] == '\r' || s[n - 1] == '\n')) --n;
|
|
if ((s2 = VisualizeControlCodes(s, n, &n2))) {
|
|
if ((s3 = IndentLines(s2, n2, &n3, 1))) {
|
|
LOGF("%s %s %,ld byte payload\n%.*s", clientaddrstr, d, n, n3, s3);
|
|
free(s3);
|
|
}
|
|
free(s2);
|
|
}
|
|
}
|
|
|
|
static ssize_t SendMessageString(const char *s) {
|
|
size_t n;
|
|
ssize_t rc;
|
|
n = strlen(s);
|
|
LogMessage("sending", s, n);
|
|
return 0;
|
|
for (;;) {
|
|
if ((rc = write(client, s, n)) != -1 || errno != EINTR) {
|
|
return rc;
|
|
}
|
|
}
|
|
}
|
|
|
|
static ssize_t SendContinue(void) {
|
|
return SendMessageString("\
|
|
HTTP/1.1 100 Continue\r\n\
|
|
\r\n");
|
|
}
|
|
|
|
static ssize_t SendTimeout(void) {
|
|
return SendMessageString("\
|
|
HTTP/1.1 408 Request Timeout\r\n\
|
|
Connection: close\r\n\
|
|
Content-Length: 0\r\n\
|
|
\r\n");
|
|
}
|
|
|
|
static ssize_t SendServiceUnavailable(void) {
|
|
return SendMessageString("\
|
|
HTTP/1.1 503 Service Unavailable\r\n\
|
|
Connection: close\r\n\
|
|
Content-Length: 0\r\n\
|
|
\r\n");
|
|
}
|
|
|
|
static void LogClose(const char *reason) {
|
|
if (amtread) {
|
|
WARNF("%s %s with %,ld bytes unprocessed", clientaddrstr, reason);
|
|
} else {
|
|
DEBUGF("%s %s", clientaddrstr, reason);
|
|
}
|
|
}
|
|
|
|
static const char *DescribeClose(void) {
|
|
if (killed) return "killed";
|
|
if (meltdown) return "meltdown";
|
|
if (terminated) return "terminated";
|
|
if (connectionclose) return "connectionclose";
|
|
return "destroyed";
|
|
}
|
|
|
|
static char *HandleMessage(void) {
|
|
long r;
|
|
ssize_t cl, rc;
|
|
struct Asset *a;
|
|
size_t got, need;
|
|
httpversion =
|
|
ParseHttpVersion(inbuf.p + msg.version.a, msg.version.b - msg.version.a);
|
|
if (httpversion > 101) {
|
|
return ServeError(505, "HTTP Version Not Supported");
|
|
}
|
|
if (msg.method > kHttpPost ||
|
|
(HasHeader(kHttpTransferEncoding) &&
|
|
!HeaderEquals(kHttpTransferEncoding, "identity"))) {
|
|
return ServeError(501, "Not Implemented");
|
|
}
|
|
if (HasHeader(kHttpExpect) && !HeaderEquals(kHttpExpect, "100-continue")) {
|
|
return ServeError(417, "Expectation Failed");
|
|
}
|
|
if ((cl = ParseContentLength(inbuf.p + msg.headers[kHttpContentLength].a,
|
|
msg.headers[kHttpContentLength].b -
|
|
msg.headers[kHttpContentLength].a)) == -1) {
|
|
if (HasHeader(kHttpContentLength)) {
|
|
return ServeError(400, "Bad Request");
|
|
} else if (msg.method != kHttpGet && msg.method != kHttpHead &&
|
|
msg.method != kHttpOptions) {
|
|
return ServeError(411, "Length Required");
|
|
} else {
|
|
cl = 0;
|
|
}
|
|
}
|
|
need = hdrsize + cl; /* synchronization is possible */
|
|
if (need > inbuf.n) {
|
|
return ServeError(413, "Payload Too Large");
|
|
}
|
|
if (HeaderEquals(kHttpExpect, "100-continue") && httpversion >= 101) {
|
|
SendContinue();
|
|
}
|
|
while (amtread < need) {
|
|
if (++frags == 64) {
|
|
LogClose("payload fragged");
|
|
return ServeError(408, "Request Timeout");
|
|
}
|
|
if ((rc = read(client, inbuf.p + amtread, inbuf.n - amtread)) != -1) {
|
|
if (!(got = rc)) {
|
|
LogClose("payload disconnect");
|
|
return ServeError(400, "Bad Request");
|
|
}
|
|
amtread += got;
|
|
} else if (errno == ECONNRESET) {
|
|
LogClose("payload reset");
|
|
return ServeError(400, "Bad Request");
|
|
} else if (errno == EINTR) {
|
|
if (killed || ((meltdown || terminated) && nowl() - startread > 1)) {
|
|
LogClose(DescribeClose());
|
|
return ServeError(503, "Service Unavailable");
|
|
}
|
|
} else {
|
|
WARNF("%s payload recv %s", clientaddrstr, strerror(errno));
|
|
return ServeError(500, "Internal Server Error");
|
|
}
|
|
}
|
|
msgsize = need; /* we are now synchronized */
|
|
LogBody("received", inbuf.p + hdrsize, msgsize - hdrsize);
|
|
if (httpversion != 101 || IsConnectionClose()) {
|
|
connectionclose = true;
|
|
}
|
|
if (!ParseRequestUri()) {
|
|
WARNF("%s could not parse request request %`'.*s", clientaddrstr,
|
|
msg.uri.b - msg.uri.a, inbuf.p + msg.uri.a);
|
|
connectionclose = true;
|
|
return ServeError(400, "Bad Request");
|
|
}
|
|
if (HeaderEquals(kHttpContentType, "application/x-www-form-urlencoded")) {
|
|
ParseFormParams();
|
|
}
|
|
VERBOSEF("%s %s %`'.*s referrer %`'.*s", clientaddrstr,
|
|
kHttpMethod[msg.method], request.path.n, request.path.p,
|
|
msg.headers[kHttpReferer].b - msg.headers[kHttpReferer].a,
|
|
inbuf.p + msg.headers[kHttpReferer].a);
|
|
if ((a = LocateAsset(request.path.p, request.path.n))) {
|
|
return HandleAsset(a, request.path.p, request.path.n);
|
|
} else if ((r = FindRedirect(request.path.p, request.path.n)) != -1) {
|
|
return HandleRedirect(redirects.p + r);
|
|
} else {
|
|
return ServeError(404, "Not Found");
|
|
}
|
|
}
|
|
|
|
static bool HandleRequest(void) {
|
|
int rc;
|
|
char *p;
|
|
int iovlen;
|
|
struct iovec iov[4];
|
|
long actualcontentlength;
|
|
if ((rc = ParseHttpRequest(&msg, inbuf.p, amtread)) != -1) {
|
|
if (!rc) return false;
|
|
hdrsize = rc;
|
|
LogMessage("received", inbuf.p, hdrsize);
|
|
p = HandleMessage();
|
|
} else {
|
|
httpversion = 101;
|
|
p = ServeError(400, "Bad Request");
|
|
}
|
|
if (!msgsize) {
|
|
amtread = 0;
|
|
connectionclose = true;
|
|
DEBUGF("%s could not synchronize message stream", clientaddrstr);
|
|
} else if (msgsize < amtread) {
|
|
DEBUGF("%s has %,ld pipelined bytes", clientaddrstr, amtread - msgsize);
|
|
memmove(inbuf.p, inbuf.p + msgsize, amtread - msgsize);
|
|
amtread -= msgsize;
|
|
} else {
|
|
amtread = 0;
|
|
}
|
|
if (httpversion >= 100) {
|
|
p = AppendHeader(p, "Date", currentdate);
|
|
if (!branded) {
|
|
p = AppendHeader(p, "Server", serverheader);
|
|
}
|
|
if (connectionclose) {
|
|
p = AppendHeader(p, "Connection", "close");
|
|
}
|
|
actualcontentlength = contentlength;
|
|
if (gzipped) {
|
|
actualcontentlength += sizeof(kGzipHeader) + sizeof(gzip_footer);
|
|
}
|
|
p = AppendContentLength(p, actualcontentlength);
|
|
p = AppendCrlf(p);
|
|
CHECK_LE(p - hdrbuf.p, hdrbuf.n);
|
|
LogMessage("sending", hdrbuf.p, p - hdrbuf.p);
|
|
iov[0].iov_base = hdrbuf.p;
|
|
iov[0].iov_len = p - hdrbuf.p;
|
|
iovlen = 1;
|
|
if (!MustNotIncludeMessageBody()) {
|
|
if (gzipped) {
|
|
iov[iovlen].iov_base = kGzipHeader;
|
|
iov[iovlen].iov_len = sizeof(kGzipHeader);
|
|
++iovlen;
|
|
}
|
|
iov[iovlen].iov_base = content;
|
|
iov[iovlen].iov_len = contentlength;
|
|
++iovlen;
|
|
if (gzipped) {
|
|
iov[iovlen].iov_base = gzip_footer;
|
|
iov[iovlen].iov_len = sizeof(gzip_footer);
|
|
++iovlen;
|
|
}
|
|
}
|
|
} else {
|
|
iov[0].iov_base = content;
|
|
iov[0].iov_len = contentlength;
|
|
iovlen = 1;
|
|
}
|
|
Send(iov, iovlen);
|
|
CollectGarbage();
|
|
return true;
|
|
}
|
|
|
|
static void InitRequest(void) {
|
|
frags = 0;
|
|
msgsize = 0;
|
|
content = NULL;
|
|
gzipped = false;
|
|
branded = false;
|
|
contentlength = 0;
|
|
InitHttpRequest(&msg);
|
|
}
|
|
|
|
static void ProcessRequests(void) {
|
|
ssize_t rc;
|
|
size_t got;
|
|
long double now;
|
|
for (;;) {
|
|
InitRequest();
|
|
startread = nowl();
|
|
for (;;) {
|
|
if (!msg.i && amtread && HandleRequest()) break;
|
|
if ((rc = read(client, inbuf.p + amtread, inbuf.n - amtread)) != -1) {
|
|
startrequest = now = nowl();
|
|
if (now - nowish > 1) UpdateCurrentDate(now);
|
|
got = rc;
|
|
amtread += got;
|
|
if (amtread) {
|
|
if (HandleRequest()) {
|
|
break;
|
|
} else if (got) {
|
|
if (++frags == 32) {
|
|
SendTimeout();
|
|
LogClose("fragged");
|
|
return;
|
|
} else {
|
|
DEBUGF("%s fragmented msg %,ld %,ld", clientaddrstr, amtread,
|
|
got);
|
|
}
|
|
}
|
|
}
|
|
if (!got) {
|
|
LogClose("disconnect");
|
|
return;
|
|
}
|
|
} else if (errno == ECONNRESET) {
|
|
LogClose("reset");
|
|
return;
|
|
} else if (errno != EINTR) {
|
|
WARNF("%s recv msg %s", clientaddrstr, strerror(errno));
|
|
return;
|
|
}
|
|
if (killed || (terminated && !amtread) ||
|
|
(meltdown && (!amtread || nowl() - startread > 1))) {
|
|
if (amtread) SendServiceUnavailable();
|
|
LogClose(DescribeClose());
|
|
return;
|
|
}
|
|
}
|
|
if (connectionclose || killed || ((terminated || meltdown) && !amtread)) {
|
|
LogClose(DescribeClose());
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ProcessConnection(void) {
|
|
int pid;
|
|
clientaddrsize = sizeof(clientaddr);
|
|
if ((client = accept4(server, &clientaddr, &clientaddrsize, SOCK_CLOEXEC)) !=
|
|
-1) {
|
|
startconnection = nowl();
|
|
if (uniprocess) {
|
|
pid = -1;
|
|
connectionclose = true;
|
|
} else {
|
|
switch ((pid = fork())) {
|
|
case 0:
|
|
meltdown = false;
|
|
connectionclose = false;
|
|
break;
|
|
case -1:
|
|
WARNF("redbean is entering meltdown mode");
|
|
LOGIFNEG1(kill(0, SIGUSR2));
|
|
SendServiceUnavailable();
|
|
/* fallthrough */
|
|
default:
|
|
close(client);
|
|
return;
|
|
}
|
|
}
|
|
DescribeAddress(clientaddrstr, &clientaddr);
|
|
DEBUGF("%s accept", clientaddrstr);
|
|
ProcessRequests();
|
|
LOGIFNEG1(close(client));
|
|
if (!pid) _exit(0);
|
|
} else if (errno != EINTR) {
|
|
FATALF("%s accept error %s", serveraddrstr, strerror(errno));
|
|
}
|
|
}
|
|
|
|
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 RedBean(void) {
|
|
uint32_t addrsize;
|
|
if (IsWindows()) uniprocess = true;
|
|
gmtoff = GetGmtOffset();
|
|
OpenZip((const char *)getauxval(AT_EXECFN));
|
|
IndexAssets();
|
|
xsigaction(SIGINT, OnInt, 0, 0, 0);
|
|
xsigaction(SIGHUP, OnHup, 0, 0, 0);
|
|
xsigaction(SIGTERM, OnTerm, 0, 0, 0);
|
|
xsigaction(SIGCHLD, OnChld, 0, 0, 0);
|
|
xsigaction(SIGUSR1, OnUsr1, 0, 0, 0);
|
|
xsigaction(SIGUSR2, OnUsr2, 0, 0, 0);
|
|
xsigaction(SIGALRM, OnAlrm, 0, 0, 0);
|
|
xsigaction(SIGPIPE, SIG_IGN, 0, 0, 0);
|
|
if (setitimer(ITIMER_REAL, &kHeartbeat, NULL) == -1) {
|
|
heartless = true;
|
|
}
|
|
CHECK_NE(-1,
|
|
(server = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP)));
|
|
TuneServerSocket();
|
|
if (bind(server, &serveraddr, sizeof(serveraddr)) == -1) {
|
|
if (errno == EADDRINUSE) {
|
|
fprintf(stderr, "error: address in use\n"
|
|
"try passing the -p PORT flag\n");
|
|
} else {
|
|
fprintf(stderr, "error: bind() failed: %s\n", strerror(errno));
|
|
}
|
|
exit(1);
|
|
}
|
|
if (daemonize) Daemonize();
|
|
CHECK_NE(-1, listen(server, 10));
|
|
addrsize = sizeof(serveraddr);
|
|
CHECK_NE(-1, getsockname(server, &serveraddr, &addrsize));
|
|
DescribeAddress(serveraddrstr, &serveraddr);
|
|
VERBOSEF("%s listen", serveraddrstr);
|
|
if (printport) {
|
|
printf("%d\n", ntohs(serveraddr.sin_port));
|
|
fflush(stdout);
|
|
}
|
|
UpdateCurrentDate(nowl());
|
|
inbuf.n = 64 * 1024;
|
|
inbuf.p = xvalloc(inbuf.n);
|
|
hdrbuf.n = 4 * 1024;
|
|
hdrbuf.p = xvalloc(hdrbuf.n);
|
|
LuaInit();
|
|
while (!terminated) {
|
|
if (zombied) {
|
|
ReapZombies();
|
|
} else if (invalidated) {
|
|
LuaReload();
|
|
invalidated = false;
|
|
} else if (heartbeat) {
|
|
UpdateCurrentDate(nowl());
|
|
heartbeat = false;
|
|
} else {
|
|
if (heartless) {
|
|
UpdateCurrentDate(nowl());
|
|
}
|
|
ProcessConnection();
|
|
}
|
|
}
|
|
VERBOSEF("%s terminated", serveraddrstr);
|
|
LOGIFNEG1(close(server));
|
|
if (!keyboardinterrupt) {
|
|
if (!killed) {
|
|
terminated = false;
|
|
}
|
|
LOGIFNEG1(kill(0, SIGTERM));
|
|
}
|
|
WaitAll();
|
|
}
|
|
|
|
int main(int argc, char *argv[]) {
|
|
showcrashreports();
|
|
SetDefaults();
|
|
GetOpts(argc, argv);
|
|
RedBean();
|
|
return 0;
|
|
}
|