/*-*- 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 "ape/sections.internal.h" #include "libc/assert.h" #include "libc/atomic.h" #include "libc/calls/calls.h" #include "libc/calls/pledge.h" #include "libc/calls/struct/dirent.h" #include "libc/calls/struct/flock.h" #include "libc/calls/struct/iovec.h" #include "libc/calls/struct/rusage.h" #include "libc/calls/struct/sigaction.h" #include "libc/calls/struct/sigset.h" #include "libc/calls/struct/stat.h" #include "libc/calls/struct/termios.h" #include "libc/calls/struct/timespec.h" #include "libc/calls/termios.h" #include "libc/dce.h" #include "libc/dns/dns.h" #include "libc/dns/hoststxt.h" #include "libc/dos.internal.h" #include "libc/errno.h" #include "libc/fmt/conv.h" #include "libc/fmt/itoa.h" #include "libc/fmt/wintime.internal.h" #include "libc/intrin/atomic.h" #include "libc/intrin/bits.h" #include "libc/intrin/bsr.h" #include "libc/intrin/likely.h" #include "libc/intrin/nomultics.internal.h" #include "libc/intrin/safemacros.internal.h" #include "libc/log/appendresourcereport.internal.h" #include "libc/log/check.h" #include "libc/log/log.h" #include "libc/macros.internal.h" #include "libc/math.h" #include "libc/mem/alloca.h" #include "libc/mem/gc.h" #include "libc/mem/gc.internal.h" #include "libc/mem/mem.h" #include "libc/nexgen32e/crc32.h" #include "libc/nexgen32e/rdtsc.h" #include "libc/nexgen32e/vendor.internal.h" #include "libc/nexgen32e/x86feature.h" #include "libc/nt/enum/fileflagandattributes.h" #include "libc/runtime/clktck.h" #include "libc/runtime/internal.h" #include "libc/runtime/memtrack.internal.h" #include "libc/runtime/runtime.h" #include "libc/runtime/stack.h" #include "libc/sock/goodsocket.internal.h" #include "libc/sock/sock.h" #include "libc/sock/struct/pollfd.h" #include "libc/sock/struct/sockaddr.h" #include "libc/stdio/append.h" #include "libc/stdio/hex.internal.h" #include "libc/stdio/rand.h" #include "libc/stdio/stdio.h" #include "libc/str/slice.h" #include "libc/str/str.h" #include "libc/str/strwidth.h" #include "libc/sysv/consts/af.h" #include "libc/sysv/consts/auxv.h" #include "libc/sysv/consts/clock.h" #include "libc/sysv/consts/clone.h" #include "libc/sysv/consts/dt.h" #include "libc/sysv/consts/ex.h" #include "libc/sysv/consts/exit.h" #include "libc/sysv/consts/f.h" #include "libc/sysv/consts/hwcap.h" #include "libc/sysv/consts/inaddr.h" #include "libc/sysv/consts/ipproto.h" #include "libc/sysv/consts/madv.h" #include "libc/sysv/consts/map.h" #include "libc/sysv/consts/o.h" #include "libc/sysv/consts/poll.h" #include "libc/sysv/consts/pr.h" #include "libc/sysv/consts/prot.h" #include "libc/sysv/consts/rusage.h" #include "libc/sysv/consts/s.h" #include "libc/sysv/consts/sa.h" #include "libc/sysv/consts/sig.h" #include "libc/sysv/consts/sock.h" #include "libc/sysv/consts/termios.h" #include "libc/sysv/consts/timer.h" #include "libc/sysv/consts/w.h" #include "libc/sysv/errfuns.h" #include "libc/thread/thread.h" #include "libc/thread/tls.h" #include "libc/x/x.h" #include "libc/x/xasprintf.h" #include "libc/zip.internal.h" #include "net/http/escape.h" #include "net/http/http.h" #include "net/http/ip.h" #include "net/http/tokenbucket.h" #include "net/http/url.h" #include "net/https/https.h" #include "third_party/getopt/getopt.internal.h" #include "third_party/lua/cosmo.h" #include "third_party/lua/lauxlib.h" #include "third_party/lua/lrepl.h" #include "third_party/lua/lualib.h" #include "third_party/lua/lunix.h" #include "third_party/mbedtls/ctr_drbg.h" #include "third_party/mbedtls/debug.h" #include "third_party/mbedtls/iana.h" #include "third_party/mbedtls/net_sockets.h" #include "third_party/mbedtls/oid.h" #include "third_party/mbedtls/san.h" #include "third_party/mbedtls/ssl.h" #include "third_party/mbedtls/ssl_ticket.h" #include "third_party/mbedtls/x509.h" #include "third_party/mbedtls/x509_crt.h" #include "third_party/zlib/zlib.h" #include "tool/args/args.h" #include "tool/build/lib/case.h" #include "tool/net/lfinger.h" #include "tool/net/lfuncs.h" #include "tool/net/ljson.h" #include "tool/net/lpath.h" #include "tool/net/luacheck.h" #include "tool/net/sandbox.h" #pragma GCC diagnostic ignored "-Wunused-variable" STATIC_STACK_ALIGN(GetStackSize()); __static_yoink("zipos"); #ifdef USE_BLINK __static_yoink("blink_linux_aarch64"); // for raspberry pi __static_yoink("blink_xnu_aarch64"); // is apple silicon #endif /** * @fileoverview redbean - single-file distributable web server * * redbean makes it possible to share web applications that run offline * as a single-file αcτµαlly pδrταblε εxεcµταblε zip archive which * contains your assets. All you need to do is download the redbean.com * program below, change the filename to .zip, add your content in a zip * editing tool, and then change the extension back to .com. * * redbean can serve 1 million+ gzip encoded responses per second on a * cheap personal computer. That performance is thanks to zip and gzip * using the same compression format, which enables kernelspace copies. * Another reason redbean goes fast is that it's a tiny static binary, * which makes fork memory paging nearly free. * * redbean is also easy to modify to suit your own needs. The program * itself is written as a single .c file. It embeds the Lua programming * language and SQLite which let you write dynamic pages. */ #ifndef REDBEAN #define REDBEAN "redbean" #endif #define VERSION 0x020200 #define HASH_LOAD_FACTOR /* 1. / */ 4 #define MONITOR_MICROS 150000 #define READ(F, P, N) readv(F, &(struct iovec){P, N}, 1) #define WRITE(F, P, N) writev(F, &(struct iovec){P, N}, 1) #define AppendCrlf(P) mempcpy(P, "\r\n", 2) #define HasHeader(H) (!!cpm.msg.headers[H].a) #define HeaderData(H) (inbuf.p + cpm.msg.headers[H].a) #define HeaderLength(H) (cpm.msg.headers[H].b - cpm.msg.headers[H].a) #define HeaderEqualCase(H, S) \ SlicesEqualCase(S, strlen(S), HeaderData(H), HeaderLength(H)) #define LockInc(P) \ atomic_fetch_add_explicit((_Atomic(typeof(*(P))) *)(P), +1, \ memory_order_relaxed) #define LockDec(P) \ atomic_fetch_add_explicit((_Atomic(typeof(*(P))) *)(P), -1, \ memory_order_relaxed) #define TRACE_BEGIN \ do { \ if (!IsTiny()) { \ if (funtrace) { \ ftrace_enabled(+1); \ } \ if (systrace) { \ strace_enabled(+1); \ } \ } \ } while (0) #define TRACE_END \ do { \ if (!IsTiny()) { \ if (funtrace) { \ ftrace_enabled(-1); \ } \ if (systrace) { \ strace_enabled(-1); \ } \ } \ } while (0) // letters not used: INOQYnoqwxy // digits not used: 0123456789 // puncts not used: !"#$&'()+,-./;<=>@[\]^_`{|}~ #define GETOPTS \ "*%BEJSVXZabdfghijkmsuvzA:C:D:F:G:H:K:L:M:P:R:T:U:W:c:e:l:p:r:t:w:" 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 *const kIndexPaths[] = { #ifndef STATIC "index.lua", #endif "index.html", }; static const char *const kAlpn[] = { "http/1.1", NULL, }; struct Buffer { size_t n, c; char *p; }; struct TlsBio { int fd, c; unsigned a, b; unsigned char t[4000]; unsigned char u[1430]; }; struct Strings { size_t n, c; struct String { size_t n; const char *s; } * p; }; struct DeflateGenerator { int t; void *b; size_t i; uint32_t c; uint32_t z; z_stream s; struct Asset *a; }; static struct Ips { size_t n; uint32_t *p; } ips; static struct Ports { size_t n; uint16_t *p; } ports; static struct Servers { size_t n; struct Server { int fd; struct sockaddr_in addr; } * p; } servers; static struct Freelist { size_t n, c; void **p; } freelist; static struct Unmaplist { size_t n, c; struct Unmap { int f; void *p; size_t n; } * p; } unmaplist; static struct Psks { size_t n; struct Psk { char *key; size_t key_len; char *identity; size_t identity_len; char *s; } * p; } psks; static struct Suites { size_t n; uint16_t *p; } suites; static struct Certs { size_t n; struct Cert *p; } certs; static struct Redirects { size_t n; struct Redirect { int code; struct String path; struct String location; } * p; } redirects; static struct Assets { uint32_t n; struct Asset { bool istext; uint32_t hash; uint64_t cf; uint64_t lf; int64_t lastmodified; char *lastmodifiedstr; struct File { struct String path; struct stat st; } * file; } * p; } assets; static struct TrustedIps { size_t n; struct TrustedIp { uint32_t ip; uint32_t mask; } * p; } trustedips; struct TokenBucket { signed char cidr; signed char reject; signed char ignore; signed char ban; struct timespec replenish; union { atomic_schar *b; atomic_uint_fast64_t *w; }; } tokenbucket; struct Blackhole { struct sockaddr_un addr; int fd; } blackhole; static struct Shared { int workers; struct timespec nowish; struct timespec lastreindex; struct timespec lastmeltdown; char currentdate[32]; struct rusage server; struct rusage children; struct Counters { #define C(x) long x; #include "tool/net/counters.inc" #undef C } c; pthread_spinlock_t montermlock; } * shared; static const char kCounterNames[] = #define C(x) #x "\0" #include "tool/net/counters.inc" #undef C ; typedef ssize_t (*reader_f)(int, void *, size_t); typedef ssize_t (*writer_f)(int, struct iovec *, int); struct ClearedPerMessage { bool istext; bool branded; bool hascontenttype; bool gotcachecontrol; bool gotxcontenttypeoptions; int frags; int statuscode; int isyielding; char *outbuf; char *content; size_t gzipped; size_t contentlength; char *luaheaderp; const char *referrerpolicy; size_t msgsize; ssize_t (*generator)(struct iovec[3]); struct Strings loops; struct HttpMessage msg; } cpm; static bool suiteb; static bool killed; static bool zombied; static bool usingssl; static bool funtrace; static bool systrace; static bool meltdown; static bool unsecure; static bool norsagen; static bool printport; static bool daemonize; static bool logrusage; static bool logbodies; static bool requiressl; static bool sslcliused; static bool loglatency; static bool terminated; static bool uniprocess; static bool invalidated; static bool logmessages; static bool isinitialized; static bool sslinitialized; static bool sslfetchverify; static bool selfmodifiable; static bool interpretermode; static bool sslclientverify; static bool connectionclose; static bool hasonloglatency; static bool hasonworkerstop; static bool isexitingworker; static bool hasonworkerstart; static bool leakcrashreports; static bool hasonhttprequest; static bool ishandlingrequest; static bool listeningonport443; static bool hasonprocesscreate; static bool hasonprocessdestroy; static bool ishandlingconnection; static bool hasonclientconnection; static bool evadedragnetsurveillance; static int zfd; static int gmtoff; static int client; static int mainpid; static int sandboxed; static int changeuid; static int changegid; static int maxworkers; static int shutdownsig; static int sslpskindex; static int oldloglevel; static int messageshandled; static int sslticketlifetime; static uint32_t clientaddrsize; static atomic_int terminatemonitor; static char *brand; static size_t zsize; static lua_State *GL; static lua_State *YL; static uint8_t *zmap; static uint8_t *zcdir; static size_t hdrsize; static size_t amtread; static reader_f reader; static writer_f writer; static char *extrahdrs; static const char *zpath; static char *serverheader; static char gzip_footer[8]; static long maxpayloadsize; static const char *pidpath; static const char *logpath; static uint32_t *interfaces; static struct pollfd *polls; static size_t payloadlength; static int64_t cacheseconds; static char *cachedirective; static const char *monitortty; static struct Strings stagedirs; static struct Strings hidepaths; static const char *launchbrowser; static const char ctIdx = 'c'; // a pseudo variable to get address of static pthread_t monitorth; static struct Buffer inbuf_actual; static struct Buffer inbuf; static struct Buffer oldin; static struct Buffer hdrbuf; static struct timeval timeout; static struct Buffer effectivepath; static struct timespec heartbeatinterval; static struct Url url; static struct stat zst; static struct timespec startread; static struct timespec lastrefresh; static struct timespec startserver; static struct timespec startrequest; static struct timespec lastheartbeat; static struct timespec startconnection; static struct sockaddr_in clientaddr; static struct sockaddr_in *serveraddr; static mbedtls_ssl_config conf; static mbedtls_ssl_context ssl; static mbedtls_ctr_drbg_context rng; static mbedtls_ssl_ticket_context ssltick; static mbedtls_ssl_config confcli; static mbedtls_ssl_context sslcli; static mbedtls_ctr_drbg_context rngcli; static struct TlsBio g_bio; static char slashpath[PATH_MAX]; static struct DeflateGenerator dg; static char *Route(const char *, size_t, const char *, size_t); static char *RouteHost(const char *, size_t, const char *, size_t); static char *RoutePath(const char *, size_t); static char *HandleAsset(struct Asset *, const char *, size_t); static char *ServeAsset(struct Asset *, const char *, size_t); static char *SetStatus(unsigned, const char *); static void TlsInit(void); static void OnChld(void) { zombied = true; } static void OnUsr1(void) { invalidated = true; } static void OnUsr2(void) { meltdown = true; } static void OnTerm(int sig) { if (!terminated) { shutdownsig = sig; terminated = true; } else { killed = true; } } static void OnInt(int sig) { OnTerm(sig); } static void OnHup(int sig) { if (daemonize) { OnUsr1(); } else { OnTerm(sig); } } static void Free(void *p) { free(*(void **)p); *(void **)p = 0; } static long ParseInt(const char *s) { return strtol(s, 0, 0); } static void *FreeLater(void *p) { if (p) { if (++freelist.n > freelist.c) { freelist.c = freelist.n + (freelist.n >> 1); freelist.p = xrealloc(freelist.p, freelist.c * sizeof(*freelist.p)); } freelist.p[freelist.n - 1] = p; } return p; } static void UnmapLater(int f, void *p, size_t n) { if (++unmaplist.n > unmaplist.c) { unmaplist.c = unmaplist.n + (unmaplist.n >> 1); unmaplist.p = xrealloc(unmaplist.p, unmaplist.c * sizeof(*unmaplist.p)); } unmaplist.p[unmaplist.n - 1].f = f; unmaplist.p[unmaplist.n - 1].p = p; unmaplist.p[unmaplist.n - 1].n = n; } static void CollectGarbage(void) { __log_level = oldloglevel; DestroyHttpMessage(&cpm.msg); while (freelist.n) { free(freelist.p[--freelist.n]); } while (unmaplist.n) { --unmaplist.n; LOGIFNEG1(munmap(unmaplist.p[unmaplist.n].p, unmaplist.p[unmaplist.n].n)); LOGIFNEG1(close(unmaplist.p[unmaplist.n].f)); } } static void UseOutput(void) { cpm.content = FreeLater(cpm.outbuf); cpm.contentlength = appendz(cpm.outbuf).i; cpm.outbuf = 0; } static void DropOutput(void) { FreeLater(cpm.outbuf); cpm.outbuf = 0; } static bool ShouldAvoidGzip(void) { return (IsGenuineBlink() && !X86_HAVE(JIT)); } static char *MergePaths(const char *p, size_t n, const char *q, size_t m, size_t *z) { char *r; if (n && p[n - 1] == '/') --n; if (m && q[0] == '/') ++q, --m; r = xmalloc(n + 1 + m + 1); mempcpy(mempcpy(mempcpy(mempcpy(r, p, n), "/", 1), q, m), "", 1); if (z) *z = n + 1 + m; return r; } static long FindRedirect(const char *s, size_t n) { int c, m, l, r; l = 0; r = redirects.n - 1; while (l <= r) { m = (l & r) + ((l ^ r) >> 1); // floor((a+b)/2) c = CompareSlices(redirects.p[m].path.s, redirects.p[m].path.n, s, n); if (c < 0) { l = m + 1; } else if (c > 0) { r = m - 1; } else { return m; } } return -1; } static mbedtls_x509_crt *GetTrustedCertificate(mbedtls_x509_name *name) { size_t i; for (i = 0; i < certs.n; ++i) { if (certs.p[i].cert && !mbedtls_x509_name_cmp(name, &certs.p[i].cert->subject)) { return certs.p[i].cert; } } return 0; } static void UseCertificate(mbedtls_ssl_config *c, struct Cert *kp, const char *role) { VERBOSEF("(ssl) using %s certificate %`'s for HTTPS %s", mbedtls_pk_get_name(&kp->cert->pk), _gc(FormatX509Name(&kp->cert->subject)), role); CHECK_EQ(0, mbedtls_ssl_conf_own_cert(c, kp->cert, kp->key)); } static void AppendCert(mbedtls_x509_crt *cert, mbedtls_pk_context *key) { certs.p = realloc(certs.p, ++certs.n * sizeof(*certs.p)); certs.p[certs.n - 1].cert = cert; certs.p[certs.n - 1].key = key; } static void InternCertificate(mbedtls_x509_crt *cert, mbedtls_x509_crt *prev) { int r; size_t i; if (cert->next) InternCertificate(cert->next, cert); if (prev) { if (mbedtls_x509_crt_check_parent(prev, cert, 1)) { DEBUGF("(ssl) unbundling %`'s from %`'s", _gc(FormatX509Name(&prev->subject)), _gc(FormatX509Name(&cert->subject))); prev->next = 0; } else if ((r = mbedtls_x509_crt_check_signature(prev, cert, 0))) { WARNF("(ssl) invalid signature for %`'s -> %`'s (-0x%04x)", _gc(FormatX509Name(&prev->subject)), _gc(FormatX509Name(&cert->subject)), -r); } } if (mbedtls_x509_time_is_past(&cert->valid_to)) { WARNF("(ssl) certificate %`'s is expired", _gc(FormatX509Name(&cert->subject))); } else if (mbedtls_x509_time_is_future(&cert->valid_from)) { WARNF("(ssl) certificate %`'s is from the future", _gc(FormatX509Name(&cert->subject))); } for (i = 0; i < certs.n; ++i) { if (!certs.p[i].cert && certs.p[i].key && !mbedtls_pk_check_pair(&cert->pk, certs.p[i].key)) { certs.p[i].cert = cert; return; } } LogCertificate("loaded certificate", cert); if (!cert->next && !IsSelfSigned(cert) && cert->max_pathlen) { for (i = 0; i < certs.n; ++i) { if (!certs.p[i].cert) continue; if (mbedtls_pk_can_do(&cert->pk, certs.p[i].cert->sig_pk) && !mbedtls_x509_crt_check_parent(cert, certs.p[i].cert, 1) && !IsSelfSigned(certs.p[i].cert)) { if (ChainCertificate(cert, certs.p[i].cert)) break; } } } if (!IsSelfSigned(cert)) { for (i = 0; i < certs.n; ++i) { if (!certs.p[i].cert) continue; if (certs.p[i].cert->next) continue; if (certs.p[i].cert->max_pathlen && mbedtls_pk_can_do(&certs.p[i].cert->pk, cert->sig_pk) && !mbedtls_x509_crt_check_parent(certs.p[i].cert, cert, 1)) { ChainCertificate(certs.p[i].cert, cert); } } } AppendCert(cert, 0); } static void ProgramCertificate(const char *p, size_t n) { int rc; unsigned char *waqapi; mbedtls_x509_crt *cert; waqapi = malloc(n + 1); memcpy(waqapi, p, n); waqapi[n] = 0; cert = calloc(1, sizeof(mbedtls_x509_crt)); rc = mbedtls_x509_crt_parse(cert, waqapi, n + 1); mbedtls_platform_zeroize(waqapi, n); free(waqapi); if (rc < 0) { WARNF("(ssl) failed to load certificate (grep -0x%04x)", rc); return; } else if (rc > 0) { VERBOSEF("(ssl) certificate bundle partially loaded"); } InternCertificate(cert, 0); } static void ProgramPrivateKey(const char *p, size_t n) { int rc; size_t i; unsigned char *waqapi; mbedtls_pk_context *key; waqapi = malloc(n + 1); memcpy(waqapi, p, n); waqapi[n] = 0; key = calloc(1, sizeof(mbedtls_pk_context)); rc = mbedtls_pk_parse_key(key, waqapi, n + 1, 0, 0); mbedtls_platform_zeroize(waqapi, n); free(waqapi); if (rc != 0) FATALF("(ssl) error: load key (grep -0x%04x)", -rc); for (i = 0; i < certs.n; ++i) { if (certs.p[i].cert && !certs.p[i].key && !mbedtls_pk_check_pair(&certs.p[i].cert->pk, key)) { certs.p[i].key = key; return; } } VERBOSEF("(ssl) loaded private key"); AppendCert(0, key); } static void ProgramFile(const char *path, void program(const char *, size_t)) { char *p; size_t n; DEBUGF("(srvr) ProgramFile(%`'s)", path); if ((p = xslurp(path, &n))) { program(p, n); mbedtls_platform_zeroize(p, n); free(p); } else { FATALF("(srvr) error: failed to read file %`'s", path); } } static void ProgramPort(long port) { if (!(0 <= port && port <= 65535)) { FATALF("(cfg) error: bad port: %d", port); } if (port == 443) listeningonport443 = true; ports.p = realloc(ports.p, ++ports.n * sizeof(*ports.p)); ports.p[ports.n - 1] = port; } static void ProgramMaxPayloadSize(long x) { maxpayloadsize = MAX(1450, x); } static void ProgramSslTicketLifetime(long x) { sslticketlifetime = x; } static void ProgramAddr(const char *addr) { ssize_t rc; int64_t ip; if ((ip = ParseIp(addr, -1)) == -1) { if (!IsTiny()) { struct addrinfo *ai = NULL; struct addrinfo hint = {AI_NUMERICSERV, AF_INET, SOCK_STREAM, IPPROTO_TCP}; if ((rc = getaddrinfo(addr, "0", &hint, &ai)) != EAI_SUCCESS) { FATALF("(cfg) error: bad addr: %s (EAI_%s)", addr, gai_strerror(rc)); } ip = ntohl(ai->ai_addr4->sin_addr.s_addr); freeaddrinfo(ai); } else { FATALF("(cfg) error: ProgramAddr() needs an IP in MODE=tiny: %s", addr); } } ips.p = realloc(ips.p, ++ips.n * sizeof(*ips.p)); ips.p[ips.n - 1] = ip; } static void ProgramRedirect(int code, const char *sp, size_t sn, const char *dp, size_t dn) { long i, j; struct Redirect r; if (code && code != 301 && code != 302 && code != 307 && code != 308) { FATALF("(cfg) error: unsupported redirect code %d", code); } if (!(FreeLater(EncodeHttpHeaderValue(dp, dn, 0)))) { FATALF("(cfg) error: invalid location %s", dp); } r.code = code; r.path.s = sp; r.path.n = sn; r.location.s = dp; r.location.n = dn; if ((i = FindRedirect(r.path.s, r.path.n)) != -1) { redirects.p[i] = r; } else { i = redirects.n; redirects.p = xrealloc(redirects.p, (i + 1) * sizeof(*redirects.p)); for (j = i; j; --j) { if (CompareSlices(r.path.s, r.path.n, redirects.p[j - 1].path.s, redirects.p[j - 1].path.n) < 0) { redirects.p[j] = redirects.p[j - 1]; } else { break; } } redirects.p[j] = r; ++redirects.n; } } static void ProgramRedirectArg(int code, const char *s) { size_t n; const char *p; n = strlen(s); if (!(p = memchr(s, '=', n))) { FATALF("(cfg) error: redirect arg missing '='"); } ProgramRedirect(code, s, p - s, p + 1, n - (p - s + 1)); } static void ProgramTrustedIp(uint32_t ip, int cidr) { uint32_t mask; mask = 0xffffffffu << (32 - cidr); trustedips.p = xrealloc(trustedips.p, ++trustedips.n * sizeof(*trustedips.p)); trustedips.p[trustedips.n - 1].ip = ip; trustedips.p[trustedips.n - 1].mask = mask; } static bool IsTrustedIp(uint32_t ip) { int i; uint32_t *p; if (interfaces) { for (p = interfaces; *p; ++p) { if (ip == *p && !IsTestnetIp(ip)) { DEBUGF("(token) ip is trusted because it's %s", "a local interface"); return true; } } } if (trustedips.n) { for (i = 0; i < trustedips.n; ++i) { if ((ip & trustedips.p[i].mask) == trustedips.p[i].ip) { DEBUGF("(token) ip is trusted because it's %s", "whitelisted"); return true; } } return false; } else if (IsPrivateIp(ip) && !IsTestnetIp(ip)) { DEBUGF("(token) ip is trusted because it's %s", "private"); return true; } else if (IsLoopbackIp(ip)) { DEBUGF("(token) ip is trusted because it's %s", "loopback"); return true; } else { return false; } } static void DescribeAddress(char buf[40], uint32_t addr, uint16_t port) { char *p; p = buf; p = FormatUint32(p, (addr & 0xFF000000) >> 030), *p++ = '.'; p = FormatUint32(p, (addr & 0x00FF0000) >> 020), *p++ = '.'; p = FormatUint32(p, (addr & 0x0000FF00) >> 010), *p++ = '.'; p = FormatUint32(p, (addr & 0x000000FF) >> 000), *p++ = ':'; p = FormatUint32(p, port); *p = '\0'; assert(p - buf < 40); } static inline int GetServerAddr(uint32_t *ip, uint16_t *port) { *ip = ntohl(serveraddr->sin_addr.s_addr); if (port) *port = ntohs(serveraddr->sin_port); return 0; } static inline int GetClientAddr(uint32_t *ip, uint16_t *port) { *ip = ntohl(clientaddr.sin_addr.s_addr); if (port) *port = ntohs(clientaddr.sin_port); return 0; } static inline int GetRemoteAddr(uint32_t *ip, uint16_t *port) { GetClientAddr(ip, port); if (HasHeader(kHttpXForwardedFor)) { if (IsTrustedIp(*ip)) { if (ParseForwarded(HeaderData(kHttpXForwardedFor), HeaderLength(kHttpXForwardedFor), ip, port) == -1) { VERBOSEF("could not parse x-forwarded-for %`'.*s len=%ld", HeaderLength(kHttpXForwardedFor), HeaderData(kHttpXForwardedFor), HeaderLength(kHttpXForwardedFor)); return -1; } } else { WARNF( "%hhu.%hhu.%hhu.%hhu isn't authorized to send x-forwarded-for %`'.*s", *ip >> 24, *ip >> 16, *ip >> 8, *ip, HeaderLength(kHttpXForwardedFor), HeaderData(kHttpXForwardedFor)); } } return 0; } static char *DescribeClient(void) { char str[40]; uint16_t port; uint32_t client; static char description[128]; GetClientAddr(&client, &port); if (HasHeader(kHttpXForwardedFor) && IsTrustedIp(client)) { DescribeAddress(str, client, port); snprintf(description, sizeof(description), "%'.*s via %s", HeaderLength(kHttpXForwardedFor), HeaderData(kHttpXForwardedFor), str); } else { DescribeAddress(description, client, port); } return description; } static char *DescribeServer(void) { uint32_t ip; uint16_t port; static char serveraddrstr[40]; GetServerAddr(&ip, &port); DescribeAddress(serveraddrstr, ip, port); return serveraddrstr; } static void ProgramBrand(const char *s) { char *p; free(brand); free(serverheader); if (!(p = EncodeHttpHeaderValue(s, -1, 0))) { FATALF("(cfg) error: brand isn't latin1 encodable: %`'s", s); } brand = strdup(s); serverheader = xasprintf("Server: %s\r\n", p); free(p); } static void ProgramUid(long x) { changeuid = x; } static void ProgramGid(long x) { changegid = x; } #define MINTIMEOUT 10 static void ProgramTimeout(long ms) { ldiv_t d; if (ms < 0) { timeout.tv_sec = ms; /* -(keepalive seconds) */ timeout.tv_usec = 0; } else { if (ms < MINTIMEOUT) { FATALF("(cfg) error: timeout needs to be %dms or greater", MINTIMEOUT); } d = ldiv(ms, 1000); timeout.tv_sec = d.quot; timeout.tv_usec = d.rem * 1000; } } static void ProgramCache(long x, const char *s) { cacheseconds = x; if (s) cachedirective = strdup(s); } static void SetDefaults(void) { ProgramBrand(_gc(xasprintf("%s/%hhd.%hhd.%hhd", REDBEAN, VERSION >> 020, VERSION >> 010, VERSION >> 000))); __log_level = kLogInfo; maxpayloadsize = 64 * 1024; ProgramCache(-1, "must-revalidate"); ProgramTimeout(60 * 1000); ProgramSslTicketLifetime(24 * 60 * 60); sslfetchverify = true; } static void AddString(struct Strings *l, const char *s, size_t n) { if (++l->n > l->c) { l->c = l->n + (l->n >> 1); l->p = realloc(l->p, l->c * sizeof(*l->p)); } l->p[l->n - 1].s = s; l->p[l->n - 1].n = n; } static bool HasString(struct Strings *l, const char *s, size_t n) { size_t i; for (i = 0; i < l->n; ++i) { if (SlicesEqual(l->p[i].s, l->p[i].n, s, n)) { return true; } } return false; } const char *DEFAULTLUAPATH = "/zip/.lua/?.lua;/zip/.lua/?/init.lua"; static void UpdateLuaPath(const char *s) { #ifndef STATIC lua_State *L = GL; char *curpath = ""; char *respath = 0; char *t; int n = lua_gettop(L); lua_getglobal(L, "package"); if (lua_istable(L, -1)) { lua_getfield(L, -1, "path"); curpath = (void *)luaL_optstring(L, -1, ""); if ((t = strstr(curpath, DEFAULTLUAPATH))) { // if the DEFAULT path is found, prepend the path in front of it respath = xasprintf("%.*s%s/.lua/?.lua;%s/.lua/?/init.lua;%s", t - curpath, curpath, s, s, t); } else { // if the DEFAULT path is not found, append to the end respath = xasprintf("%s;%s/.lua/?.lua;%s/.lua/?/init.lua", curpath, s, s); } lua_pushstring(L, _gc(respath)); lua_setfield(L, -3, "path"); } lua_settop(L, n); #endif } static void ProgramDirectory(const char *path) { char *s; size_t n; struct stat st; if (stat(path, &st) == -1 || !S_ISDIR(st.st_mode)) { FATALF("(cfg) error: not a directory: %`'s", path); } s = strdup(path); n = strlen(s); INFOF("(cfg) program directory: %s", s); AddString(&stagedirs, s, n); UpdateLuaPath(s); } static void ProgramHeader(const char *s) { char *p, *v; if ((p = strchr(s, ':')) && IsValidHttpToken(s, p - s) && (v = EncodeLatin1(p + 1, -1, 0, kControlC0 | kControlC1 | kControlWs))) { switch (GetHttpHeader(s, p - s)) { case kHttpDate: case kHttpConnection: case kHttpContentLength: case kHttpContentEncoding: case kHttpContentRange: case kHttpLocation: FATALF("(cfg) error: can't program header: %`'s", s); case kHttpServer: ProgramBrand(p + 1); break; default: p = xasprintf("%s%.*s:%s\r\n", extrahdrs ? extrahdrs : "", p - s, s, v); free(extrahdrs); extrahdrs = p; break; } free(v); } else { FATALF("(cfg) error: illegal header: %`'s", s); } } static void ProgramLogPath(const char *s) { int fd; logpath = strdup(s); fd = open(logpath, O_APPEND | O_WRONLY | O_CREAT, 0640); if (fd == -1) { WARNF("(srvr) open(%`'s) failed: %m", logpath); return; } if (fd != 2) { dup2(fd, 2); close(fd); } } static void ProgramPidPath(const char *s) { pidpath = strdup(s); } static bool IsServerFd(int fd) { size_t i; for (i = 0; i < servers.n; ++i) { if (servers.p[i].fd == fd) { return true; } } return false; } static void ChangeUser(void) { if (changegid) { if (setgid(changegid)) { FATALF("(cfg) setgid() failed: %m"); } } // order matters if (changeuid) { if (setuid(changeuid)) { FATALF("(cfg) setuid() failed: %m"); } } } static void Daemonize(void) { if (fork() > 0) exit(0); setsid(); if (fork() > 0) _exit(0); umask(0); } static void LogLuaError(const char *hook, const char *err) { ERRORF("(lua) failed to run %s: %s", hook, err); } // handles `-e CODE` (frontloads web server code) // handles `-i -e CODE` (interprets expression and exits) static void LuaEvalCode(const char *code) { lua_State *L = GL; int status = luaL_loadstring(L, code); if (status != LUA_OK || LuaCallWithTrace(L, 0, 0, NULL) != LUA_OK) { LogLuaError("lua code", lua_tostring(L, -1)); lua_pop(L, 1); // pop error exit(1); } AssertLuaStackIsAt(L, 0); } // handle `-F PATH` arg static void LuaEvalFile(const char *path) { char *f = _gc(xslurp(path, 0)); if (!f) FATALF("(cfg) error: failed to read file %`'s", path); LuaEvalCode(f); } static bool LuaOnClientConnection(void) { bool dropit = false; #ifndef STATIC uint32_t ip, serverip; uint16_t port, serverport; lua_State *L = GL; lua_getglobal(L, "OnClientConnection"); GetClientAddr(&ip, &port); GetServerAddr(&serverip, &serverport); lua_pushinteger(L, ip); lua_pushinteger(L, port); lua_pushinteger(L, serverip); lua_pushinteger(L, serverport); if (LuaCallWithTrace(L, 4, 1, NULL) == LUA_OK) { dropit = lua_toboolean(L, -1); } else { LogLuaError("OnClientConnection", lua_tostring(L, -1)); } lua_pop(L, 1); // pop result or error AssertLuaStackIsAt(L, 0); #endif return dropit; } static void LuaOnLogLatency(long reqtime, long contime) { #ifndef STATIC lua_State *L = GL; int n = lua_gettop(L); lua_getglobal(L, "OnLogLatency"); lua_pushinteger(L, reqtime); lua_pushinteger(L, contime); if (LuaCallWithTrace(L, 2, 0, NULL) != LUA_OK) { LogLuaError("OnLogLatency", lua_tostring(L, -1)); lua_pop(L, 1); // pop error } AssertLuaStackIsAt(L, n); #endif } static void LuaOnProcessCreate(int pid) { #ifndef STATIC uint32_t ip, serverip; uint16_t port, serverport; lua_State *L = GL; lua_getglobal(L, "OnProcessCreate"); GetClientAddr(&ip, &port); GetServerAddr(&serverip, &serverport); lua_pushinteger(L, pid); lua_pushinteger(L, ip); lua_pushinteger(L, port); lua_pushinteger(L, serverip); lua_pushinteger(L, serverport); if (LuaCallWithTrace(L, 5, 0, NULL) != LUA_OK) { LogLuaError("OnProcessCreate", lua_tostring(L, -1)); lua_pop(L, 1); // pop error } AssertLuaStackIsAt(L, 0); #endif } static bool LuaOnServerListen(int fd, uint32_t ip, uint16_t port) { bool nouse = false; #ifndef STATIC lua_State *L = GL; lua_getglobal(L, "OnServerListen"); lua_pushinteger(L, fd); lua_pushinteger(L, ip); lua_pushinteger(L, port); if (LuaCallWithTrace(L, 3, 1, NULL) == LUA_OK) { nouse = lua_toboolean(L, -1); } else { LogLuaError("OnServerListen", lua_tostring(L, -1)); } lua_pop(L, 1); // pop result or error AssertLuaStackIsAt(L, 0); #endif return nouse; } static void LuaOnProcessDestroy(int pid) { #ifndef STATIC lua_State *L = GL; lua_getglobal(L, "OnProcessDestroy"); lua_pushinteger(L, pid); if (LuaCallWithTrace(L, 1, 0, NULL) != LUA_OK) { LogLuaError("OnProcessDestroy", lua_tostring(L, -1)); lua_pop(L, 1); // pop error } AssertLuaStackIsAt(L, 0); #endif } static inline bool IsHookDefined(const char *s) { #ifndef STATIC lua_State *L = GL; bool res = !!lua_getglobal(L, s); lua_pop(L, 1); return res; #else return false; #endif } static void CallSimpleHook(const char *s) { #ifndef STATIC lua_State *L = GL; int n = lua_gettop(L); lua_getglobal(L, s); if (LuaCallWithTrace(L, 0, 0, NULL) != LUA_OK) { LogLuaError(s, lua_tostring(L, -1)); lua_pop(L, 1); // pop error } AssertLuaStackIsAt(L, n); #endif } static void CallSimpleHookIfDefined(const char *s) { if (IsHookDefined(s)) { CallSimpleHook(s); } } static void ReportWorkerExit(int pid, int ws) { int workers; workers = atomic_fetch_sub(&shared->workers, 1) - 1; if (WIFEXITED(ws)) { if (WEXITSTATUS(ws)) { LockInc(&shared->c.failedchildren); WARNF("(stat) %d exited with %d (%,d workers remain)", pid, WEXITSTATUS(ws), workers); } else { DEBUGF("(stat) %d exited (%,d workers remain)", pid, workers); } } else { LockInc(&shared->c.terminatedchildren); WARNF("(stat) %d terminated with %s (%,d workers remain)", pid, strsignal(WTERMSIG(ws)), workers); } } static void ReportWorkerResources(int pid, struct rusage *ru) { char *s, *b = 0; if (logrusage || LOGGABLE(kLogDebug)) { AppendResourceReport(&b, ru, "\n"); if (b) { if ((s = IndentLines(b, appendz(b).i - 1, 0, 1))) { LOGF(kLogDebug, "(stat) resource report for pid %d\n%s", pid, s); free(s); } free(b); } } } static void HandleWorkerExit(int pid, int ws, struct rusage *ru) { LockInc(&shared->c.connectionshandled); rusage_add(&shared->children, ru); ReportWorkerExit(pid, ws); ReportWorkerResources(pid, ru); if (hasonprocessdestroy) { LuaOnProcessDestroy(pid); } } static void KillGroupImpl(int sig) { LOGIFNEG1(kill(0, sig)); } static void KillGroup(void) { KillGroupImpl(SIGTERM); } static void WaitAll(void) { int ws, pid; struct rusage ru; for (;;) { if ((pid = wait4(-1, &ws, 0, &ru)) != -1) { HandleWorkerExit(pid, ws, &ru); } else { if (errno == ECHILD) { errno = 0; break; } if (errno == EINTR) { if (killed) { killed = false; terminated = false; WARNF("(srvr) server shall terminate harder"); KillGroup(); } errno = 0; continue; } DIEF("(srvr) wait error: %m"); } } } static void ReapZombies(void) { int ws, pid; struct rusage ru; do { zombied = false; if ((pid = wait4(-1, &ws, WNOHANG, &ru)) != -1) { if (pid) { HandleWorkerExit(pid, ws, &ru); } else { break; } } else { if (errno == ECHILD) { errno = 0; break; } if (errno == EINTR) { errno = 0; continue; } DIEF("(srvr) wait error: %m"); } } while (!terminated); } static ssize_t ReadAll(int fd, char *p, size_t n) { ssize_t rc; size_t i, got; for (i = 0; i < n;) { rc = READ(fd, p + i, n - i); if (rc != -1) { got = rc; i += got; } else if (errno != EINTR) { WARNF("(file) read error: %m"); return -1; } } return i; } static bool IsTakingTooLong(void) { return meltdown && timespec_cmp(timespec_sub(timespec_real(), startread), (struct timespec){2}) >= 0; } static ssize_t WritevAll(int fd, struct iovec *iov, int iovlen) { int i; ssize_t rc; size_t wrote, total; i = 0; total = 0; do { if (i) { while (i < iovlen && !iov[i].iov_len) ++i; if (i == iovlen) break; } if ((rc = writev(fd, iov + i, iovlen - i)) != -1) { wrote = rc; total += wrote; do { if (wrote >= iov[i].iov_len) { wrote -= iov[i++].iov_len; } else { iov[i].iov_base = (char *)iov[i].iov_base + wrote; iov[i].iov_len -= wrote; wrote = 0; } } while (wrote); } else if (errno == EINTR) { errno = 0; LockInc(&shared->c.writeinterruputs); if (killed || IsTakingTooLong()) { return total ? total : -1; } } else { return total ? total : -1; } } while (i < iovlen); return total; } static int TlsFlush(struct TlsBio *bio, const unsigned char *buf, size_t len) { struct iovec v[2]; if (len || bio->c > 0) { v[0].iov_base = bio->u; v[0].iov_len = MAX(0, bio->c); v[1].iov_base = (void *)buf; v[1].iov_len = len; if (WritevAll(bio->fd, v, 2) != -1) { if (bio->c > 0) bio->c = 0; } else if (errno == EINTR) { errno = 0; return MBEDTLS_ERR_NET_CONN_RESET; } else if (errno == EAGAIN) { errno = 0; return MBEDTLS_ERR_SSL_TIMEOUT; } else if (errno == EPIPE || errno == ECONNRESET || errno == ENETRESET) { return MBEDTLS_ERR_NET_CONN_RESET; } else { WARNF("(ssl) TlsSend error: %m"); return MBEDTLS_ERR_NET_SEND_FAILED; } } return 0; } static int TlsSend(void *ctx, const unsigned char *buf, size_t len) { int rc; struct TlsBio *bio = ctx; if (bio->c >= 0 && bio->c + len <= sizeof(bio->u)) { memcpy(bio->u + bio->c, buf, len); bio->c += len; return len; } if ((rc = TlsFlush(bio, buf, len)) < 0) return rc; return len; } static int TlsRecvImpl(void *ctx, unsigned char *p, size_t n, uint32_t o) { int r; struct iovec v[2]; struct TlsBio *bio = ctx; if ((r = TlsFlush(bio, 0, 0)) < 0) return r; if (bio->a < bio->b) { r = MIN(n, bio->b - bio->a); memcpy(p, bio->t + bio->a, r); if ((bio->a += r) == bio->b) bio->a = bio->b = 0; return r; } v[0].iov_base = p; v[0].iov_len = n; v[1].iov_base = bio->t; v[1].iov_len = sizeof(bio->t); while ((r = readv(bio->fd, v, 2)) == -1) { if (errno == EINTR) { errno = 0; return MBEDTLS_ERR_SSL_WANT_READ; } else if (errno == EAGAIN) { errno = 0; return MBEDTLS_ERR_SSL_TIMEOUT; } else if (errno == EPIPE || errno == ECONNRESET || errno == ENETRESET) { return MBEDTLS_ERR_NET_CONN_RESET; } else { WARNF("(ssl) tls read() error: %m"); return MBEDTLS_ERR_NET_RECV_FAILED; } } if (r > n) bio->b = r - n; return MIN(n, r); } static int TlsRecv(void *ctx, unsigned char *buf, size_t len, uint32_t tmo) { int rc; if (oldin.n) { rc = MIN(oldin.n, len); memcpy(buf, oldin.p, rc); oldin.p += rc; oldin.n -= rc; return rc; } return TlsRecvImpl(ctx, buf, len, tmo); } static ssize_t SslRead(int fd, void *buf, size_t size) { int rc; rc = mbedtls_ssl_read(&ssl, buf, size); if (!rc) { errno = ECONNRESET; rc = -1; } else if (rc < 0) { if (rc == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) { rc = 0; } else if (rc == MBEDTLS_ERR_NET_CONN_RESET || rc == MBEDTLS_ERR_SSL_TIMEOUT) { errno = ECONNRESET; rc = -1; } else if (rc == MBEDTLS_ERR_SSL_WANT_READ) { errno = EINTR; rc = -1; errno = 0; } else if (rc == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE) { WARNF("(ssl) %s SslRead error -0x%04x (%s)", DescribeClient(), -rc, "fatal alert message"); errno = EIO; rc = -1; } else if (rc == MBEDTLS_ERR_SSL_INVALID_RECORD) { WARNF("(ssl) %s SslRead error -0x%04x (%s)", DescribeClient(), -rc, "invalid record"); errno = EIO; rc = -1; } else if (rc == MBEDTLS_ERR_SSL_INVALID_MAC) { WARNF("(ssl) %s SslRead error -0x%04x (%s)", DescribeClient(), -rc, "hmac verification failed"); errno = EIO; rc = -1; } else { WARNF("(ssl) %s SslRead error -0x%04x", DescribeClient(), -rc); errno = EIO; rc = -1; } } return rc; } static ssize_t SslWrite(int fd, struct iovec *iov, int iovlen) { int i; size_t n; ssize_t rc; const unsigned char *p; for (i = 0; i < iovlen; ++i) { p = iov[i].iov_base; n = iov[i].iov_len; while (n) { if ((rc = mbedtls_ssl_write(&ssl, p, n)) > 0) { p += rc; n -= rc; } else if (rc == MBEDTLS_ERR_NET_CONN_RESET) { errno = ECONNRESET; return -1; } else if (rc == MBEDTLS_ERR_SSL_TIMEOUT) { errno = ETIMEDOUT; return -1; } else { WARNF("(ssl) %s SslWrite error -0x%04x", DescribeClient(), -rc); errno = EIO; return -1; } } } return 0; } static void NotifyClose(void) { #ifndef UNSECURE if (usingssl) { DEBUGF("(ssl) SSL notifying close"); mbedtls_ssl_close_notify(&ssl); } #endif } static void WipeSigningKeys(void) { size_t i; if (uniprocess) return; for (i = 0; i < certs.n; ++i) { if (!certs.p[i].key) continue; if (!certs.p[i].cert) continue; if (!certs.p[i].cert->ca_istrue) continue; mbedtls_pk_free(certs.p[i].key); Free(&certs.p[i].key); } } static void PsksDestroy(void) { size_t i; for (i = 0; i < psks.n; ++i) { mbedtls_platform_zeroize(psks.p[i].key, psks.p[i].key_len); free(psks.p[i].key); free(psks.p[i].identity); } Free(&psks.p); psks.n = 0; } static void CertsDestroy(void) { size_t i; // break up certificate chains to prevent double free for (i = 0; i < certs.n; ++i) { if (certs.p[i].cert) { certs.p[i].cert->next = 0; } } for (i = 0; i < certs.n; ++i) { mbedtls_x509_crt_free(certs.p[i].cert); free(certs.p[i].cert); mbedtls_pk_free(certs.p[i].key); free(certs.p[i].key); } Free(&certs.p); certs.n = 0; } static void WipeServingKeys(void) { if (uniprocess) return; mbedtls_ssl_ticket_free(&ssltick); mbedtls_ssl_key_cert_free(conf.key_cert), conf.key_cert = 0; CertsDestroy(); PsksDestroy(); } static bool CertHasCommonName(const mbedtls_x509_crt *cert, const void *s, size_t n) { const mbedtls_x509_name *name; for (name = &cert->subject; name; name = name->next) { if (!MBEDTLS_OID_CMP(MBEDTLS_OID_AT_CN, &name->oid)) { if (SlicesEqualCase(s, n, name->val.p, name->val.len)) { return true; } break; } } return false; } static bool TlsRouteFind(mbedtls_pk_type_t type, mbedtls_ssl_context *ssl, const unsigned char *host, size_t size, int64_t ip) { int i; for (i = 0; i < certs.n; ++i) { if (IsServerCert(certs.p + i, type) && (((certs.p[i].cert->ext_types & MBEDTLS_X509_EXT_SUBJECT_ALT_NAME) && (ip == -1 ? CertHasHost(certs.p[i].cert, host, size) : CertHasIp(certs.p[i].cert, ip))) || CertHasCommonName(certs.p[i].cert, host, size))) { CHECK_EQ( 0, mbedtls_ssl_set_hs_own_cert(ssl, certs.p[i].cert, certs.p[i].key)); DEBUGF("(ssl) TlsRoute(%s, %`'.*s) %s %`'s", mbedtls_pk_type_name(type), size, host, mbedtls_pk_get_name(&certs.p[i].cert->pk), _gc(FormatX509Name(&certs.p[i].cert->subject))); return true; } } return false; } static bool TlsRouteFirst(mbedtls_pk_type_t type, mbedtls_ssl_context *ssl) { int i; for (i = 0; i < certs.n; ++i) { if (IsServerCert(certs.p + i, type)) { CHECK_EQ( 0, mbedtls_ssl_set_hs_own_cert(ssl, certs.p[i].cert, certs.p[i].key)); DEBUGF("(ssl) TlsRoute(%s) %s %`'s", mbedtls_pk_type_name(type), mbedtls_pk_get_name(&certs.p[i].cert->pk), _gc(FormatX509Name(&certs.p[i].cert->subject))); return true; } } return false; } static int TlsRoute(void *ctx, mbedtls_ssl_context *ssl, const unsigned char *host, size_t size) { int64_t ip; bool ok1, ok2; ip = ParseIp((const char *)host, size); ok1 = TlsRouteFind(MBEDTLS_PK_ECKEY, ssl, host, size, ip); ok2 = TlsRouteFind(MBEDTLS_PK_RSA, ssl, host, size, ip); if (!ok1 && !ok2) { WARNF("(ssl) TlsRoute(%`'.*s) not found", size, host); ok1 = TlsRouteFirst(MBEDTLS_PK_ECKEY, ssl); ok2 = TlsRouteFirst(MBEDTLS_PK_RSA, ssl); } return ok1 || ok2 ? 0 : -1; } static int TlsRoutePsk(void *ctx, mbedtls_ssl_context *ssl, const unsigned char *identity, size_t identity_len) { size_t i; for (i = 0; i < psks.n; ++i) { if (SlicesEqual((void *)identity, identity_len, psks.p[i].identity, psks.p[i].identity_len)) { DEBUGF("(ssl) TlsRoutePsk(%`'.*s)", identity_len, identity); mbedtls_ssl_set_hs_psk(ssl, psks.p[i].key, psks.p[i].key_len); // keep track of selected psk to report its identity sslpskindex = i + 1; // use index+1 to check against 0 (when not set) return 0; } } WARNF("(ssl) TlsRoutePsk(%`'.*s) not found", identity_len, identity); return -1; } static bool TlsSetup(void) { int r; oldin.p = inbuf.p; oldin.n = amtread; inbuf.p += amtread; inbuf.n -= amtread; inbuf.c = amtread; amtread = 0; g_bio.fd = client; g_bio.a = 0; g_bio.b = 0; g_bio.c = 0; sslpskindex = 0; for (;;) { if (!(r = mbedtls_ssl_handshake(&ssl)) && TlsFlush(&g_bio, 0, 0) != -1) { LockInc(&shared->c.sslhandshakes); g_bio.c = -1; usingssl = true; reader = SslRead; writer = SslWrite; WipeServingKeys(); VERBOSEF("(ssl) shaken %s %s %s%s %s", DescribeClient(), mbedtls_ssl_get_ciphersuite(&ssl), mbedtls_ssl_get_version(&ssl), ssl.session->compression ? " COMPRESSED" : "", ssl.curve ? ssl.curve->name : "uncurved"); DEBUGF("(ssl) client ciphersuite preference was %s", _gc(FormatSslClientCiphers(&ssl))); return true; } else if (r == MBEDTLS_ERR_SSL_WANT_READ) { LockInc(&shared->c.handshakeinterrupts); if (terminated || killed || IsTakingTooLong()) { return false; } } else { LockInc(&shared->c.sslhandshakefails); mbedtls_ssl_session_reset(&ssl); switch (r) { case MBEDTLS_ERR_SSL_CONN_EOF: DEBUGF("(ssl) %s SSL handshake EOF", DescribeClient()); return false; case MBEDTLS_ERR_NET_CONN_RESET: DEBUGF("(ssl) %s SSL handshake reset", DescribeClient()); return false; case MBEDTLS_ERR_SSL_TIMEOUT: LockInc(&shared->c.ssltimeouts); DEBUGF("(ssl) %s %s", DescribeClient(), "ssltimeouts"); return false; case MBEDTLS_ERR_SSL_NO_CIPHER_CHOSEN: LockInc(&shared->c.sslnociphers); WARNF("(ssl) %s %s %s", DescribeClient(), "sslnociphers", _gc(FormatSslClientCiphers(&ssl))); return false; case MBEDTLS_ERR_SSL_NO_USABLE_CIPHERSUITE: LockInc(&shared->c.sslcantciphers); WARNF("(ssl) %s %s %s", DescribeClient(), "sslcantciphers", _gc(FormatSslClientCiphers(&ssl))); return false; case MBEDTLS_ERR_SSL_BAD_HS_PROTOCOL_VERSION: LockInc(&shared->c.sslnoversion); WARNF("(ssl) %s %s %s", DescribeClient(), "sslnoversion", mbedtls_ssl_get_version(&ssl)); return false; case MBEDTLS_ERR_SSL_INVALID_MAC: LockInc(&shared->c.sslshakemacs); WARNF("(ssl) %s %s", DescribeClient(), "sslshakemacs"); return false; case MBEDTLS_ERR_SSL_NO_CLIENT_CERTIFICATE: LockInc(&shared->c.sslnoclientcert); WARNF("(ssl) %s %s", DescribeClient(), "sslnoclientcert"); NotifyClose(); return false; case MBEDTLS_ERR_X509_CERT_VERIFY_FAILED: LockInc(&shared->c.sslverifyfailed); WARNF("(ssl) %s SSL %s", DescribeClient(), _gc(DescribeSslVerifyFailure( ssl.session_negotiate->verify_result))); return false; case MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE: switch (ssl.fatal_alert) { case MBEDTLS_SSL_ALERT_MSG_CERT_UNKNOWN: LockInc(&shared->c.sslunknowncert); DEBUGF("(ssl) %s %s", DescribeClient(), "sslunknowncert"); return false; case MBEDTLS_SSL_ALERT_MSG_UNKNOWN_CA: LockInc(&shared->c.sslunknownca); DEBUGF("(ssl) %s %s", DescribeClient(), "sslunknownca"); return false; default: WARNF("(ssl) %s SSL shakealert %s", DescribeClient(), GetAlertDescription(ssl.fatal_alert)); return false; } default: WARNF("(ssl) %s SSL handshake failed -0x%04x", DescribeClient(), -r); return false; } } } } static void ConfigureCertificate(mbedtls_x509write_cert *cw, struct Cert *ca, int usage, int type) { int r; const char *s; bool isduplicate; size_t i, j, k, nsan; struct mbedtls_san *san; const struct HostsTxt *htxt; char *name, *subject, *issuer, notbefore[16], notafter[16], hbuf[256]; san = 0; nsan = 0; name = 0; htxt = GetHostsTxt(); strcpy(hbuf, "localhost"); gethostname(hbuf, sizeof(hbuf)); for (i = 0; i < htxt->entries.i; ++i) { for (j = 0; j < ips.n; ++j) { if (IsLoopbackIp(ips.p[j])) continue; if (ips.p[j] == READ32BE(htxt->entries.p[i].ip)) { isduplicate = false; s = htxt->strings.p + htxt->entries.p[i].name; if (!name) name = (void *)s; for (k = 0; k < nsan; ++k) { if (san[k].tag == MBEDTLS_X509_SAN_DNS_NAME && !strcasecmp(s, san[k].val)) { isduplicate = true; break; } } if (!isduplicate) { san = realloc(san, (nsan += 2) * sizeof(*san)); san[nsan - 2].tag = MBEDTLS_X509_SAN_DNS_NAME; san[nsan - 2].val = s; san[nsan - 1].tag = MBEDTLS_X509_SAN_DNS_NAME; san[nsan - 1].val = _gc(xasprintf("*.%s", s)); } } } } for (i = 0; i < ips.n; ++i) { if (IsLoopbackIp(ips.p[i])) continue; san = realloc(san, ++nsan * sizeof(*san)); san[nsan - 1].tag = MBEDTLS_X509_SAN_IP_ADDRESS; san[nsan - 1].ip4 = ips.p[i]; } ChooseCertificateLifetime(notbefore, notafter); subject = xasprintf("CN=%s", name ? name : hbuf); if (ca) { issuer = calloc(1, 1000); CHECK_GT(mbedtls_x509_dn_gets(issuer, 1000, &ca->cert->subject), 0); } else { issuer = strdup(subject); } if ((r = mbedtls_x509write_crt_set_subject_alternative_name(cw, san, nsan)) || (r = mbedtls_x509write_crt_set_validity(cw, notbefore, notafter)) || (r = mbedtls_x509write_crt_set_basic_constraints(cw, false, -1)) || #if defined(MBEDTLS_SHA1_C) (r = mbedtls_x509write_crt_set_subject_key_identifier(cw)) || (r = mbedtls_x509write_crt_set_authority_key_identifier(cw)) || #endif (r = mbedtls_x509write_crt_set_key_usage(cw, usage)) || (r = mbedtls_x509write_crt_set_ext_key_usage(cw, type)) || (r = mbedtls_x509write_crt_set_subject_name(cw, subject)) || (r = mbedtls_x509write_crt_set_issuer_name(cw, issuer))) { FATALF("(ssl) configure certificate (grep -0x%04x)", -r); } free(subject); free(issuer); free(san); } static struct Cert GetKeySigningKey(void) { size_t i; for (i = 0; i < certs.n; ++i) { if (!certs.p[i].key) continue; if (!certs.p[i].cert) continue; if (!certs.p[i].cert->ca_istrue) continue; if (mbedtls_x509_crt_check_key_usage(certs.p[i].cert, MBEDTLS_X509_KU_KEY_CERT_SIGN)) { continue; } return certs.p[i]; } return (struct Cert){0}; } static struct Cert GenerateEcpCertificate(struct Cert *ca) { mbedtls_pk_context *key; mbedtls_md_type_t md_alg; mbedtls_x509write_cert wcert; md_alg = suiteb ? MBEDTLS_MD_SHA384 : MBEDTLS_MD_SHA256; key = InitializeKey(ca, &wcert, md_alg, MBEDTLS_PK_ECKEY); CHECK_EQ(0, mbedtls_ecp_gen_key( suiteb ? MBEDTLS_ECP_DP_SECP384R1 : MBEDTLS_ECP_DP_SECP256R1, mbedtls_pk_ec(*key), GenerateHardRandom, 0)); GenerateCertificateSerial(&wcert); ConfigureCertificate(&wcert, ca, MBEDTLS_X509_KU_DIGITAL_SIGNATURE, MBEDTLS_X509_NS_CERT_TYPE_SSL_SERVER | MBEDTLS_X509_NS_CERT_TYPE_SSL_CLIENT); return FinishCertificate(ca, &wcert, key); } static struct Cert GenerateRsaCertificate(struct Cert *ca) { mbedtls_pk_context *key; mbedtls_md_type_t md_alg; mbedtls_x509write_cert wcert; md_alg = suiteb ? MBEDTLS_MD_SHA384 : MBEDTLS_MD_SHA256; key = InitializeKey(ca, &wcert, md_alg, MBEDTLS_PK_RSA); CHECK_EQ(0, mbedtls_rsa_gen_key(mbedtls_pk_rsa(*key), GenerateHardRandom, 0, suiteb ? 4096 : 2048, 65537)); GenerateCertificateSerial(&wcert); ConfigureCertificate( &wcert, ca, MBEDTLS_X509_KU_DIGITAL_SIGNATURE | MBEDTLS_X509_KU_KEY_ENCIPHERMENT, MBEDTLS_X509_NS_CERT_TYPE_SSL_SERVER | MBEDTLS_X509_NS_CERT_TYPE_SSL_CLIENT); return FinishCertificate(ca, &wcert, key); } static void LoadCertificates(void) { size_t i; struct Cert ksk, ecp, rsa; bool havecert, haveclientcert; havecert = false; haveclientcert = false; for (i = 0; i < certs.n; ++i) { if (certs.p[i].key && certs.p[i].cert && !certs.p[i].cert->ca_istrue && !mbedtls_x509_crt_check_key_usage(certs.p[i].cert, MBEDTLS_X509_KU_DIGITAL_SIGNATURE)) { if (!mbedtls_x509_crt_check_extended_key_usage( certs.p[i].cert, MBEDTLS_OID_SERVER_AUTH, MBEDTLS_OID_SIZE(MBEDTLS_OID_SERVER_AUTH))) { LogCertificate("using server certificate", certs.p[i].cert); UseCertificate(&conf, certs.p + i, "server"); havecert = true; } if (!mbedtls_x509_crt_check_extended_key_usage( certs.p[i].cert, MBEDTLS_OID_CLIENT_AUTH, MBEDTLS_OID_SIZE(MBEDTLS_OID_CLIENT_AUTH))) { LogCertificate("using client certificate", certs.p[i].cert); UseCertificate(&confcli, certs.p + i, "client"); haveclientcert = true; } } } if (!havecert && (!psks.n || ksk.key)) { if ((ksk = GetKeySigningKey()).key) { DEBUGF("(ssl) generating ssl certificates using %`'s", _gc(FormatX509Name(&ksk.cert->subject))); } else { VERBOSEF("(ssl) could not find non-CA SSL certificate key pair with" " -addext keyUsage=digitalSignature" " -addext extendedKeyUsage=serverAuth"); VERBOSEF("(ssl) could not find CA key signing key pair with" " -addext keyUsage=keyCertSign"); VERBOSEF("(ssl) generating self-signed ssl certificates"); } #ifdef MBEDTLS_ECP_C ecp = GenerateEcpCertificate(ksk.key ? &ksk : 0); if (!havecert) UseCertificate(&conf, &ecp, "server"); if (!haveclientcert && ksk.key) { UseCertificate(&confcli, &ecp, "client"); } AppendCert(ecp.cert, ecp.key); #endif #ifdef MBEDTLS_RSA_C if (!norsagen) { rsa = GenerateRsaCertificate(ksk.key ? &ksk : 0); if (!havecert) UseCertificate(&conf, &rsa, "server"); if (!haveclientcert && ksk.key) { UseCertificate(&confcli, &rsa, "client"); } AppendCert(rsa.cert, rsa.key); } #endif } WipeSigningKeys(); } static bool ClientAcceptsGzip(void) { return cpm.msg.version >= 10 && /* RFC1945 § 3.5 */ HeaderHas(&cpm.msg, inbuf.p, kHttpAcceptEncoding, "gzip", 4); } char *FormatUnixHttpDateTime(char *s, int64_t t) { struct tm tm; gmtime_r(&t, &tm); FormatHttpDateTime(s, &tm); return s; } static void UpdateCurrentDate(struct timespec now) { int64_t t; struct tm tm; t = now.tv_sec; shared->nowish = now; gmtime_r(&t, &tm); FormatHttpDateTime(shared->currentdate, &tm); } static int64_t GetGmtOffset(int64_t t) { struct tm tm; localtime_r(&t, &tm); return tm.tm_gmtoff; } forceinline bool IsCompressed(struct Asset *a) { return !a->file && ZIP_LFILE_COMPRESSIONMETHOD(zmap + a->lf) == kZipCompressionDeflate; } forceinline int GetMode(struct Asset *a) { return a->file ? a->file->st.st_mode : GetZipCfileMode(zmap + a->cf); } forceinline bool IsCompressionMethodSupported(int method) { return method == kZipCompressionNone || method == kZipCompressionDeflate; } static inline unsigned Hash(const void *p, unsigned long n) { unsigned h, i; for (h = i = 0; i < n; i++) { h += ((unsigned char *)p)[i]; h *= 0x9e3779b1; } return MAX(1, h); } static void FreeAssets(void) { size_t i; for (i = 0; i < assets.n; ++i) { Free(&assets.p[i].lastmodifiedstr); } Free(&assets.p); assets.n = 0; } static void FreeStrings(struct Strings *l) { size_t i; for (i = 0; i < l->n; ++i) { Free(&l->p[i].s); } Free(&l->p); l->n = 0; } static unsigned long roundup2pow(unsigned long x) { return x > 1 ? 2ul << _bsrl(x - 1) : x ? 1 : 0; } static void IndexAssets(void) { uint64_t cf; struct Asset *p; struct timespec lm; uint32_t i, n, m, step, hash; DEBUGF("(zip) indexing assets (inode %#lx)", zst.st_ino); FreeAssets(); CHECK_GE(HASH_LOAD_FACTOR, 2); CHECK(READ32LE(zcdir) == kZipCdir64HdrMagic || READ32LE(zcdir) == kZipCdirHdrMagic); n = GetZipCdirRecords(zcdir); m = roundup2pow(MAX(1, n) * HASH_LOAD_FACTOR); p = xcalloc(m, sizeof(struct Asset)); for (cf = GetZipCdirOffset(zcdir); n--; cf += ZIP_CFILE_HDRSIZE(zmap + cf)) { CHECK_EQ(kZipCfileHdrMagic, ZIP_CFILE_MAGIC(zmap + cf)); if (!IsCompressionMethodSupported(ZIP_CFILE_COMPRESSIONMETHOD(zmap + cf))) { WARNF("(zip) don't understand zip compression method %d used by %`'.*s", ZIP_CFILE_COMPRESSIONMETHOD(zmap + cf), ZIP_CFILE_NAMESIZE(zmap + cf), ZIP_CFILE_NAME(zmap + cf)); continue; } 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); GetZipCfileTimestamps(zmap + cf, &lm, 0, 0, gmtoff); p[i].hash = hash; p[i].cf = cf; p[i].lf = GetZipCfileOffset(zmap + cf); p[i].istext = !!(ZIP_CFILE_INTERNALATTRIBUTES(zmap + cf) & kZipIattrText); p[i].lastmodified = lm.tv_sec; p[i].lastmodifiedstr = FormatUnixHttpDateTime(xmalloc(30), lm.tv_sec); } assets.p = p; assets.n = m; } static bool OpenZip(bool force) { int fd; size_t n; uint8_t *m, *d; struct stat st; if (stat(zpath, &st) != -1) { if (force || st.st_ino != zst.st_ino || st.st_size > zst.st_size) { if (st.st_ino == zst.st_ino) { fd = zfd; } else if ((fd = open(zpath, O_RDWR)) == -1) { WARNF("(zip) open() error: %m"); return false; } if ((m = mmap(0, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) != MAP_FAILED) { n = st.st_size; if ((d = GetZipEocd(m, n, 0))) { if (zmap) { LOGIFNEG1(munmap(zmap, zsize)); } zmap = m; zsize = n; zcdir = d; DCHECK(IsZipEocd32(zmap, zsize, zcdir - zmap) == kZipOk || IsZipEocd64(zmap, zsize, zcdir - zmap) == kZipOk); memcpy(&zst, &st, sizeof(st)); IndexAssets(); return true; } else { WARNF("(zip) couldn't locate central directory"); } } else { WARNF("(zip) mmap() error: %m"); } } } else { // avoid noise if we setuid to user who can't see executable if (errno == EACCES) { VERBOSEF("(zip) stat(%`'s) error: %m", zpath); } else { WARNF("(zip) stat(%`'s) error: %m", zpath); } } return false; } static struct Asset *GetAssetZip(const char *path, size_t pathlen) { uint32_t i, step, hash; if (pathlen > 1 && 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(zmap + assets.p[i].cf) && memcmp(path, ZIP_CFILE_NAME(zmap + assets.p[i].cf), pathlen) == 0) { return &assets.p[i]; } } } static struct Asset *GetAssetFile(const char *path, size_t pathlen) { size_t i; 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) { LockInc(&shared->c.stats); a->file->path.s = FreeLater(MergePaths(stagedirs.p[i].s, stagedirs.p[i].n, path, pathlen, &a->file->path.n)); if (stat(a->file->path.s, &a->file->st) != -1) { a->lastmodifiedstr = FormatUnixHttpDateTime( FreeLater(xmalloc(30)), (a->lastmodified = a->file->st.st_mtim.tv_sec)); return a; } else { LockInc(&shared->c.statfails); } } } return NULL; } static struct Asset *GetAsset(const char *path, size_t pathlen) { struct Asset *a; if (!(a = GetAssetFile(path, pathlen))) { if (!(a = GetAssetZip(path, pathlen))) { if (pathlen > 1 && path[pathlen - 1] != '/' && pathlen + 1 <= sizeof(slashpath)) { memcpy(mempcpy(slashpath, path, pathlen), "/", 1); a = GetAssetZip(slashpath, pathlen + 1); } } } return a; } static char *AppendHeader(char *p, const char *k, const char *v) { if (!v) return p; return AppendCrlf(stpcpy(stpcpy(stpcpy(p, k), ": "), v)); } static char *AppendContentType(char *p, const char *ct) { p = stpcpy(p, "Content-Type: "); p = stpcpy(p, ct); if ((cpm.istext = startswith(ct, "text/"))) { if (!strchr(ct + 5, ';')) { p = stpcpy(p, "; charset=utf-8"); } if (!cpm.referrerpolicy && startswith(ct + 5, "html")) { cpm.referrerpolicy = "no-referrer-when-downgrade"; } } cpm.hascontenttype = true; return AppendCrlf(p); } static char *AppendExpires(char *p, int64_t t) { struct tm tm; gmtime_r(&t, &tm); p = stpcpy(p, "Expires: "); p = FormatHttpDateTime(p, &tm); return AppendCrlf(p); } static char *AppendCache(char *p, int64_t seconds, char *directive) { if (seconds < 0) return p; p = stpcpy(p, "Cache-Control: max-age="); p = FormatUint64(p, seconds); if (!seconds) { p = stpcpy(p, ", no-store"); } else if (directive && *directive) { p = stpcpy(p, ", "); p = stpcpy(p, directive); } p = AppendCrlf(p); return AppendExpires(p, shared->nowish.tv_sec + seconds); } static inline char *AppendContentLength(char *p, size_t n) { p = stpcpy(p, "Content-Length: "); p = FormatUint64(p, n); return AppendCrlf(p); } static char *AppendContentRange(char *p, long a, long b, long c) { p = stpcpy(p, "Content-Range: bytes "); if (a >= 0 && b > 0) { p = FormatUint64(p, a); *p++ = '-'; p = FormatUint64(p, a + b - 1); } else { *p++ = '*'; } *p++ = '/'; p = FormatUint64(p, c); return AppendCrlf(p); } static bool Inflate(void *dp, size_t dn, const void *sp, size_t sn) { LockInc(&shared->c.inflates); return !__inflate(dp, dn, sp, sn); } static bool Verify(void *data, size_t size, uint32_t crc) { uint32_t got; LockInc(&shared->c.verifies); if (crc == (got = crc32_z(0, data, size))) { return true; } else { LockInc(&shared->c.thiscorruption); WARNF("(zip) corrupt zip file at %`'.*s had crc 0x%08x but expected 0x%08x", cpm.msg.uri.b - cpm.msg.uri.a, inbuf.p + cpm.msg.uri.a, got, crc); return false; } } static void *Deflate(const void *data, size_t size, size_t *out_size) { void *res; z_stream zs = {0}; LockInc(&shared->c.deflates); CHECK_EQ(Z_OK, deflateInit2(&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 (S_ISDIR(GetMode(a))) { WARNF("(srvr) can't load directory"); return NULL; } if (!a->file) { size = GetZipLfileUncompressedSize(zmap + a->lf); if (size == SIZE_MAX || !(data = malloc(size + 1))) return NULL; if (IsCompressed(a)) { if (!Inflate(data, size, ZIP_LFILE_CONTENT(zmap + a->lf), GetZipCfileCompressedSize(zmap + a->cf))) { free(data); return NULL; } } else { memcpy(data, ZIP_LFILE_CONTENT(zmap + a->lf), size); } if (!Verify(data, size, ZIP_LFILE_CRC32(zmap + a->lf))) { free(data); return NULL; } data[size] = '\0'; if (out_size) *out_size = size; return data; } else { LockInc(&shared->c.slurps); return xslurp(a->file->path.s, out_size); } } static wontreturn void PrintUsage(int fd, int rc) { size_t n; const char *p; struct Asset *a; if (!(a = GetAssetZip("/help.txt", 9)) || !(p = LoadAsset(a, &n))) { fprintf(stderr, "error: /help.txt is not a zip asset\n"); exit(1); } if (IsTiny()) { write(fd, p, strlen(p)); } else { __paginate(fd, p); } exit(rc); } static void AppendLogo(void) { size_t n; char *p, *q; struct Asset *a; if ((a = GetAsset("/redbean.png", 12)) && (p = LoadAsset(a, &n))) { if ((q = EncodeBase64(p, n, &n))) { appends(&cpm.outbuf, "\"[logo]\"\r\n"); free(q); } free(p); } } static ssize_t Send(struct iovec *iov, int iovlen) { ssize_t rc; if ((rc = writer(client, iov, iovlen)) == -1) { if (errno == ECONNRESET) { LockInc(&shared->c.writeresets); DEBUGF("(rsp) %s write reset", DescribeClient()); } else if (errno == EAGAIN) { LockInc(&shared->c.writetimeouts); WARNF("(rsp) %s write timeout", DescribeClient()); errno = 0; } else { LockInc(&shared->c.writeerrors); if (errno == EBADF) { // don't warn on close/bad fd DEBUGF("(rsp) %s write badf", DescribeClient()); } else { WARNF("(rsp) %s write error: %m", DescribeClient()); } } connectionclose = true; } return rc; } static bool IsSslCompressed(void) { return usingssl && ssl.session->compression; } static char *CommitOutput(char *p) { uint32_t crc; size_t outbuflen; if (!cpm.contentlength) { outbuflen = appendz(cpm.outbuf).i; if (cpm.istext && !cpm.isyielding && outbuflen >= 100) { if (!IsTiny() && !IsSslCompressed()) { p = stpcpy(p, "Vary: Accept-Encoding\r\n"); } if (!IsTiny() && // !IsSslCompressed() && // ClientAcceptsGzip() && // !ShouldAvoidGzip()) { cpm.gzipped = outbuflen; crc = crc32_z(0, cpm.outbuf, outbuflen); WRITE32LE(gzip_footer + 0, crc); WRITE32LE(gzip_footer + 4, outbuflen); cpm.content = FreeLater(Deflate(cpm.outbuf, outbuflen, &cpm.contentlength)); DropOutput(); } else { UseOutput(); } } else { UseOutput(); } } else { DropOutput(); } return p; } static char *ServeDefaultErrorPage(char *p, unsigned code, const char *reason, const char *details) { p = AppendContentType(p, "text/html; charset=ISO-8859-1"); reason = FreeLater(EscapeHtml(reason, -1, 0)); appends(&cpm.outbuf, "\ \r\n\ "); appendf(&cpm.outbuf, "%d %s", code, reason); appends(&cpm.outbuf, "\ \r\n\ \r\n\

\r\n"); AppendLogo(); appendf(&cpm.outbuf, "%d %s\r\n", code, reason); appends(&cpm.outbuf, "

\r\n"); if (details) { appendf(&cpm.outbuf, "
%s
\r\n", FreeLater(EscapeHtml(details, -1, 0))); } UseOutput(); return p; } static char *ServeErrorImpl(unsigned code, const char *reason, const char *details) { size_t n; char *p, *s; struct Asset *a; LockInc(&shared->c.errors); DropOutput(); p = SetStatus(code, reason); s = xasprintf("/%d.html", code); a = GetAsset(s, strlen(s)); free(s); if (!a) { return ServeDefaultErrorPage(p, code, reason, details); } else if (a->file) { LockInc(&shared->c.slurps); cpm.content = FreeLater(xslurp(a->file->path.s, &cpm.contentlength)); return AppendContentType(p, "text/html; charset=utf-8"); } else { cpm.content = (char *)ZIP_LFILE_CONTENT(zmap + a->lf); cpm.contentlength = GetZipCfileCompressedSize(zmap + a->cf); if (IsCompressed(a)) { n = GetZipLfileUncompressedSize(zmap + a->lf); if ((s = FreeLater(malloc(n))) && Inflate(s, n, cpm.content, cpm.contentlength)) { cpm.content = s; cpm.contentlength = n; } else { return ServeDefaultErrorPage(p, code, reason, details); } } if (Verify(cpm.content, cpm.contentlength, ZIP_LFILE_CRC32(zmap + a->lf))) { return AppendContentType(p, "text/html; charset=utf-8"); } else { return ServeDefaultErrorPage(p, code, reason, details); } } } static char *ServeErrorWithPath(unsigned code, const char *reason, const char *path, size_t pathlen) { ERRORF("(srvr) server error: %d %s %`'.*s", code, reason, pathlen, path); return ServeErrorImpl(code, reason, NULL); } static char *ServeErrorWithDetail(unsigned code, const char *reason, const char *details) { ERRORF("(srvr) server error: %d %s", code, reason); return ServeErrorImpl(code, reason, details); } static char *ServeError(unsigned code, const char *reason) { return ServeErrorWithDetail(code, reason, NULL); } static char *ServeFailure(unsigned code, const char *reason) { ERRORF("(srvr) failure: %d %s %s HTTP%02d %.*s %`'.*s %`'.*s %`'.*s %`'.*s", code, reason, DescribeClient(), cpm.msg.version, cpm.msg.xmethod.b - cpm.msg.xmethod.a, inbuf.p + cpm.msg.xmethod.a, HeaderLength(kHttpHost), HeaderData(kHttpHost), cpm.msg.uri.b - cpm.msg.uri.a, inbuf.p + cpm.msg.uri.a, HeaderLength(kHttpReferer), HeaderData(kHttpReferer), HeaderLength(kHttpUserAgent), HeaderData(kHttpUserAgent)); return ServeErrorImpl(code, reason, NULL); } static ssize_t YieldGenerator(struct iovec v[3]) { int nresults, status; if (cpm.isyielding > 1) { do { if (!YL || lua_status(YL) != LUA_YIELD) return 0; // done yielding cpm.contentlength = 0; status = lua_resume(YL, NULL, 0, &nresults); if (status != LUA_OK && status != LUA_YIELD) { LogLuaError("resume", lua_tostring(YL, -1)); lua_pop(YL, 1); return -1; } lua_pop(YL, nresults); if (!cpm.contentlength) UseOutput(); // continue yielding if nothing to return to keep generator running } while (!cpm.contentlength); } DEBUGF("(lua) yielded with %ld bytes generated", cpm.contentlength); cpm.isyielding++; v[0].iov_base = cpm.content; v[0].iov_len = cpm.contentlength; return cpm.contentlength; } static void OnLuaServerPageCtrlc(int i) { lua_sigint(GL, i); } static int LuaCallWithYield(lua_State *L) { int status; // since yield may happen in OnHttpRequest and in ServeLua, // need to fully restart the yield generator; // the second set of headers is not going to be sent struct sigaction sa, saold; lua_State *co = lua_newthread(L); if (__ttyconf.replmode) { sa.sa_flags = SA_RESETHAND; sa.sa_handler = OnLuaServerPageCtrlc; sigemptyset(&sa.sa_mask); sigaction(SIGINT, &sa, &saold); } status = LuaCallWithTrace(L, 0, 0, co); if (__ttyconf.replmode) { sigaction(SIGINT, &saold, 0); } if (status == LUA_YIELD) { CHECK_GT(lua_gettop(L), 0); // make sure that coroutine is anchored YL = co; cpm.generator = YieldGenerator; if (!cpm.isyielding) cpm.isyielding = 1; status = LUA_OK; } return status; } static ssize_t DeflateGenerator(struct iovec v[3]) { int i, rc; size_t no; i = 0; if (!dg.t) { v[0].iov_base = (void *)kGzipHeader; v[0].iov_len = sizeof(kGzipHeader); ++dg.t; ++i; } else if (dg.t == 3) { return 0; } if (dg.t != 2) { CHECK_EQ(0, dg.s.avail_in); dg.s.next_in = (void *)(cpm.content + dg.i); dg.s.avail_in = MIN(dg.z, cpm.contentlength - dg.i); dg.c = crc32_z(dg.c, dg.s.next_in, dg.s.avail_in); dg.i += dg.s.avail_in; } dg.s.next_out = dg.b; dg.s.avail_out = dg.z; no = dg.s.avail_in; rc = deflate(&dg.s, dg.i < cpm.contentlength ? Z_SYNC_FLUSH : Z_FINISH); if (rc != Z_OK && rc != Z_STREAM_END) { DIEF("(zip) deflate()→%d oldin:%,zu/%,zu in:%,zu/%,zu out:%,zu/%,zu", rc, no, dg.z, dg.s.avail_in, dg.z, dg.s.avail_out, dg.z); } else { NOISEF("(zip) deflate()→%d oldin:%,zu/%,zu in:%,zu/%,zu out:%,zu/%,zu", rc, no, dg.z, dg.s.avail_in, dg.z, dg.s.avail_out, dg.z); } no = dg.z - dg.s.avail_out; if (no) { v[i].iov_base = dg.b; v[i].iov_len = no; ++i; } if (rc == Z_OK) { CHECK_GT(no, 0); if (dg.s.avail_out) { dg.t = 1; } else { dg.t = 2; } } else if (rc == Z_STREAM_END) { CHECK_EQ(cpm.contentlength, dg.i); CHECK_EQ(Z_OK, deflateEnd(&dg.s)); WRITE32LE(gzip_footer + 0, dg.c); WRITE32LE(gzip_footer + 4, cpm.contentlength); v[i].iov_base = gzip_footer; v[i].iov_len = sizeof(gzip_footer); dg.t = 3; } return v[0].iov_len + v[1].iov_len + v[2].iov_len; } static char *ServeAssetCompressed(struct Asset *a) { char *p; LockInc(&shared->c.deflates); LockInc(&shared->c.compressedresponses); DEBUGF("(srvr) ServeAssetCompressed()"); dg.t = 0; dg.i = 0; dg.c = 0; if (usingssl) { dg.z = 512 + (_rand64() & 1023); } else { dg.z = 65536; } cpm.gzipped = -1; // signal generator usage with the exact size unknown cpm.generator = DeflateGenerator; bzero(&dg.s, sizeof(dg.s)); CHECK_EQ(Z_OK, deflateInit2(&dg.s, 4, Z_DEFLATED, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY)); dg.b = FreeLater(malloc(dg.z)); p = SetStatus(200, "OK"); p = stpcpy(p, "Content-Encoding: gzip\r\n"); return p; } static ssize_t InflateGenerator(struct iovec v[3]) { int i, rc; size_t no; i = 0; if (!dg.t) { ++dg.t; } else if (dg.t == 3) { return 0; } if (dg.t != 2) { CHECK_EQ(0, dg.s.avail_in); dg.s.next_in = (void *)(cpm.content + dg.i); dg.s.avail_in = MIN(dg.z, cpm.contentlength - dg.i); dg.i += dg.s.avail_in; } dg.s.next_out = dg.b; dg.s.avail_out = dg.z; rc = inflate(&dg.s, Z_NO_FLUSH); if (rc != Z_OK && rc != Z_STREAM_END) DIEF("(zip) inflate()→%d", rc); no = dg.z - dg.s.avail_out; if (no) { v[i].iov_base = dg.b; v[i].iov_len = no; dg.c = crc32_z(dg.c, dg.b, no); ++i; } if (rc == Z_OK) { CHECK_GT(no, 0); dg.t = dg.s.avail_out ? 1 : 2; } else if (rc == Z_STREAM_END) { CHECK_EQ(Z_OK, inflateEnd(&dg.s)); CHECK_EQ(ZIP_CFILE_CRC32(zmap + dg.a->cf), dg.c); dg.t = 3; } return v[0].iov_len + v[1].iov_len + v[2].iov_len; } static char *ServeAssetDecompressed(struct Asset *a) { char *p; size_t size; LockInc(&shared->c.inflates); LockInc(&shared->c.decompressedresponses); size = GetZipCfileUncompressedSize(zmap + a->cf); DEBUGF("(srvr) ServeAssetDecompressed(%ld)→%ld", cpm.contentlength, size); if (cpm.msg.method == kHttpHead) { cpm.content = 0; cpm.contentlength = size; return SetStatus(200, "OK"); } else if (!IsTiny()) { dg.t = 0; dg.i = 0; dg.c = 0; dg.a = a; dg.z = 65536; CHECK_EQ(Z_OK, inflateInit2(&dg.s, -MAX_WBITS)); cpm.generator = InflateGenerator; dg.b = FreeLater(malloc(dg.z)); return SetStatus(200, "OK"); } else if ((p = FreeLater(malloc(size))) && Inflate(p, size, cpm.content, cpm.contentlength) && Verify(p, size, ZIP_CFILE_CRC32(zmap + a->cf))) { cpm.content = p; cpm.contentlength = size; return SetStatus(200, "OK"); } else { return ServeError(500, "Internal Server Error"); } } static inline char *ServeAssetIdentity(struct Asset *a, const char *ct) { LockInc(&shared->c.identityresponses); DEBUGF("(srvr) ServeAssetIdentity(%`'s)", ct); return SetStatus(200, "OK"); } static inline char *ServeAssetPrecompressed(struct Asset *a) { size_t size; uint32_t crc; DEBUGF("(srvr) ServeAssetPrecompressed()"); LockInc(&shared->c.precompressedresponses); crc = ZIP_CFILE_CRC32(zmap + a->cf); size = GetZipCfileUncompressedSize(zmap + a->cf); cpm.gzipped = size; WRITE32LE(gzip_footer + 0, crc); WRITE32LE(gzip_footer + 4, size); return SetStatus(200, "OK"); } static char *ServeAssetRange(struct Asset *a) { char *p; long rangestart, rangelength; DEBUGF("(srvr) ServeAssetRange()"); if (ParseHttpRange(HeaderData(kHttpRange), HeaderLength(kHttpRange), cpm.contentlength, &rangestart, &rangelength) && rangestart >= 0 && rangelength >= 0 && rangestart < cpm.contentlength && rangestart + rangelength <= cpm.contentlength) { LockInc(&shared->c.partialresponses); p = SetStatus(206, "Partial Content"); p = AppendContentRange(p, rangestart, rangelength, cpm.contentlength); cpm.content += rangestart; cpm.contentlength = rangelength; return p; } else { LockInc(&shared->c.badranges); WARNF("(client) bad range %`'.*s", HeaderLength(kHttpRange), HeaderData(kHttpRange)); p = SetStatus(416, "Range Not Satisfiable"); p = AppendContentRange(p, -1, -1, cpm.contentlength); cpm.content = ""; cpm.contentlength = 0; return p; } } static char *GetAssetPath(uint8_t *zcf, size_t *out_size) { char *p2; size_t n1, n2; const char *p1; p1 = ZIP_CFILE_NAME(zcf); n1 = ZIP_CFILE_NAMESIZE(zcf); n2 = 1 + n1 + 1; p2 = xmalloc(n2); p2[0] = '/'; memcpy(p2 + 1, p1, n1); p2[1 + n1] = '\0'; if (out_size) *out_size = 1 + n1; return p2; } static bool IsHiddenPath(const char *s, size_t n) { size_t i; for (i = 0; i < hidepaths.n; ++i) { if (n >= hidepaths.p[i].n && !memcmp(s, hidepaths.p[i].s, hidepaths.p[i].n)) { return true; } } return false; } static char *GetBasicAuthorization(size_t *z) { size_t n; const char *p, *q; struct HttpSlice *g; g = cpm.msg.headers + (HasHeader(kHttpProxyAuthorization) ? kHttpProxyAuthorization : kHttpAuthorization); p = inbuf.p + g->a; n = g->b - g->a; if ((q = memchr(p, ' ', n)) && SlicesEqualCase(p, q - p, "Basic", 5)) { return DecodeBase64(q + 1, n - (q + 1 - p), z); } else { return NULL; } } static const char *GetSystemUrlLauncherCommand(void) { if (IsWindows()) { return "explorer.exe"; } else if (IsXnu()) { return "open"; } else { return "xdg-open"; } } static void LaunchBrowser(const char *path) { int pid, ws; struct in_addr addr; const char *u, *prog; sigset_t chldmask, savemask; struct sigaction ignore, saveint, savequit; uint16_t port = 80; path = firstnonnull(path, "/"); // use the first server address if there is at least one server if (servers.n) { addr = servers.p[0].addr.sin_addr; port = ntohs(servers.p[0].addr.sin_port); } // assign a loopback address if no server or unknown server address if (!servers.n || !addr.s_addr) addr.s_addr = htonl(INADDR_LOOPBACK); if (*path != '/') path = _gc(xasprintf("/%s", path)); if ((prog = commandv(GetSystemUrlLauncherCommand(), _gc(malloc(PATH_MAX)), PATH_MAX))) { u = _gc(xasprintf("http://%s:%d%s", inet_ntoa(addr), port, path)); DEBUGF("(srvr) opening browser with command %`'s %s", prog, u); ignore.sa_flags = 0; ignore.sa_handler = SIG_IGN; sigemptyset(&ignore.sa_mask); sigaction(SIGINT, &ignore, &saveint); sigaction(SIGQUIT, &ignore, &savequit); sigemptyset(&chldmask); sigaddset(&chldmask, SIGCHLD); sigprocmask(SIG_BLOCK, &chldmask, &savemask); CHECK_NE(-1, (pid = fork())); if (!pid) { setpgrp(); // ctrl-c'ing redbean shouldn't kill browser sigaction(SIGINT, &saveint, 0); sigaction(SIGQUIT, &savequit, 0); sigprocmask(SIG_SETMASK, &savemask, 0); execv(prog, (char *const[]){(char *)prog, (char *)u, 0}); _Exit(127); } while (wait4(pid, &ws, 0, 0) == -1) { CHECK_EQ(EINTR, errno); errno = 0; } sigaction(SIGINT, &saveint, 0); sigaction(SIGQUIT, &savequit, 0); sigprocmask(SIG_SETMASK, &savemask, 0); if (!(WIFEXITED(ws) && WEXITSTATUS(ws) == 0)) { WARNF("(srvr) command %`'s exited with %d", GetSystemUrlLauncherCommand(), WIFEXITED(ws) ? WEXITSTATUS(ws) : 128 + WEXITSTATUS(ws)); } } else { WARNF("(srvr) can't launch browser because %`'s isn't installed", GetSystemUrlLauncherCommand()); } } static char *BadMethod(void) { LockInc(&shared->c.badmethods); return stpcpy(ServeError(405, "Method Not Allowed"), "Allow: GET, HEAD\r\n"); } static int GetDecimalWidth(long x) { return LengthInt64Thousands(x); } static int GetOctalWidth(int x) { return !x ? 1 : x < 8 ? 2 : 1 + _bsr(x) / 3; } static const char *DescribeCompressionRatio(char rb[8], uint8_t *zcf) { long percent; if (ZIP_CFILE_COMPRESSIONMETHOD(zcf) == kZipCompressionNone) { return "n/a"; } else { percent = lround(100 - (double)GetZipCfileCompressedSize(zcf) / GetZipCfileUncompressedSize(zcf) * 100); sprintf(rb, "%ld%%", MIN(999, MAX(-999, percent))); return rb; } } static char *ServeListing(void) { long x; ldiv_t y; int w[3]; uint8_t *zcf; struct tm tm; char *p, *path; const char *and; struct timespec lastmod; size_t n, pathlen, rn[6]; char rb[8], tb[20], *rp[6]; LockInc(&shared->c.listingrequests); if (cpm.msg.method != kHttpGet && cpm.msg.method != kHttpHead) return BadMethod(); appends(&cpm.outbuf, "\ \r\n\ \r\n\ redbean zip listing\r\n\ \r\n\

\r\n"); AppendLogo(); rp[0] = EscapeHtml(brand, -1, &rn[0]); appendd(&cpm.outbuf, rp[0], rn[0]); free(rp[0]); appendf(&cpm.outbuf, "

\r\n" "
%.*s
\r\n" "
\r\n" "
\r\n" "
\r\n",
          strnlen(GetZipCdirComment(zcdir), GetZipCdirCommentSize(zcdir)),
          GetZipCdirComment(zcdir));
  bzero(w, sizeof(w));
  n = GetZipCdirRecords(zcdir);
  for (zcf = zmap + GetZipCdirOffset(zcdir); n--;
       zcf += ZIP_CFILE_HDRSIZE(zcf)) {
    CHECK_EQ(kZipCfileHdrMagic, ZIP_CFILE_MAGIC(zcf));
    path = GetAssetPath(zcf, &pathlen);
    if (!IsHiddenPath(path, pathlen)) {
      w[0] = min(80, max(w[0], strwidth(path, 0) + 2));
      w[1] = max(w[1], GetOctalWidth(GetZipCfileMode(zcf)));
      w[2] = max(w[2], GetDecimalWidth(GetZipCfileUncompressedSize(zcf)));
    }
    free(path);
  }
  n = GetZipCdirRecords(zcdir);
  for (zcf = zmap + GetZipCdirOffset(zcdir); n--;
       zcf += ZIP_CFILE_HDRSIZE(zcf)) {
    CHECK_EQ(kZipCfileHdrMagic, ZIP_CFILE_MAGIC(zcf));
    path = GetAssetPath(zcf, &pathlen);
    if (!IsHiddenPath(path, pathlen)) {
      rp[0] = VisualizeControlCodes(path, pathlen, &rn[0]);
      rp[1] = EscapePath(path, pathlen, &rn[1]);
      rp[2] = EscapeHtml(rp[1], rn[1], &rn[2]);
      rp[3] = VisualizeControlCodes(
          ZIP_CFILE_COMMENT(zcf),
          strnlen(ZIP_CFILE_COMMENT(zcf), ZIP_CFILE_COMMENTSIZE(zcf)), &rn[3]);
      rp[4] = EscapeHtml(rp[0], rn[0], &rn[4]);
      GetZipCfileTimestamps(zcf, &lastmod, 0, 0, gmtoff);
      localtime_r(&lastmod.tv_sec, &tm);
      iso8601(tb, &tm);
      if (IsCompressionMethodSupported(ZIP_CFILE_COMPRESSIONMETHOD(zcf)) &&
          IsAcceptablePath(path, pathlen)) {
        appendf(&cpm.outbuf,
                "%-*.*s %s  %0*o %4s  %,*ld  %'s\r\n",
                rn[2], rp[2], w[0], rn[4], rp[4], tb, w[1],
                GetZipCfileMode(zcf), DescribeCompressionRatio(rb, zcf), w[2],
                GetZipCfileUncompressedSize(zcf), rp[3]);
      } else {
        appendf(&cpm.outbuf, "%-*.*s %s  %0*o %4s  %,*ld  %'s\r\n", w[0], rn[4],
                rp[4], tb, w[1], GetZipCfileMode(zcf),
                DescribeCompressionRatio(rb, zcf), w[2],
                GetZipCfileUncompressedSize(zcf), rp[3]);
      }
      free(rp[4]);
      free(rp[3]);
      free(rp[2]);
      free(rp[1]);
      free(rp[0]);
    }
    free(path);
  }
  appends(&cpm.outbuf, "\
\r\n"); p = SetStatus(200, "OK"); p = AppendContentType(p, "text/html"); if (cpm.msg.version >= 11) { p = stpcpy(p, "Cache-Control: no-store\r\n"); } return CommitOutput(p); } static const char *MergeNames(const char *a, const char *b) { return FreeLater(xasprintf("%s.%s", a, b)); } static void AppendLong1(const char *a, long x) { if (x) appendf(&cpm.outbuf, "%s: %ld\r\n", a, x); } static void AppendLong2(const char *a, const char *b, long x) { if (x) appendf(&cpm.outbuf, "%s.%s: %ld\r\n", a, b, x); } static void AppendTimeval(const char *a, struct timeval *tv) { AppendLong2(a, "tv_sec", tv->tv_sec); AppendLong2(a, "tv_usec", tv->tv_usec); } static void AppendRusage(const char *a, struct rusage *ru) { AppendTimeval(MergeNames(a, "ru_utime"), &ru->ru_utime); AppendTimeval(MergeNames(a, "ru_stime"), &ru->ru_stime); AppendLong2(a, "ru_maxrss", ru->ru_maxrss); AppendLong2(a, "ru_ixrss", ru->ru_ixrss); AppendLong2(a, "ru_idrss", ru->ru_idrss); AppendLong2(a, "ru_isrss", ru->ru_isrss); AppendLong2(a, "ru_minflt", ru->ru_minflt); AppendLong2(a, "ru_majflt", ru->ru_majflt); AppendLong2(a, "ru_nswap", ru->ru_nswap); AppendLong2(a, "ru_inblock", ru->ru_inblock); AppendLong2(a, "ru_oublock", ru->ru_oublock); AppendLong2(a, "ru_msgsnd", ru->ru_msgsnd); AppendLong2(a, "ru_msgrcv", ru->ru_msgrcv); AppendLong2(a, "ru_nsignals", ru->ru_nsignals); AppendLong2(a, "ru_nvcsw", ru->ru_nvcsw); AppendLong2(a, "ru_nivcsw", ru->ru_nivcsw); } static void ServeCounters(void) { const long *c; const char *s; for (c = (const long *)&shared->c, s = kCounterNames; *s; ++c, s += strlen(s) + 1) { AppendLong1(s, *c); } } static char *ServeStatusz(void) { char *p; LockInc(&shared->c.statuszrequests); if (cpm.msg.method != kHttpGet && cpm.msg.method != kHttpHead) { return BadMethod(); } AppendLong1("pid", getpid()); AppendLong1("ppid", getppid()); AppendLong1("now", timespec_real().tv_sec); AppendLong1("nowish", shared->nowish.tv_sec); AppendLong1("gmtoff", gmtoff); AppendLong1("CLK_TCK", CLK_TCK); AppendLong1("startserver", startserver.tv_sec); AppendLong1("lastmeltdown", shared->lastmeltdown.tv_sec); AppendLong1("workers", shared->workers); AppendLong1("assets.n", assets.n); #ifndef STATIC lua_State *L = GL; AppendLong1("lua.memory", lua_gc(L, LUA_GCCOUNT) * 1024 + lua_gc(L, LUA_GCCOUNTB)); #endif ServeCounters(); AppendRusage("server", &shared->server); AppendRusage("children", &shared->children); p = SetStatus(200, "OK"); p = AppendContentType(p, "text/plain"); if (cpm.msg.version >= 11) { p = stpcpy(p, "Cache-Control: no-store\r\n"); } return CommitOutput(p); } static char *RedirectSlash(void) { size_t n, i; char *p, *e; LockInc(&shared->c.redirects); p = SetStatus(307, "Temporary Redirect"); p = stpcpy(p, "Location: "); e = EscapePath(url.path.p, url.path.n, &n); p = mempcpy(p, e, n); p = stpcpy(p, "/"); for (i = 0; i < url.params.n; ++i) { p = stpcpy(p, i == 0 ? "?" : "&"); p = mempcpy(p, url.params.p[i].key.p, url.params.p[i].key.n); if (url.params.p[i].val.p) { p = stpcpy(p, "="); p = mempcpy(p, url.params.p[i].val.p, url.params.p[i].val.n); } } p = stpcpy(p, "\r\n"); free(e); return p; } static char *ServeIndex(const char *path, size_t pathlen) { size_t i, n; char *p, *q; for (p = 0, i = 0; !p && i < ARRAYLEN(kIndexPaths); ++i) { q = MergePaths(path, pathlen, kIndexPaths[i], strlen(kIndexPaths[i]), &n); p = RoutePath(q, n); free(q); } return p; } static char *GetLuaResponse(void) { return cpm.luaheaderp ? cpm.luaheaderp : SetStatus(200, "OK"); } static bool ShouldServeCrashReportDetails(void) { uint32_t ip; uint16_t port; if (leakcrashreports) { return true; } else { return !GetRemoteAddr(&ip, &port) && (IsLoopbackIp(ip) || IsPrivateIp(ip)); } } static char *LuaOnHttpRequest(void) { char *error; lua_State *L = GL; effectivepath.p = url.path.p; effectivepath.n = url.path.n; lua_settop(L, 0); // clear Lua stack, as it needs to start fresh lua_getglobal(L, "OnHttpRequest"); if (LuaCallWithYield(L) == LUA_OK) { return CommitOutput(GetLuaResponse()); } else { LogLuaError("OnHttpRequest", lua_tostring(L, -1)); error = ServeErrorWithDetail( 500, "Internal Server Error", ShouldServeCrashReportDetails() ? lua_tostring(L, -1) : NULL); lua_pop(L, 1); // pop error return error; } } static char *ServeLua(struct Asset *a, const char *s, size_t n) { char *code; size_t codelen; lua_State *L = GL; LockInc(&shared->c.dynamicrequests); effectivepath.p = (void *)s; effectivepath.n = n; if ((code = FreeLater(LoadAsset(a, &codelen)))) { int status = luaL_loadbuffer(L, code, codelen, FreeLater(xasprintf("@%s", FreeLater(strndup(s, n))))); if (status == LUA_OK && LuaCallWithYield(L) == LUA_OK) { return CommitOutput(GetLuaResponse()); } else { char *error; LogLuaError("lua code", lua_tostring(L, -1)); error = ServeErrorWithDetail( 500, "Internal Server Error", ShouldServeCrashReportDetails() ? lua_tostring(L, -1) : NULL); lua_pop(L, 1); // pop error return error; } } return ServeError(500, "Internal Server Error"); } static char *HandleRedirect(struct Redirect *r) { int code; struct Asset *a; if (!r->code && (a = GetAsset(r->location.s, r->location.n))) { LockInc(&shared->c.rewrites); DEBUGF("(rsp) internal redirect to %`'s", r->location.s); if (!HasString(&cpm.loops, r->location.s, r->location.n)) { AddString(&cpm.loops, r->location.s, r->location.n); return RoutePath(r->location.s, r->location.n); } else { LockInc(&shared->c.loops); return SetStatus(508, "Loop Detected"); } } else if (cpm.msg.version < 10) { return ServeError(505, "HTTP Version Not Supported"); } else { LockInc(&shared->c.redirects); code = r->code; if (!code) code = 307; DEBUGF("(rsp) %d redirect to %`'s", code, r->location.s); return AppendHeader( SetStatus(code, GetHttpReason(code)), "Location", FreeLater(EncodeHttpHeaderValue(r->location.s, r->location.n, 0))); } } static char *HandleFolder(const char *path, size_t pathlen) { char *p; if (url.path.n && url.path.p[url.path.n - 1] != '/' && SlicesEqual(path, pathlen, url.path.p, url.path.n)) { return RedirectSlash(); } if ((p = ServeIndex(path, pathlen))) { return p; } else { LockInc(&shared->c.forbiddens); WARNF("(srvr) directory %`'.*s lacks index page", pathlen, path); return ServeErrorWithPath(403, "Forbidden", path, pathlen); } } static bool Reindex(void) { if (OpenZip(false)) { LockInc(&shared->c.reindexes); return true; } else { return false; } } static const char *LuaCheckPath(lua_State *L, int idx, size_t *pathlen) { const char *path; if (lua_isnoneornil(L, idx)) { path = url.path.p; *pathlen = url.path.n; } else { path = luaL_checklstring(L, idx, pathlen); if (!IsReasonablePath(path, *pathlen)) { WARNF("(srvr) bad path %`'.*s", *pathlen, path); luaL_argerror(L, idx, "bad path"); __builtin_unreachable(); } } return path; } static const char *LuaCheckHost(lua_State *L, int idx, size_t *hostlen) { const char *host; if (lua_isnoneornil(L, idx)) { host = url.host.p; *hostlen = url.host.n; } else { host = luaL_checklstring(L, idx, hostlen); if (!IsAcceptableHost(host, *hostlen)) { WARNF("(srvr) bad host %`'.*s", *hostlen, host); luaL_argerror(L, idx, "bad host"); __builtin_unreachable(); } } return host; } static void OnlyCallFromInitLua(lua_State *L, const char *api) { if (isinitialized) { luaL_error(L, "%s() should be called %s", api, "from the global scope of .init.lua"); __builtin_unreachable(); } } static void OnlyCallFromMainProcess(lua_State *L, const char *api) { if (__isworker) { luaL_error(L, "%s() should be called %s", api, "from .init.lua or the repl"); __builtin_unreachable(); } } static void OnlyCallDuringConnection(lua_State *L, const char *api) { if (!ishandlingconnection) { luaL_error(L, "%s() can only be called %s", api, "while handling a connection"); __builtin_unreachable(); } } static void OnlyCallDuringRequest(lua_State *L, const char *api) { if (!ishandlingrequest) { luaL_error(L, "%s() can only be called %s", api, "while handling a request"); __builtin_unreachable(); } } static int LuaServe(lua_State *L, const char *api, char *impl(void)) { OnlyCallDuringRequest(L, api); cpm.luaheaderp = impl(); return 0; } static int LuaServeListing(lua_State *L) { return LuaServe(L, "ServeListing", ServeListing); } static int LuaServeStatusz(lua_State *L) { return LuaServe(L, "ServeStatusz", ServeStatusz); } static int LuaServeAsset(lua_State *L) { size_t pathlen; struct Asset *a; const char *path; OnlyCallDuringRequest(L, "ServeAsset"); path = LuaCheckPath(L, 1, &pathlen); if ((a = GetAsset(path, pathlen)) && !S_ISDIR(GetMode(a))) { cpm.luaheaderp = ServeAsset(a, path, pathlen); lua_pushboolean(L, true); } else { lua_pushboolean(L, false); } return 1; } static int LuaServeIndex(lua_State *L) { size_t pathlen; const char *path; OnlyCallDuringRequest(L, "ServeIndex"); path = LuaCheckPath(L, 1, &pathlen); lua_pushboolean(L, !!(cpm.luaheaderp = ServeIndex(path, pathlen))); return 1; } static int LuaServeRedirect(lua_State *L) { int code; char *eval; size_t loclen; const char *location; OnlyCallDuringRequest(L, "ServeRedirect"); code = luaL_checkinteger(L, 1); if (!(300 <= code && code <= 399)) { luaL_argerror(L, 1, "bad status code"); __builtin_unreachable(); } location = luaL_checklstring(L, 2, &loclen); if (cpm.msg.version < 10) { (void)ServeError(505, "HTTP Version Not Supported"); lua_pushboolean(L, false); } else { if (!(eval = EncodeHttpHeaderValue(location, loclen, 0))) { luaL_argerror(L, 2, "invalid location"); __builtin_unreachable(); } VERBOSEF("(rsp) %d redirect to %`'s", code, location); cpm.luaheaderp = AppendHeader(SetStatus(code, GetHttpReason(code)), "Location", eval); free(eval); lua_pushboolean(L, true); } return 1; } static int LuaRoutePath(lua_State *L) { size_t pathlen; const char *path; OnlyCallDuringRequest(L, "RoutePath"); path = LuaCheckPath(L, 1, &pathlen); lua_pushboolean(L, !!(cpm.luaheaderp = RoutePath(path, pathlen))); return 1; } static int LuaRouteHost(lua_State *L) { size_t hostlen, pathlen; const char *host, *path; OnlyCallDuringRequest(L, "RouteHost"); host = LuaCheckHost(L, 1, &hostlen); path = LuaCheckPath(L, 2, &pathlen); lua_pushboolean(L, !!(cpm.luaheaderp = RouteHost(host, hostlen, path, pathlen))); return 1; } static int LuaRoute(lua_State *L) { size_t hostlen, pathlen; const char *host, *path; OnlyCallDuringRequest(L, "Route"); host = LuaCheckHost(L, 1, &hostlen); path = LuaCheckPath(L, 2, &pathlen); lua_pushboolean(L, !!(cpm.luaheaderp = Route(host, hostlen, path, pathlen))); return 1; } static int LuaProgramTrustedIp(lua_State *L) { lua_Integer ip, cidr; uint32_t ip32, imask; ip = luaL_checkinteger(L, 1); cidr = luaL_optinteger(L, 2, 32); if (!(0 <= ip && ip <= 0xffffffff)) { luaL_argerror(L, 1, "ip out of range"); __builtin_unreachable(); } if (!(0 <= cidr && cidr <= 32)) { luaL_argerror(L, 2, "cidr should be 0 .. 32"); __builtin_unreachable(); } ip32 = ip; imask = ~(0xffffffffu << (32 - cidr)); if (ip32 & imask) { luaL_argerror(L, 1, "ip address isn't the network address; " "it has bits masked by the cidr"); __builtin_unreachable(); } ProgramTrustedIp(ip, cidr); return 0; } static int LuaIsTrusted(lua_State *L) { lua_Integer ip; ip = luaL_checkinteger(L, 1); if (!(0 <= ip && ip <= 0xffffffff)) { luaL_argerror(L, 1, "ip out of range"); __builtin_unreachable(); } lua_pushboolean(L, IsTrustedIp(ip)); return 1; } static int LuaRespond(lua_State *L, char *R(unsigned, const char *)) { char *p; int code; size_t reasonlen; const char *reason; OnlyCallDuringRequest(L, "Respond"); code = luaL_checkinteger(L, 1); if (!(100 <= code && code <= 999)) { luaL_argerror(L, 1, "bad status code"); __builtin_unreachable(); } if (lua_isnoneornil(L, 2)) { cpm.luaheaderp = R(code, GetHttpReason(code)); } else { reason = lua_tolstring(L, 2, &reasonlen); if ((p = EncodeHttpHeaderValue(reason, MIN(reasonlen, 128), 0))) { cpm.luaheaderp = R(code, p); free(p); } else { luaL_argerror(L, 2, "invalid"); __builtin_unreachable(); } } return 0; } static int LuaSetStatus(lua_State *L) { return LuaRespond(L, SetStatus); } static int LuaGetStatus(lua_State *L) { OnlyCallDuringRequest(L, "GetStatus"); if (!cpm.statuscode) { lua_pushnil(L); } else { lua_pushinteger(L, cpm.statuscode); } return 1; } static int LuaGetSslIdentity(lua_State *L) { const mbedtls_x509_crt *cert; OnlyCallDuringRequest(L, "GetSslIdentity"); if (!usingssl) { lua_pushnil(L); } else { if (sslpskindex) { CHECK((sslpskindex - 1) >= 0 && (sslpskindex - 1) < psks.n); lua_pushlstring(L, psks.p[sslpskindex - 1].identity, psks.p[sslpskindex - 1].identity_len); } else { cert = mbedtls_ssl_get_peer_cert(&ssl); lua_pushstring(L, cert ? _gc(FormatX509Name(&cert->subject)) : ""); } } return 1; } static int LuaServeError(lua_State *L) { return LuaRespond(L, ServeError); } static int LuaLoadAsset(lua_State *L) { void *data; struct Asset *a; const char *path; size_t size, pathlen; path = LuaCheckPath(L, 1, &pathlen); if ((a = GetAsset(path, pathlen))) { if (!a->file && !IsCompressed(a)) { /* fast path: this avoids extra copy */ data = ZIP_LFILE_CONTENT(zmap + a->lf); size = GetZipLfileUncompressedSize(zmap + a->lf); if (Verify(data, size, ZIP_LFILE_CRC32(zmap + a->lf))) { lua_pushlstring(L, data, size); return 1; } // any error from Verify has already been reported } else if ((data = LoadAsset(a, &size))) { lua_pushlstring(L, data, size); free(data); return 1; } else { WARNF("(srvr) could not load asset: %`'.*s", pathlen, path); } } else { WARNF("(srvr) could not find asset: %`'.*s", pathlen, path); } return 0; } static void GetDosLocalTime(int64_t utcunixts, uint16_t *out_time, uint16_t *out_date) { struct tm tm; CHECK_NOTNULL(localtime_r(&utcunixts, &tm)); *out_time = DOS_TIME(tm.tm_hour, tm.tm_min, tm.tm_sec); *out_date = DOS_DATE(tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday + 1); } static void StoreAsset(const char *path, size_t pathlen, const char *data, size_t datalen, int mode) { int64_t ft; uint32_t crc; char *comp, *p; struct timespec now; struct Asset *a; struct iovec v[13]; uint8_t era; const char *use; uint16_t gflags, iattrs, mtime, mdate, dosmode, method, disk; size_t oldcdirsize, oldcdiroffset, records, cdiroffset, cdirsize, complen, uselen; if (IsOpenbsd() || IsNetbsd() || IsWindows()) { FATALF("(cfg) StoreAsset() not available on Windows/NetBSD/OpenBSD yet"); } INFOF("(srvr) storing asset %`'s", path); disk = gflags = iattrs = 0; if (isutf8(path, pathlen)) gflags |= kZipGflagUtf8; if (istext(data, datalen)) iattrs |= kZipIattrText; crc = crc32_z(0, data, datalen); if (datalen < 100) { method = kZipCompressionNone; comp = 0; use = data; uselen = datalen; era = kZipEra1989; } else { comp = Deflate(data, datalen, &complen); if (complen < datalen) { method = kZipCompressionDeflate; use = comp; uselen = complen; era = kZipEra1993; } else { method = kZipCompressionNone; use = data; uselen = datalen; era = kZipEra1989; } } ////////////////////////////////////////////////////////////////////////////// if (-1 == fcntl(zfd, F_SETLKW, &(struct flock){F_WRLCK})) { WARNF("(srvr) can't place write lock on file descriptor %d: %s", zfd, strerror(errno)); return; } OpenZip(false); now = timespec_real(); a = GetAssetZip(path, pathlen); if (!mode) mode = a ? GetMode(a) : 0644; if (!(mode & S_IFMT)) mode |= S_IFREG; if (pathlen > 1 && path[0] == '/') ++path, --pathlen; dosmode = !(mode & 0200) ? kNtFileAttributeReadonly : 0; ft = (now.tv_sec + MODERNITYSECONDS) * HECTONANOSECONDS; GetDosLocalTime(now.tv_sec, &mtime, &mdate); // local file header if (uselen >= 0xffffffff || datalen >= 0xffffffff) { era = kZipEra2001; v[2].iov_base = p = alloca((v[2].iov_len = 2 + 2 + 8 + 8)); p = WRITE16LE(p, kZipExtraZip64); p = WRITE16LE(p, 8 + 8); p = WRITE64LE(p, uselen); p = WRITE64LE(p, datalen); } else { v[2].iov_len = 0; v[2].iov_base = 0; } v[0].iov_base = p = alloca((v[0].iov_len = kZipLfileHdrMinSize)); p = WRITE32LE(p, kZipLfileHdrMagic); *p++ = era; *p++ = kZipOsDos; p = WRITE16LE(p, gflags); p = WRITE16LE(p, method); p = WRITE16LE(p, mtime); p = WRITE16LE(p, mdate); p = WRITE32LE(p, crc); p = WRITE32LE(p, MIN(uselen, 0xffffffff)); p = WRITE32LE(p, MIN(datalen, 0xffffffff)); p = WRITE16LE(p, pathlen); p = WRITE16LE(p, v[2].iov_len); v[1].iov_len = pathlen; v[1].iov_base = (void *)path; // file data v[3].iov_len = uselen; v[3].iov_base = (void *)use; // old central directory entries oldcdirsize = GetZipCdirSize(zcdir); oldcdiroffset = GetZipCdirOffset(zcdir); if (a) { // to remove an existing asset, // first copy the central directory part before its record v[4].iov_base = zmap + oldcdiroffset; v[4].iov_len = a->cf - oldcdiroffset; // and then the rest of the central directory v[5].iov_base = zmap + oldcdiroffset + (v[4].iov_len + ZIP_CFILE_HDRSIZE(zmap + a->cf)); v[5].iov_len = oldcdirsize - (v[4].iov_len + ZIP_CFILE_HDRSIZE(zmap + a->cf)); } else { v[4].iov_base = zmap + oldcdiroffset; v[4].iov_len = oldcdirsize; v[5].iov_base = 0; v[5].iov_len = 0; } // new central directory entry if (uselen >= 0xffffffff || datalen >= 0xffffffff || zsize >= 0xffffffff) { v[8].iov_base = p = alloca((v[8].iov_len = 2 + 2 + 8 + 8 + 8)); p = WRITE16LE(p, kZipExtraZip64); p = WRITE16LE(p, 8 + 8 + 8); p = WRITE64LE(p, uselen); p = WRITE64LE(p, datalen); p = WRITE64LE(p, zsize); } else { v[8].iov_len = 0; v[8].iov_base = 0; } v[9].iov_base = p = alloca((v[9].iov_len = 2 + 2 + 4 + 2 + 2 + 8 + 8 + 8)); p = WRITE16LE(p, kZipExtraNtfs); p = WRITE16LE(p, 4 + 2 + 2 + 8 + 8 + 8); p = WRITE32LE(p, 0); p = WRITE16LE(p, 1); p = WRITE16LE(p, 8 + 8 + 8); p = WRITE64LE(p, ft); p = WRITE64LE(p, ft); p = WRITE64LE(p, ft); v[6].iov_base = p = alloca((v[6].iov_len = kZipCfileHdrMinSize)); p = WRITE32LE(p, kZipCfileHdrMagic); *p++ = kZipCosmopolitanVersion; *p++ = kZipOsUnix; *p++ = era; *p++ = kZipOsDos; p = WRITE16LE(p, gflags); p = WRITE16LE(p, method); p = WRITE16LE(p, mtime); p = WRITE16LE(p, mdate); p = WRITE32LE(p, crc); p = WRITE32LE(p, MIN(uselen, 0xffffffff)); p = WRITE32LE(p, MIN(datalen, 0xffffffff)); p = WRITE16LE(p, pathlen); p = WRITE16LE(p, v[8].iov_len + v[9].iov_len); p = WRITE16LE(p, 0); p = WRITE16LE(p, disk); p = WRITE16LE(p, iattrs); p = WRITE16LE(p, dosmode); p = WRITE16LE(p, mode); p = WRITE32LE(p, MIN(zsize, 0xffffffff)); v[7].iov_len = pathlen; v[7].iov_base = (void *)path; // zip64 end of central directory cdiroffset = zsize + v[0].iov_len + v[1].iov_len + v[2].iov_len + v[3].iov_len; cdirsize = v[4].iov_len + v[5].iov_len + v[6].iov_len + v[7].iov_len + v[8].iov_len + v[9].iov_len; records = GetZipCdirRecords(zcdir) + !a; if (records >= 0xffff || cdiroffset >= 0xffffffff || cdirsize >= 0xffffffff) { v[10].iov_base = p = alloca((v[10].iov_len = kZipCdir64HdrMinSize + kZipCdir64LocatorSize)); p = WRITE32LE(p, kZipCdir64HdrMagic); p = WRITE64LE(p, 2 + 2 + 4 + 4 + 8 + 8 + 8 + 8); p = WRITE16LE(p, kZipCosmopolitanVersion); p = WRITE16LE(p, kZipEra2001); p = WRITE32LE(p, disk); p = WRITE32LE(p, disk); p = WRITE64LE(p, records); p = WRITE64LE(p, records); p = WRITE64LE(p, cdirsize); p = WRITE64LE(p, cdiroffset); p = WRITE32LE(p, kZipCdir64LocatorMagic); p = WRITE32LE(p, disk); p = WRITE64LE(p, cdiroffset + cdirsize); p = WRITE32LE(p, disk); } else { v[10].iov_len = 0; v[10].iov_base = 0; } // end of central directory v[12].iov_base = (void *)GetZipCdirComment(zcdir); v[12].iov_len = GetZipCdirCommentSize(zcdir); v[11].iov_base = p = alloca((v[11].iov_len = kZipCdirHdrMinSize)); p = WRITE32LE(p, kZipCdirHdrMagic); p = WRITE16LE(p, disk); p = WRITE16LE(p, disk); p = WRITE16LE(p, MIN(records, 0xffff)); p = WRITE16LE(p, MIN(records, 0xffff)); p = WRITE32LE(p, MIN(cdirsize, 0xffffffff)); p = WRITE32LE(p, MIN(cdiroffset, 0xffffffff)); p = WRITE16LE(p, v[12].iov_len); CHECK_NE(-1, lseek(zfd, zmap + zsize - zmap, SEEK_SET)); CHECK_NE(-1, WritevAll(zfd, v, 13)); CHECK_NE(-1, fcntl(zfd, F_SETLK, &(struct flock){F_UNLCK})); ////////////////////////////////////////////////////////////////////////////// OpenZip(false); free(comp); } static void StoreFile(const char *path) { char *p; struct stat st; size_t plen, tlen; const char *target = path; if (startswith(target, "./")) target += 2; tlen = strlen(target); if (!IsReasonablePath(target, tlen)) FATALF("(cfg) error: can't store %`'s: contains '.' or '..' segments", target); if (lstat(path, &st) == -1) FATALF("(cfg) error: can't stat %`'s: %m", path); if (!(p = xslurp(path, &plen))) FATALF("(cfg) error: can't read %`'s: %m", path); StoreAsset(target, tlen, p, plen, st.st_mode & 0777); free(p); } static void StorePath(const char *dirpath) { DIR *d; char *path; struct dirent *e; if (!isdirectory(dirpath) && !endswith(dirpath, "/")) { return StoreFile(dirpath); } if (!(d = opendir(dirpath))) FATALF("(cfg) error: can't open %`'s", dirpath); while ((e = readdir(d))) { if (strcmp(e->d_name, ".") == 0) continue; if (strcmp(e->d_name, "..") == 0) continue; path = _gc(xjoinpaths(dirpath, e->d_name)); if (e->d_type == DT_DIR) { StorePath(path); } else { StoreFile(path); } } closedir(d); } static int LuaStoreAsset(lua_State *L) { const char *path, *data; size_t pathlen, datalen; int mode; path = LuaCheckPath(L, 1, &pathlen); if (pathlen > 0xffff) { return luaL_argerror(L, 1, "path too long"); } data = luaL_checklstring(L, 2, &datalen); mode = luaL_optinteger(L, 3, 0); StoreAsset(path, pathlen, data, datalen, mode); return 0; } static void ReseedRng(mbedtls_ctr_drbg_context *r, const char *s) { #ifndef UNSECURE if (unsecure) return; CHECK_EQ(0, mbedtls_ctr_drbg_reseed(r, (void *)s, strlen(s))); #endif } static void LogMessage(const char *d, const char *s, size_t n) { size_t n2, n3; char *s2, *s3; if (!LOGGABLE(kLogInfo)) return; while (n && (s[n - 1] == '\r' || s[n - 1] == '\n')) --n; if ((s2 = DecodeLatin1(s, n, &n2))) { if ((s3 = IndentLines(s2, n2, &n3, 1))) { INFOF("(stat) %s %,ld byte message\r\n%.*s", d, n, n3, s3); 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) return; if (!LOGGABLE(kLogInfo)) 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))) { INFOF("(stat) %s %,ld byte payload\r\n%.*s", d, n, n3, s3); free(s3); } free(s2); } } static int LuaNilError(lua_State *L, const char *fmt, ...) { va_list argp; va_start(argp, fmt); lua_pushnil(L); lua_pushvfstring(L, fmt, argp); va_end(argp); return 2; } static int LuaNilTlsError(lua_State *L, const char *s, int r) { return LuaNilError(L, "tls %s failed (%s %s)", s, IsTiny() ? "grep" : GetTlsError(r), _gc(xasprintf("-0x%04x", -r))); } #include "tool/net/fetch.inc" static int LuaGetDate(lua_State *L) { lua_pushinteger(L, shared->nowish.tv_sec); return 1; } static int LuaGetHttpVersion(lua_State *L) { OnlyCallDuringRequest(L, "GetHttpVersion"); lua_pushinteger(L, cpm.msg.version); return 1; } static int LuaGetRedbeanVersion(lua_State *L) { lua_pushinteger(L, VERSION); return 1; } static int LuaGetMethod(lua_State *L) { OnlyCallDuringRequest(L, "GetMethod"); if (cpm.msg.method) { lua_pushstring(L, kHttpMethod[cpm.msg.method]); } else { lua_pushlstring(L, inbuf.p + cpm.msg.xmethod.a, cpm.msg.xmethod.b - cpm.msg.xmethod.a); } return 1; } static int LuaGetAddr(lua_State *L, int GetAddr(uint32_t *, uint16_t *)) { uint32_t ip; uint16_t port; if (!GetAddr(&ip, &port)) { lua_pushinteger(L, ip); lua_pushinteger(L, port); return 2; } else { lua_pushnil(L); return 1; } } static int LuaGetServerAddr(lua_State *L) { OnlyCallDuringConnection(L, "GetServerAddr"); return LuaGetAddr(L, GetServerAddr); } static int LuaGetClientAddr(lua_State *L) { OnlyCallDuringConnection(L, "GetClientAddr"); return LuaGetAddr(L, GetClientAddr); } static int LuaGetRemoteAddr(lua_State *L) { OnlyCallDuringRequest(L, "GetRemoteAddr"); return LuaGetAddr(L, GetRemoteAddr); } static int LuaLog(lua_State *L) { int level, line; lua_Debug ar; const char *msg, *module; level = luaL_checkinteger(L, 1); if (LOGGABLE(level)) { msg = luaL_checkstring(L, 2); if (lua_getstack(L, 1, &ar) && lua_getinfo(L, "Sl", &ar)) { module = ar.short_src; line = ar.currentline; } else { module = _gc(strndup(effectivepath.p, effectivepath.n)); line = -1; } flogf(level, module, line, NULL, "%s", msg); } return 0; } static int LuaEncodeSmth(lua_State *L, int Encoder(lua_State *, char **, int, struct EncoderConfig)) { char *p = 0; int useoutput = false; struct EncoderConfig conf = { .maxdepth = 64, .sorted = true, .pretty = false, .indent = " ", }; if (lua_istable(L, 2)) { lua_settop(L, 2); // discard any extra arguments lua_getfield(L, 2, "useoutput"); // ignore useoutput outside of request handling if (ishandlingrequest && lua_isboolean(L, -1)) { useoutput = lua_toboolean(L, -1); } lua_getfield(L, 2, "maxdepth"); if (!lua_isnoneornil(L, -1)) { lua_Integer n = lua_tointeger(L, -1); n = MAX(0, MIN(n, SHRT_MAX)); conf.maxdepth = n; } lua_getfield(L, 2, "sorted"); if (!lua_isnoneornil(L, -1)) { conf.sorted = lua_toboolean(L, -1); } lua_getfield(L, 2, "pretty"); if (!lua_isnoneornil(L, -1)) { conf.pretty = lua_toboolean(L, -1); lua_getfield(L, 2, "indent"); if (!lua_isnoneornil(L, -1)) { conf.indent = luaL_checkstring(L, -1); } } } lua_settop(L, 1); // keep the passed argument on top if (Encoder(L, useoutput ? &cpm.outbuf : &p, -1, conf) == -1) { free(p); return 2; } if (useoutput) { lua_pushboolean(L, true); } else { lua_pushstring(L, p); free(p); } return 1; } static int LuaEncodeJson(lua_State *L) { return LuaEncodeSmth(L, LuaEncodeJsonData); } static int LuaEncodeLua(lua_State *L) { return LuaEncodeSmth(L, LuaEncodeLuaData); } static int LuaDecodeJson(lua_State *L) { size_t n; const char *p; struct DecodeJson r; p = luaL_checklstring(L, 1, &n); r = DecodeJson(L, p, n); if (UNLIKELY(!r.rc)) { lua_pushnil(L); lua_pushstring(L, "unexpected eof"); return 2; } if (UNLIKELY(r.rc == -1)) { lua_pushnil(L); lua_pushstring(L, r.p); return 2; } r = DecodeJson(L, r.p, n - (r.p - p)); if (UNLIKELY(r.rc)) { lua_pushnil(L); lua_pushstring(L, "junk after expression"); return 2; } return 1; } static int LuaGetUrl(lua_State *L) { char *p; size_t n; OnlyCallDuringRequest(L, "GetUrl"); p = EncodeUrl(&url, &n); lua_pushlstring(L, p, n); free(p); return 1; } static int LuaGetScheme(lua_State *L) { OnlyCallDuringRequest(L, "GetScheme"); LuaPushUrlView(L, &url.scheme); return 1; } static int LuaGetPath(lua_State *L) { OnlyCallDuringRequest(L, "GetPath"); LuaPushUrlView(L, &url.path); return 1; } static int LuaGetEffectivePath(lua_State *L) { OnlyCallDuringRequest(L, "GetEffectivePath"); lua_pushlstring(L, effectivepath.p, effectivepath.n); return 1; } static int LuaGetFragment(lua_State *L) { OnlyCallDuringRequest(L, "GetFragment"); LuaPushUrlView(L, &url.fragment); return 1; } static int LuaGetUser(lua_State *L) { size_t n; const char *p, *q; OnlyCallDuringRequest(L, "GetUser"); if (url.user.p) { LuaPushUrlView(L, &url.user); } else if ((p = gc(GetBasicAuthorization(&n)))) { if (!(q = memchr(p, ':', n))) q = p + n; lua_pushlstring(L, p, q - p); } else { lua_pushnil(L); } return 1; } static int LuaGetPass(lua_State *L) { size_t n; const char *p, *q; OnlyCallDuringRequest(L, "GetPass"); if (url.user.p) { LuaPushUrlView(L, &url.pass); } else if ((p = gc(GetBasicAuthorization(&n)))) { if ((q = memchr(p, ':', n))) { lua_pushlstring(L, q + 1, p + n - (q + 1)); } else { lua_pushnil(L); } } else { lua_pushnil(L); } return 1; } static int LuaGetHost(lua_State *L) { char b[16]; OnlyCallDuringRequest(L, "GetHost"); if (url.host.n) { lua_pushlstring(L, url.host.p, url.host.n); } else { inet_ntop(AF_INET, &serveraddr->sin_addr.s_addr, b, sizeof(b)); lua_pushstring(L, b); } return 1; } static int LuaGetPort(lua_State *L) { int i, x = 0; OnlyCallDuringRequest(L, "GetPort"); for (i = 0; i < url.port.n; ++i) x = url.port.p[i] - '0' + x * 10; if (!x) x = ntohs(serveraddr->sin_port); lua_pushinteger(L, x); return 1; } static int LuaGetBody(lua_State *L) { OnlyCallDuringRequest(L, "GetBody"); lua_pushlstring(L, inbuf.p + hdrsize, payloadlength); return 1; } static int LuaGetResponseBody(lua_State *L) { char *s = ""; // response can be gzipped (>0), text (=0), or generator (<0) int size = cpm.gzipped > 0 ? cpm.gzipped // original size : cpm.gzipped == 0 ? cpm.contentlength : 0; OnlyCallDuringRequest(L, "GetResponseBody"); if (cpm.gzipped > 0 && (!(s = FreeLater(malloc(cpm.gzipped))) || !Inflate(s, cpm.gzipped, cpm.content, cpm.contentlength))) { return LuaNilError(L, "failed to decompress response"); } lua_pushlstring(L, cpm.gzipped > 0 ? s // return decompressed : cpm.gzipped == 0 ? cpm.content : "", size); return 1; } static int LuaGetHeader(lua_State *L) { int h; const char *key; size_t i, keylen; OnlyCallDuringRequest(L, "GetHeader"); key = luaL_checklstring(L, 1, &keylen); if ((h = GetHttpHeader(key, keylen)) != -1) { if (cpm.msg.headers[h].a) { return LuaPushHeader(L, &cpm.msg, inbuf.p, h); } } else { for (i = 0; i < cpm.msg.xheaders.n; ++i) { if (SlicesEqualCase( key, keylen, inbuf.p + cpm.msg.xheaders.p[i].k.a, cpm.msg.xheaders.p[i].k.b - cpm.msg.xheaders.p[i].k.a)) { LuaPushLatin1(L, inbuf.p + cpm.msg.xheaders.p[i].v.a, cpm.msg.xheaders.p[i].v.b - cpm.msg.xheaders.p[i].v.a); return 1; } } } lua_pushnil(L); return 1; } static int LuaGetHeaders(lua_State *L) { OnlyCallDuringRequest(L, "GetHeaders"); return LuaPushHeaders(L, &cpm.msg, inbuf.p); } static int LuaSetHeader(lua_State *L) { int h; char *eval; char *p, *q; const char *key, *val; size_t keylen, vallen, evallen; OnlyCallDuringRequest(L, "SetHeader"); key = luaL_checklstring(L, 1, &keylen); val = luaL_optlstring(L, 2, 0, &vallen); if (!val) return 0; if ((h = GetHttpHeader(key, keylen)) == -1) { if (!IsValidHttpToken(key, keylen)) { luaL_argerror(L, 1, "invalid"); __builtin_unreachable(); } } if (!(eval = EncodeHttpHeaderValue(val, vallen, &evallen))) { luaL_argerror(L, 2, "invalid"); __builtin_unreachable(); } p = GetLuaResponse(); while (p - hdrbuf.p + keylen + 2 + evallen + 2 + 512 > hdrbuf.n) { hdrbuf.n += hdrbuf.n >> 1; q = xrealloc(hdrbuf.p, hdrbuf.n); cpm.luaheaderp = p = q + (p - hdrbuf.p); hdrbuf.p = q; } switch (h) { case kHttpConnection: connectionclose = SlicesEqualCase(eval, evallen, "close", 5); break; case kHttpContentType: p = AppendContentType(p, eval); break; case kHttpReferrerPolicy: cpm.referrerpolicy = FreeLater(strdup(eval)); break; case kHttpServer: cpm.branded = true; p = AppendHeader(p, "Server", eval); break; case kHttpExpires: case kHttpCacheControl: cpm.gotcachecontrol = true; p = AppendHeader(p, key, eval); break; case kHttpXContentTypeOptions: cpm.gotxcontenttypeoptions = true; p = AppendHeader(p, key, eval); break; default: p = AppendHeader(p, key, eval); break; } cpm.luaheaderp = p; free(eval); return 0; } static int LuaGetCookie(lua_State *L) { char *cookie = 0, *cookietmpl, *cookieval; OnlyCallDuringRequest(L, "GetCookie"); cookietmpl = _gc(xasprintf(" %s=", luaL_checkstring(L, 1))); if (HasHeader(kHttpCookie)) { appends(&cookie, " "); // prepend space to simplify cookie search appendd(&cookie, HeaderData(kHttpCookie), HeaderLength(kHttpCookie)); } if (cookie && (cookieval = strstr(cookie, cookietmpl))) { cookieval += strlen(cookietmpl); lua_pushlstring(L, cookieval, strchrnul(cookieval, ';') - cookieval); } else { lua_pushnil(L); } if (cookie) free(cookie); return 1; } static int LuaSetCookie(lua_State *L) { const char *key, *val; size_t keylen, vallen; char *expires, *samesite = ""; char *buf = 0; bool ishostpref, issecurepref; const char *hostpref = "__Host-"; const char *securepref = "__Secure-"; OnlyCallDuringRequest(L, "SetCookie"); key = luaL_checklstring(L, 1, &keylen); val = luaL_checklstring(L, 2, &vallen); if (!IsValidHttpToken(key, keylen)) { luaL_argerror(L, 1, "invalid"); __builtin_unreachable(); } if (!IsValidCookieValue(val, vallen)) { luaL_argerror(L, 2, "invalid"); __builtin_unreachable(); } ishostpref = keylen > strlen(hostpref) && SlicesEqual(key, strlen(hostpref), hostpref, strlen(hostpref)); issecurepref = keylen > strlen(securepref) && SlicesEqual(key, strlen(securepref), securepref, strlen(securepref)); if ((ishostpref || issecurepref) && !usingssl) { luaL_argerror( L, 1, _gc(xasprintf("%s and %s prefixes require SSL", hostpref, securepref))); __builtin_unreachable(); } appends(&buf, key); appends(&buf, "="); appends(&buf, val); if (lua_istable(L, 3)) { if (lua_getfield(L, 3, "expires") != LUA_TNIL || lua_getfield(L, 3, "Expires") != LUA_TNIL) { if (lua_isnumber(L, -1)) { expires = FormatUnixHttpDateTime(FreeLater(xmalloc(30)), lua_tonumber(L, -1)); } else { expires = (void *)lua_tostring(L, -1); if (!ParseHttpDateTime(expires, -1)) { luaL_argerror(L, 3, "invalid data format in Expires"); __builtin_unreachable(); } } appends(&buf, "; Expires="); appends(&buf, expires); } if ((lua_getfield(L, 3, "maxage") == LUA_TNUMBER || lua_getfield(L, 3, "MaxAge") == LUA_TNUMBER) && lua_isinteger(L, -1)) { appends(&buf, "; Max-Age="); appends(&buf, lua_tostring(L, -1)); } if (lua_getfield(L, 3, "samesite") == LUA_TSTRING || lua_getfield(L, 3, "SameSite") == LUA_TSTRING) { samesite = (void *)lua_tostring(L, -1); // also used in the Secure check appends(&buf, "; SameSite="); appends(&buf, samesite); } // Secure attribute is required for __Host and __Secure prefixes // as well as for the SameSite=None if (ishostpref || issecurepref || !strcmp(samesite, "None") || ((lua_getfield(L, 3, "secure") == LUA_TBOOLEAN || lua_getfield(L, 3, "Secure") == LUA_TBOOLEAN) && lua_toboolean(L, -1))) { appends(&buf, "; Secure"); } if (!ishostpref && (lua_getfield(L, 3, "domain") == LUA_TSTRING || lua_getfield(L, 3, "Domain") == LUA_TSTRING)) { appends(&buf, "; Domain="); appends(&buf, lua_tostring(L, -1)); } if (ishostpref || lua_getfield(L, 3, "path") == LUA_TSTRING || lua_getfield(L, 3, "Path") == LUA_TSTRING) { appends(&buf, "; Path="); appends(&buf, ishostpref ? "/" : lua_tostring(L, -1)); } if ((lua_getfield(L, 3, "httponly") == LUA_TBOOLEAN || lua_getfield(L, 3, "HttpOnly") == LUA_TBOOLEAN) && lua_toboolean(L, -1)) { appends(&buf, "; HttpOnly"); } } DEBUGF("(srvr) Set-Cookie: %s", buf); // empty the stack and push header name/value lua_settop(L, 0); lua_pushliteral(L, "Set-Cookie"); lua_pushstring(L, buf); free(buf); return LuaSetHeader(L); } static int LuaHasParam(lua_State *L) { size_t i, n; const char *s; OnlyCallDuringRequest(L, "HasParam"); s = luaL_checklstring(L, 1, &n); for (i = 0; i < url.params.n; ++i) { if (SlicesEqual(s, n, url.params.p[i].key.p, url.params.p[i].key.n)) { lua_pushboolean(L, true); return 1; } } lua_pushboolean(L, false); return 1; } static int LuaGetParam(lua_State *L) { size_t i, n; const char *s; OnlyCallDuringRequest(L, "GetParam"); s = luaL_checklstring(L, 1, &n); for (i = 0; i < url.params.n; ++i) { if (SlicesEqual(s, n, url.params.p[i].key.p, url.params.p[i].key.n)) { if (url.params.p[i].val.p) { lua_pushlstring(L, url.params.p[i].val.p, url.params.p[i].val.n); return 1; } else { break; } } } lua_pushnil(L); return 1; } static int LuaGetParams(lua_State *L) { OnlyCallDuringRequest(L, "GetParams"); LuaPushUrlParams(L, &url.params); return 1; } static int LuaWrite(lua_State *L) { size_t size; const char *data; OnlyCallDuringRequest(L, "Write"); if (!lua_isnil(L, 1)) { data = luaL_checklstring(L, 1, &size); appendd(&cpm.outbuf, data, size); } return 0; } static dontinline int LuaProgramInt(lua_State *L, void P(long)) { P(luaL_checkinteger(L, 1)); return 0; } static int LuaProgramPort(lua_State *L) { OnlyCallFromInitLua(L, "ProgramPort"); return LuaProgramInt(L, ProgramPort); } static int LuaProgramCache(lua_State *L) { OnlyCallFromMainProcess(L, "ProgramCache"); ProgramCache(luaL_checkinteger(L, 1), luaL_optstring(L, 2, NULL)); return 0; } static int LuaProgramTimeout(lua_State *L) { OnlyCallFromInitLua(L, "ProgramTimeout"); return LuaProgramInt(L, ProgramTimeout); } static int LuaProgramUid(lua_State *L) { OnlyCallFromInitLua(L, "ProgramUid"); return LuaProgramInt(L, ProgramUid); } static int LuaProgramGid(lua_State *L) { OnlyCallFromInitLua(L, "ProgramGid"); return LuaProgramInt(L, ProgramGid); } static int LuaProgramMaxPayloadSize(lua_State *L) { OnlyCallFromInitLua(L, "ProgramMaxPayloadSize"); return LuaProgramInt(L, ProgramMaxPayloadSize); } static int LuaGetClientFd(lua_State *L) { OnlyCallDuringConnection(L, "GetClientFd"); lua_pushinteger(L, client); return 1; } static int LuaIsClientUsingSsl(lua_State *L) { OnlyCallDuringConnection(L, "IsClientUsingSsl"); lua_pushboolean(L, usingssl); return 1; } static int LuaProgramSslTicketLifetime(lua_State *L) { OnlyCallFromInitLua(L, "ProgramSslTicketLifetime"); return LuaProgramInt(L, ProgramSslTicketLifetime); } static int LuaProgramUniprocess(lua_State *L) { OnlyCallFromInitLua(L, "ProgramUniprocess"); if (!lua_isboolean(L, 1) && !lua_isnoneornil(L, 1)) { return luaL_argerror(L, 1, "invalid uniprocess mode; boolean expected"); } lua_pushboolean(L, uniprocess); if (lua_isboolean(L, 1)) uniprocess = lua_toboolean(L, 1); return 1; } static int LuaProgramHeartbeatInterval(lua_State *L) { int64_t millis; OnlyCallFromMainProcess(L, "ProgramHeartbeatInterval"); if (!lua_isnoneornil(L, 1)) { millis = luaL_checkinteger(L, 1); millis = MAX(100, millis); heartbeatinterval = timespec_frommillis(millis); } lua_pushinteger(L, timespec_tomillis(heartbeatinterval)); return 1; } static int LuaProgramMaxWorkers(lua_State *L) { OnlyCallFromMainProcess(L, "ProgramMaxWorkers"); if (!lua_isinteger(L, 1) && !lua_isnoneornil(L, 1)) { return luaL_argerror(L, 1, "invalid number of workers; integer expected"); } lua_pushinteger(L, maxworkers); if (lua_isinteger(L, 1)) maxworkers = lua_tointeger(L, 1); maxworkers = MAX(maxworkers, 1); return 1; } static int LuaProgramAddr(lua_State *L) { uint32_t ip; OnlyCallFromInitLua(L, "ProgramAddr"); if (lua_isinteger(L, 1)) { ip = luaL_checkinteger(L, 1); ips.p = realloc(ips.p, ++ips.n * sizeof(*ips.p)); ips.p[ips.n - 1] = ip; } else { ProgramAddr(luaL_checkstring(L, 1)); } return 0; } static dontinline int LuaProgramString(lua_State *L, void P(const char *)) { P(luaL_checkstring(L, 1)); return 0; } static int LuaProgramBrand(lua_State *L) { OnlyCallFromMainProcess(L, "ProgramBrand"); return LuaProgramString(L, ProgramBrand); } static int LuaProgramDirectory(lua_State *L) { struct stat st; const char *path = luaL_checkstring(L, 1); // check to raise a Lua error, to allow it to be handled if (stat(path, &st) == -1 || !S_ISDIR(st.st_mode)) { return luaL_argerror(L, 1, "not a directory"); } return LuaProgramString(L, ProgramDirectory); } static int LuaProgramLogPath(lua_State *L) { OnlyCallFromInitLua(L, "ProgramLogPath"); return LuaProgramString(L, ProgramLogPath); } static int LuaProgramPidPath(lua_State *L) { OnlyCallFromInitLua(L, "ProgramPidPath"); return LuaProgramString(L, ProgramPidPath); } static int LuaProgramSslPresharedKey(lua_State *L) { struct Psk psk; size_t n1, n2, i; const char *p1, *p2; OnlyCallFromMainProcess(L, "ProgramSslPresharedKey"); p1 = luaL_checklstring(L, 1, &n1); p2 = luaL_checklstring(L, 2, &n2); if (!n1 || n1 > MBEDTLS_PSK_MAX_LEN || !n2) { luaL_argerror(L, 1, "bad preshared key length"); __builtin_unreachable(); } psk.key = memcpy(malloc(n1), p1, n1); psk.key_len = n1; psk.identity = memcpy(malloc(n2), p2, n2); psk.identity_len = n2; for (i = 0; i < psks.n; ++i) { if (SlicesEqual(psk.identity, psk.identity_len, psks.p[i].identity, psks.p[i].identity_len)) { mbedtls_platform_zeroize(psks.p[i].key, psks.p[i].key_len); free(psks.p[i].key); free(psks.p[i].identity); psks.p[i] = psk; return 0; } } psks.p = realloc(psks.p, ++psks.n * sizeof(*psks.p)); psks.p[psks.n - 1] = psk; return 0; } static int LuaProgramSslCiphersuite(lua_State *L) { const mbedtls_ssl_ciphersuite_t *suite; OnlyCallFromInitLua(L, "ProgramSslCiphersuite"); if (!(suite = GetCipherSuite(luaL_checkstring(L, 1)))) { luaL_argerror(L, 1, "unsupported or unknown ciphersuite"); __builtin_unreachable(); } suites.p = realloc(suites.p, (++suites.n + 1) * sizeof(*suites.p)); suites.p[suites.n - 1] = suite->id; suites.p[suites.n - 0] = 0; return 0; } static int LuaProgramPrivateKey(lua_State *L) { size_t n; const char *p; OnlyCallFromInitLua(L, "ProgramPrivateKey"); p = luaL_checklstring(L, 1, &n); ProgramPrivateKey(p, n); return 0; } static int LuaProgramCertificate(lua_State *L) { size_t n; const char *p; OnlyCallFromInitLua(L, "ProgramCertificate"); p = luaL_checklstring(L, 1, &n); ProgramCertificate(p, n); return 0; } static int LuaProgramHeader(lua_State *L) { ProgramHeader( _gc(xasprintf("%s: %s", luaL_checkstring(L, 1), luaL_checkstring(L, 2)))); return 0; } static int LuaProgramRedirect(lua_State *L) { int code; const char *from, *to; code = luaL_checkinteger(L, 1); from = luaL_checkstring(L, 2); to = luaL_checkstring(L, 3); ProgramRedirect(code, strdup(from), strlen(from), strdup(to), strlen(to)); return 0; } static dontinline int LuaProgramBool(lua_State *L, bool *b) { *b = lua_toboolean(L, 1); return 0; } static int LuaProgramSslClientVerify(lua_State *L) { OnlyCallFromInitLua(L, "ProgramSslClientVerify"); return LuaProgramBool(L, &sslclientverify); } static int LuaProgramSslRequired(lua_State *L) { OnlyCallFromInitLua(L, "ProgramSslRequired"); return LuaProgramBool(L, &requiressl); } static int LuaProgramSslFetchVerify(lua_State *L) { OnlyCallFromMainProcess(L, "ProgramSslFetchVerify"); return LuaProgramBool(L, &sslfetchverify); } static int LuaProgramSslInit(lua_State *L) { OnlyCallFromInitLua(L, "SslInit"); TlsInit(); return 0; } static int LuaProgramLogMessages(lua_State *L) { return LuaProgramBool(L, &logmessages); } static int LuaProgramLogBodies(lua_State *L) { return LuaProgramBool(L, &logbodies); } static int LuaEvadeDragnetSurveillance(lua_State *L) { return LuaProgramBool(L, &evadedragnetsurveillance); } static int LuaHidePath(lua_State *L) { size_t pathlen; const char *path; path = luaL_checklstring(L, 1, &pathlen); AddString(&hidepaths, memcpy(malloc(pathlen), path, pathlen), pathlen); return 0; } static int LuaIsHiddenPath(lua_State *L) { size_t n; const char *s; s = luaL_checklstring(L, 1, &n); lua_pushboolean(L, IsHiddenPath(s, n)); return 1; } static int LuaGetZipPaths(lua_State *L) { char *path; uint8_t *zcf; size_t i, n, pathlen, prefixlen; const char *prefix = luaL_optlstring(L, 1, "", &prefixlen); lua_newtable(L); i = 0; n = GetZipCdirRecords(zcdir); for (zcf = zmap + GetZipCdirOffset(zcdir); n--; zcf += ZIP_CFILE_HDRSIZE(zcf)) { CHECK_EQ(kZipCfileHdrMagic, ZIP_CFILE_MAGIC(zcf)); path = GetAssetPath(zcf, &pathlen); if (prefixlen == 0 || startswith(path, prefix)) { lua_pushlstring(L, path, pathlen); lua_seti(L, -2, ++i); } free(path); } return 1; } static int LuaGetAssetMode(lua_State *L) { size_t pathlen; struct Asset *a; const char *path; path = LuaCheckPath(L, 1, &pathlen); if ((a = GetAsset(path, pathlen))) { lua_pushinteger(L, GetMode(a)); } else { lua_pushnil(L); } return 1; } static int LuaGetAssetLastModifiedTime(lua_State *L) { size_t pathlen; struct Asset *a; const char *path; struct timespec lm; int64_t zuluseconds; path = LuaCheckPath(L, 1, &pathlen); if ((a = GetAsset(path, pathlen))) { if (a->file) { zuluseconds = a->file->st.st_mtim.tv_sec; } else { GetZipCfileTimestamps(zmap + a->cf, &lm, 0, 0, gmtoff); zuluseconds = lm.tv_sec; } lua_pushinteger(L, zuluseconds); } else { lua_pushnil(L); } return 1; } static int LuaGetAssetSize(lua_State *L) { size_t pathlen; struct Asset *a; const char *path; path = LuaCheckPath(L, 1, &pathlen); if ((a = GetAsset(path, pathlen))) { if (a->file) { lua_pushinteger(L, a->file->st.st_size); } else { lua_pushinteger(L, GetZipLfileUncompressedSize(zmap + a->lf)); } } else { lua_pushnil(L); } return 1; } static int LuaIsAssetCompressed(lua_State *L) { size_t pathlen; struct Asset *a; const char *path; path = LuaCheckPath(L, 1, &pathlen); if ((a = GetAsset(path, pathlen))) { lua_pushboolean(L, IsCompressed(a)); } else { lua_pushnil(L); } return 1; } static bool Blackhole(uint32_t ip) { char buf[4]; if (blackhole.fd <= 0) return false; WRITE32BE(buf, ip); if (sendto(blackhole.fd, &buf, 4, 0, (struct sockaddr *)&blackhole.addr, sizeof(blackhole.addr)) != -1) { return true; } else { VERBOSEF("(token) sendto(%s) failed: %m", blackhole.addr.sun_path); errno = 0; return false; } } static int LuaBlackhole(lua_State *L) { lua_Integer ip; ip = luaL_checkinteger(L, 1); if (!(0 <= ip && ip <= 0xffffffff)) { luaL_argerror(L, 1, "ip out of range"); __builtin_unreachable(); } lua_pushboolean(L, Blackhole(ip)); return 1; } wontreturn static void Replenisher(void) { struct timespec ts; VERBOSEF("(token) replenish worker started"); strace_enabled(-1); signal(SIGINT, OnTerm); signal(SIGHUP, OnTerm); signal(SIGTERM, OnTerm); signal(SIGUSR1, SIG_IGN); // make sure reload won't kill this signal(SIGUSR2, SIG_IGN); // make sure meltdown won't kill this ts = timespec_real(); while (!terminated) { if (clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &ts, 0)) { errno = 0; continue; } ReplenishTokens(tokenbucket.w, (1ul << tokenbucket.cidr) / 8); ts = timespec_add(ts, tokenbucket.replenish); DEBUGF("(token) replenished tokens"); } VERBOSEF("(token) replenish worker exiting"); _Exit(0); } static int LuaAcquireToken(lua_State *L) { uint32_t ip; if (!tokenbucket.cidr) { luaL_error(L, "ProgramTokenBucket() needs to be called first"); __builtin_unreachable(); } GetClientAddr(&ip, 0); lua_pushinteger(L, AcquireToken(tokenbucket.b, luaL_optinteger(L, 1, ip), tokenbucket.cidr)); return 1; } static int LuaCountTokens(lua_State *L) { uint32_t ip; if (!tokenbucket.cidr) { luaL_error(L, "ProgramTokenBucket() needs to be called first"); __builtin_unreachable(); } GetClientAddr(&ip, 0); lua_pushinteger(L, CountTokens(tokenbucket.b, luaL_optinteger(L, 1, ip), tokenbucket.cidr)); return 1; } static int LuaProgramTokenBucket(lua_State *L) { if (tokenbucket.cidr) { luaL_error(L, "ProgramTokenBucket() can only be called once"); __builtin_unreachable(); } lua_Number replenish = luaL_optnumber(L, 1, 1); // per second lua_Integer cidr = luaL_optinteger(L, 2, 24); lua_Integer reject = luaL_optinteger(L, 3, 30); lua_Integer ignore = luaL_optinteger(L, 4, MIN(reject / 2, 15)); lua_Integer ban = luaL_optinteger(L, 5, MIN(ignore / 10, 1)); if (!(1 / 3600. <= replenish && replenish <= 1e6)) { luaL_argerror(L, 1, "require 1/3600 <= replenish <= 1e6"); __builtin_unreachable(); } if (!(8 <= cidr && cidr <= 32)) { luaL_argerror(L, 2, "require 8 <= cidr <= 32"); __builtin_unreachable(); } if (!(-1 <= reject && reject <= 126)) { luaL_argerror(L, 3, "require -1 <= reject <= 126"); __builtin_unreachable(); } if (!(-1 <= ignore && ignore <= 126)) { luaL_argerror(L, 4, "require -1 <= ignore <= 126"); __builtin_unreachable(); } if (!(-1 <= ban && ban <= 126)) { luaL_argerror(L, 5, "require -1 <= ban <= 126"); __builtin_unreachable(); } if (!(ignore <= reject)) { luaL_argerror(L, 4, "require ignore <= reject"); __builtin_unreachable(); } if (!(ban <= ignore)) { luaL_argerror(L, 5, "require ban <= ignore"); __builtin_unreachable(); } VERBOSEF("(token) deploying %,ld buckets " "(one for every %ld ips) " "each holding 127 tokens which " "replenish %g times per second, " "reject at %d tokens, " "ignore at %d tokens, and " "ban at %d tokens", 1L << cidr, // 4294967296 / (1L << cidr), // replenish, // reject, // ignore, // ban); if (ignore == -1) ignore = -128; if (ban == -1) ban = -128; if (ban >= 0 && (IsLinux() || IsBsd())) { uint32_t testip = 0; blackhole.addr.sun_family = AF_UNIX; strlcpy(blackhole.addr.sun_path, "/var/run/blackhole.sock", sizeof(blackhole.addr.sun_path)); if ((blackhole.fd = socket(AF_UNIX, SOCK_DGRAM, 0)) == -1) { WARNF("(token) socket(AF_UNIX) failed: %m"); ban = -1; } else if (sendto(blackhole.fd, &testip, 4, 0, (struct sockaddr *)&blackhole.addr, sizeof(blackhole.addr)) == -1) { VERBOSEF("(token) error: sendto(%`'s) failed: %m", blackhole.addr.sun_path); VERBOSEF("(token) redbean isn't able to protect your kernel from ddos"); VERBOSEF("(token) please run the blackholed program; see our website!"); } } tokenbucket.b = _mapshared(ROUNDUP(1ul << cidr, FRAMESIZE)); memset(tokenbucket.b, 127, 1ul << cidr); tokenbucket.cidr = cidr; tokenbucket.reject = reject; tokenbucket.ignore = ignore; tokenbucket.ban = ban; tokenbucket.replenish = timespec_fromnanos(1 / replenish * 1e9); int pid = fork(); npassert(pid != -1); if (!pid) Replenisher(); ++shared->workers; return 0; } static const char *GetContentTypeExt(const char *path, size_t n) { const char *r, *e; int top; lua_State *L = GL; if ((r = FindContentType(path, n))) return r; // extract the last .; use the entire path if none is present if ((e = memrchr(path, '.', n))) { n -= e - path + 1; path = e + 1; } top = lua_gettop(L); lua_pushlightuserdata(L, (void *)&ctIdx); // push address as unique key CHECK_EQ(lua_gettable(L, LUA_REGISTRYINDEX), LUA_TTABLE); lua_pushlstring(L, path, n); if (lua_gettable(L, -2) == LUA_TSTRING) r = FreeLater(strdup(lua_tostring(L, -1))); lua_settop(L, top); return r; } static int LuaProgramContentType(lua_State *L) { const char *ext = luaL_checkstring(L, 1); const char *ct; int n = lua_gettop(L); lua_pushlightuserdata(L, (void *)&ctIdx); // push address as unique key CHECK_EQ(lua_gettable(L, LUA_REGISTRYINDEX), LUA_TTABLE); if (n == 1) { ext = FreeLater(xasprintf(".%s", ext)); if ((ct = GetContentTypeExt(ext, strlen(ext)))) { lua_pushstring(L, ct); } else { lua_pushnil(L); } return 1; } else { ct = luaL_checkstring(L, 2); lua_pushstring(L, ext); lua_pushstring(L, ct); lua_settable(L, -3); // table[ext] = ct return 0; } } static int LuaIsDaemon(lua_State *L) { lua_pushboolean(L, daemonize); return 1; } static int LuaGetAssetComment(lua_State *L) { struct Asset *a; const char *path; size_t pathlen, m; path = LuaCheckPath(L, 1, &pathlen); if ((a = GetAssetZip(path, pathlen)) && (m = strnlen(ZIP_CFILE_COMMENT(zmap + a->cf), ZIP_CFILE_COMMENTSIZE(zmap + a->cf)))) { lua_pushlstring(L, ZIP_CFILE_COMMENT(zmap + a->cf), m); } else { lua_pushnil(L); } return 1; } static int LuaLaunchBrowser(lua_State *L) { OnlyCallFromInitLua(L, "LaunchBrowser"); launchbrowser = strdup(luaL_optstring(L, 1, "/")); return 0; } static bool LuaRunAsset(const char *path, bool mandatory) { int status; struct Asset *a; const char *code; size_t pathlen, codelen; pathlen = strlen(path); if ((a = GetAsset(path, pathlen))) { if ((code = FreeLater(LoadAsset(a, &codelen)))) { lua_State *L = GL; effectivepath.p = (void *)path; effectivepath.n = pathlen; DEBUGF("(lua) LuaRunAsset(%`'s)", path); status = luaL_loadbuffer( L, code, codelen, FreeLater(xasprintf("@%s%s", a->file ? "" : "/zip", path))); if (status != LUA_OK || LuaCallWithTrace(L, 0, 0, NULL) != LUA_OK) { LogLuaError("lua code", lua_tostring(L, -1)); lua_pop(L, 1); // pop error if (mandatory) exit(1); } } } return !!a; } // // list of functions that can't be run from the repl static const char *const kDontAutoComplete[] = { "Compress", // deprecated "GetBody", // "GetClientAddr", // "GetClientFd", // "GetComment", // deprecated "GetCookie", // "GetEffectivePath", // "GetFragment", // "GetHeader", // "GetHeaders", // "GetHost", // "GetHttpVersion", // "GetLastModifiedTime", // deprecated "GetMethod", // "GetParam", // "GetParams", // "GetPass", // "GetPath", // "GetPayload", // deprecated "GetPort", // "GetRemoteAddr", // "GetResponseBody", // "GetScheme", // "GetServerAddr", // "GetSslIdentity", // "GetStatus", // "GetUrl", // "GetUser", // "GetVersion", // deprecated "HasParam", // "IsClientUsingSsl", // "IsCompressed", // deprecated "LaunchBrowser", // "LuaProgramSslRequired", // TODO "ProgramAddr", // TODO "ProgramBrand", // "ProgramCertificate", // TODO "ProgramGid", // "ProgramLogPath", // TODO "ProgramMaxPayloadSize", // TODO "ProgramPidPath", // TODO "ProgramPort", // TODO "ProgramPrivateKey", // TODO "ProgramSslCiphersuite", // TODO "ProgramSslClientVerify", // TODO "ProgramSslTicketLifetime", // "ProgramTimeout", // TODO "ProgramUid", // "ProgramUniprocess", // "Respond", // "Route", // "RouteHost", // "RoutePath", // "ServeAsset", // "ServeIndex", // "ServeListing", // "ServeRedirect", // "ServeStatusz", // "SetCookie", // "SetHeader", // "SslInit", // TODO "Uncompress", // deprecated "Write", // }; // static const luaL_Reg kLuaFuncs[] = { {"Barf", LuaBarf}, // {"Benchmark", LuaBenchmark}, // {"Bsf", LuaBsf}, // {"Bsr", LuaBsr}, // {"CategorizeIp", LuaCategorizeIp}, // {"Compress", LuaCompress}, // {"Crc32", LuaCrc32}, // {"Crc32c", LuaCrc32c}, // {"Decimate", LuaDecimate}, // {"DecodeBase32", LuaDecodeBase32}, // {"DecodeBase64", LuaDecodeBase64}, // {"DecodeHex", LuaDecodeHex}, // {"DecodeJson", LuaDecodeJson}, // {"DecodeLatin1", LuaDecodeLatin1}, // {"Deflate", LuaDeflate}, // {"EncodeBase32", LuaEncodeBase32}, // {"EncodeBase64", LuaEncodeBase64}, // {"EncodeHex", LuaEncodeHex}, // {"EncodeJson", LuaEncodeJson}, // {"EncodeLatin1", LuaEncodeLatin1}, // {"EncodeLua", LuaEncodeLua}, // {"EncodeUrl", LuaEncodeUrl}, // {"EscapeFragment", LuaEscapeFragment}, // {"EscapeHost", LuaEscapeHost}, // {"EscapeHtml", LuaEscapeHtml}, // {"EscapeIp", LuaEscapeIp}, // {"EscapeLiteral", LuaEscapeLiteral}, // {"EscapeParam", LuaEscapeParam}, // {"EscapePass", LuaEscapePass}, // {"EscapePath", LuaEscapePath}, // {"EscapeSegment", LuaEscapeSegment}, // {"EscapeUser", LuaEscapeUser}, // {"Fetch", LuaFetch}, // {"FormatHttpDateTime", LuaFormatHttpDateTime}, // {"FormatIp", LuaFormatIp}, // {"GetAssetComment", LuaGetAssetComment}, // {"GetAssetLastModifiedTime", LuaGetAssetLastModifiedTime}, // {"GetAssetMode", LuaGetAssetMode}, // {"GetAssetSize", LuaGetAssetSize}, // {"GetBody", LuaGetBody}, // {"GetClientAddr", LuaGetClientAddr}, // {"GetClientFd", LuaGetClientFd}, // {"GetCookie", LuaGetCookie}, // {"GetCpuCore", LuaGetCpuCore}, // {"GetCpuCount", LuaGetCpuCount}, // {"GetCpuNode", LuaGetCpuNode}, // {"GetCryptoHash", LuaGetCryptoHash}, // {"GetDate", LuaGetDate}, // {"GetEffectivePath", LuaGetEffectivePath}, // {"GetFragment", LuaGetFragment}, // {"GetHeader", LuaGetHeader}, // {"GetHeaders", LuaGetHeaders}, // {"GetHost", LuaGetHost}, // {"GetHostIsa", LuaGetHostIsa}, // {"GetHostOs", LuaGetHostOs}, // {"GetHttpReason", LuaGetHttpReason}, // {"GetHttpVersion", LuaGetHttpVersion}, // {"GetLogLevel", LuaGetLogLevel}, // {"GetMethod", LuaGetMethod}, // {"GetMonospaceWidth", LuaGetMonospaceWidth}, // {"GetParam", LuaGetParam}, // {"GetParams", LuaGetParams}, // {"GetPass", LuaGetPass}, // {"GetPath", LuaGetPath}, // {"GetPort", LuaGetPort}, // {"GetRandomBytes", LuaGetRandomBytes}, // {"GetRedbeanVersion", LuaGetRedbeanVersion}, // {"GetRemoteAddr", LuaGetRemoteAddr}, // {"GetResponseBody", LuaGetResponseBody}, // {"GetScheme", LuaGetScheme}, // {"GetServerAddr", LuaGetServerAddr}, // {"GetStatus", LuaGetStatus}, // {"GetTime", LuaGetTime}, // {"GetUrl", LuaGetUrl}, // {"GetUser", LuaGetUser}, // {"GetZipPaths", LuaGetZipPaths}, // {"HasControlCodes", LuaHasControlCodes}, // {"HasParam", LuaHasParam}, // {"HidePath", LuaHidePath}, // {"HighwayHash64", LuaHighwayHash64}, // {"IndentLines", LuaIndentLines}, // {"Inflate", LuaInflate}, // {"IsAcceptableHost", LuaIsAcceptableHost}, // {"IsAcceptablePath", LuaIsAcceptablePath}, // {"IsAcceptablePort", LuaIsAcceptablePort}, // {"IsAssetCompressed", LuaIsAssetCompressed}, // {"IsClientUsingSsl", LuaIsClientUsingSsl}, // {"IsDaemon", LuaIsDaemon}, // {"IsHeaderRepeatable", LuaIsHeaderRepeatable}, // {"IsHiddenPath", LuaIsHiddenPath}, // {"IsLoopbackIp", LuaIsLoopbackIp}, // {"IsPrivateIp", LuaIsPrivateIp}, // {"IsPublicIp", LuaIsPublicIp}, // {"IsReasonablePath", LuaIsReasonablePath}, // {"IsTrustedIp", LuaIsTrusted}, // undocumented {"IsValidHttpToken", LuaIsValidHttpToken}, // {"LaunchBrowser", LuaLaunchBrowser}, // {"Lemur64", LuaLemur64}, // {"LoadAsset", LuaLoadAsset}, // {"Log", LuaLog}, // {"Md5", LuaMd5}, // {"MeasureEntropy", LuaMeasureEntropy}, // {"ParseHost", LuaParseHost}, // {"ParseHttpDateTime", LuaParseHttpDateTime}, // {"ParseIp", LuaParseIp}, // {"ParseParams", LuaParseParams}, // {"ParseUrl", LuaParseUrl}, // {"Popcnt", LuaPopcnt}, // {"ProgramAddr", LuaProgramAddr}, // {"ProgramBrand", LuaProgramBrand}, // {"ProgramCache", LuaProgramCache}, // {"ProgramContentType", LuaProgramContentType}, // {"ProgramDirectory", LuaProgramDirectory}, // {"ProgramGid", LuaProgramGid}, // {"ProgramHeader", LuaProgramHeader}, // {"ProgramHeartbeatInterval", LuaProgramHeartbeatInterval}, // {"ProgramLogBodies", LuaProgramLogBodies}, // {"ProgramLogMessages", LuaProgramLogMessages}, // {"ProgramLogPath", LuaProgramLogPath}, // {"ProgramMaxPayloadSize", LuaProgramMaxPayloadSize}, // {"ProgramMaxWorkers", LuaProgramMaxWorkers}, // {"ProgramPidPath", LuaProgramPidPath}, // {"ProgramPort", LuaProgramPort}, // {"ProgramRedirect", LuaProgramRedirect}, // {"ProgramTimeout", LuaProgramTimeout}, // {"ProgramTrustedIp", LuaProgramTrustedIp}, // undocumented {"ProgramUid", LuaProgramUid}, // {"ProgramUniprocess", LuaProgramUniprocess}, // {"Rand64", LuaRand64}, // {"Rdrand", LuaRdrand}, // {"Rdseed", LuaRdseed}, // {"Rdtsc", LuaRdtsc}, // {"ResolveIp", LuaResolveIp}, // {"Route", LuaRoute}, // {"RouteHost", LuaRouteHost}, // {"RoutePath", LuaRoutePath}, // {"ServeAsset", LuaServeAsset}, // {"ServeError", LuaServeError}, // {"ServeIndex", LuaServeIndex}, // {"ServeListing", LuaServeListing}, // {"ServeRedirect", LuaServeRedirect}, // {"ServeStatusz", LuaServeStatusz}, // {"SetCookie", LuaSetCookie}, // {"SetHeader", LuaSetHeader}, // {"SetLogLevel", LuaSetLogLevel}, // {"SetStatus", LuaSetStatus}, // {"Sha1", LuaSha1}, // {"Sha224", LuaSha224}, // {"Sha256", LuaSha256}, // {"Sha384", LuaSha384}, // {"Sha512", LuaSha512}, // {"Sleep", LuaSleep}, // {"Slurp", LuaSlurp}, // {"StoreAsset", LuaStoreAsset}, // {"Uncompress", LuaUncompress}, // {"Underlong", LuaUnderlong}, // {"VisualizeControlCodes", LuaVisualizeControlCodes}, // {"Write", LuaWrite}, // {"bin", LuaBin}, // {"hex", LuaHex}, // {"oct", LuaOct}, // #ifndef UNSECURE {"AcquireToken", LuaAcquireToken}, // {"Blackhole", LuaBlackhole}, // undocumented {"CountTokens", LuaCountTokens}, // {"EvadeDragnetSurveillance", LuaEvadeDragnetSurveillance}, // {"GetSslIdentity", LuaGetSslIdentity}, // {"ProgramCertificate", LuaProgramCertificate}, // {"ProgramPrivateKey", LuaProgramPrivateKey}, // {"ProgramSslCiphersuite", LuaProgramSslCiphersuite}, // {"ProgramSslClientVerify", LuaProgramSslClientVerify}, // {"ProgramSslFetchVerify", LuaProgramSslFetchVerify}, // {"ProgramSslInit", LuaProgramSslInit}, // {"ProgramSslPresharedKey", LuaProgramSslPresharedKey}, // {"ProgramSslRequired", LuaProgramSslRequired}, // {"ProgramSslTicketLifetime", LuaProgramSslTicketLifetime}, // {"ProgramTokenBucket", LuaProgramTokenBucket}, // #endif // deprecated {"GetPayload", LuaGetBody}, // {"GetComment", LuaGetAssetComment}, // {"GetVersion", LuaGetHttpVersion}, // {"IsCompressed", LuaIsAssetCompressed}, // {"GetLastModifiedTime", LuaGetAssetLastModifiedTime}, // }; static const luaL_Reg kLuaLibs[] = { {"argon2", luaopen_argon2}, // {"lsqlite3", luaopen_lsqlite3}, // {"maxmind", LuaMaxmind}, // {"finger", LuaFinger}, // {"path", LuaPath}, // {"re", LuaRe}, // {"unix", LuaUnix}, // }; static void LuaSetArgv(lua_State *L) { int i, j = -1; lua_newtable(L); lua_pushstring(L, __argv[0]); lua_seti(L, -2, j++); if (!interpretermode) { lua_pushstring(L, "/zip/.init.lua"); lua_seti(L, -2, j++); } for (i = optind; i < __argc; ++i) { lua_pushstring(L, __argv[i]); lua_seti(L, -2, j++); } lua_pushvalue(L, -1); lua_setglobal(L, "argv"); // deprecated lua_setglobal(L, "arg"); } static void LuaSetConstant(lua_State *L, const char *s, long x) { lua_pushinteger(L, x); lua_setglobal(L, s); } static void LuaStart(void) { #ifndef STATIC size_t i; lua_State *L = GL = luaL_newstate(); g_lua_path_default = DEFAULTLUAPATH; luaL_openlibs(L); for (i = 0; i < ARRAYLEN(kLuaLibs); ++i) { luaL_requiref(L, kLuaLibs[i].name, kLuaLibs[i].func, 1); lua_pop(L, 1); } for (i = 0; i < ARRAYLEN(kLuaFuncs); ++i) { lua_pushcfunction(L, kLuaFuncs[i].func); lua_setglobal(L, kLuaFuncs[i].name); } LuaSetConstant(L, "kLogDebug", kLogDebug); LuaSetConstant(L, "kLogVerbose", kLogVerbose); LuaSetConstant(L, "kLogInfo", kLogInfo); LuaSetConstant(L, "kLogWarn", kLogWarn); LuaSetConstant(L, "kLogError", kLogError); LuaSetConstant(L, "kLogFatal", kLogFatal); LuaSetConstant(L, "kUrlPlus", kUrlPlus); LuaSetConstant(L, "kUrlLatin1", kUrlLatin1); // create a list of custom content types lua_pushlightuserdata(L, (void *)&ctIdx); // push address as unique key lua_newtable(L); lua_settable(L, LUA_REGISTRYINDEX); // registry[&ctIdx] = {} #endif } static bool ShouldAutocomplete(const char *s) { int c, m, l, r; l = 0; r = ARRAYLEN(kDontAutoComplete) - 1; while (l <= r) { m = (l & r) + ((l ^ r) >> 1); // floor((a+b)/2) c = strcmp(kDontAutoComplete[m], s); if (c < 0) { l = m + 1; } else if (c > 0) { r = m - 1; } else { return false; } } return true; } static void HandleCompletions(const char *p, linenoiseCompletions *c) { size_t i, j; for (j = i = 0; i < c->len; ++i) { if (ShouldAutocomplete(c->cvec[i])) { c->cvec[j++] = c->cvec[i]; } else { free(c->cvec[i]); } } c->len = j; } static void LuaPrint(lua_State *L) { int i, n; char *b = 0; n = lua_gettop(L); if (n > 0) { for (i = 1; i <= n; i++) { if (i > 1) appendw(&b, '\t'); struct EncoderConfig conf = { .maxdepth = 64, .sorted = true, .pretty = true, .indent = " ", }; LuaEncodeLuaData(L, &b, i, conf); } appendw(&b, '\n'); WRITE(1, b, appendz(b).i); free(b); } } static void EnableRawMode(void) { if (lua_repl_isterminal) { strace_enabled(-1); linenoiseEnableRawMode(0); strace_enabled(+1); } } static void DisableRawMode(void) { strace_enabled(-1); linenoiseDisableRawMode(); strace_enabled(+1); } static int LuaInterpreter(lua_State *L) { int i, n, sig, status; const char *script; if (optind < __argc) { script = __argv[optind]; if (!strcmp(script, "-")) script = 0; if ((status = luaL_loadfile(L, script)) == LUA_OK) { lua_getglobal(L, "arg"); n = luaL_len(L, -1); luaL_checkstack(L, n + 3, "too many script args"); for (i = 1; i <= n; i++) lua_rawgeti(L, -i, i); lua_remove(L, -i); // remove arg table from stack TRACE_BEGIN; status = lua_runchunk(L, n, LUA_MULTRET); TRACE_END; } return lua_report(L, status); } else { lua_repl_blocking = true; lua_repl_completions_callback = HandleCompletions; lua_initrepl(GL); EnableRawMode(); for (;;) { status = lua_loadline(L); if (status == -1) break; // eof if (status == -2) { if (errno == EINTR) { if ((sig = linenoiseGetInterrupt())) { kill(0, sig); } } fprintf(stderr, "i/o error: %m\n"); exit(1); } if (status == LUA_OK) { TRACE_BEGIN; status = lua_runchunk(GL, 0, LUA_MULTRET); TRACE_END; } if (status == LUA_OK) { LuaPrint(GL); } else { lua_report(GL, status); } } DisableRawMode(); lua_freerepl(); lua_settop(GL, 0); // clear stack if ((sig = linenoiseGetInterrupt())) { raise(sig); } return status; } } static void LuaDestroy(void) { #ifndef STATIC lua_State *L = GL; lua_close(L); #endif } static void MemDestroy(void) { FreeAssets(); CollectGarbage(); inbuf.p = 0, inbuf.n = 0, inbuf.c = 0; Free(&inbuf_actual.p), inbuf_actual.n = inbuf_actual.c = 0; Free(&unmaplist.p), unmaplist.n = unmaplist.c = 0; Free(&freelist.p), freelist.n = freelist.c = 0; Free(&hdrbuf.p), hdrbuf.n = hdrbuf.c = 0; Free(&servers.p), servers.n = 0; Free(&ports.p), ports.n = 0; Free(&ips.p), ips.n = 0; Free(&cpm.outbuf); FreeStrings(&stagedirs); FreeStrings(&hidepaths); Free(&cachedirective); Free(&launchbrowser); Free(&serverheader); Free(&trustedips.p); Free(&interfaces); Free(&extrahdrs); Free(&pidpath); Free(&logpath); Free(&brand); Free(&polls); } static void LuaInit(void) { #ifndef STATIC lua_State *L = GL; LuaSetArgv(L); if (interpretermode) { int rc = LuaInterpreter(L); LuaDestroy(); MemDestroy(); if (IsModeDbg()) { CheckForMemoryLeaks(); } exit(rc); } if (LuaRunAsset("/.init.lua", true)) { hasonhttprequest = IsHookDefined("OnHttpRequest"); hasonclientconnection = IsHookDefined("OnClientConnection"); hasonprocesscreate = IsHookDefined("OnProcessCreate"); hasonprocessdestroy = IsHookDefined("OnProcessDestroy"); hasonworkerstart = IsHookDefined("OnWorkerStart"); hasonworkerstop = IsHookDefined("OnWorkerStop"); hasonloglatency = IsHookDefined("OnLogLatency"); } else { DEBUGF("(srvr) no /.init.lua defined"); } #endif } static void LuaOnServerReload(bool reindex) { #ifndef STATIC if (!LuaRunAsset("/.reload.lua", false)) { DEBUGF("(srvr) no /.reload.lua defined"); } lua_State *L = GL; lua_getglobal(L, "OnServerReload"); lua_pushboolean(L, reindex); if (LuaCallWithTrace(L, 1, 0, NULL) != LUA_OK) { LogLuaError("OnServerReload", lua_tostring(L, -1)); lua_pop(L, 1); // pop error } AssertLuaStackIsAt(L, 0); #endif } static const char *DescribeClose(void) { if (killed) return "killed"; if (meltdown) return "meltdown"; if (terminated) return "terminated"; if (connectionclose) return "connection closed"; return "destroyed"; } static void LogClose(const char *reason) { if (amtread || meltdown || killed) { LockInc(&shared->c.fumbles); INFOF("(stat) %s %s with %,ld unprocessed and %,d handled (%,d workers)", DescribeClient(), reason, amtread, messageshandled, shared->workers); } else { DEBUGF("(stat) %s %s with %,d messages handled", DescribeClient(), reason, messageshandled); } } static ssize_t SendString(const char *s) { size_t n; ssize_t rc; struct iovec iov; n = strlen(s); iov.iov_base = (void *)s; iov.iov_len = n; if (logmessages) { LogMessage("sending", s, n); } for (;;) { if ((rc = writer(client, &iov, 1)) != -1 || errno != EINTR) { return rc; } errno = 0; } } static ssize_t SendContinue(void) { return SendString("\ HTTP/1.1 100 Continue\r\n\ \r\n"); } static ssize_t SendTimeout(void) { return SendString("\ 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 SendString("\ HTTP/1.1 503 Service Unavailable\r\n\ Connection: close\r\n\ Content-Length: 0\r\n\ \r\n"); } static ssize_t SendTooManyRequests(void) { return SendString("\ HTTP/1.1 429 Too Many Requests\r\n\ Connection: close\r\n\ Content-Type: text/plain\r\n\ Content-Length: 22\r\n\ \r\n\ 429 Too Many Requests\n"); } static void EnterMeltdownMode(void) { if (timespec_cmp(timespec_sub(timespec_real(), shared->lastmeltdown), (struct timespec){1}) < 0) { return; } WARNF("(srvr) server is melting down (%,d workers)", shared->workers); LOGIFNEG1(kill(0, SIGUSR2)); shared->lastmeltdown = timespec_real(); ++shared->c.meltdowns; } static char *HandlePayloadDisconnect(void) { LockInc(&shared->c.payloaddisconnects); LogClose("payload disconnect"); return ServeFailure(400, "Bad Request"); /* XXX */ } static char *HandlePayloadDrop(void) { LockInc(&shared->c.dropped); LogClose(DescribeClose()); return ServeFailure(503, "Service Unavailable"); } static char *HandleBadContentLength(void) { LockInc(&shared->c.badlengths); return ServeFailure(400, "Bad Content Length"); } static char *HandleLengthRequired(void) { LockInc(&shared->c.missinglengths); return ServeFailure(411, "Length Required"); } static char *HandleVersionNotSupported(void) { LockInc(&shared->c.http12); return ServeFailure(505, "HTTP Version Not Supported"); } static char *HandleConnectRefused(void) { LockInc(&shared->c.connectsrefused); return ServeFailure(501, "Not Implemented"); } static char *HandleExpectFailed(void) { LockInc(&shared->c.expectsrefused); return ServeFailure(417, "Expectation Failed"); } static char *HandleHugePayload(void) { LockInc(&shared->c.hugepayloads); return ServeFailure(413, "Payload Too Large"); } static char *HandleTransferRefused(void) { LockInc(&shared->c.transfersrefused); return ServeFailure(501, "Not Implemented"); } static char *HandleMapFailed(struct Asset *a, int fd) { LockInc(&shared->c.mapfails); WARNF("(srvr) mmap(%`'s) error: %m", a->file->path); close(fd); return ServeError(500, "Internal Server Error"); } static void LogAcceptError(const char *s) { LockInc(&shared->c.accepterrors); WARNF("(srvr) %s accept error: %s", DescribeServer(), s); } static char *HandleOpenFail(struct Asset *a) { LockInc(&shared->c.openfails); WARNF("(srvr) open(%`'s) error: %m", a->file->path); if (errno == ENFILE) { LockInc(&shared->c.enfiles); return ServeError(503, "Service Unavailable"); } else if (errno == EMFILE) { LockInc(&shared->c.emfiles); return ServeError(503, "Service Unavailable"); } else { return ServeError(500, "Internal Server Error"); } } static char *HandlePayloadReadError(void) { if (errno == ECONNRESET) { LockInc(&shared->c.readresets); LogClose("payload reset"); return ServeFailure(400, "Bad Request"); /* XXX */ } else if (errno == EAGAIN || errno == EWOULDBLOCK) { LockInc(&shared->c.readtimeouts); LogClose("payload read timeout"); return ServeFailure(408, "Request Timeout"); } else { LockInc(&shared->c.readerrors); INFOF("(clnt) %s payload read error: %m", DescribeClient()); return ServeFailure(500, "Internal Server Error"); } } static void HandleForkFailure(void) { LockInc(&shared->c.forkerrors); LockInc(&shared->c.dropped); EnterMeltdownMode(); SendServiceUnavailable(); close(client); WARNF("(srvr) too many processes: %m"); } static void HandleFrag(size_t got) { LockInc(&shared->c.frags); DEBUGF("(stat) %s fragged msg added %,ld bytes to %,ld byte buffer", DescribeClient(), amtread, got); } static void HandleReload(void) { LockInc(&shared->c.reloads); LuaOnServerReload(Reindex()); invalidated = false; } static void HandleHeartbeat(void) { size_t i; UpdateCurrentDate(timespec_real()); Reindex(); getrusage(RUSAGE_SELF, &shared->server); #ifndef STATIC CallSimpleHookIfDefined("OnServerHeartbeat"); CollectGarbage(); #endif for (i = 1; i < servers.n; ++i) { if (polls[i].fd < 0) { polls[i].fd = -polls[i].fd; } } } // returns 0 on success or response on error static char *OpenAsset(struct Asset *a) { int fd; void *data; size_t size; struct stat *st; if (a->file->st.st_size) { size = a->file->st.st_size; if (cpm.msg.method == kHttpHead) { cpm.content = 0; cpm.contentlength = size; } else { OpenAgain: if ((fd = open(a->file->path.s, O_RDONLY)) != -1) { data = mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0); if (data != MAP_FAILED) { LockInc(&shared->c.maps); UnmapLater(fd, data, size); cpm.content = data; cpm.contentlength = size; } else if ((st = _gc(malloc(sizeof(struct stat)))) && fstat(fd, st) != -1 && (data = malloc(st->st_size))) { /* probably empty file or zipos handle */ LockInc(&shared->c.slurps); FreeLater(data); if (ReadAll(fd, data, st->st_size) != -1) { cpm.content = data; cpm.contentlength = st->st_size; close(fd); } else { return HandleMapFailed(a, fd); } } else { return HandleMapFailed(a, fd); } } else if (errno == EINTR) { errno = 0; goto OpenAgain; } else { return HandleOpenFail(a); } } } else { cpm.content = ""; cpm.contentlength = 0; } return 0; } static char *ServeServerOptions(void) { char *p; LockInc(&shared->c.serveroptions); p = SetStatus(200, "OK"); #ifdef STATIC p = stpcpy(p, "Allow: GET, HEAD, OPTIONS\r\n"); #else p = stpcpy(p, "Accept: */*\r\n" "Accept-Charset: utf-8,ISO-8859-1;q=0.7,*;q=0.5\r\n" "Allow: GET, HEAD, POST, PUT, DELETE, OPTIONS\r\n"); #endif return p; } static void SendContinueIfNeeded(void) { if (cpm.msg.version >= 11 && HeaderEqualCase(kHttpExpect, "100-continue")) { LockInc(&shared->c.continues); SendContinue(); } } static char *ReadMore(void) { size_t got; ssize_t rc; LockInc(&shared->c.frags); if ((rc = reader(client, inbuf.p + amtread, inbuf.n - amtread)) != -1) { if (!(got = rc)) return HandlePayloadDisconnect(); amtread += got; } else if (errno == EINTR) { LockInc(&shared->c.readinterrupts); if (killed || ((meltdown || terminated) && timespec_cmp(timespec_sub(timespec_real(), startread), (struct timespec){1}) >= 0)) { return HandlePayloadDrop(); } } else { return HandlePayloadReadError(); } return NULL; } static char *SynchronizeLength(void) { char *p; if (hdrsize + payloadlength > amtread) { if (hdrsize + payloadlength > inbuf.n) return HandleHugePayload(); SendContinueIfNeeded(); while (amtread < hdrsize + payloadlength) { if ((p = ReadMore())) return p; } } cpm.msgsize = hdrsize + payloadlength; return NULL; } static char *SynchronizeChunked(void) { char *p; ssize_t transferlength; struct HttpUnchunker u = {0}; SendContinueIfNeeded(); while (!(transferlength = Unchunk(&u, inbuf.p + hdrsize, amtread - hdrsize, &payloadlength))) { if ((p = ReadMore())) return p; } if (transferlength == -1) return HandleHugePayload(); cpm.msgsize = hdrsize + transferlength; return NULL; } static char *SynchronizeStream(void) { int64_t cl; if (HasHeader(kHttpTransferEncoding) && !HeaderEqualCase(kHttpTransferEncoding, "identity")) { if (HeaderEqualCase(kHttpTransferEncoding, "chunked")) { return SynchronizeChunked(); } else { return HandleTransferRefused(); } } else if (HasHeader(kHttpContentLength)) { if ((cl = ParseContentLength(HeaderData(kHttpContentLength), HeaderLength(kHttpContentLength))) != -1) { payloadlength = cl; return SynchronizeLength(); } else { return HandleBadContentLength(); } } else if (cpm.msg.method == kHttpPost || cpm.msg.method == kHttpPut) { return HandleLengthRequired(); } else { cpm.msgsize = hdrsize; payloadlength = 0; return NULL; } } static void ParseRequestParameters(void) { uint32_t ip; FreeLater(ParseUrl(inbuf.p + cpm.msg.uri.a, cpm.msg.uri.b - cpm.msg.uri.a, &url, kUrlPlus | kUrlLatin1)); if (!url.host.p) { if (HasHeader(kHttpXForwardedHost) && // !GetRemoteAddr(&ip, 0) && IsTrustedIp(ip)) { FreeLater(ParseHost(HeaderData(kHttpXForwardedHost), HeaderLength(kHttpXForwardedHost), &url)); } else if (HasHeader(kHttpHost)) { FreeLater( ParseHost(HeaderData(kHttpHost), HeaderLength(kHttpHost), &url)); } else { FreeLater(ParseHost(DescribeServer(), -1, &url)); } } else if (!url.path.n) { url.path.p = "/"; url.path.n = 1; } if (!url.scheme.n) { if (usingssl) { url.scheme.p = "https"; url.scheme.n = 5; } else { url.scheme.p = "http"; url.scheme.n = 4; } } } static bool HasAtMostThisElement(int h, const char *s) { size_t i, n; struct HttpHeader *x; if (HasHeader(h)) { n = strlen(s); if (!SlicesEqualCase(s, n, HeaderData(h), HeaderLength(h))) { return false; } for (i = 0; i < cpm.msg.xheaders.n; ++i) { x = cpm.msg.xheaders.p + i; if (GetHttpHeader(inbuf.p + x->k.a, x->k.b - x->k.a) == h && !SlicesEqualCase(inbuf.p + x->v.a, x->v.b - x->v.a, s, n)) { return false; } } } return true; } static char *HandleRequest(void) { char *p; if (cpm.msg.version == 11) { LockInc(&shared->c.http11); } else if (cpm.msg.version < 10) { LockInc(&shared->c.http09); } else if (cpm.msg.version == 10) { LockInc(&shared->c.http10); } else { return HandleVersionNotSupported(); } if ((p = SynchronizeStream())) return p; if (logbodies) LogBody("received", inbuf.p + hdrsize, payloadlength); if (cpm.msg.version < 11 || HeaderEqualCase(kHttpConnection, "close")) { connectionclose = true; } if (cpm.msg.method == kHttpOptions && SlicesEqual(inbuf.p + cpm.msg.uri.a, cpm.msg.uri.b - cpm.msg.uri.a, "*", 1)) { return ServeServerOptions(); } if (cpm.msg.method == kHttpConnect) { return HandleConnectRefused(); } if (!HasAtMostThisElement(kHttpExpect, "100-continue")) { return HandleExpectFailed(); } ParseRequestParameters(); if (!url.host.n || !url.path.n || url.path.p[0] != '/' || !IsAcceptablePath(url.path.p, url.path.n) || !IsAcceptableHost(url.host.p, url.host.n) || !IsAcceptablePort(url.port.p, url.port.n)) { free(url.params.p); LockInc(&shared->c.urisrefused); return ServeFailure(400, "Bad URI"); } INFOF("(req) received %s HTTP%02d %.*s %s %`'.*s %`'.*s", DescribeClient(), cpm.msg.version, cpm.msg.xmethod.b - cpm.msg.xmethod.a, inbuf.p + cpm.msg.xmethod.a, FreeLater(EncodeUrl(&url, 0)), HeaderLength(kHttpReferer), HeaderData(kHttpReferer), HeaderLength(kHttpUserAgent), HeaderData(kHttpUserAgent)); if (HasHeader(kHttpContentType) && IsMimeType(HeaderData(kHttpContentType), HeaderLength(kHttpContentType), "application/x-www-form-urlencoded")) { FreeLater(ParseParams(inbuf.p + hdrsize, payloadlength, &url.params)); } FreeLater(url.params.p); #ifndef STATIC if (hasonhttprequest) return LuaOnHttpRequest(); #endif return Route(url.host.p, url.host.n, url.path.p, url.path.n); } static char *Route(const char *host, size_t hostlen, const char *path, size_t pathlen) { char *p; // reset the redirect loop check, as it can only be looping inside // this function (as it always serves something); otherwise // successful RoutePath and Route may fail with "508 loop detected" cpm.loops.n = 0; if (hostlen && (p = RouteHost(host, hostlen, path, pathlen))) { return p; } if (SlicesEqual(path, pathlen, "/", 1)) { if ((p = ServeIndex("/", 1))) return p; return ServeListing(); } else if ((p = RoutePath(path, pathlen))) { return p; } else if (SlicesEqual(path, pathlen, "/statusz", 8)) { return ServeStatusz(); } else { LockInc(&shared->c.notfounds); return ServeErrorWithPath(404, "Not Found", path, pathlen); } } static char *RoutePath(const char *path, size_t pathlen) { int m; long r; struct Asset *a; DEBUGF("(srvr) RoutePath(%`'.*s)", pathlen, path); if ((a = GetAsset(path, pathlen))) { // only allow "read other" permissions for security // and consistency with handling of "external" files // in this and other webservers if ((m = GetMode(a)) & 0004) { if (!S_ISDIR(m)) { return HandleAsset(a, path, pathlen); } else { return HandleFolder(path, pathlen); } } else { LockInc(&shared->c.forbiddens); WARNF("(srvr) asset %`'.*s %#o isn't readable", pathlen, path, m); return ServeErrorWithPath(403, "Forbidden", path, pathlen); } } else if ((r = FindRedirect(path, pathlen)) != -1) { return HandleRedirect(redirects.p + r); } else { return NULL; } } static char *RouteHost(const char *host, size_t hostlen, const char *path, size_t pathlen) { size_t hn, hm; char *hp, *p, b[96]; if (hostlen) { hn = 1 + hostlen + url.path.n; hm = 3 + 1 + hn; hp = hm <= sizeof(b) ? b : FreeLater(xmalloc(hm)); hp[0] = '/'; mempcpy(mempcpy(hp + 1, host, hostlen), path, pathlen); if ((p = RoutePath(hp, hn))) return p; if (!isdigit(host[0])) { if (hostlen > 4 && READ32LE(host) == ('w' | 'w' << 8 | 'w' << 16 | '.' << 24)) { mempcpy(mempcpy(hp + 1, host + 4, hostlen - 4), path, pathlen); if ((p = RoutePath(hp, hn - 4))) return p; } else { mempcpy(mempcpy(mempcpy(hp + 1, "www.", 4), host, hostlen), path, pathlen); if ((p = RoutePath(hp, hn + 4))) return p; } } } return NULL; } static inline bool IsLua(struct Asset *a) { size_t n; const char *p; if (a->file && a->file->path.n >= 4 && READ32LE(a->file->path.s + a->file->path.n - 4) == ('.' | 'l' << 8 | 'u' << 16 | 'a' << 24)) { return true; } p = ZIP_CFILE_NAME(zmap + a->cf); n = ZIP_CFILE_NAMESIZE(zmap + a->cf); return n > 4 && READ32LE(p + n - 4) == ('.' | 'l' << 8 | 'u' << 16 | 'a' << 24); } static char *HandleAsset(struct Asset *a, const char *path, size_t pathlen) { char *p; #ifndef STATIC if (IsLua(a)) return ServeLua(a, path, pathlen); #endif if (cpm.msg.method == kHttpGet || cpm.msg.method == kHttpHead) { LockInc(&shared->c.staticrequests); p = ServeAsset(a, path, pathlen); if (!cpm.gotxcontenttypeoptions) { p = stpcpy(p, "X-Content-Type-Options: nosniff\r\n"); } return p; } else { return BadMethod(); } } static const char *GetContentType(struct Asset *a, const char *path, size_t n) { const char *r; if (a->file && (r = GetContentTypeExt(a->file->path.s, a->file->path.n))) { return r; } return firstnonnull( GetContentTypeExt(path, n), firstnonnull(GetContentTypeExt(ZIP_CFILE_NAME(zmap + a->cf), ZIP_CFILE_NAMESIZE(zmap + a->cf)), a->istext ? "text/plain" : "application/octet-stream")); } static bool IsNotModified(struct Asset *a) { if (cpm.msg.version < 10) return false; if (!HasHeader(kHttpIfModifiedSince)) return false; return a->lastmodified <= ParseHttpDateTime(HeaderData(kHttpIfModifiedSince), HeaderLength(kHttpIfModifiedSince)); } static char *ServeAsset(struct Asset *a, const char *path, size_t pathlen) { char *p; const char *ct; ct = GetContentType(a, path, pathlen); if (IsNotModified(a)) { LockInc(&shared->c.notmodifieds); p = SetStatus(304, "Not Modified"); } else { if (!a->file) { cpm.content = (char *)ZIP_LFILE_CONTENT(zmap + a->lf); cpm.contentlength = GetZipCfileCompressedSize(zmap + a->cf); } else if ((p = OpenAsset(a))) { return p; } if (IsCompressed(a)) { if (ClientAcceptsGzip()) { p = ServeAssetPrecompressed(a); } else { p = ServeAssetDecompressed(a); } } else if (cpm.msg.version >= 11 && HasHeader(kHttpRange)) { p = ServeAssetRange(a); } else if (!a->file) { LockInc(&shared->c.identityresponses); DEBUGF("(zip) ServeAssetZipIdentity(%`'s)", ct); if (Verify(cpm.content, cpm.contentlength, ZIP_LFILE_CRC32(zmap + a->lf))) { p = SetStatus(200, "OK"); } else { return ServeError(500, "Internal Server Error"); } } else if (!IsTiny() && cpm.msg.method != kHttpHead && !IsSslCompressed() && ClientAcceptsGzip() && !ShouldAvoidGzip() && !(a->file && IsNoCompressExt(a->file->path.s, a->file->path.n)) && ((cpm.contentlength >= 100 && startswithi(ct, "text/")) || (cpm.contentlength >= 1000 && MeasureEntropy(cpm.content, 1000) < 7))) { VERBOSEF("serving compressed asset"); p = ServeAssetCompressed(a); } else { p = ServeAssetIdentity(a, ct); } } p = AppendContentType(p, ct); p = stpcpy(p, "Vary: Accept-Encoding\r\n"); p = AppendHeader(p, "Last-Modified", a->lastmodifiedstr); if (cpm.msg.version >= 11) { if (!cpm.gotcachecontrol) { p = AppendCache(p, cacheseconds, cachedirective); } if (!IsCompressed(a)) { p = stpcpy(p, "Accept-Ranges: bytes\r\n"); } } return p; } static char *SetStatus(unsigned code, const char *reason) { if (cpm.msg.version == 10) { if (code == 307) code = 302; if (code == 308) code = 301; } cpm.statuscode = code; cpm.hascontenttype = false; // reset, as the headers are reset. // istext is `true`, as the default content type // is text/html, which will be set later cpm.istext = true; cpm.gotxcontenttypeoptions = 0; cpm.gotcachecontrol = 0; cpm.referrerpolicy = 0; cpm.branded = 0; stpcpy(hdrbuf.p, "HTTP/1.0 000 "); hdrbuf.p[7] += cpm.msg.version & 1; hdrbuf.p[9] += code / 100; hdrbuf.p[10] += code / 10 % 10; hdrbuf.p[11] += code % 10; VERBOSEF("(rsp) %d %s", code, reason); return AppendCrlf(stpcpy(hdrbuf.p + 13, reason)); } static inline bool MustNotIncludeMessageBody(void) { /* RFC2616 § 4.4 */ return cpm.msg.method == kHttpHead || (100 <= cpm.statuscode && cpm.statuscode <= 199) || cpm.statuscode == 204 || cpm.statuscode == 304; } static bool TransmitResponse(char *p) { int iovlen; struct iovec iov[4]; long actualcontentlength; if (cpm.msg.version >= 10) { actualcontentlength = cpm.contentlength; if (cpm.gzipped) { actualcontentlength += sizeof(kGzipHeader) + sizeof(gzip_footer); p = stpcpy(p, "Content-Encoding: gzip\r\n"); } p = AppendContentLength(p, actualcontentlength); p = AppendCrlf(p); CHECK_LE(p - hdrbuf.p, hdrbuf.n); if (logmessages) { 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 (cpm.gzipped) { iov[iovlen].iov_base = (void *)kGzipHeader; iov[iovlen].iov_len = sizeof(kGzipHeader); ++iovlen; } iov[iovlen].iov_base = cpm.content; iov[iovlen].iov_len = cpm.contentlength; ++iovlen; if (cpm.gzipped) { iov[iovlen].iov_base = gzip_footer; iov[iovlen].iov_len = sizeof(gzip_footer); ++iovlen; } } } else { iov[0].iov_base = cpm.content; iov[0].iov_len = cpm.contentlength; iovlen = 1; } Send(iov, iovlen); LockInc(&shared->c.messageshandled); ++messageshandled; return true; } static bool StreamResponse(char *p) { int rc; struct iovec iov[6]; char *s, chunkbuf[23]; assert(!MustNotIncludeMessageBody()); if (cpm.msg.version >= 11) { p = stpcpy(p, "Transfer-Encoding: chunked\r\n"); } else { assert(connectionclose); } p = AppendCrlf(p); CHECK_LE(p - hdrbuf.p, hdrbuf.n); if (logmessages) { LogMessage("sending", hdrbuf.p, p - hdrbuf.p); } bzero(iov, sizeof(iov)); if (cpm.msg.version >= 10) { iov[0].iov_base = hdrbuf.p; iov[0].iov_len = p - hdrbuf.p; } if (cpm.msg.version >= 11) { iov[5].iov_base = "\r\n"; iov[5].iov_len = 2; } for (;;) { iov[2].iov_base = 0; iov[2].iov_len = 0; iov[3].iov_base = 0; iov[3].iov_len = 0; iov[4].iov_base = 0; iov[4].iov_len = 0; if ((rc = cpm.generator(iov + 2)) <= 0) break; if (cpm.msg.version >= 11) { s = chunkbuf; s += uint64toarray_radix16(rc, s); s = AppendCrlf(s); iov[1].iov_base = chunkbuf; iov[1].iov_len = s - chunkbuf; } if (Send(iov, 6) == -1) break; iov[0].iov_base = 0; iov[0].iov_len = 0; } if (rc != -1) { if (cpm.msg.version >= 11) { iov[0].iov_base = "0\r\n\r\n"; iov[0].iov_len = 5; Send(iov, 1); } } else { connectionclose = true; } return true; } static bool HandleMessageActual(void) { int rc; long reqtime, contime; char *p; struct timespec now; if ((rc = ParseHttpMessage(&cpm.msg, inbuf.p, amtread)) != -1) { if (!rc) return false; hdrsize = rc; if (logmessages) { LogMessage("received", inbuf.p, hdrsize); } p = HandleRequest(); } else { LockInc(&shared->c.badmessages); connectionclose = true; if ((p = DumpHexc(inbuf.p, MIN(amtread, 256), 0))) { INFOF("(clnt) %s sent garbage %s", DescribeClient(), p); } return true; } if (!cpm.msgsize) { amtread = 0; connectionclose = true; LockInc(&shared->c.synchronizationfailures); DEBUGF("(clnt) could not synchronize message stream"); } if (cpm.msg.version >= 10) { p = AppendCrlf(stpcpy(stpcpy(p, "Date: "), shared->currentdate)); if (!cpm.branded) p = stpcpy(p, serverheader); if (extrahdrs) p = stpcpy(p, extrahdrs); if (connectionclose) { p = stpcpy(p, "Connection: close\r\n"); } else if (timeout.tv_sec < 0 && cpm.msg.version >= 11) { p = stpcpy(p, "Connection: keep-alive\r\n"); } } // keep content-type update *before* referrerpolicy // https://datatracker.ietf.org/doc/html/rfc2616#section-7.2.1 if (!cpm.hascontenttype && cpm.contentlength > 0) { p = AppendContentType(p, "text/html"); } if (cpm.referrerpolicy) { p = stpcpy(p, "Referrer-Policy: "); p = stpcpy(p, cpm.referrerpolicy); p = stpcpy(p, "\r\n"); } if (loglatency || LOGGABLE(kLogDebug) || hasonloglatency) { now = timespec_real(); reqtime = timespec_tomicros(timespec_sub(now, startrequest)); contime = timespec_tomicros(timespec_sub(now, startconnection)); if (hasonloglatency) LuaOnLogLatency(reqtime, contime); if (loglatency || LOGGABLE(kLogDebug)) LOGF(kLogDebug, "(stat) %`'.*s latency r: %,ldµs c: %,ldµs", cpm.msg.uri.b - cpm.msg.uri.a, inbuf.p + cpm.msg.uri.a, reqtime, contime); } if (!cpm.generator) { return TransmitResponse(p); } else { return StreamResponse(p); } } static bool HandleMessage(void) { bool r; ishandlingrequest = true; r = HandleMessageActual(); ishandlingrequest = false; return r; } static void InitRequest(void) { assert(!cpm.outbuf); bzero(&cpm, sizeof(cpm)); } static bool IsSsl(unsigned char c) { if (c == 22) return true; if (!(c & 128)) return false; /* RHEL5 sends SSLv2 hello but supports TLS */ DEBUGF("(ssl) %s SSLv2 hello D:", DescribeClient()); return true; } static void HandleMessages(void) { bool once; ssize_t rc; size_t got; (void)once; for (once = false;;) { InitRequest(); startread = timespec_real(); for (;;) { if (!cpm.msg.i && amtread) { startrequest = timespec_real(); if (HandleMessage()) break; } if ((rc = reader(client, inbuf.p + amtread, inbuf.n - amtread)) != -1) { startrequest = timespec_real(); got = rc; amtread += got; if (amtread) { #ifndef UNSECURE if (!once) { once = true; if (!unsecure) { if (IsSsl(inbuf.p[0])) { if (TlsSetup()) { continue; } else { return; } } else if (requiressl) { INFOF("(clnt) %s didn't send an ssl hello", DescribeClient()); return; } else { WipeServingKeys(); } } } #endif DEBUGF("(stat) %s read %,zd bytes", DescribeClient(), got); if (HandleMessage()) { break; } else if (got) { HandleFrag(got); } } if (!got) { NotifyClose(); LogClose("disconnect"); return; } } else if (errno == EINTR) { LockInc(&shared->c.readinterrupts); errno = 0; } else if (errno == EAGAIN) { LockInc(&shared->c.readtimeouts); if (amtread) SendTimeout(); NotifyClose(); LogClose("read timeout"); return; } else if (errno == ECONNRESET) { LockInc(&shared->c.readresets); LogClose("read reset"); return; } else { LockInc(&shared->c.readerrors); if (errno == EBADF) { // don't warn on close/bad fd LogClose("read badf"); } else { WARNF("(clnt) %s read error: %m", DescribeClient()); } return; } if (killed || (terminated && !amtread) || (meltdown && (!amtread || timespec_cmp(timespec_sub(timespec_real(), startread), (struct timespec){1}) >= 0))) { if (amtread) { LockInc(&shared->c.dropped); SendServiceUnavailable(); } NotifyClose(); LogClose(DescribeClose()); return; } if (invalidated) { HandleReload(); } } if (cpm.msgsize == amtread) { amtread = 0; if (killed) { LogClose(DescribeClose()); return; } else if (connectionclose || terminated || meltdown) { NotifyClose(); LogClose(DescribeClose()); return; } } else { CHECK_LT(cpm.msgsize, amtread); LockInc(&shared->c.pipelinedrequests); DEBUGF("(stat) %,ld pipelinedrequest bytes", amtread - cpm.msgsize); memmove(inbuf.p, inbuf.p + cpm.msgsize, amtread - cpm.msgsize); amtread -= cpm.msgsize; if (killed) { LogClose(DescribeClose()); return; } else if (connectionclose) { NotifyClose(); LogClose(DescribeClose()); return; } } CollectGarbage(); if (invalidated) { HandleReload(); } } } static void CloseServerFds(void) { size_t i; for (i = 0; i < servers.n; ++i) { close(servers.p[i].fd); } } static int ExitWorker(void) { if (IsModeDbg() && !sandboxed) { isexitingworker = true; return eintr(); } if (monitortty) { terminatemonitor = true; if (monitorth) { pthread_join(monitorth, 0); monitorth = 0; } } _Exit(0); } static void UnveilRedbean(void) { size_t i; for (i = 0; i < stagedirs.n; ++i) { unveil(stagedirs.p[i].s, "r"); } unveil(0, 0); } static int EnableSandbox(void) { __pledge_mode = PLEDGE_PENALTY_RETURN_EPERM | PLEDGE_STDERR_LOGGING; switch (sandboxed) { case 0: return 0; case 1: // -S DEBUGF("(stat) applying '%s' sandbox policy", "online"); UnveilRedbean(); return pledge("stdio rpath inet dns id", 0); case 2: // -SS DEBUGF("(stat) applying '%s' sandbox policy", "offline"); UnveilRedbean(); return pledge("stdio rpath id", 0); default: // -SSS DEBUGF("(stat) applying '%s' sandbox policy", "contained"); UnveilRedbean(); return pledge("stdio", 0); } } static void *MemoryMonitor(void *arg) { static struct termios oldterm; static int tty; sigset_t ss; bool ok; size_t intervals; struct winsize ws; unsigned char rez; struct termios term; char *b, *addr; struct MemoryInterval *mi, *mi2; long i, j, gen, pages; int rc, id, color, color2, workers; id = atomic_load_explicit(&shared->workers, memory_order_relaxed); DEBUGF("(memv) started for pid %d on tid %d", getpid(), gettid()); sigemptyset(&ss); sigaddset(&ss, SIGHUP); sigaddset(&ss, SIGINT); sigaddset(&ss, SIGQUIT); sigaddset(&ss, SIGTERM); sigaddset(&ss, SIGPIPE); sigaddset(&ss, SIGUSR1); sigaddset(&ss, SIGUSR2); sigprocmask(SIG_BLOCK, &ss, 0); pthread_spin_lock(&shared->montermlock); if (!id) { if ((tty = open(monitortty, O_RDWR | O_NOCTTY)) != -1) { tcgetattr(tty, &oldterm); term = oldterm; term.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); term.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); term.c_oflag |= OPOST | ONLCR; term.c_iflag |= IUTF8; term.c_cflag |= CS8; term.c_cc[VMIN] = 1; term.c_cc[VTIME] = 0; tcsetattr(tty, TCSANOW, &term); WRITE(tty, "\e[?25l", 6); } } pthread_spin_unlock(&shared->montermlock); if (tty != -1) { for (gen = 0, mi = 0, b = 0; !terminatemonitor;) { workers = atomic_load_explicit(&shared->workers, memory_order_relaxed); if (id) id = MAX(1, MIN(id, workers)); if (!id && workers) { usleep(50000); continue; } ++gen; intervals = atomic_load_explicit(&_mmi.i, memory_order_relaxed); if ((mi2 = realloc(mi, (intervals += 3) * sizeof(*mi)))) { mi = mi2; mi[0].x = (intptr_t)__executable_start >> 16; mi[0].size = _etext - __executable_start; mi[0].flags = 0; mi[1].x = (intptr_t)_etext >> 16; mi[1].size = _edata - _etext; mi[1].flags = 0; mi[2].x = (intptr_t)_edata >> 16; mi[2].size = _end - _edata; mi[2].flags = 0; __mmi_lock(); if (_mmi.i == intervals - 3) { memcpy(mi + 3, _mmi.p, _mmi.i * sizeof(*mi)); ok = true; } else { ok = false; } __mmi_unlock(); if (!ok) { VERBOSEF("(memv) retrying due to contention on mmap table"); continue; } ws.ws_col = 80; ws.ws_row = 40; tcgetwinsize(tty, &ws); appendr(&b, 0); appends(&b, "\e[H\e[1m"); for (i = 0; i < intervals; ++i) { addr = (char *)((int64_t)((uint64_t)mi[i].x << 32) >> 16); color = 0; appendf(&b, "\e[0m%lx", addr); int pagesz = getauxval(AT_PAGESZ); pages = (mi[i].size + pagesz - 1) / pagesz; for (j = 0; j < pages; ++j) { rc = mincore(addr + j * pagesz, pagesz, &rez); if (!rc) { if (rez & 1) { if (mi[i].flags & MAP_SHARED) { color2 = 105; } else { color2 = 42; } } else { color2 = 41; } } else { errno = 0; color2 = 0; } if (color != color2) { color = color2; appendf(&b, "\e[%dm", color); } if (mi[i].flags & MAP_ANONYMOUS) { appendw(&b, ' '); } else { appendw(&b, '/'); } } } appendf(&b, "\e[0m ID=%d PID=%d WS=%dx%d WORKERS=%d MODE=" MODE " GEN=%ld\e[J", id, getpid(), ws.ws_col, ws.ws_row, workers, gen); pthread_spin_lock(&shared->montermlock); WRITE(tty, b, appendz(b).i); appendr(&b, 0); usleep(MONITOR_MICROS); pthread_spin_unlock(&shared->montermlock); } else { // running out of memory temporarily is a real possibility here // the right thing to do, is stand aside and let lua try to fix WARNF("(memv) we require more vespene gas"); usleep(MONITOR_MICROS); } } if (!id) { appendr(&b, 0); appends(&b, "\e[H\e[J\e[?25h"); WRITE(tty, b, appendz(b).i); tcsetattr(tty, TCSANOW, &oldterm); } DEBUGF("(memv) exiting..."); close(tty); free(mi); free(b); } DEBUGF("(memv) done"); return 0; } static void MonitorMemory(void) { errno_t err; if ((err = pthread_create(&monitorth, 0, MemoryMonitor, 0))) { WARNF("(memv) failed to start memory monitor %s", strerror(err)); } } static int HandleConnection(size_t i) { uint32_t ip; int pid, tok, rc = 0; clientaddrsize = sizeof(clientaddr); if ((client = accept4(servers.p[i].fd, (struct sockaddr *)&clientaddr, &clientaddrsize, SOCK_CLOEXEC)) != -1) { LockInc(&shared->c.accepts); GetClientAddr(&ip, 0); if (tokenbucket.cidr && tokenbucket.reject >= 0) { if (!IsTrustedIp(ip)) { tok = AcquireToken(tokenbucket.b, ip, tokenbucket.cidr); if (tok <= tokenbucket.ban && tokenbucket.ban >= 0) { WARNF("(token) banning %hhu.%hhu.%hhu.%hhu who only has %d tokens", ip >> 24, ip >> 16, ip >> 8, ip, tok); LockInc(&shared->c.bans); Blackhole(ip); close(client); return 0; } else if (tok <= tokenbucket.ignore && tokenbucket.ignore >= 0) { DEBUGF("(token) ignoring %hhu.%hhu.%hhu.%hhu who only has %d tokens", ip >> 24, ip >> 16, ip >> 8, ip, tok); LockInc(&shared->c.ignores); close(client); return 0; } else if (tok < tokenbucket.reject) { WARNF("(token) rejecting %hhu.%hhu.%hhu.%hhu who only has %d tokens", ip >> 24, ip >> 16, ip >> 8, ip, tok); LockInc(&shared->c.rejects); SendTooManyRequests(); close(client); return 0; } else { DEBUGF("(token) %hhu.%hhu.%hhu.%hhu has %d tokens", ip >> 24, ip >> 16, ip >> 8, ip, tok - 1); } } else { DEBUGF("(token) won't acquire token for trusted ip %hhu.%hhu.%hhu.%hhu", ip >> 24, ip >> 16, ip >> 8, ip); } } else { DEBUGF("(token) can't acquire accept() token for client"); } startconnection = timespec_real(); if (UNLIKELY(maxworkers) && shared->workers >= maxworkers) { EnterMeltdownMode(); SendServiceUnavailable(); close(client); return 0; } VERBOSEF("(srvr) accept %s via %s", DescribeClient(), DescribeServer()); messageshandled = 0; if (hasonclientconnection && LuaOnClientConnection()) { close(client); return 0; } if (uniprocess) { pid = -1; connectionclose = true; } else { switch ((pid = fork())) { case 0: if (!IsTiny() && monitortty) { MonitorMemory(); } meltdown = false; __isworker = true; connectionclose = false; if (!IsTiny() && systrace) { kStartTsc = rdtsc(); } TRACE_BEGIN; if (sandboxed) { CHECK_NE(-1, EnableSandbox()); } if (hasonworkerstart) { CallSimpleHook("OnWorkerStart"); } break; case -1: HandleForkFailure(); return 0; default: LockInc(&shared->workers); close(client); ReseedRng(&rng, "parent"); if (hasonprocesscreate) { LuaOnProcessCreate(pid); } return 0; } } if (!pid && !IsWindows()) { CloseServerFds(); } HandleMessages(); DEBUGF("(stat) %s closing after %,ldµs", DescribeClient(), timespec_tomicros(timespec_sub(timespec_real(), startconnection))); if (!pid) { if (hasonworkerstop) { CallSimpleHook("OnWorkerStop"); } rc = ExitWorker(); } else { close(client); oldin.p = 0; oldin.n = 0; if (inbuf.c) { inbuf.p -= inbuf.c; inbuf.n += inbuf.c; inbuf.c = 0; } #ifndef UNSECURE if (usingssl) { usingssl = false; reader = read; writer = WritevAll; mbedtls_ssl_session_reset(&ssl); } #endif } CollectGarbage(); } else { if (errno == EINTR || errno == EAGAIN) { LockInc(&shared->c.acceptinterrupts); } else if (errno == ENFILE) { LockInc(&shared->c.enfiles); LogAcceptError("enfile: too many open files"); meltdown = true; } else if (errno == EMFILE) { LockInc(&shared->c.emfiles); LogAcceptError("emfile: ran out of open file quota"); meltdown = true; } else if (errno == ENOMEM) { LockInc(&shared->c.enomems); LogAcceptError("enomem: ran out of memory"); meltdown = true; } else if (errno == ENOBUFS) { LockInc(&shared->c.enobufs); LogAcceptError("enobuf: ran out of buffer"); meltdown = true; } else if (errno == ENONET) { LockInc(&shared->c.enonets); LogAcceptError("enonet: network gone"); polls[i].fd = -polls[i].fd; } else if (errno == ENETDOWN) { LockInc(&shared->c.enetdowns); LogAcceptError("enetdown: network down"); polls[i].fd = -polls[i].fd; } else if (errno == ECONNABORTED) { LockInc(&shared->c.accepterrors); LockInc(&shared->c.acceptresets); WARNF("(srvr) %s accept error: %s", DescribeServer(), "acceptreset: connection reset before accept"); } else if (errno == ENETUNREACH || errno == EHOSTUNREACH || errno == EOPNOTSUPP || errno == ENOPROTOOPT || errno == EPROTO) { LockInc(&shared->c.accepterrors); LockInc(&shared->c.acceptflakes); WARNF("(srvr) accept error: %s ephemeral accept error: %m", DescribeServer()); } else { WARNF("(srvr) %s accept error: %m", DescribeServer()); } errno = 0; } return rc; } static void MakeExecutableModifiable(void) { #ifdef __x86_64__ int ft; if (!(SUPPORT_VECTOR & (_HOSTMETAL | _HOSTWINDOWS | _HOSTXNU))) return; if (IsWindows()) return; // TODO if (IsOpenbsd()) return; // TODO if (IsNetbsd()) return; // TODO if (endswith(zpath, ".com.dbg")) return; close(zfd); ft = ftrace_enabled(0); if ((zfd = __open_executable()) == -1) { WARNF("(srvr) can't open executable for modification: %m"); } if (ft > 0) { __ftrace = 0; ftrace_install(); ftrace_enabled(ft); } #else // TODO #endif } static int HandleReadline(void) { int status; lua_State *L = GL; for (;;) { status = lua_loadline(L); if (status < 0) { if (status == -1) { OnTerm(SIGHUP); // eof VERBOSEF("(repl) eof"); return -1; } else if (errno == EINTR) { errno = 0; VERBOSEF("(repl) interrupt"); shutdownsig = SIGINT; OnInt(SIGINT); kill(0, SIGINT); return -1; } else if (errno == EAGAIN) { errno = 0; return 0; } else { WARNF("(srvr) unexpected terminal error %d% m", status); errno = 0; return 0; } } DisableRawMode(); lua_repl_lock(); if (status == LUA_OK) { status = lua_runchunk(L, 0, LUA_MULTRET); } if (status == LUA_OK) { LuaPrint(L); } else { lua_report(L, status); } lua_repl_unlock(); EnableRawMode(); } } static int HandlePoll(int ms) { int rc, nfds; size_t pollid, serverid; if ((nfds = poll(polls, 1 + servers.n, ms)) != -1) { if (nfds) { // handle pollid/o events for (pollid = 0; pollid < 1 + servers.n; ++pollid) { if (!polls[pollid].revents) continue; if (polls[pollid].fd < 0) continue; if (polls[pollid].fd) { // handle listen socket lua_repl_lock(); serverid = pollid - 1; assert(0 <= serverid && serverid < servers.n); serveraddr = &servers.p[serverid].addr; ishandlingconnection = true; rc = HandleConnection(serverid); ishandlingconnection = false; lua_repl_unlock(); if (rc == -1) return -1; #ifndef STATIC } else { // handle standard input rc = HandleReadline(); if (rc == -1) return rc; #endif } } #ifndef STATIC } else if (__ttyconf.replmode) { // handle refresh repl line rc = HandleReadline(); if (rc < 0) return rc; #endif } } else { if (errno == EINTR || errno == EAGAIN) { LockInc(&shared->c.pollinterrupts); } else if (errno == ENOMEM) { LockInc(&shared->c.enomems); WARNF("(srvr) poll error: ran out of memory"); meltdown = true; } else { DIEF("(srvr) poll error: %m"); } errno = 0; } return 0; } static void Listen(void) { char ipbuf[16]; size_t i, j, n; uint32_t ip, port, addrsize, *ifp; bool hasonserverlisten = IsHookDefined("OnServerListen"); if (!ports.n) { ProgramPort(8080); } if (!ips.n) { if (interfaces && *interfaces) { for (ifp = interfaces; *ifp; ++ifp) { sprintf(ipbuf, "%hhu.%hhu.%hhu.%hhu", *ifp >> 24, *ifp >> 16, *ifp >> 8, *ifp); ProgramAddr(ipbuf); } } else { ProgramAddr("0.0.0.0"); } } servers.p = malloc(ips.n * ports.n * sizeof(*servers.p)); for (n = i = 0; i < ips.n; ++i) { for (j = 0; j < ports.n; ++j, ++n) { bzero(servers.p + n, sizeof(*servers.p)); servers.p[n].addr.sin_family = AF_INET; servers.p[n].addr.sin_port = htons(ports.p[j]); servers.p[n].addr.sin_addr.s_addr = htonl(ips.p[i]); if ((servers.p[n].fd = GoodSocket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP, true, &timeout)) == -1) { DIEF("(srvr) socket: %m"); } if (hasonserverlisten && LuaOnServerListen(servers.p[n].fd, ips.p[i], ports.p[j])) { close(servers.p[n].fd); n--; // skip this server instance continue; } if (bind(servers.p[n].fd, (struct sockaddr *)&servers.p[n].addr, sizeof(servers.p[n].addr)) == -1) { DIEF("(srvr) bind error: %m: %hhu.%hhu.%hhu.%hhu:%hu", ips.p[i] >> 24, ips.p[i] >> 16, ips.p[i] >> 8, ips.p[i], ports.p[j]); } if (listen(servers.p[n].fd, 10) == -1) { DIEF("(srvr) listen error: %m"); } addrsize = sizeof(servers.p[n].addr); if (getsockname(servers.p[n].fd, (struct sockaddr *)&servers.p[n].addr, &addrsize) == -1) { DIEF("(srvr) getsockname error: %m"); } port = ntohs(servers.p[n].addr.sin_port); ip = ntohl(servers.p[n].addr.sin_addr.s_addr); if (ip == INADDR_ANY) ip = INADDR_LOOPBACK; INFOF("(srvr) listen http://%hhu.%hhu.%hhu.%hhu:%d", ip >> 24, ip >> 16, ip >> 8, ip, port); if (printport && !ports.p[j]) { printf("%d\r\n", port); fflush(stdout); } } } // shrink allocated memory in case some of the sockets were skipped if (n < ips.n * ports.n) servers.p = realloc(servers.p, n * sizeof(*servers.p)); servers.n = n; polls = malloc((1 + n) * sizeof(*polls)); polls[0].fd = -1; polls[0].events = POLLIN; polls[0].revents = 0; for (i = 0; i < n; ++i) { polls[1 + i].fd = servers.p[i].fd; polls[1 + i].events = POLLIN; polls[1 + i].revents = 0; } } static void HandleShutdown(void) { CloseServerFds(); INFOF("(srvr) received %s", strsignal(shutdownsig)); if (shutdownsig != SIGINT && shutdownsig != SIGQUIT) { if (!killed) terminated = false; INFOF("(srvr) killing process group"); KillGroup(); } WaitAll(); INFOF("(srvr) shutdown complete"); } // this function coroutines with linenoise int EventLoop(int ms) { struct timespec t; DEBUGF("(repl) event loop"); while (!terminated) { errno = 0; if (zombied) { lua_repl_lock(); ReapZombies(); lua_repl_unlock(); } else if (invalidated) { lua_repl_lock(); HandleReload(); lua_repl_unlock(); } else if (meltdown) { lua_repl_lock(); EnterMeltdownMode(); lua_repl_unlock(); meltdown = false; } else if (timespec_cmp(timespec_sub((t = timespec_real()), lastheartbeat), heartbeatinterval) >= 0) { lastheartbeat = t; HandleHeartbeat(); } else if (HandlePoll(ms) == -1) { break; } } return -1; } static void ReplEventLoop(void) { lua_State *L = GL; DEBUGF("ReplEventLoop()"); polls[0].fd = 0; lua_repl_completions_callback = HandleCompletions; lua_initrepl(L); EnableRawMode(); EventLoop(-1); DisableRawMode(); lua_freerepl(); lua_settop(L, 0); // clear stack polls[0].fd = -1; } static void InstallSignalHandler(int sig, void *handler) { struct sigaction sa = {.sa_sigaction = handler}; if (sigaction(sig, &sa, 0) == -1) WARNF("(srvr) failed to set signal handler #%d: %m", sig); } static void SigInit(void) { InstallSignalHandler(SIGINT, OnInt); InstallSignalHandler(SIGHUP, OnHup); InstallSignalHandler(SIGTERM, OnTerm); InstallSignalHandler(SIGCHLD, OnChld); InstallSignalHandler(SIGUSR1, OnUsr1); InstallSignalHandler(SIGUSR2, OnUsr2); InstallSignalHandler(SIGPIPE, SIG_IGN); } static void TlsInit(void) { #ifndef UNSECURE int suite; if (unsecure) return; if (suiteb && !mbedtls_aes_uses_hardware()) { WARNF("(srvr) requested suiteb crypto, but hardware aes not present"); } if (!sslinitialized) { InitializeRng(&rng); InitializeRng(&rngcli); suite = suiteb ? MBEDTLS_SSL_PRESET_SUITEB : MBEDTLS_SSL_PRESET_SUITEC; mbedtls_ssl_config_defaults(&conf, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, suite); mbedtls_ssl_config_defaults(&confcli, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, suite); } // the following setting can be re-applied even when SSL/TLS is initialized if (suites.n) { mbedtls_ssl_conf_ciphersuites(&conf, suites.p); mbedtls_ssl_conf_ciphersuites(&confcli, suites.p); } if (psks.n) { mbedtls_ssl_conf_psk_cb(&conf, TlsRoutePsk, 0); DCHECK_EQ(0, mbedtls_ssl_conf_psk(&confcli, psks.p[0].key, psks.p[0].key_len, psks.p[0].identity, psks.p[0].identity_len)); } if (sslticketlifetime > 0) { mbedtls_ssl_ticket_setup(&ssltick, mbedtls_ctr_drbg_random, &rng, MBEDTLS_CIPHER_AES_256_GCM, sslticketlifetime); mbedtls_ssl_conf_session_tickets_cb(&conf, mbedtls_ssl_ticket_write, mbedtls_ssl_ticket_parse, &ssltick); } if (sslinitialized) return; sslinitialized = true; LoadCertificates(); mbedtls_ssl_conf_sni(&conf, TlsRoute, 0); mbedtls_ssl_conf_dbg(&conf, TlsDebug, 0); mbedtls_ssl_conf_dbg(&confcli, TlsDebug, 0); mbedtls_ssl_conf_rng(&conf, mbedtls_ctr_drbg_random, &rng); mbedtls_ssl_conf_rng(&confcli, mbedtls_ctr_drbg_random, &rngcli); if (sslclientverify) { mbedtls_ssl_conf_ca_chain(&conf, GetSslRoots(), 0); mbedtls_ssl_conf_authmode(&conf, MBEDTLS_SSL_VERIFY_REQUIRED); } if (sslfetchverify) { mbedtls_ssl_conf_ca_chain(&confcli, GetSslRoots(), 0); mbedtls_ssl_conf_authmode(&confcli, MBEDTLS_SSL_VERIFY_REQUIRED); } else { mbedtls_ssl_conf_authmode(&confcli, MBEDTLS_SSL_VERIFY_NONE); } mbedtls_ssl_set_bio(&ssl, &g_bio, TlsSend, 0, TlsRecv); conf.disable_compression = confcli.disable_compression = true; DCHECK_EQ(0, mbedtls_ssl_conf_alpn_protocols(&conf, (void *)kAlpn)); DCHECK_EQ(0, mbedtls_ssl_conf_alpn_protocols(&confcli, (void *)kAlpn)); DCHECK_EQ(0, mbedtls_ssl_setup(&ssl, &conf)); DCHECK_EQ(0, mbedtls_ssl_setup(&sslcli, &confcli)); #endif } static void TlsDestroy(void) { #ifndef UNSECURE if (unsecure) return; mbedtls_ssl_free(&ssl); mbedtls_ssl_free(&sslcli); mbedtls_ctr_drbg_free(&rng); mbedtls_ctr_drbg_free(&rngcli); mbedtls_ssl_config_free(&conf); mbedtls_ssl_config_free(&confcli); mbedtls_ssl_ticket_free(&ssltick); CertsDestroy(); PsksDestroy(); Free(&suites.p), suites.n = 0; #endif } static void GetOpts(int argc, char *argv[]) { int opt; bool got_e_arg = false; bool storeasset = false; // only generate ecp cert under blinkenlights (rsa is slow) norsagen = IsGenuineBlink(); while ((opt = getopt(argc, argv, GETOPTS)) != -1) { switch (opt) { CASE('S', ++sandboxed); CASE('v', ++__log_level); CASE('s', --__log_level); CASE('X', unsecure = true); CASE('%', norsagen = true); CASE('Z', systrace = true); CASE('b', logbodies = true); CASE('z', printport = true); CASE('d', daemonize = true); CASE('a', logrusage = true); CASE('J', requiressl = true); CASE('u', uniprocess = true); CASE('g', loglatency = true); CASE('m', logmessages = true); CASE('w', launchbrowser = strdup(optarg)); CASE('l', ProgramAddr(optarg)); CASE('H', ProgramHeader(optarg)); CASE('L', ProgramLogPath(optarg)); CASE('P', ProgramPidPath(optarg)); CASE('D', ProgramDirectory(optarg)); CASE('U', ProgramUid(atoi(optarg))); CASE('G', ProgramGid(atoi(optarg))); CASE('p', ProgramPort(ParseInt(optarg))); CASE('R', ProgramRedirectArg(0, optarg)); case 'c':; // accept "num" or "num,directive" char *p; long ret = strtol(optarg, &p, 0); ProgramCache(ret, *p ? p + 1 : NULL); // skip separator, if any break; CASE('r', ProgramRedirectArg(307, optarg)); CASE('t', ProgramTimeout(ParseInt(optarg))); CASE('h', PrintUsage(1, EXIT_SUCCESS)); CASE('M', ProgramMaxPayloadSize(ParseInt(optarg))); #if !IsTiny() CASE('W', monitortty = optarg); case 'f': funtrace = true; if (ftrace_install() == -1) { WARNF("(srvr) ftrace failed to install %m"); } break; #endif #ifndef STATIC CASE('F', LuaEvalFile(optarg)); CASE('*', selfmodifiable = true); CASE('i', interpretermode = true); CASE('E', leakcrashreports = true); case 'e': got_e_arg = true; LuaEvalCode(optarg); break; case 'A': if (!storeasset) { storeasset = true; MakeExecutableModifiable(); } StorePath(optarg); break; #endif #ifndef UNSECURE CASE('B', suiteb = true); CASE('V', ++mbedtls_debug_threshold); CASE('k', sslfetchverify = false); CASE('j', sslclientverify = true); CASE('T', ProgramSslTicketLifetime(ParseInt(optarg))); CASE('C', ProgramFile(optarg, ProgramCertificate)); CASE('K', ProgramFile(optarg, ProgramPrivateKey)); #endif default: PrintUsage(2, EX_USAGE); } } // if storing asset(s) is requested, don't need to continue if (storeasset) exit(0); // we don't want to drop into a repl after using -e in -i mode if (interpretermode && got_e_arg) exit(0); } void RedBean(int argc, char *argv[]) { char ibuf[21]; int fd; if (IsLinux()) { // disable weird linux capabilities for (int e = errno, i = 0;; ++i) { if (prctl(PR_CAPBSET_DROP, i) == -1) { errno = e; break; } } // disable sneak privilege since we don't use them // seccomp will fail later if this fails prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); } reader = read; writer = WritevAll; gmtoff = GetGmtOffset((lastrefresh = startserver = timespec_real()).tv_sec); mainpid = getpid(); heartbeatinterval.tv_sec = 5; CHECK_GT(CLK_TCK, 0); CHECK_NE(MAP_FAILED, (shared = mmap(NULL, ROUNDUP(sizeof(struct Shared), FRAMESIZE), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0))); if (daemonize) { for (int i = 0; i < 256; ++i) { close(i); } open("/dev/null", O_RDONLY); open("/dev/null", O_WRONLY); open("/dev/null", O_WRONLY); } zpath = GetProgramExecutableName(); CHECK_NE(-1, (zfd = open(zpath, O_RDONLY))); CHECK_NE(-1, fstat(zfd, &zst)); OpenZip(true); SetDefaults(); // this can fail with EPERM if we're running under pledge() if (!interpretermode && !(interfaces = GetHostIps())) { WARNF("(srvr) failed to query system network interface addresses: %m"); } LuaStart(); GetOpts(argc, argv); #ifndef STATIC if (selfmodifiable) { MakeExecutableModifiable(); } #endif LuaInit(); oldloglevel = __log_level; if (uniprocess) { shared->workers = 1; } if (daemonize) { if (!logpath) ProgramLogPath("/dev/null"); dup2(2, 1); } SigInit(); Listen(); TlsInit(); if (launchbrowser) { LaunchBrowser(launchbrowser); } if (daemonize) { Daemonize(); } if (pidpath) { fd = open(pidpath, O_CREAT | O_WRONLY | O_TRUNC, 0644); WRITE(fd, ibuf, FormatInt32(ibuf, getpid()) - ibuf); close(fd); } ChangeUser(); UpdateCurrentDate(timespec_real()); CollectGarbage(); hdrbuf.n = 4 * 1024; hdrbuf.p = xmalloc(hdrbuf.n); inbuf_actual.n = maxpayloadsize; inbuf_actual.p = xmalloc(inbuf_actual.n); inbuf = inbuf_actual; isinitialized = true; CallSimpleHookIfDefined("OnServerStart"); if (!IsTiny()) { if (monitortty && (daemonize || uniprocess)) { monitortty = 0; } if (monitortty) { MonitorMemory(); } } #ifdef STATIC EventLoop(timespec_tomillis(heartbeatinterval)); #else GetHostsTxt(); // for effect GetResolvConf(); // for effect if (daemonize || uniprocess || !linenoiseIsTerminal()) { EventLoop(timespec_tomillis(heartbeatinterval)); } else { ReplEventLoop(); } #endif if (!isexitingworker) { if (!IsTiny()) { terminatemonitor = true; if (monitorth) { pthread_join(monitorth, 0); monitorth = 0; } } HandleShutdown(); CallSimpleHookIfDefined("OnServerStop"); } if (!IsTiny()) { LuaDestroy(); TlsDestroy(); MemDestroy(); } } int main(int argc, char *argv[]) { lua_progname = "redbean"; #if !IsTiny() ShowCrashReports(); #endif LoadZipArgs(&argc, &argv); RedBean(argc, argv); // try to detect memory leaks upon: // 1. main process exit // 2. unwound worker exit if (IsModeDbg()) { if (isexitingworker) { linenoiseDisableRawMode(); linenoiseHistoryFree(); } CheckForMemoryLeaks(); } return 0; }