diff --git a/libc/zip.h b/libc/zip.h index 00946f118..619dfcf3b 100644 --- a/libc/zip.h +++ b/libc/zip.h @@ -69,6 +69,7 @@ #define kZipLfileOffsetLastmodifieddate 12 #define kZipLfileOffsetCrc32 14 #define kZipLfileOffsetCompressedsize 18 +#define kZipLfileOffsetUncompressedsize 22 #define kZipGflagUtf8 0x800 @@ -140,11 +141,12 @@ #define ZIP_LFILE_CRC32(P) READ32LE((P) + kZipLfileOffsetCrc32) #define ZIP_LFILE_COMPRESSEDSIZE(P) \ READ32LE((P) + kZipLfileOffsetCompressedsize) -#define ZIP_LFILE_UNCOMPRESSEDSIZE(P) READ32LE((P) + 22) -#define ZIP_LFILE_NAMESIZE(P) READ16LE((P) + 26) -#define ZIP_LFILE_EXTRASIZE(P) READ16LE((P) + 28) -#define ZIP_LFILE_NAME(P) ((const char *)(&(P)[30])) -#define ZIP_LFILE_EXTRA(P) (&(P)[30 + ZIP_LFILE_NAMESIZE(P)]) +#define ZIP_LFILE_UNCOMPRESSEDSIZE(P) \ + READ32LE((P) + kZipLfileOffsetUncompressedsize) +#define ZIP_LFILE_NAMESIZE(P) READ16LE((P) + 26) +#define ZIP_LFILE_EXTRASIZE(P) READ16LE((P) + 28) +#define ZIP_LFILE_NAME(P) ((const char *)(&(P)[30])) +#define ZIP_LFILE_EXTRA(P) (&(P)[30 + ZIP_LFILE_NAMESIZE(P)]) #define ZIP_LFILE_HDRSIZE(P) \ (ZIP_LFILE_NAMESIZE(P) + ZIP_LFILE_EXTRASIZE(P) + kZipLfileHdrMinSize) #define ZIP_LFILE_CONTENT(P) ((P) + ZIP_LFILE_HDRSIZE(P)) diff --git a/net/http/escape.h b/net/http/escape.h new file mode 100644 index 000000000..c8549cea3 --- /dev/null +++ b/net/http/escape.h @@ -0,0 +1,21 @@ +#ifndef COSMOPOLITAN_NET_HTTP_ESCAPE_H_ +#define COSMOPOLITAN_NET_HTTP_ESCAPE_H_ +#if !(__ASSEMBLER__ + __LINKER__ + 0) +COSMOPOLITAN_C_START_ + +struct EscapeResult { + char *data; + size_t size; +}; + +struct EscapeResult EscapeHtml(const char *, size_t); +struct EscapeResult EscapeUrl(const char *, size_t, const char[hasatleast 256]); +struct EscapeResult EscapeUrlPath(const char *, size_t); +struct EscapeResult EscapeUrlParam(const char *, size_t); +struct EscapeResult EscapeUrlFragment(const char *, size_t); +struct EscapeResult EscapeUrlPathSegment(const char *, size_t); +struct EscapeResult EscapeJsStringLiteral(const char *, size_t); + +COSMOPOLITAN_C_END_ +#endif /* !(__ASSEMBLER__ + __LINKER__ + 0) */ +#endif /* COSMOPOLITAN_NET_HTTP_ESCAPE_H_ */ diff --git a/net/http/escapehtml.c b/net/http/escapehtml.c new file mode 100644 index 000000000..4e7f90f35 --- /dev/null +++ b/net/http/escapehtml.c @@ -0,0 +1,81 @@ +/*-*- 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 2021 Justine Alexandra Roberts Tunney │ +│ │ +│ Permission to use, copy, modify, and/or distribute this software for │ +│ any purpose with or without fee is hereby granted, provided that the │ +│ above copyright notice and this permission notice appear in all copies. │ +│ │ +│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │ +│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │ +│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │ +│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │ +│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │ +│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │ +│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ +│ PERFORMANCE OF THIS SOFTWARE. │ +╚─────────────────────────────────────────────────────────────────────────────*/ +#include "libc/x/x.h" +#include "net/http/escape.h" + +/** + * Escapes HTML entities. + */ +struct EscapeResult EscapeHtml(const char *data, size_t size) { + int c; + char *p; + size_t i; + struct EscapeResult r; + p = r.data = xmalloc(size * 6 + 1); + for (i = 0; i < size; ++i) { + switch ((c = data[i])) { + case '&': + p[0] = '&'; + p[1] = 'a'; + p[2] = 'm'; + p[3] = 'p'; + p[4] = ';'; + p += 5; + break; + case '<': + p[0] = '&'; + p[1] = 'l'; + p[2] = 't'; + p[3] = ';'; + p += 4; + break; + case '>': + p[0] = '&'; + p[1] = 'g'; + p[2] = 't'; + p[3] = ';'; + p += 4; + break; + case '"': + p[0] = '&'; + p[1] = 'q'; + p[2] = 'u'; + p[3] = 'o'; + p[4] = 't'; + p[5] = ';'; + p += 6; + break; + case '\'': + p[0] = '&'; + p[1] = '#'; + p[2] = '3'; + p[3] = '9'; + p[4] = ';'; + p += 5; + break; + default: + *p++ = c; + break; + } + } + r.size = p - r.data; + r.data = xrealloc(r.data, r.size + 1); + r.data[r.size] = '\0'; + return r; +} diff --git a/net/http/escapejsstringliteral.c b/net/http/escapejsstringliteral.c new file mode 100644 index 000000000..ad2648069 --- /dev/null +++ b/net/http/escapejsstringliteral.c @@ -0,0 +1,135 @@ +/*-*- 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 2021 Justine Alexandra Roberts Tunney │ +│ │ +│ Permission to use, copy, modify, and/or distribute this software for │ +│ any purpose with or without fee is hereby granted, provided that the │ +│ above copyright notice and this permission notice appear in all copies. │ +│ │ +│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │ +│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │ +│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │ +│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │ +│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │ +│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │ +│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ +│ PERFORMANCE OF THIS SOFTWARE. │ +╚─────────────────────────────────────────────────────────────────────────────*/ +#include "libc/str/thompike.h" +#include "libc/str/utf16.h" +#include "libc/x/x.h" +#include "net/http/escape.h" + +/** + * Escapes UTF-8 data for JavaScript or JSON string literal. + * + * HTML entities and forward slash are escaped too for added safety. + * + * We assume the UTF-8 is well-formed and can be represented as UTF-16. + * Things that can't be decoded or encoded will be replaced with invalid + * code-point markers. This function is agnostic to numbers that have + * been used with malicious intent in the past under buggy software. + * Noncanonical encodings such as overlong NUL are canonicalized as NUL. + */ +struct EscapeResult EscapeJsStringLiteral(const char *data, size_t size) { + char *p; + size_t i; + unsigned n; + uint64_t w; + wint_t x, y; + struct EscapeResult r; + p = r.data = xmalloc(size * 6 + 6 + 1); + for (i = 0; i < size;) { + x = data[i++] & 0xff; + if (x >= 0200) { + if (x >= 0300) { + n = ThomPikeLen(x); + x = ThomPikeByte(x); + while (--n) { + if (i < size) { + y = data[i++] & 0xff; + if (ThomPikeCont(y)) { + x = ThomPikeMerge(x, y); + } else { + x = 0xFFFD; + break; + } + } else { + x = 0xFFFD; + break; + } + } + } else { + x = 0xFFFD; + } + } + switch (x) { + case '\t': + p[0] = '\\'; + p[1] = 't'; + p += 2; + break; + case '\n': + p[0] = '\\'; + p[1] = 'n'; + p += 2; + break; + case '\r': + p[0] = '\\'; + p[1] = 'r'; + p += 2; + break; + case '\f': + p[0] = '\\'; + p[1] = 'f'; + p += 2; + break; + case '\\': + p[0] = '\\'; + p[1] = '\\'; + p += 2; + break; + case '/': + p[0] = '\\'; + p[1] = '/'; + p += 2; + break; + case '"': + p[0] = '\\'; + p[1] = '"'; + p += 2; + break; + case '\'': + p[0] = '\\'; + p[1] = '\''; + p += 2; + break; + default: + if (0x20 <= x && x < 0x7F) { + *p++ = x; + break; + } + /* fallthrough */ + case '<': + case '>': + case '&': + case '=': + w = EncodeUtf16(x); + do { + p[0] = '\\'; + p[1] = 'u'; + p[2] = "0123456789ABCDEF"[(w & 0xF000) >> 014]; + p[3] = "0123456789ABCDEF"[(w & 0x0F00) >> 010]; + p[4] = "0123456789ABCDEF"[(w & 0x00F0) >> 004]; + p[5] = "0123456789ABCDEF"[(w & 0x000F) >> 000]; + p += 6; + } while ((w >>= 16)); + break; + } + } + r.size = p - r.data; + r.data = xrealloc(r.data, r.size + 1); + r.data[r.size] = '\0'; + return r; +} diff --git a/net/http/escapeurl.c b/net/http/escapeurl.c new file mode 100644 index 000000000..2d2b5efb4 --- /dev/null +++ b/net/http/escapeurl.c @@ -0,0 +1,61 @@ +/*-*- 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 2021 Justine Alexandra Roberts Tunney │ +│ │ +│ Permission to use, copy, modify, and/or distribute this software for │ +│ any purpose with or without fee is hereby granted, provided that the │ +│ above copyright notice and this permission notice appear in all copies. │ +│ │ +│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │ +│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │ +│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │ +│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │ +│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │ +│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │ +│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ +│ PERFORMANCE OF THIS SOFTWARE. │ +╚─────────────────────────────────────────────────────────────────────────────*/ +#include "libc/x/x.h" +#include "net/http/escape.h" + +/** + * Escapes URL component using generic table. + * + * This function is agnostic to the underlying charset. + * Always using UTF-8 is a good idea. + * + * @see EscapeUrlParam + * @see EscapeUrlFragment + * @see EscapeUrlPathSegment + */ +struct EscapeResult EscapeUrl(const char *data, size_t size, + const char xlat[hasatleast 256]) { + int c; + char *p; + size_t i; + struct EscapeResult r; + p = r.data = xmalloc(size * 6 + 1); + for (i = 0; i < size; ++i) { + switch (xlat[(c = data[i] & 0xff)]) { + case 0: + *p++ = c; + break; + case 1: + *p++ = '+'; + break; + case 2: + p[0] = '%'; + p[1] = "0123456789ABCDEF"[(c & 0xF0) >> 4]; + p[2] = "0123456789ABCDEF"[(c & 0x0F) >> 0]; + p += 3; + break; + default: + unreachable; + } + } + r.size = p - r.data; + r.data = xrealloc(r.data, r.size + 1); + r.data[r.size] = '\0'; + return r; +} diff --git a/net/http/escapeurlfragment.c b/net/http/escapeurlfragment.c new file mode 100644 index 000000000..1f3610acc --- /dev/null +++ b/net/http/escapeurlfragment.c @@ -0,0 +1,52 @@ +/*-*- 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 2021 Justine Alexandra Roberts Tunney │ +│ │ +│ Permission to use, copy, modify, and/or distribute this software for │ +│ any purpose with or without fee is hereby granted, provided that the │ +│ above copyright notice and this permission notice appear in all copies. │ +│ │ +│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │ +│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │ +│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │ +│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │ +│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │ +│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │ +│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ +│ PERFORMANCE OF THIS SOFTWARE. │ +╚─────────────────────────────────────────────────────────────────────────────*/ +#include "libc/x/x.h" +#include "net/http/escape.h" + +// url fragment dispatch +// - 0 is -/?.~_@:!$&'()*+,;=0-9A-Za-z +// - 2 is everything else which needs uppercase hex %XX +// note that '& can break html +// note that '() can break css urls +// note that unicode can still be wild +static const char kEscapeUrlFragment[256] = { + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0x00 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0x10 + 2, 0, 2, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x20 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, // 0x30 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x40 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, // 0x50 + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x60 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 2, // 0x70 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0x80 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0x90 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xa0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xb0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xc0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xd0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xe0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xf0 +}; + +/** + * Escapes URL fragment. + */ +struct EscapeResult EscapeUrlFragment(const char *data, size_t size) { + return EscapeUrl(data, size, kEscapeUrlFragment); +} diff --git a/net/http/escapeurlparam.c b/net/http/escapeurlparam.c new file mode 100644 index 000000000..cbf421386 --- /dev/null +++ b/net/http/escapeurlparam.c @@ -0,0 +1,51 @@ +/*-*- 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 2021 Justine Alexandra Roberts Tunney │ +│ │ +│ Permission to use, copy, modify, and/or distribute this software for │ +│ any purpose with or without fee is hereby granted, provided that the │ +│ above copyright notice and this permission notice appear in all copies. │ +│ │ +│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │ +│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │ +│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │ +│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │ +│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │ +│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │ +│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ +│ PERFORMANCE OF THIS SOFTWARE. │ +╚─────────────────────────────────────────────────────────────────────────────*/ +#include "libc/x/x.h" +#include "net/http/escape.h" + +// url query/form name/parameter dispatch +// - 0 is -.*_0-9A-Za-z +// - 1 is ' ' which becomes '+' +// - 2 is everything else which needs uppercase hex %XX +// note that unicode can still be wild +static const char kEscapeUrlParam[256] = { + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0x00 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0x10 + 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 0, 0, 2, // 0x20 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, // 0x30 + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x40 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, // 0x50 + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x60 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, // 0x70 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0x80 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0x90 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xa0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xb0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xc0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xd0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xe0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xf0 +}; + +/** + * Escapes query/form name/parameter. + */ +struct EscapeResult EscapeUrlParam(const char *data, size_t size) { + return EscapeUrl(data, size, kEscapeUrlParam); +} diff --git a/net/http/escapeurlpath.c b/net/http/escapeurlpath.c new file mode 100644 index 000000000..ac23e6f1b --- /dev/null +++ b/net/http/escapeurlpath.c @@ -0,0 +1,54 @@ +/*-*- 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 2021 Justine Alexandra Roberts Tunney │ +│ │ +│ Permission to use, copy, modify, and/or distribute this software for │ +│ any purpose with or without fee is hereby granted, provided that the │ +│ above copyright notice and this permission notice appear in all copies. │ +│ │ +│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │ +│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │ +│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │ +│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │ +│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │ +│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │ +│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ +│ PERFORMANCE OF THIS SOFTWARE. │ +╚─────────────────────────────────────────────────────────────────────────────*/ +#include "libc/x/x.h" +#include "net/http/escape.h" + +// url path dispatch +// - 0 is -.~_@:!$&'()*+,;=0-9A-Za-z/ +// - 2 is everything else which needs uppercase hex %XX +// note that '& can break html +// note that '() can break css urls +// note that unicode can still be wild +static const char kEscapeUrlPath[256] = { + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0x00 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0x10 + 2, 0, 2, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x20 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 2, // 0x30 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x40 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, // 0x50 + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x60 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 2, // 0x70 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0x80 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0x90 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xa0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xb0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xc0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xd0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xe0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xf0 +}; + +/** + * Escapes URL path. + * + * This is the same as EscapeUrlPathSegment() except slash is allowed. + */ +struct EscapeResult EscapeUrlPath(const char *data, size_t size) { + return EscapeUrl(data, size, kEscapeUrlPath); +} diff --git a/net/http/escapeurlpathsegment.c b/net/http/escapeurlpathsegment.c new file mode 100644 index 000000000..1f4310268 --- /dev/null +++ b/net/http/escapeurlpathsegment.c @@ -0,0 +1,55 @@ +/*-*- 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 2021 Justine Alexandra Roberts Tunney │ +│ │ +│ Permission to use, copy, modify, and/or distribute this software for │ +│ any purpose with or without fee is hereby granted, provided that the │ +│ above copyright notice and this permission notice appear in all copies. │ +│ │ +│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │ +│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │ +│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │ +│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │ +│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │ +│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │ +│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ +│ PERFORMANCE OF THIS SOFTWARE. │ +╚─────────────────────────────────────────────────────────────────────────────*/ +#include "libc/x/x.h" +#include "net/http/escape.h" + +// url path segment dispatch +// - 0 is -.~_@:!$&'()*+,;=0-9A-Za-z +// - 2 is everything else which needs uppercase hex %XX +// note that '& can break html +// note that '() can break css urls +// note that unicode can still be wild +static const char kEscapeUrlPathSegment[256] = { + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0x00 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0x10 + 2, 0, 2, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, // 0x20 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 2, // 0x30 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x40 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, // 0x50 + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x60 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 2, // 0x70 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0x80 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0x90 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xa0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xb0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xc0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xd0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xe0 + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xf0 +}; + +/** + * Escapes URL path segment. + * + * Please note this will URI encode the slash character. That's because + * segments are the labels between the slashes in a path. + */ +struct EscapeResult EscapeUrlPathSegment(const char *data, size_t size) { + return EscapeUrl(data, size, kEscapeUrlPathSegment); +} diff --git a/net/http/formathttpdatetime.c b/net/http/formathttpdatetime.c index fca81763e..847ac8c8b 100644 --- a/net/http/formathttpdatetime.c +++ b/net/http/formathttpdatetime.c @@ -16,6 +16,7 @@ │ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ │ PERFORMANCE OF THIS SOFTWARE. │ ╚─────────────────────────────────────────────────────────────────────────────*/ +#include "libc/macros.internal.h" #include "libc/str/str.h" #include "libc/time/struct/tm.h" #include "libc/time/time.h" @@ -34,27 +35,28 @@ char *FormatHttpDateTime(char p[hasatleast 30], struct tm *tm) { p = mempcpy(p, kWeekdayNameShort[tm->tm_wday], 3); *p++ = ','; *p++ = ' '; - i = tm->tm_mday; + i = MIN(MAX(tm->tm_mday, 0), 31); *p++ = '0' + i / 10; *p++ = '0' + i % 10; *p++ = ' '; - p = mempcpy(p, kMonthNameShort[tm->tm_mon], 3); + i = MIN(MAX(tm->tm_mon, 0), 11); + p = mempcpy(p, kMonthNameShort[i], 3); *p++ = ' '; - i = tm->tm_year + 1900; + i = MIN(MAX(tm->tm_year + 1900, 0), 9999); *p++ = '0' + i / 1000; *p++ = '0' + i / 100 % 10; *p++ = '0' + i / 10 % 10; *p++ = '0' + i % 10; *p++ = ' '; - i = tm->tm_hour; + i = MIN(MAX(tm->tm_hour, 0), 23); *p++ = '0' + i / 10; *p++ = '0' + i % 10; *p++ = ':'; - i = tm->tm_min; + i = MIN(MAX(tm->tm_min, 0), 59); *p++ = '0' + i / 10; *p++ = '0' + i % 10; *p++ = ':'; - i = tm->tm_sec; + i = MIN(MAX(tm->tm_sec, 0), 59); *p++ = '0' + i / 10; *p++ = '0' + i % 10; *p++ = ' '; diff --git a/net/http/gethttpreason.c b/net/http/gethttpreason.c new file mode 100644 index 000000000..550a488cf --- /dev/null +++ b/net/http/gethttpreason.c @@ -0,0 +1,110 @@ +/*-*- 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 2021 Justine Alexandra Roberts Tunney │ +│ │ +│ Permission to use, copy, modify, and/or distribute this software for │ +│ any purpose with or without fee is hereby granted, provided that the │ +│ above copyright notice and this permission notice appear in all copies. │ +│ │ +│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │ +│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │ +│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │ +│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │ +│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │ +│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │ +│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ +│ PERFORMANCE OF THIS SOFTWARE. │ +╚─────────────────────────────────────────────────────────────────────────────*/ +#include "libc/fmt/itoa.h" +#include "libc/macros.internal.h" +#include "net/http/http.h" + +static const struct HttpReason { + int code; + const char *name; +} kHttpReason[] = { + {100, "Continue"}, + {101, "Switching Protocols"}, + {102, "Processing"}, + {200, "OK"}, + {201, "Created"}, + {202, "Accepted"}, + {203, "Non-authoritative Information"}, + {204, "No Content"}, + {205, "Reset Content"}, + {206, "Partial Content"}, + {207, "Multi-Status"}, + {208, "Already Reported"}, + {226, "IM Used"}, + {300, "Multiple Choices"}, + {301, "Moved Permanently"}, + {302, "Found"}, + {303, "See Other"}, + {304, "Not Modified"}, + {305, "Use Proxy"}, + {307, "Temporary Redirect"}, + {308, "Permanent Redirect"}, + {400, "Bad Request"}, + {401, "Unauthorized"}, + {402, "Payment Required"}, + {403, "Forbidden"}, + {404, "Not Found"}, + {405, "Method Not Allowed"}, + {406, "Not Acceptable"}, + {407, "Proxy Authentication Required"}, + {408, "Request Timeout"}, + {409, "Conflict"}, + {410, "Gone"}, + {411, "Length Required"}, + {412, "Precondition Failed"}, + {413, "Payload Too Large"}, + {414, "Request-URI Too Long"}, + {415, "Unsupported Media Type"}, + {416, "Requested Range Not Satisfiable"}, + {417, "Expectation Failed"}, + {418, "I'm a teapot"}, + {421, "Misdirected Request"}, + {422, "Unprocessable Entity"}, + {423, "Locked"}, + {424, "Failed Dependency"}, + {426, "Upgrade Required"}, + {428, "Precondition Required"}, + {429, "Too Many Requests"}, + {431, "Request Header Fields Too Large"}, + {444, "Connection Closed Without Response"}, + {451, "Unavailable For Legal Reasons"}, + {499, "Client Closed Request"}, + {500, "Internal Server Error"}, + {501, "Not Implemented"}, + {502, "Bad Gateway"}, + {503, "Service Unavailable"}, + {504, "Gateway Timeout"}, + {505, "HTTP Version Not Supported"}, + {506, "Variant Also Negotiates"}, + {507, "Insufficient Storage"}, + {508, "Loop Detected"}, + {510, "Not Extended"}, + {511, "Network Authentication Required"}, + {599, "Network Connect Timeout Error"}, +}; + +/** + * Returns string describing HTTP reason phrase. + */ +const char *GetHttpReason(int code) { + int m, l, r; + l = 0; + r = ARRAYLEN(kHttpReason) - 1; + while (l <= r) { + m = (l + r) >> 1; + if (kHttpReason[m].code < code) { + l = m + 1; + } else if (kHttpReason[m].code > code) { + r = m - 1; + } else { + return kHttpReason[m].name; + } + } + return ""; +} diff --git a/net/http/http.h b/net/http/http.h index ddd8931f3..e48421aad 100644 --- a/net/http/http.h +++ b/net/http/http.h @@ -80,8 +80,8 @@ struct HttpRequestSlice { }; struct HttpRequest { + int i, t, a, h; int method; - int length; struct HttpRequestSlice uri; struct HttpRequestSlice version; struct HttpRequestSlice scratch; @@ -92,14 +92,16 @@ extern const char kHttpMethod[17][8]; int GetHttpHeader(const char *, size_t); int GetHttpMethod(const char *, size_t); +void InitHttpRequest(struct HttpRequest *); int ParseHttpRequest(struct HttpRequest *, const char *, size_t); int NegotiateHttpRequest(int, const char *, uint32_t *, char *, uint32_t *, uint32_t *, bool, long double); -long ParseContentLength(const struct HttpRequest *, const char *); +ssize_t ParseContentLength(const char *, size_t); char *FormatHttpDateTime(char[hasatleast 30], struct tm *); bool ParseHttpRange(const char *, size_t, long, long *, long *); unsigned ParseHttpVersion(const char *, size_t); int64_t ParseHttpDateTime(const char *, size_t); +const char *GetHttpReason(int); COSMOPOLITAN_C_END_ #endif /* !(__ASSEMBLER__ + __LINKER__ + 0) */ diff --git a/net/http/parsecontentlength.c b/net/http/parsecontentlength.c index b7721ed4e..f10909e48 100644 --- a/net/http/parsecontentlength.c +++ b/net/http/parsecontentlength.c @@ -19,16 +19,18 @@ #include "libc/str/str.h" #include "net/http/http.h" -long ParseContentLength(const struct HttpRequest *req, const char *p) { - long i, r, n = 0; - for (i = req->headers[kHttpContentLength].a; - i < req->headers[kHttpContentLength].b; ++i) { - if (isdigit(p[i])) { - if (!__builtin_mul_overflow(n, 10, &r) && - !__builtin_add_overflow(r, p[i] - '0', &r)) { - n = r; - } - } +/** + * Parses Content-Length header. + * + * @return -1 on invalid or overflow, otherwise >=0 value + */ +ssize_t ParseContentLength(const char *s, size_t n) { + int i, r = 0; + if (!n) return -1; + for (i = 0; i < n; ++i) { + if (!isdigit(s[i])) return -1; + if (__builtin_mul_overflow(r, 10, &r)) return -1; + if (__builtin_add_overflow(r, s[i] - '0', &r)) return -1; } - return n; + return r; } diff --git a/net/http/parsehttprequest.c b/net/http/parsehttprequest.c index 6ebbf39d0..2a6fcb30a 100644 --- a/net/http/parsehttprequest.c +++ b/net/http/parsehttprequest.c @@ -19,106 +19,119 @@ #include "libc/alg/alg.h" #include "libc/alg/arraylist.internal.h" #include "libc/limits.h" +#include "libc/macros.internal.h" #include "libc/stdio/stdio.h" #include "libc/str/str.h" #include "libc/sysv/errfuns.h" #include "libc/x/x.h" #include "net/http/http.h" -enum ParseHttpRequestState { - METHOD, - URI, - VERSION, - HKEY, - HSEP, - HVAL, - CR1, - LF1, - LF2 -}; +#define LIMIT (SHRT_MAX - 1) + +enum { START, METHOD, URI, VERSION, HKEY, HSEP, HVAL, CR1, LF1, LF2 }; /** - * Parses HTTP request header. + * Initializes HTTP request parser. */ -int ParseHttpRequest(struct HttpRequest *req, const char *p, size_t n) { - int a, h, c, i, x; - enum ParseHttpRequestState t; - memset(req, 0, sizeof(*req)); - a = h = 0; - t = METHOD; - if (n > SHRT_MAX - 1) n = SHRT_MAX - 1; - for (i = 0; i < n; ++i) { - c = p[i] & 0xFF; - switch (t) { +void InitHttpRequest(struct HttpRequest *r) { + memset(r, 0, sizeof(*r)); +} + +/** + * Parses HTTP request. + */ +int ParseHttpRequest(struct HttpRequest *r, const char *p, size_t n) { + int c; + for (n = MIN(n, LIMIT); r->i < n; ++r->i) { + c = p[r->i] & 0xff; + switch (r->t) { + case START: + if (c == '\r' || c == '\n') { + ++r->a; /* RFC7230 § 3.5 */ + break; + } + r->t = METHOD; + /* fallthrough */ case METHOD: - if (c == '\r' || c == '\n') break; /* RFC7230 § 3.5 */ if (c == ' ') { - if (!i) return ebadmsg(); - if ((x = GetHttpMethod(p, i)) == -1) return ebadmsg(); - req->method = x; - req->uri.a = i + 1; - t = URI; + if ((r->method = GetHttpMethod(p + r->a, r->i - r->a)) != -1) { + r->uri.a = r->i + 1; + r->t = URI; + } else { + return ebadmsg(); + } } break; case URI: - if (c == ' ') { - req->uri.b = i; - req->version.a = i + 1; - if (req->uri.a == req->uri.b) return ebadmsg(); - t = VERSION; + if (c == ' ' || c == '\r' || c == '\n') { + if (r->i == r->uri.a) return ebadmsg(); + r->uri.b = r->i; + if (c == ' ') { + r->version.a = r->i + 1; + r->t = VERSION; + } else if (c == '\r') { + r->t = CR1; + } else { + r->t = LF1; + } } break; case VERSION: if (c == '\r' || c == '\n') { - req->version.b = i; - t = c == '\r' ? CR1 : LF1; + r->version.b = r->i; + r->t = c == '\r' ? CR1 : LF1; } break; case CR1: if (c != '\n') return ebadmsg(); - t = LF1; + r->t = LF1; break; case LF1: if (c == '\r') { - t = LF2; + r->t = LF2; break; } else if (c == '\n') { - return 0; - } else if (c == ' ' || c == '\t') { /* line folding!!! */ - return eprotonosupport(); /* RFC7230 § 3.2.4 */ + return ++r->i; + } else if (c == ':') { + return ebadmsg(); + } else if (c == ' ' || c == '\t') { + return ebadmsg(); /* RFC7230 § 3.2.4 */ } - a = i; - t = HKEY; - /* εpsilon transition */ + r->a = r->i; + r->t = HKEY; + break; case HKEY: if (c == ':') { - h = GetHttpHeader(p + a, i - a); - t = HSEP; + r->h = GetHttpHeader(p + r->a, r->i - r->a); + r->t = HSEP; } break; case HSEP: if (c == ' ' || c == '\t') break; - a = i; - t = HVAL; - /* εpsilon transition */ + r->a = r->i; + r->t = HVAL; + /* fallthrough */ case HVAL: if (c == '\r' || c == '\n') { - if (h != -1) { - req->headers[h].a = a; - req->headers[h].b = i; + if (r->h != -1) { + r->headers[r->h].a = r->a; + r->headers[r->h].b = r->i; } - t = c == '\r' ? CR1 : LF1; + r->t = c == '\r' ? CR1 : LF1; } break; case LF2: if (c == '\n') { - req->length = i + 1; - return i + 1; + return ++r->i; } return ebadmsg(); default: unreachable; } } - return 0; + if (r->i < LIMIT) { + return 0; + } else { + return ebadmsg(); + } } diff --git a/net/http/parsehttpversion.c b/net/http/parsehttpversion.c index e85326d3c..d04333c2b 100644 --- a/net/http/parsehttpversion.c +++ b/net/http/parsehttpversion.c @@ -21,6 +21,7 @@ unsigned ParseHttpVersion(const char *p, size_t n) { unsigned x; + if (!n) return 9; if (n >= 8 && READ32LE(p) == ('H' | 'T' << 8 | 'T' << 16 | 'P' << 24)) { if (READ32LE(p + 4) == ('/' | '1' << 8 | '.' << 16 | '1' << 24)) { return 101; diff --git a/test/net/http/escapehtml_test.c b/test/net/http/escapehtml_test.c new file mode 100644 index 000000000..432d3a7bf --- /dev/null +++ b/test/net/http/escapehtml_test.c @@ -0,0 +1,41 @@ +/*-*- 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 2021 Justine Alexandra Roberts Tunney │ +│ │ +│ Permission to use, copy, modify, and/or distribute this software for │ +│ any purpose with or without fee is hereby granted, provided that the │ +│ above copyright notice and this permission notice appear in all copies. │ +│ │ +│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │ +│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │ +│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │ +│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │ +│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │ +│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │ +│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ +│ PERFORMANCE OF THIS SOFTWARE. │ +╚─────────────────────────────────────────────────────────────────────────────*/ +#include "libc/runtime/gc.internal.h" +#include "libc/testlib/testlib.h" +#include "net/http/escape.h" + +char *escapehtml(const char *s) { + struct EscapeResult r; + r = EscapeHtml(s, strlen(s)); + ASSERT_EQ(strlen(r.data), r.size); + return r.data; +} + +TEST(escapehtml, test) { + EXPECT_STREQ("abc&<>"'\1\2", + gc(escapehtml("abc&<>\"'\1\2"))); +} + +TEST(escapehtml, testLargeGrowth) { + EXPECT_STREQ(""""", gc(escapehtml("\"\"\""))); +} + +TEST(escapehtml, testEmpty) { + EXPECT_STREQ("", gc(escapehtml(""))); +} diff --git a/test/net/http/parsecontentlength_test.c b/test/net/http/parsecontentlength_test.c new file mode 100644 index 000000000..097e69132 --- /dev/null +++ b/test/net/http/parsecontentlength_test.c @@ -0,0 +1,32 @@ +/*-*- 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 2021 Justine Alexandra Roberts Tunney │ +│ │ +│ Permission to use, copy, modify, and/or distribute this software for │ +│ any purpose with or without fee is hereby granted, provided that the │ +│ above copyright notice and this permission notice appear in all copies. │ +│ │ +│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │ +│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │ +│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │ +│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │ +│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │ +│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │ +│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ +│ PERFORMANCE OF THIS SOFTWARE. │ +╚─────────────────────────────────────────────────────────────────────────────*/ +#include "libc/testlib/testlib.h" +#include "net/http/http.h" + +TEST(ParseContentLength, test) { + EXPECT_EQ(-1, ParseContentLength("", 0)); + EXPECT_EQ(-1, ParseContentLength("-1", 2)); + EXPECT_EQ(-1, ParseContentLength("-2", 2)); + EXPECT_EQ(0, ParseContentLength("0", 1)); + EXPECT_EQ(1, ParseContentLength("1", 1)); + EXPECT_EQ(0x7fffffff, ParseContentLength("2147483647", 10)); + EXPECT_EQ(-1, ParseContentLength("2147483648", 10)); + EXPECT_EQ(-1, ParseContentLength("9223372036854775808", 19)); + EXPECT_EQ(-1, ParseContentLength("88223372036854775808", 20)); +} diff --git a/test/net/http/parsehttprequest_test.c b/test/net/http/parsehttprequest_test.c index edf418c76..3e8b99ad1 100644 --- a/test/net/http/parsehttprequest_test.c +++ b/test/net/http/parsehttprequest_test.c @@ -17,6 +17,7 @@ │ PERFORMANCE OF THIS SOFTWARE. │ ╚─────────────────────────────────────────────────────────────────────────────*/ #include "libc/bits/bits.h" +#include "libc/errno.h" #include "libc/mem/mem.h" #include "libc/runtime/gc.internal.h" #include "libc/stdio/stdio.h" @@ -24,6 +25,7 @@ #include "libc/testlib/testlib.h" #include "libc/x/x.h" #include "net/http/http.h" +#include "net/http/uri.h" struct HttpRequest req[1]; @@ -35,16 +37,23 @@ static char *slice(const char *m, struct HttpRequestSlice s) { return p; } +static unsigned version(const char *m) { + return ParseHttpVersion(m + req->version.a, req->version.b - req->version.a); +} + TEST(ParseHttpRequest, testEmpty_tooShort) { + InitHttpRequest(req); EXPECT_EQ(0, ParseHttpRequest(req, "", 0)); } TEST(ParseHttpRequest, testTooShort) { + InitHttpRequest(req); EXPECT_EQ(0, ParseHttpRequest(req, "\r\n", 2)); } TEST(ParseHttpRequest, testNoHeaders) { static const char m[] = "GET /foo HTTP/1.0\r\n\r\n"; + InitHttpRequest(req); EXPECT_EQ(strlen(m), ParseHttpRequest(req, m, strlen(m))); EXPECT_EQ(kHttpGet, req->method); EXPECT_STREQ("/foo", gc(slice(m, req->uri))); @@ -57,6 +66,7 @@ POST /foo?bar%20hi HTTP/1.0\r\n\ Host: foo.example\r\n\ Content-Length: 0\r\n\ \r\n"; + InitHttpRequest(req); EXPECT_EQ(strlen(m), ParseHttpRequest(req, m, strlen(m))); EXPECT_EQ(kHttpPost, req->method); EXPECT_STREQ("/foo?bar%20hi", gc(slice(m, req->uri))); @@ -65,3 +75,83 @@ Content-Length: 0\r\n\ EXPECT_STREQ("0", gc(slice(m, req->headers[kHttpContentLength]))); EXPECT_STREQ("", gc(slice(m, req->headers[kHttpEtag]))); } + +TEST(ParseHttpRequest, testHttp101) { + static const char m[] = "GET / HTTP/1.1\r\n\r\n"; + InitHttpRequest(req); + EXPECT_EQ(strlen(m), ParseHttpRequest(req, m, strlen(m))); + EXPECT_EQ(kHttpGet, req->method); + EXPECT_STREQ("/", gc(slice(m, req->uri))); + EXPECT_STREQ("HTTP/1.1", gc(slice(m, req->version))); + EXPECT_EQ(101, version(m)); +} + +TEST(ParseHttpRequest, testHttp100) { + static const char m[] = "GET / HTTP/1.0\r\n\r\n"; + InitHttpRequest(req); + EXPECT_EQ(strlen(m), ParseHttpRequest(req, m, strlen(m))); + EXPECT_EQ(kHttpGet, req->method); + EXPECT_STREQ("/", gc(slice(m, req->uri))); + EXPECT_STREQ("HTTP/1.0", gc(slice(m, req->version))); + EXPECT_EQ(100, version(m)); +} + +TEST(ParseHttpRequest, testHttp009) { + static const char m[] = "GET /\r\n\r\n"; + InitHttpRequest(req); + EXPECT_EQ(strlen(m), ParseHttpRequest(req, m, strlen(m))); + EXPECT_EQ(kHttpGet, req->method); + EXPECT_STREQ("/", gc(slice(m, req->uri))); + EXPECT_STREQ("", gc(slice(m, req->version))); + EXPECT_EQ(9, version(m)); +} + +TEST(ParseHttpRequest, testLeadingLineFeeds_areIgnored) { + static const char m[] = "\ +\r\n\ +GET /foo?bar%20hi HTTP/1.0\r\n\ +User-Agent: hi\r\n\ +\r\n"; + InitHttpRequest(req); + EXPECT_EQ(strlen(m), ParseHttpRequest(req, m, strlen(m))); + EXPECT_STREQ("/foo?bar%20hi", gc(slice(m, req->uri))); +} + +TEST(ParseHttpRequest, testLineFolding_isRejected) { + static const char m[] = "\ +GET /foo?bar%20hi HTTP/1.0\r\n\ +User-Agent: hi\r\n\ + there\r\n\ +\r\n"; + InitHttpRequest(req); + EXPECT_EQ(-1, ParseHttpRequest(req, m, strlen(m))); + EXPECT_EQ(EBADMSG, errno); +} + +TEST(ParseHttpRequest, testEmptyHeaderName_isRejected) { + static const char m[] = "\ +GET /foo?bar%20hi HTTP/1.0\r\n\ +User-Agent: hi\r\n\ +: hi\r\n\ +\r\n"; + InitHttpRequest(req); + EXPECT_EQ(-1, ParseHttpRequest(req, m, strlen(m))); + EXPECT_EQ(EBADMSG, errno); +} + +TEST(ParseHttpRequest, testUnixNewlines) { + static const char m[] = "\ +POST /foo?bar%20hi HTTP/1.0\n\ +Host: foo.example\n\ +Content-Length: 0\n\ +\n\ +\n"; + InitHttpRequest(req); + EXPECT_EQ(strlen(m) - 1, ParseHttpRequest(req, m, strlen(m))); + EXPECT_EQ(kHttpPost, req->method); + EXPECT_STREQ("/foo?bar%20hi", gc(slice(m, req->uri))); + EXPECT_STREQ("HTTP/1.0", gc(slice(m, req->version))); + EXPECT_STREQ("foo.example", gc(slice(m, req->headers[kHttpHost]))); + EXPECT_STREQ("0", gc(slice(m, req->headers[kHttpContentLength]))); + EXPECT_STREQ("", gc(slice(m, req->headers[kHttpEtag]))); +} diff --git a/third_party/chibicc/dox2.c b/third_party/chibicc/dox2.c index 09b91db35..7fcc96867 100644 --- a/third_party/chibicc/dox2.c +++ b/third_party/chibicc/dox2.c @@ -350,7 +350,7 @@ static void PrintText(FILE *f, const char *s) { bol = false; break; case '\'': - fprintf(f, "'"); + fprintf(f, "'"); bol = false; break; case '`': diff --git a/third_party/lua/lbaselib.c b/third_party/lua/lbaselib.c index fe58794eb..35f007ff8 100644 --- a/third_party/lua/lbaselib.c +++ b/third_party/lua/lbaselib.c @@ -479,7 +479,7 @@ static int luaB_tostring (lua_State *L) { static const luaL_Reg base_funcs[] = { {"assert", luaB_assert}, {"collectgarbage", luaB_collectgarbage}, - {"dofile", luaB_dofile}, + /* {"dofile", luaB_dofile}, */ {"error", luaB_error}, {"getmetatable", luaB_getmetatable}, {"ipairs", luaB_ipairs}, diff --git a/tool/build/runitd.c b/tool/build/runitd.c index 4bf4e56c7..ed1b03518 100644 --- a/tool/build/runitd.c +++ b/tool/build/runitd.c @@ -1,7 +1,7 @@ /*-*- 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 │ +│ Copyright 2021 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 │ diff --git a/tool/net/redbean.c b/tool/net/redbean.c index bbbdd50b0..2c31b5cf7 100644 --- a/tool/net/redbean.c +++ b/tool/net/redbean.c @@ -37,12 +37,13 @@ #include "libc/mem/mem.h" #include "libc/nexgen32e/crc32.h" #include "libc/rand/rand.h" -#include "libc/runtime/gc.internal.h" #include "libc/runtime/runtime.h" #include "libc/sock/sock.h" #include "libc/stdio/stdio.h" #include "libc/str/str.h" +#include "libc/str/thompike.h" #include "libc/str/undeflate.h" +#include "libc/str/utf16.h" #include "libc/sysv/consts/af.h" #include "libc/sysv/consts/auxv.h" #include "libc/sysv/consts/clock.h" @@ -63,12 +64,14 @@ #include "libc/sysv/consts/sock.h" #include "libc/sysv/consts/sol.h" #include "libc/sysv/consts/tcp.h" +#include "libc/sysv/consts/w.h" #include "libc/sysv/errfuns.h" #include "libc/time/struct/tm.h" #include "libc/time/time.h" #include "libc/x/x.h" #include "libc/zip.h" #include "libc/zipos/zipos.internal.h" +#include "net/http/escape.h" #include "net/http/http.h" #include "third_party/getopt/getopt.h" #include "third_party/lua/lauxlib.h" @@ -77,14 +80,12 @@ #include "third_party/lua/lualib.h" #include "third_party/zlib/zlib.h" -/* TODO(jart): Implement more lenient message framing */ - #define USAGE \ " [-hvdsm] [-p PORT]\n\ \n\ DESCRIPTION\n\ \n\ - redbean distributable static web server\n\ + redbean - single-file distributable web server\n\ \n\ FLAGS\n\ \n\ @@ -106,8 +107,11 @@ FLAGS\n\ \n\ FEATURES\n\ \n\ + - HTTP v0.9\n\ + - HTTP v1.0\n\ - HTTP v1.1\n\ - - Content-Encoding\n\ + - Lua v5.4.x\n\ + - Gzip Content-Encoding\n\ - Range / Content-Range\n\ - Last-Modified / If-Modified-Since\n\ \n\ @@ -124,30 +128,40 @@ USAGE\n\ zip redbean.com index.html # adds file\n\ zip -0 redbean.com video.mp4 # adds without compression\n\ \n\ - Each connection uses a point in time snapshot of your ZIP file.\n\ - If your ZIP is deleted then serving continues. If it's replaced\n\ - then issuing SIGUSR1 (or SIGHUP if daemon) will reindex the zip\n\ - for subsequent connections without interrupting active ones. If\n\ - SIGINT or SIGTERM is issued then a graceful shutdown is started\n\ - but if it's issued a second time, active connections are reset.\n\ + You can run redbean interactively in your terminal as follows:\n\ +\n\ + redbean.com -vv\n\ + CTRL-C # 1x: graceful shutdown\n\ + CTRL-C # 2x: forceful shutdown\n\ \n\ You can have redbean run as a daemon by doing the following:\n\ \n\ redbean.com -vv -d -L redbean.log -P redbean.pid\n\ kill -HUP $(cat redbean.pid)\n\ - kill -TERM $(cat redbean.pid)\n\ + kill -TERM $(cat redbean.pid) # 1x: graceful shutdown\n\ + kill -TERM $(cat redbean.pid) # 2x: forceful shutdown\n\ +\n\ + Each connection uses a point in time snapshot of your ZIP file.\n\ + If your ZIP is deleted then serving continues. If it's replaced\n\ + then issuing SIGUSR1 (or SIGHUP if daemon) will reindex the zip\n\ + for subsequent connections without interrupting active ones. If\n\ + Ctrl-C or SIGTERM is issued then a graceful shutdown is started\n\ + but if it's issued a second time, active connections are reset.\n\ +\n\ + redbean imposes a 32kb limit on requests to limit the memory of\n\ + connection processes, which grow to whatever number your system\n\ + limits and tcp stack configuration allow. If fork() should fail\n\ + then redbean starts shutting idle connections down.\n\ +\n\ + Redirects emit a 307 response unless the location exists in\n\ + the zip directory, in which case it transparently rewrites.\n\ \n\ \n" -#define HASH_LOAD_FACTOR /* 1. / */ 4 -#define DEFAULT_PORT 8080 -#define DEFAULT_SERVER "redbean/0.1" -#define DEFAULT_CONTENT_TYPE "application/octet-stream" -#define DEFAULT_PATH "/tool/net/redbean.html" -#define FAVICON "tool/net/redbean.ico" +#define HASH_LOAD_FACTOR /* 1. / */ 4 +#define DEFAULT_PORT 8080 -#define STPCPY(p, s) mempcpy(p, s, strlen(s)) -#define AppendHeaderName(p, s) STPCPY(STPCPY(p, s), ": ") +#define AppendHeaderName(p, s) stpcpy(stpcpy(p, s), ": ") static const struct itimerval kHeartbeat = { {0, 500000}, @@ -167,6 +181,25 @@ static const uint8_t kGzipHeader[] = { kZipOsUnix, // OS }; +static const char kHexToInt[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x20 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, // 0x30 + 0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x40 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x50 + 0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x60 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x70 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x80 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x90 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xa0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xb0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xc0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xd0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xe0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xf0 +}; + static const struct ContentTypeExtension { unsigned char ext[8]; const char *mime; @@ -206,56 +239,108 @@ static const struct ContentTypeExtension { {"zip", "application/zip"}, // }; +struct Buffer { + size_t n; + char *p; +}; + +struct UriParser { + int i; + int c; + const char *data; + int size; + char *p; + char *q; +}; + +struct Params { + size_t n; + struct Param { + struct Buffer key; + struct Buffer val; // val.n may be SIZE_MAX + } * p; +}; + +static struct Freelist { + size_t n; + void **p; +} freelist; + static struct Redirects { - size_t i, n; + size_t n; struct Redirect { const char *path; size_t pathlen; - const char *dest; + const char *location; } * p; } redirects; static struct Assets { uint32_t n; struct Asset { + uint32_t lf; uint32_t hash; - uint32_t cf; int64_t lastmodified; char *lastmodifiedstr; } * p; } assets; static bool killed; -static bool notimer; +static bool istext; +static bool zombied; +static bool gzipped; +static bool branded; +static bool meltdown; +static bool heartless; static bool printport; static bool heartbeat; static bool daemonize; static bool terminated; static bool uniprocess; -static bool legacyhttp; static bool invalidated; static bool logmessages; +static bool checkedmethod; +static bool connectionclose; +static bool keyboardinterrupt; +static int frags; static int gmtoff; static int server; static int client; static int daemonuid; static int daemongid; -static int cacheseconds; +static int statuscode; +static unsigned httpversion; static uint32_t clientaddrsize; static void *zmap; +static lua_State *L; +static void *content; static uint8_t *zbase; static uint8_t *zcdir; +static size_t msgsize; +static size_t amtread; static size_t zmapsize; +static char *luaheaderp; static const char *pidpath; static const char *logpath; static int64_t programtime; +static size_t contentlength; +static int64_t cacheseconds; +static uint8_t gzip_footer[8]; static const char *programfile; static const char *serverheader; -static lua_State *L; + +static struct Buffer body; +static struct Buffer inbuf; +static struct Buffer hdrbuf; +static struct Buffer outbuf; +static struct Buffer uripath; +static struct Params uriparams; +static struct Buffer urifragment; static long double nowish; +static long double startread; static long double startrequest; static long double startconnection; static struct sockaddr_in serveraddr; @@ -265,73 +350,102 @@ static struct HttpRequest req; static char currentdate[32]; static char clientaddrstr[32]; static char serveraddrstr[32]; -static char inbuf[PAGESIZE]; -static char outbuf[PAGESIZE]; -static void OnHup(void) { - invalidated = true; +static void OnChld(void) { + zombied = true; } -static void OnAlarm(void) { +static void OnAlrm(void) { heartbeat = true; } -static void OnTerminate(void) { - if (terminated) { - killed = true; - } else { +static void OnUsr1(void) { + invalidated = true; +} + +static void OnUsr2(void) { + meltdown = true; +} + +static void OnTerm(void) { + if (!terminated) { terminated = true; + } else { + killed = true; } } +static void OnInt(void) { + keyboardinterrupt = true; + OnTerm(); +} + +static void OnHup(void) { + if (daemonize) { + OnUsr1(); + } else { + OnTerm(); + } +} + +static int ComparePaths(const char *a, size_t n, const char *b, size_t m) { + int c; + if ((c = memcmp(a, b, MIN(n, m)))) return c; + if (n < m) return -1; + if (n > m) return +1; + return 0; +} + +static long FindRedirect(const char *path, size_t n) { + int c, m, l, r, z; + l = 0; + r = redirects.n - 1; + while (l <= r) { + m = (l + r) >> 1; + c = ComparePaths(redirects.p[m].path, redirects.p[m].pathlen, path, n); + if (c < 0) { + l = m + 1; + } else if (c > 0) { + r = m - 1; + } else { + return m; + } + } + return -1; +} + static void AddRedirect(const char *arg) { + long i, j; const char *p; struct Redirect r; CHECK_NOTNULL((p = strchr(arg, '='))); CHECK_GT(p - arg, 0); r.path = arg; r.pathlen = p - arg; - r.dest = strdup(p + 1); - APPEND(&redirects.p, &redirects.i, &redirects.n, &r); -} - -static int CompareRedirects(const struct Redirect *a, - const struct Redirect *b) { - return strcmp(a->path, b->path); -} - -static void SortRedirects(void) { - qsort(redirects.p, redirects.i, sizeof(struct Redirect), - (void *)CompareRedirects); -} - -static const char *LookupRedirect(const char *path, size_t n) { - int c, m, l, r, z; - l = 0; - r = redirects.i - 1; - while (l <= r) { - m = (l + r) >> 1; - c = memcmp(redirects.p[m].path, path, MIN(redirects.p[m].pathlen, n)); - if (c < 0) { - l = m + 1; - } else if (c > 0) { - r = m - 1; - } else if (redirects.p[m].pathlen < n) { - l = m + 1; - } else if (redirects.p[m].pathlen > n) { - r = m - 1; - } else { - return redirects.p[m].dest; + r.location = p + 1; + if ((i = FindRedirect(r.path, r.pathlen)) != -1) { + redirects.p[i] = r; + } else { + i = redirects.n; + redirects.p = xrealloc(redirects.p, (i + 1) * sizeof(*redirects.p)); + for (j = i; j > 0; --j) { + if (ComparePaths(r.path, r.pathlen, redirects.p[j - 1].path, + redirects.p[j - 1].pathlen) < 0) { + redirects.p[j] = redirects.p[j - 1]; + } else { + break; + } } + redirects.p[j] = r; + ++redirects.n; } - return NULL; } static int CompareInts(const uint64_t x, uint64_t y) { return x > y ? 1 : x < y ? -1 : 0; } -static const char *LookupContentType(uint64_t ext) { +static const char *FindContentType(uint64_t ext) { int c, m, l, r; l = 0; r = ARRAYLEN(kContentTypeExtension) - 1; @@ -346,22 +460,23 @@ static const char *LookupContentType(uint64_t ext) { return kContentTypeExtension[m].mime; } } - return DEFAULT_CONTENT_TYPE; + return NULL; } static const char *GetContentType(const char *path, size_t n) { size_t i; uint64_t x; - const char *p; + const char *p, *r; if ((p = memrchr(path, '.', n))) { for (x = 0, i = n; i-- > p + 1 - path;) { x <<= 8; x |= path[i] & 0xFF; } - return LookupContentType(bswap_64(x)); - } else { - return DEFAULT_CONTENT_TYPE; + if ((r = FindContentType(bswap_64(x)))) { + return r; + } } + return "application/octet-stream"; } static wontreturn void PrintUsage(FILE *f, int rc) { @@ -380,11 +495,19 @@ static void DescribeAddress(char buf[32], const struct sockaddr_in *addr) { *p = '\0'; } -void GetOpts(int argc, char *argv[]) { - int opt; +static void SetDefaults(void) { + cacheseconds = -1; + serverheader = "redbean/0.1"; serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(DEFAULT_PORT); serveraddr.sin_addr.s_addr = INADDR_ANY; + AddRedirect("/=/tool/net/redbean.html"); + AddRedirect("/index.html=/tool/net/redbean.html"); + AddRedirect("/favicon.ico=/tool/net/redbean.ico"); +} + +static void GetOpts(int argc, char *argv[]) { + int opt; while ((opt = getopt(argc, argv, "zhduvml:p:w:r:c:L:P:U:G:B:")) != -1) { switch (opt) { case 'v': @@ -406,7 +529,7 @@ void GetOpts(int argc, char *argv[]) { AddRedirect(optarg); break; case 'c': - cacheseconds = atoi(optarg); + cacheseconds = strtol(optarg, NULL, 0); break; case 'p': CHECK_NE(0xFFFF, (serveraddr.sin_port = htons(parseport(optarg)))); @@ -435,7 +558,6 @@ void GetOpts(int argc, char *argv[]) { PrintUsage(stderr, EX_USAGE); } } - SortRedirects(); if (logpath) { CHECK_NOTNULL(freopen(logpath, "a", stderr)); } @@ -460,38 +582,53 @@ static void Daemonize(void) { freopen(logpath, "a", stderr); } -static int CompareHeaderValue(int h, const char *s) { - return strncmp(s, inbuf + req.headers[h].a, - req.headers[h].b - req.headers[h].a); -} - -static void KillAll(void) { - CHECK_NE(-1, kill(0, SIGTERM)); +static void OnWorkerExit(int pid, int ws) { + if (WIFEXITED(ws)) { + if (WEXITSTATUS(ws)) { + WARNF("worker %d exited with %d", pid, WEXITSTATUS(ws)); + } else { + DEBUGF("worker %d exited", pid); + } + } else { + WARNF("worker %d terminated with %s", pid, strsignal(WTERMSIG(ws))); + } } static void WaitAll(void) { + int ws, pid; for (;;) { - if (wait(NULL) != -1) continue; - if (errno == ECHILD) break; - if (errno == EINTR) { - if (killed) { - WARNF("%s server killed", serveraddrstr); - KillAll(); + if ((pid = wait(&ws)) != -1) { + OnWorkerExit(pid, ws); + } else { + if (errno == ECHILD) break; + if (errno == EINTR) { + if (killed) { + WARNF("%s terminating harder", serveraddrstr); + LOGIFNEG1(kill(0, SIGTERM)); + } + continue; } - continue; + FATALF("%s wait error %s", serveraddrstr, strerror(errno)); } - FATALF("%s wait error %s", serveraddrstr, strerror(errno)); } } -static size_t GetIovSize(struct iovec *iov, int iovlen) { - int i; - size_t size; - for (size = i = 0; i < iovlen; ++i) { - DCHECK_NOTNULL(iov[i].iov_base); - size += iov[i].iov_len; - } - return size; +static void ReapZombies(void) { + int ws, pid; + zombied = false; + do { + if ((pid = waitpid(-1, &ws, WNOHANG)) != -1) { + if (pid) { + OnWorkerExit(pid, ws); + } else { + break; + } + } else { + if (errno == ECHILD) break; + if (errno == EINTR) continue; + FATALF("%s wait error %s", serveraddrstr, strerror(errno)); + } + } while (!terminated); } static ssize_t WritevAll(int fd, struct iovec *iov, int iovlen) { @@ -531,7 +668,28 @@ static bool HasHeader(int h) { return req.headers[h].b > req.headers[h].a; } -int64_t GetGmtOffset(void) { +static int CompareHeaderValue(int h, const char *s) { + return strncmp(s, inbuf.p + req.headers[h].a, + req.headers[h].b - req.headers[h].a); +} + +static bool ClientAcceptsGzip(void) { + return httpversion >= 100 && + !!memmem(inbuf.p + req.headers[kHttpAcceptEncoding].a, + req.headers[kHttpAcceptEncoding].b - + req.headers[kHttpAcceptEncoding].a, + "gzip", 4); +} + +static void UpdateCurrentDate(long double now) { + int64_t t; + struct tm tm; + t = nowish = now; + gmtime_r(&t, &tm); + FormatHttpDateTime(currentdate, &tm); +} + +static int64_t GetGmtOffset(void) { int64_t t; struct tm tm; t = nowl(); @@ -543,8 +701,8 @@ static int64_t LocoTimeToZulu(int64_t x) { return x - gmtoff; } -static int64_t GetLastModifiedZip(uint8_t *cfile) { - uint8_t *p, *pe; +static int64_t GetLastModifiedZip(const uint8_t *cfile) { + const uint8_t *p, *pe; for (p = ZIP_CFILE_EXTRA(cfile), pe = p + ZIP_CFILE_EXTRASIZE(cfile); p < pe; p += ZIP_EXTRA_SIZE(p)) { if (ZIP_EXTRA_HEADERID(p) == kZipExtraNtfs) { @@ -560,17 +718,14 @@ static int64_t GetLastModifiedZip(uint8_t *cfile) { } static bool IsCompressed(struct Asset *a) { - return ZIP_CFILE_COMPRESSIONMETHOD(zbase + a->cf) == kZipCompressionDeflate; -} - -static int GetHttpVersion(void) { - return ParseHttpVersion(inbuf + req.version.a, req.version.b - req.version.a); + return ZIP_LFILE_COMPRESSIONMETHOD(zbase + a->lf) == kZipCompressionDeflate; } static bool IsNotModified(struct Asset *a) { + if (httpversion < 100) return false; if (!HasHeader(kHttpIfModifiedSince)) return false; return a->lastmodified >= - ParseHttpDateTime(inbuf + req.headers[kHttpIfModifiedSince].a, + ParseHttpDateTime(inbuf.p + req.headers[kHttpIfModifiedSince].a, req.headers[kHttpIfModifiedSince].b - req.headers[kHttpIfModifiedSince].a); } @@ -613,7 +768,7 @@ static bool IndexAssets(const uint8_t *base, const uint8_t *cdir) { } while (p[i].hash); lm = GetLastModifiedZip(base + cf); p[i].hash = hash; - p[i].cf = cf; + p[i].lf = ZIP_CFILE_OFFSET(base + cf); p[i].lastmodified = lm; p[i].lastmodifiedstr = FormatUnixHttpDateTime(xmalloc(30), lm); } else { @@ -666,28 +821,167 @@ static bool OpenZip(const char *path) { static struct Asset *FindAsset(const char *path, size_t pathlen) { uint32_t i, step, hash; - if (pathlen && path[0] == '/') ++path, --pathlen; hash = Hash(path, pathlen); for (step = 0;; ++step) { i = (hash + (step * (step + 1)) >> 1) & (assets.n - 1); if (!assets.p[i].hash) return NULL; if (hash == assets.p[i].hash && - pathlen == ZIP_CFILE_NAMESIZE(zbase + assets.p[i].cf) && - memcmp(path, ZIP_CFILE_NAME(zbase + assets.p[i].cf), pathlen) == 0) { + pathlen == ZIP_LFILE_NAMESIZE(zbase + assets.p[i].lf) && + memcmp(path, ZIP_LFILE_NAME(zbase + assets.p[i].lf), pathlen) == 0) { return &assets.p[i]; } } } -static struct Asset *FindFile(const char *path, size_t pathlen) { - char *p, *buf; - struct Asset *asset; - if ((asset = FindAsset(path, pathlen))) return asset; - if (pathlen == 12 && memcmp(path, "/favicon.ico", 12) == 0) { - return FindAsset(FAVICON, strlen(FAVICON)); - } else { - return NULL; +static struct Asset *LocateAsset(const char *path, size_t pathlen) { + char *path2; + struct Asset *a; + if (pathlen && path[0] == '/') ++path, --pathlen; + if (!(a = FindAsset(path, pathlen)) && + (!pathlen || (pathlen && path[pathlen - 1] == '/'))) { + path2 = xmalloc(pathlen + 10); + memcpy(path2, path, pathlen); + memcpy(path2 + pathlen, "index.lua", 9); + if (!(a = FindAsset(path2, pathlen + 9))) { + memcpy(path2 + pathlen, "index.html", 10); + a = FindAsset(path2, pathlen + 10); + } + free(path2); } + return a; +} + +static void *FreeLater(void *p) { + if (p) { + freelist.p = xrealloc(freelist.p, ++freelist.n * sizeof(*freelist.p)); + freelist.p[freelist.n - 1] = p; + } + return p; +} + +static void CollectGarbage(void) { + size_t i; + for (i = 0; i < freelist.n; ++i) { + free(freelist.p[i]); + } + freelist.n = 0; +} + +static void EmitUriParamKey(struct UriParser *u) { + uriparams.p = xrealloc(uriparams.p, ++uriparams.n * sizeof(*uriparams.p)); + uriparams.p[uriparams.n - 1].key.p = u->q; + uriparams.p[uriparams.n - 1].key.n = u->p - u->q; + u->q = u->p; +} + +static void EmitUriParamVal(struct UriParser *u, bool t) { + if (!t) { + if (u->p > u->q) { + EmitUriParamKey(u); + uriparams.p[uriparams.n - 1].val.p = NULL; + uriparams.p[uriparams.n - 1].val.n = SIZE_MAX; + } + } else { + uriparams.p[uriparams.n - 1].val.p = u->q; + uriparams.p[uriparams.n - 1].val.n = u->p - u->q; + u->q = u->p; + } +} + +static void ParseUriEscape(struct UriParser *u) { + int a, b; + a = u->i < u->size ? u->data[u->i++] : 0; + b = u->i < u->size ? u->data[u->i++] : 0; + *u->p++ = kHexToInt[a] << 4 | kHexToInt[b]; +} + +static void ParseUriPath(struct UriParser *u) { + if (u->c == '/') { + while (u->i < u->size) { + u->c = u->data[u->i++]; + if (u->c == '#' || u->c == '?') { + break; + } else if (u->c != '%') { + *u->p++ = u->c; + } else { + ParseUriEscape(u); + } + } + } + uripath.p = u->q; + uripath.n = u->p - u->q; + u->q = u->p; +} + +static void ParseUriParams(struct UriParser *u) { + bool t; + uriparams.n = 0; + uriparams.p = 0; + if (u->c == '?') { + t = false; + while (u->i < u->size) { + switch ((u->c = u->data[u->i++])) { + default: + *u->p++ = u->c; + break; + case '+': + *u->p++ = ' '; + break; + case '%': + ParseUriEscape(u); + break; + case '&': + EmitUriParamVal(u, t); + t = false; + break; + case '=': + if (!t) { + if (u->p > u->q) { + EmitUriParamKey(u); + t = true; + } + } else { + *u->p++ = '='; + } + break; + case '#': + goto EndOfParams; + } + } + EndOfParams: + EmitUriParamVal(u, t); + FreeLater(uriparams.p); + } +} + +static void ParseUriFragment(struct UriParser *u) { + if (u->c == '#') { + while (u->i < u->size) { + u->c = u->data[u->i++]; + if (u->c != '%') { + *u->p++ = u->c; + } else { + ParseUriEscape(u); + } + } + } + urifragment.p = u->q; + urifragment.n = u->p - u->q; + u->q = u->p; +} + +static bool ParseRequestUri(void) { + struct UriParser u; + u.i = 0; + u.c = '/'; + u.data = inbuf.p + req.uri.a; + u.size = req.uri.b - req.uri.a; + if (!u.size || *u.data != '/') return false; + u.q = u.p = FreeLater(xmalloc(u.size)); + ParseUriPath(&u); + ParseUriParams(&u); + ParseUriFragment(&u); + return u.i == u.size; } static void *AddRange(char *content, long start, long length) { @@ -705,114 +999,81 @@ static void *AddRange(char *content, long start, long length) { static bool IsConnectionClose(void) { int n; char *p; - p = inbuf + req.headers[kHttpConnection].a; + p = inbuf.p + req.headers[kHttpConnection].a; n = req.headers[kHttpConnection].b - req.headers[kHttpConnection].a; return n == 5 && memcmp(p, "close", 5) == 0; } static char *AppendCrlf(char *p) { - return STPCPY(p, "\r\n"); + p[0] = '\r'; + p[1] = '\n'; + return p + 2; } -#define AppendStatus(p, c, s) AppendStatus(p, c, s, sizeof(s) - 1) -static char *(AppendStatus)(char *p, int c, const char *s, size_t n) { - if (legacyhttp) { - p = STPCPY(p, "HTTP/1.0 "); - } else { - p = STPCPY(p, "HTTP/1.1 "); - } - p += uint64toarray_radix10(c, p); +static bool MustNotIncludeMessageBody(void) { /* RFC2616 § 4.4 */ + return req.method == kHttpHead || (100 <= statuscode && statuscode <= 199) || + statuscode == 204 || statuscode == 304; +} + +static char *SetStatus(int code, const char *reason) { + char *p; + statuscode = code; + p = hdrbuf.p; + p = stpcpy(p, "HTTP/1."); + *p++ = httpversion == 100 ? '0' : '1'; *p++ = ' '; - p = mempcpy(p, s, n); + p += uint64toarray_radix10(code, p); + *p++ = ' '; + p = stpcpy(p, reason); return AppendCrlf(p); } -static void UpdateCurrentDate(long double now) { - int64_t t; - struct tm tm; - t = nowish = now; - gmtime_r(&t, &tm); - FormatHttpDateTime(currentdate, &tm); -} - -static char *AppendDate(char *p) { - p = AppendHeaderName(p, "Date"); - p = mempcpy(p, currentdate, 29); - return AppendCrlf(p); -} - -static char *AppendLastModified(char *p, const char *s) { - p = AppendHeaderName(p, "Last-Modified"); - p = mempcpy(p, s, 29); - return AppendCrlf(p); -} - -static char *AppendServer(char *p) { - const char *s; - if (*(s = firstnonnull(serverheader, DEFAULT_SERVER))) { - p = AppendHeaderName(p, "Server"); - p = stpcpy(p, s); - p = AppendCrlf(p); - } - return p; -} - -static char *AppendConnectionClose(char *p) { - p = AppendHeaderName(p, "Connection"); - p = STPCPY(p, "close"); - return AppendCrlf(p); -} - -static char *AppendAcceptRangesBytes(char *p) { - p = AppendHeaderName(p, "Accept-Ranges"); - p = STPCPY(p, "bytes"); - return AppendCrlf(p); -} - -static char *AppendNosniff(char *p) { - p = AppendHeaderName(p, "X-Content-Type-Options"); - p = STPCPY(p, "nosniff"); - return AppendCrlf(p); +static char *AppendHeader(char *p, const char *k, const char *v) { + return AppendCrlf(stpcpy(AppendHeaderName(p, k), v)); } static char *AppendContentType(char *p, const char *ct) { p = AppendHeaderName(p, "Content-Type"); p = stpcpy(p, ct); - if (startswith(ct, "text/")) { - p = STPCPY(p, "; charset=utf-8"); + if (startswith(ct, "text/") && !strchr(ct, ';')) { + p = stpcpy(p, "; charset=utf-8"); + istext = true; } return AppendCrlf(p); } -static char *AppendContentTypeTextPlain(char *p) { - return AppendContentType(p, "text/plain"); +static char *ServeError(int code, const char *reason) { + char *p; + size_t reasonlen; + reasonlen = strlen(reason); + p = SetStatus(code, reason); + p = AppendContentType(p, "text/plain"); + content = FreeLater(xmalloc(reasonlen + 3)); + contentlength = reasonlen + 2; + stpcpy(stpcpy(content, reason), "\r\n"); + WARNF("%s %s %`'.*s %d %s", clientaddrstr, kHttpMethod[req.method], uripath.n, + uripath.p, code, reason); + return p; } static char *AppendExpires(char *p, int64_t t) { struct tm tm; gmtime_r(&t, &tm); p = AppendHeaderName(p, "Expires"); - p = FormatHttpDateTime(p, &tm); + FormatHttpDateTime(p, &tm); + p += 29; return AppendCrlf(p); } -static char *AppendVaryContentEncoding(char *p) { - p = AppendHeaderName(p, "Vary"); - p = STPCPY(p, "Accept-Encoding"); - return AppendCrlf(p); -} - -static char *AppendCache(char *p) { - int x; - x = cacheseconds; - if (!x) return p; - x = MAX(0, x); +static char *AppendCache(char *p, int64_t seconds) { + struct tm tm; + if (seconds < 0) return p; p = AppendHeaderName(p, "Cache-Control"); - p = STPCPY(p, "max-age="); - p += uint64toarray_radix10(x, p); - if (x) p = STPCPY(p, ", public"); + p = stpcpy(p, "max-age="); + p += uint64toarray_radix10(seconds, p); + if (seconds) p = stpcpy(p, ", public"); p = AppendCrlf(p); - return AppendExpires(p, nowish + cacheseconds); + return AppendExpires(p, (int64_t)nowish + seconds); } static char *AppendContentLength(char *p, size_t n) { @@ -828,7 +1089,7 @@ static char *AppendContentRange(char *p, long rangestart, long rangelength, CHECK_LE(rangestart + rangelength, contentlength); if (__builtin_add_overflow(rangestart, rangelength, &endrange)) abort(); p = AppendHeaderName(p, "Content-Range"); - p = STPCPY(p, "bytes "); + p = stpcpy(p, "bytes "); p += uint64toarray_radix10(rangestart, p); *p++ = '-'; p += uint64toarray_radix10(endrange, p); @@ -837,38 +1098,16 @@ static char *AppendContentRange(char *p, long rangestart, long rangelength, return AppendCrlf(p); } -static char *AppendContentEncodingGzip(char *p) { - p = AppendHeaderName(p, "Content-Encoding"); - p = STPCPY(p, "gzip"); - return AppendCrlf(p); -} - -static char *AppendRedirect(char *p, const char *s) { - VERBOSEF("%s %s %.*s redirect %s", clientaddrstr, kHttpMethod[req.method], - req.uri.b - req.uri.a, inbuf + req.uri.a, s); - p = AppendStatus(p, 307, "Temporary Redirect"); - p = AppendHeaderName(p, "Location"); - p = STPCPY(p, s); - return AppendCrlf(p); -} - -static bool InflateTiny(uint8_t *outbuf, size_t outsize, const uint8_t *inbuf, - size_t insize) { - struct DeflateState ds; - return undeflate(outbuf, outsize, inbuf, insize, &ds) != -1; -} - -static bool InflateZlib(uint8_t *outbuf, size_t outsize, const uint8_t *inbuf, - size_t insize) { +static bool Inflate(uint8_t *dp, size_t dn, const uint8_t *sp, size_t sn) { bool ok; z_stream zs; ok = false; - zs.next_in = inbuf; - zs.avail_in = insize; - zs.total_in = insize; - zs.next_out = outbuf; - zs.avail_out = outsize; - zs.total_out = outsize; + zs.next_in = sp; + zs.avail_in = sn; + zs.total_in = sn; + zs.next_out = dp; + zs.avail_out = dn; + zs.total_out = dn; zs.zfree = Z_NULL; zs.zalloc = Z_NULL; if (inflateInit2(&zs, -MAX_WBITS) == Z_OK) { @@ -893,299 +1132,823 @@ static bool InflateZlib(uint8_t *outbuf, size_t outsize, const uint8_t *inbuf, return ok; } -static bool Inflate(void *outbuf, size_t outsize, const uint8_t *inbuf, - size_t insize) { - if (IsTiny()) { - return InflateTiny(outbuf, outsize, inbuf, insize); - } else { - return InflateZlib(outbuf, outsize, inbuf, insize); - } +static void *Deflate(const void *data, size_t size, size_t *out_size) { + void *res; + z_stream zs; + CHECK_EQ(Z_OK, deflateInit2(memset(&zs, 0, sizeof(zs)), 4, Z_DEFLATED, + -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY)); + zs.next_in = data; + zs.avail_in = size; + zs.avail_out = compressBound(size); + zs.next_out = res = xmalloc(zs.avail_out); + CHECK_EQ(Z_STREAM_END, deflate(&zs, Z_FINISH)); + CHECK_EQ(Z_OK, deflateEnd(&zs)); + *out_size = zs.total_out; + return xrealloc(res, zs.total_out); } -static void LogRequestLatency(void) { - long double now = nowl(); - LOGF("%s latency req %,16ldns conn %,16ldns", clientaddrstr, - (long)((now - startrequest) * 1e9), - (long)((now - startconnection) * 1e9)); -} - -static nodiscard char *LoadAssetAsString(struct Asset *a) { - char *code; - code = xmalloc(ZIP_CFILE_UNCOMPRESSEDSIZE(zbase + a->cf) + 1); - if (ZIP_CFILE_COMPRESSIONMETHOD(zbase + a->cf) == kZipCompressionDeflate) { - CHECK(Inflate(code, ZIP_CFILE_UNCOMPRESSEDSIZE(zbase + a->cf), - ZIP_LFILE_CONTENT(zbase + ZIP_CFILE_OFFSET(zbase + a->cf)), - ZIP_CFILE_COMPRESSEDSIZE(zbase + a->cf))); - } else { - memcpy(code, ZIP_LFILE_CONTENT(zbase + ZIP_CFILE_OFFSET(zbase + a->cf)), - ZIP_CFILE_UNCOMPRESSEDSIZE(zbase + a->cf)); - } - code[ZIP_CFILE_UNCOMPRESSEDSIZE(zbase + a->cf)] = '\0'; - return code; -} - -static int LuaDate(lua_State *L) { - lua_pushstring(L, currentdate); - return 1; -} - -static int LuaSend(lua_State *L) { +static void *LoadAsset(struct Asset *a, size_t *out_size) { size_t size; - const char *data; - data = luaL_checklstring(L, 1, &size); - WritevAll(client, &(struct iovec){data, size}, 1); + uint8_t *data; + size = ZIP_LFILE_UNCOMPRESSEDSIZE(zbase + a->lf); + data = xmalloc(size + 1); + if (ZIP_LFILE_COMPRESSIONMETHOD(zbase + a->lf) == kZipCompressionDeflate) { + CHECK(Inflate(data, size, ZIP_LFILE_CONTENT(zbase + a->lf), + ZIP_LFILE_COMPRESSEDSIZE(zbase + a->lf))); + } else { + memcpy(data, ZIP_LFILE_CONTENT(zbase + a->lf), size); + } + data[size] = '\0'; + if (out_size) *out_size = size; + return data; +} + +static ssize_t Send(struct iovec *iov, int iovlen) { + ssize_t rc; + if ((rc = WritevAll(client, iov, iovlen)) == -1) { + if (errno == ECONNRESET) { + DEBUGF("%s send reset", clientaddrstr); + } else { + WARNF("%s send error %s", clientaddrstr, strerror(errno)); + } + connectionclose = true; + } + return rc; +} + +static char *ServeAsset(struct Asset *a, const char *path, size_t pathlen) { + char *p; + long rangestart, rangelength; + if (IsNotModified(a)) { + DEBUGF("%s %s %`'.*s not modified", clientaddrstr, kHttpMethod[req.method], + pathlen, path); + p = SetStatus(304, "Not Modified"); + } else { + content = ZIP_LFILE_CONTENT(zbase + a->lf); + contentlength = ZIP_LFILE_COMPRESSEDSIZE(zbase + a->lf); + if (IsCompressed(a)) { + if (ClientAcceptsGzip()) { + gzipped = true; + memcpy(gzip_footer + 0, zbase + a->lf + kZipLfileOffsetCrc32, 4); + memcpy(gzip_footer + 4, zbase + a->lf + kZipLfileOffsetUncompressedsize, + 4); + p = SetStatus(200, "OK"); + p = AppendHeader(p, "Content-Encoding", "gzip"); + } else { + CHECK(Inflate( + (content = + FreeLater(xmalloc(ZIP_LFILE_UNCOMPRESSEDSIZE(zbase + a->lf)))), + (contentlength = ZIP_LFILE_UNCOMPRESSEDSIZE(zbase + a->lf)), + ZIP_LFILE_CONTENT(zbase + a->lf), + ZIP_LFILE_COMPRESSEDSIZE(zbase + a->lf))); + p = SetStatus(200, "OK"); + } + } else if (httpversion >= 101 && HasHeader(kHttpRange)) { + if (ParseHttpRange(inbuf.p + req.headers[kHttpRange].a, + req.headers[kHttpRange].b - req.headers[kHttpRange].a, + contentlength, &rangestart, &rangelength)) { + p = SetStatus(206, "Partial Content"); + p = AppendContentRange(p, rangestart, rangelength, contentlength); + content = AddRange(content, rangestart, rangelength); + contentlength = rangelength; + } else { + WARNF("%s %s %`'.*s bad range %`'.*s", clientaddrstr, + kHttpMethod[req.method], pathlen, path, + req.headers[kHttpRange].b - req.headers[kHttpRange].a, + inbuf.p + req.headers[kHttpRange].a); + p = SetStatus(416, "Range Not Satisfiable"); + p = AppendContentRange(p, rangestart, rangelength, contentlength); + content = ""; + contentlength = 0; + } + } else { + p = SetStatus(200, "OK"); + } + } + if (httpversion >= 100) { + p = AppendHeader(p, "Last-Modified", a->lastmodifiedstr); + p = AppendContentType(p, GetContentType(path, pathlen)); + if (httpversion >= 101) { + p = AppendCache(p, cacheseconds); + if (!IsCompressed(a)) { + p = AppendHeader(p, "Accept-Ranges", "bytes"); + } else { + p = AppendHeader(p, "Vary", "Accept-Encoding"); + } + } + } + return p; +} + +static bool IsValidFieldName(const char *s, size_t n) { + size_t i; + if (!n) return false; + for (i = 0; i < n; ++i) { + if (!(0x20 < s[i] && s[i] < 0x7F) || s[i] == ':') { + return false; + } + } + return true; +} + +static bool IsValidFieldContent(const char *s, size_t n) { + size_t i; + for (i = 0; i < n; ++i) { + if (!(0x20 <= s[i] && s[i] < 0x7F)) { + return false; + } + } + return true; +} + +static int LuaServeAsset(lua_State *L) { + size_t pathlen; + struct Asset *a; + const char *path; + path = luaL_checklstring(L, 1, &pathlen); + if (!(a = LocateAsset(path, pathlen))) { + return luaL_argerror(L, 1, "not found"); + } + luaheaderp = ServeAsset(a, path, pathlen); return 0; } +static int LuaServeError(lua_State *L) { + int code; + size_t reasonlen; + const char *reason; + code = luaL_checkinteger(L, 1); + if (!(100 <= code && code <= 999)) { + return luaL_argerror(L, 1, "bad status code"); + } + if (lua_isnoneornil(L, 2)) { + reason = GetHttpReason(code); + } else { + reason = lua_tolstring(L, 2, &reasonlen); + if (reasonlen > 128 || !IsValidFieldContent(reason, reasonlen)) { + return luaL_argerror(L, 2, "invalid"); + } + } + luaheaderp = ServeError(code, reason); + return 0; +} + +static int LuaLoadAsset(lua_State *L) { + char *data; + struct Asset *a; + const char *path; + size_t size, pathlen; + path = luaL_checklstring(L, 1, &pathlen); + if (!(a = LocateAsset(path, pathlen))) { + return luaL_argerror(L, 1, "not found"); + } + data = LoadAsset(a, &size); + lua_pushlstring(L, data, size); + free(data); + return 1; +} + +static int LuaSetStatus(lua_State *L) { + int code; + size_t reasonlen; + const char *reason; + code = luaL_checkinteger(L, 1); + if (!(100 <= code && code <= 999)) { + return luaL_argerror(L, 1, "bad status code"); + } + if (lua_isnoneornil(L, 2)) { + reason = GetHttpReason(code); + } else { + reason = lua_tolstring(L, 2, &reasonlen); + if (reasonlen > 128 || !IsValidFieldContent(reason, reasonlen)) { + return luaL_argerror(L, 2, "invalid"); + } + } + luaheaderp = SetStatus(code, reason); + return 0; +} + +static int LuaGetDate(lua_State *L) { + lua_pushinteger(L, nowish); + return 1; +} + +static int LuaGetVersion(lua_State *L) { + lua_pushinteger(L, httpversion); + return 1; +} + +static int LuaGetMethod(lua_State *L) { + checkedmethod = true; + lua_pushstring(L, kHttpMethod[req.method]); + return 1; +} + +static int LuaGetPath(lua_State *L) { + lua_pushlstring(L, uripath.p, uripath.n); + return 1; +} + +static int LuaGetFragment(lua_State *L) { + lua_pushlstring(L, urifragment.p, urifragment.n); + return 1; +} + +static int LuaGetUri(lua_State *L) { + lua_pushlstring(L, inbuf.p + req.uri.a, req.uri.b - req.uri.a); + return 1; +} + +static int LuaFormatDate(lua_State *L) { + int64_t t; + char buf[30]; + struct tm tm; + t = luaL_checkinteger(L, 1); + gmtime_r(&t, &tm); + lua_pushstring(L, FormatHttpDateTime(buf, &tm)); + return 1; +} + +static int LuaParseDate(lua_State *L) { + size_t n; + const char *s; + s = luaL_checklstring(L, 1, &n); + lua_pushinteger(L, ParseHttpDateTime(s, n)); + return 1; +} + +static int LuaGetClientAddr(lua_State *L) { + lua_pushstring(L, clientaddrstr); + return 1; +} + +static int LuaGetServerAddr(lua_State *L) { + lua_pushstring(L, serveraddrstr); + return 1; +} + +static int LuaGetHeader(lua_State *L) { + int h; + size_t keylen, vallen; + const char *key, *val; + key = luaL_checklstring(L, 1, &keylen); + if ((h = GetHttpHeader(key, keylen)) != -1) { + val = inbuf.p + req.headers[h].a; + vallen = req.headers[h].b - req.headers[h].a; + if (vallen && IsValidFieldContent(val, vallen)) { + lua_pushlstring(L, val, vallen); + } else { + lua_pushnil(L); + } + return 1; + } else { + return luaL_argerror(L, 1, "unsupported"); + } +} + +static int LuaSetHeader(lua_State *L) { + char *p; + ssize_t rc; + const char *key, *val; + size_t i, keylen, vallen; + key = luaL_checklstring(L, 1, &keylen); + val = luaL_checklstring(L, 2, &vallen); + if (!IsValidFieldName(key, keylen)) { + return luaL_argerror(L, 1, "invalid"); + } + if (!IsValidFieldContent(val, vallen)) { + return luaL_argerror(L, 2, "invalid"); + } + if (!luaheaderp) { + p = SetStatus(200, "OK"); + } else { + while (luaheaderp - hdrbuf.p + keylen + 2 + vallen + 2 + 512 > hdrbuf.n) { + hdrbuf.n += hdrbuf.n >> 1; + p = xrealloc(hdrbuf.p, hdrbuf.n); + luaheaderp = p + (luaheaderp - hdrbuf.p); + hdrbuf.p = p; + } + p = luaheaderp; + } + switch (GetHttpHeader(key, keylen)) { + case kHttpDate: + case kHttpContentRange: + case kHttpContentLength: + case kHttpContentEncoding: + return luaL_argerror(L, 1, "abstracted"); + case kHttpConnection: + if (vallen != 5 || memcmp(val, "close", 5)) { + return luaL_argerror(L, 2, "unsupported"); + } + connectionclose = true; + break; + case kHttpContentType: + p = AppendContentType(p, val); + break; + case kHttpServer: + branded = true; + p = AppendHeader(p, key, val); + break; + default: + p = AppendHeader(p, key, val); + break; + } + luaheaderp = p; + return 0; +} + +static int LuaHasParam(lua_State *L) { + const char *key; + size_t i, keylen; + key = luaL_checklstring(L, 1, &keylen); + for (i = 0; i < uriparams.n; ++i) { + if (uriparams.p[i].key.n == keylen && + !memcmp(uriparams.p[i].key.p, key, keylen)) { + lua_pushboolean(L, true); + return 1; + } + } + lua_pushboolean(L, false); + return 1; +} + +static int LuaGetParam(lua_State *L) { + const char *key; + size_t i, keylen; + key = luaL_checklstring(L, 1, &keylen); + for (i = 0; i < uriparams.n; ++i) { + if (uriparams.p[i].key.n == keylen && + !memcmp(uriparams.p[i].key.p, key, keylen)) { + if (uriparams.p[i].val.n == SIZE_MAX) break; + lua_pushlstring(L, uriparams.p[i].val.p, uriparams.p[i].val.n); + return 1; + } + } + lua_pushnil(L); + return 1; +} + +static int LuaGetParams(lua_State *L) { + size_t i; + lua_newtable(L); + for (i = 0; i < uriparams.n; ++i) { + lua_newtable(L); + lua_pushlstring(L, uriparams.p[i].key.p, uriparams.p[i].key.n); + lua_seti(L, -2, 1); + if (uriparams.p[i].val.n != SIZE_MAX) { + lua_pushlstring(L, uriparams.p[i].val.p, uriparams.p[i].val.n); + lua_seti(L, -2, 2); + } + lua_seti(L, -2, i + 1); + } + return 1; +} + +static int LuaWrite(lua_State *L) { + int h; + size_t size; + const char *data; + data = luaL_checklstring(L, 1, &size); + outbuf.p = xrealloc(outbuf.p, outbuf.n + size); + memcpy(outbuf.p + outbuf.n, data, size); + outbuf.n += size; + return 0; +} + +static int LuaEscaper(lua_State *L, + struct EscapeResult escape(const char *, size_t)) { + size_t size; + const char *data; + struct EscapeResult r; + data = luaL_checklstring(L, 1, &size); + r = escape(data, size); + lua_pushlstring(L, r.data, r.size); + free(r.data); + return 1; +} + +static int LuaEscapeHtml(lua_State *L) { + return LuaEscaper(L, EscapeHtml); +} + +static int LuaEscapeParam(lua_State *L) { + return LuaEscaper(L, EscapeUrlParam); +} + +static int LuaEscapePath(lua_State *L) { + return LuaEscaper(L, EscapeUrlPath); +} + +static int LuaEscapeSegment(lua_State *L) { + return LuaEscaper(L, EscapeUrlPathSegment); +} + +static int LuaEscapeFragment(lua_State *L) { + return LuaEscaper(L, EscapeUrlFragment); +} + +static int LuaEscapeLiteral(lua_State *L) { + return LuaEscaper(L, EscapeJsStringLiteral); +} + +static const luaL_Reg kLuaLibs[] = { + {"_G", luaopen_base}, // + {"table", luaopen_table}, // + {"string", luaopen_string}, // + {"math", luaopen_math}, // + {"utf8", luaopen_utf8}, // +}; + static const luaL_Reg kLuaFuncs[] = { - {"date", LuaDate}, - {"send", LuaSend}, + {"EscapeFragment", LuaEscapeFragment}, // + {"EscapeHtml", LuaEscapeHtml}, // + {"EscapeLiteral", LuaEscapeLiteral}, // + {"EscapeParam", LuaEscapeParam}, // + {"EscapePath", LuaEscapePath}, // + {"EscapeSegment", LuaEscapeSegment}, // + {"FormatDate", LuaFormatDate}, // + {"GetClientAddr", LuaGetClientAddr}, // + {"GetDate", LuaGetDate}, // + {"GetFragment", LuaGetFragment}, // + {"GetHeader", LuaGetHeader}, // + {"GetMethod", LuaGetMethod}, // + {"GetParam", LuaGetParam}, // + {"GetParams", LuaGetParams}, // + {"GetPath", LuaGetPath}, // + {"GetServerAddr", LuaGetServerAddr}, // + {"GetUri", LuaGetUri}, // + {"GetVersion", LuaGetVersion}, // + {"HasParam", LuaHasParam}, // + {"LoadAsset", LuaLoadAsset}, // + {"ParseDate", LuaParseDate}, // + {"ServeAsset", LuaServeAsset}, // + {"ServeError", LuaServeError}, // + {"SetHeader", LuaSetHeader}, // + {"SetStatus", LuaSetStatus}, // + {"Write", LuaWrite}, // }; static void LuaInit(void) { size_t i; L = luaL_newstate(); + 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); } } -static void LuaRun(struct Asset *a) { - if (luaL_dostring(L, gc(LoadAssetAsString(a))) != LUA_OK) { +static char *ServeLua(struct Asset *a) { + char *p; + outbuf.n = 0; + luaheaderp = NULL; + checkedmethod = false; + if (luaL_dostring(L, FreeLater(LoadAsset(a, NULL))) == LUA_OK) { + if (!checkedmethod && req.method != kHttpGet && req.method != kHttpHead) { + return ServeError(405, "Method Not Allowed"); + } + if (!(p = luaheaderp)) { + p = SetStatus(200, "OK"); + p = AppendContentType(p, "text/html"); + } + if (istext && outbuf.n >= 100 && ClientAcceptsGzip()) { + gzipped = true; + p = AppendHeader(p, "Content-Encoding", "gzip"); + p = AppendHeader(p, "Vary", "Accept-Encoding"); + WRITE32LE(gzip_footer + 0, crc32_z(0, outbuf.p, outbuf.n)); + WRITE32LE(gzip_footer + 4, outbuf.n); + content = FreeLater(Deflate(outbuf.p, outbuf.n, &contentlength)); + } else { + content = outbuf.p; + contentlength = outbuf.n; + } + return p; + } else { WARNF("%s %s", clientaddrstr, lua_tostring(L, -1)); lua_pop(L, 1); /* remove message */ - terminated = true; + connectionclose = true; + return ServeError(500, "Internal Server Error"); } } -void HandleRequest(size_t got) { +static bool IsLua(struct Asset *a) { + return ZIP_LFILE_NAMESIZE(zbase + a->lf) >= 4 && + !memcmp(ZIP_LFILE_NAME(zbase + a->lf) + + ZIP_LFILE_NAMESIZE(zbase + a->lf) - 4, + ".lua", 4); +} + +static char *HandleAsset(struct Asset *a, const char *path, size_t pathlen) { + char *p; + if (IsLua(a)) { + p = ServeLua(a); + } else if (req.method == kHttpGet || req.method == kHttpHead) { + p = ServeAsset(a, path, pathlen); + p = AppendHeader(p, "X-Content-Type-Options", "nosniff"); + } else { + p = ServeError(405, "Method Not Allowed"); + } + return p; +} + +static char *HandleRedirect(struct Redirect *r) { char *p; - int iovlen; - bool gzipped; - void *content; - size_t pathlen; struct Asset *a; - unsigned version; - struct iovec iov[4]; - uint8_t gzip_footer[8]; - const char *path, *location; - long lf, contentlength, actualcontentlength, rangestart, rangelength; - p = outbuf; - content = ""; - gzipped = false; - contentlength = -1; - if (ParseHttpRequest(&req, inbuf, got) != -1) { - if (logmessages) { - LOGF("%s received %,d byte message\n%.*s", clientaddrstr, req.length, - req.length - 4, inbuf); + if ((a = LocateAsset(r->location, strlen(r->location)))) { + DEBUGF("%s %s %`'.*s rewritten %`'s", clientaddrstr, + kHttpMethod[req.method], uripath.n, uripath.p, r->location); + p = HandleAsset(a, r->location, strlen(r->location)); + } else if (httpversion == 9) { + p = ServeError(505, "HTTP Version Not Supported"); + } else { + DEBUGF("%s %s %`'.*s redirecting %`'s", clientaddrstr, + kHttpMethod[req.method], uripath.n, uripath.p, r->location); + p = SetStatus(307, "Temporary Redirect"); + p = AppendHeader(p, "Location", r->location); + } + return p; +} + +static const char *DescribeClose(void) { + if (killed) return "killed"; + if (meltdown) return "meltdown"; + if (terminated) return "terminated"; + if (connectionclose) return "connectionclose"; + return "destroyed"; +} + +static void LogClose(const char *reason) { + if (amtread) { + WARNF("%s %s with %,ld bytes unprocessed", clientaddrstr, reason, amtread); + } else { + DEBUGF("%s %s", clientaddrstr, reason); + } +} + +static char *HandleMessage(size_t hdrlen) { + long r; + ssize_t cl, rc; + struct Asset *a; + size_t got, need; + httpversion = + ParseHttpVersion(inbuf.p + req.version.a, req.version.b - req.version.a); + if (httpversion > 101) { + return ServeError(505, "HTTP Version Not Supported"); + } + if (req.method > kHttpPost || + (HasHeader(kHttpTransferEncoding) && + CompareHeaderValue(kHttpTransferEncoding, "identity"))) { + return ServeError(501, "Not Implemented"); + } + if ((cl = ParseContentLength(inbuf.p + req.headers[kHttpContentLength].a, + req.headers[kHttpContentLength].b - + req.headers[kHttpContentLength].a)) == -1) { + if (HasHeader(kHttpContentLength)) { + return ServeError(400, "Bad Request"); + } else if (req.method != kHttpGet && req.method != kHttpHead && + req.method != kHttpOptions) { + return ServeError(411, "Length Required"); + } else { + cl = 0; } - version = GetHttpVersion(); - if (version < 101) terminated = true, legacyhttp = true; - if (version <= 101) { - if (IsConnectionClose()) terminated = true; - path = inbuf + req.uri.a; - pathlen = req.uri.b - req.uri.a; - if (req.method == kHttpGet || req.method == kHttpHead) { - VERBOSEF("%s %s %.*s referer %.*s", clientaddrstr, - kHttpMethod[req.method], pathlen, path, - req.headers[kHttpReferer].b - req.headers[kHttpReferer].a, - inbuf + req.headers[kHttpReferer].a); - if ((location = LookupRedirect(path, pathlen))) { - p = AppendRedirect(p, location); - } else if ((a = FindFile(path, pathlen))) { - if (pathlen >= 4 && !memcmp(path + pathlen - 4, ".lua", 4)) { - LuaRun(a); - return; - } else { - if (IsNotModified(a)) { - VERBOSEF("%s %s %.*s not modified", clientaddrstr, - kHttpMethod[req.method], pathlen, path); - p = AppendStatus(p, 304, "Not Modified"); - } else { - lf = ZIP_CFILE_OFFSET(zbase + a->cf); - content = ZIP_LFILE_CONTENT(zbase + lf); - contentlength = ZIP_CFILE_COMPRESSEDSIZE(zbase + a->cf); - if (IsCompressed(a)) { - if (memmem(inbuf + req.headers[kHttpAcceptEncoding].a, - req.headers[kHttpAcceptEncoding].b - - req.headers[kHttpAcceptEncoding].a, - "gzip", 4)) { - gzipped = true; - memcpy(gzip_footer + 0, zbase + a->cf + kZipCfileOffsetCrc32, - 4); - memcpy(gzip_footer + 4, - zbase + a->cf + kZipCfileOffsetUncompressedsize, 4); - p = AppendStatus(p, 200, "OK"); - p = AppendContentEncodingGzip(p); - } else if (Inflate( - (content = gc(xmalloc(ZIP_CFILE_UNCOMPRESSEDSIZE( - zbase + a->cf)))), - (contentlength = - ZIP_CFILE_UNCOMPRESSEDSIZE(zbase + a->cf)), - ZIP_LFILE_CONTENT(zbase + lf), - ZIP_CFILE_COMPRESSEDSIZE(zbase + a->cf))) { - p = AppendStatus(p, 200, "OK"); - } else { - WARNF("%s %s %.*s internal server error", clientaddrstr, - kHttpMethod[req.method], pathlen, path); - p = AppendStatus(p, 500, "Internal Server Error"); - content = "Internal Server Error\r\n"; - contentlength = -1; - } - } else if (HasHeader(kHttpRange)) { - if (ParseHttpRange( - inbuf + req.headers[kHttpRange].a, - req.headers[kHttpRange].b - req.headers[kHttpRange].a, - contentlength, &rangestart, &rangelength)) { - p = AppendStatus(p, 206, "Partial Content"); - p = AppendContentRange(p, rangestart, rangelength, - contentlength); - content = AddRange(content, rangestart, rangelength); - contentlength = rangelength; - } else { - WARNF("%s %s %.*s bad range %`'.*s", clientaddrstr, - kHttpMethod[req.method], pathlen, path, - req.headers[kHttpRange].b - req.headers[kHttpRange].a, - inbuf + req.headers[kHttpRange].a); - p = AppendStatus(p, 416, "Range Not Satisfiable"); - p = AppendContentRange(p, rangestart, rangelength, - contentlength); - content = ""; - contentlength = 0; - } - } else { - p = AppendStatus(p, 200, "OK"); - } - } - p = AppendLastModified(p, a->lastmodifiedstr); - p = AppendContentType(p, GetContentType(path, pathlen)); - p = AppendCache(p); - if (!IsCompressed(a)) { - p = AppendAcceptRangesBytes(p); - } else { - p = AppendVaryContentEncoding(p); - } - } - } else if (!strncmp(path, "/", pathlen) || - !strncmp(path, "/index.html", pathlen)) { - p = AppendRedirect(p, DEFAULT_PATH); - } else { - WARNF("%s %s %.*s not found", clientaddrstr, kHttpMethod[req.method], - pathlen, path); - p = AppendStatus(p, 404, "Not Found"); - p = AppendContentTypeTextPlain(p); - content = "Not Found\r\n"; - } - } else { - WARNF("%s %s %.*s method not allowed", clientaddrstr, - kHttpMethod[req.method], pathlen, path); - p = AppendStatus(p, 405, "method not allowed"); - p = AppendContentTypeTextPlain(p); - content = "Method Not Allowed\r\n"; + } + need = hdrlen + cl; /* synchronization is possible */ + if (need > inbuf.n) { + return ServeError(413, "Payload Too Large"); + } + while (amtread < need) { + if (++frags == 64) { + LogClose("payload fragged"); + return ServeError(408, "Request Timeout"); + } + if ((rc = read(client, inbuf.p + amtread, inbuf.n - amtread)) != -1) { + if (!(got = rc)) { + LogClose("payload disconnect"); + return ServeError(400, "Bad Request"); + } + amtread += got; + } else if (errno == ECONNRESET) { + LogClose("payload reset"); + return ServeError(400, "Bad Request"); + } else if (errno == EINTR) { + if (killed || ((meltdown || terminated) && nowl() - startread > 1)) { + LogClose(DescribeClose()); + return ServeError(503, "Service Unavailable"); } } else { - WARNF("%s http version not supported %`'.*s", clientaddrstr, - req.version.b - req.version.a, inbuf + req.version.a); - p = AppendStatus(p, 505, "HTTP Version Not Supported"); - p = AppendContentTypeTextPlain(p); - content = "HTTP Version Not Supported\r\n"; - terminated = true; + WARNF("%s payload recv %s", clientaddrstr, strerror(errno)); + return ServeError(500, "Internal Server Error"); } + } + msgsize = need; /* we are now synchronized */ + if (httpversion != 101 || IsConnectionClose()) { + connectionclose = true; + } + if (!ParseRequestUri()) { + WARNF("%s could not parse request uri %`'.*s", clientaddrstr, + req.uri.b - req.uri.a, inbuf.p + req.uri.a); + connectionclose = true; + return ServeError(400, "Bad Request"); + } + VERBOSEF("%s %s %`'.*s referrer %`'.*s", clientaddrstr, + kHttpMethod[req.method], uripath.n, uripath.p, + req.headers[kHttpReferer].b - req.headers[kHttpReferer].a, + inbuf.p + req.headers[kHttpReferer].a); + if ((a = LocateAsset(uripath.p, uripath.n))) { + return HandleAsset(a, uripath.p, uripath.n); + } else if ((r = FindRedirect(uripath.p, uripath.n)) != -1) { + return HandleRedirect(redirects.p + r); } else { - WARNF("%s parse error %s", clientaddrstr, strerror(errno)); - p = AppendStatus(p, 400, "Bad Request"); - p = AppendContentTypeTextPlain(p); - content = "Bad Request\r\n"; - terminated = true; - } - if (terminated) LOGIFNEG1(shutdown(client, SHUT_RD)); - p = AppendDate(p); - p = AppendNosniff(p); - p = AppendServer(p); - if (terminated) p = AppendConnectionClose(p); - if (contentlength == -1) contentlength = strlen(content); - actualcontentlength = contentlength; - if (gzipped) actualcontentlength += sizeof(kGzipHeader) + sizeof(gzip_footer); - p = AppendContentLength(p, actualcontentlength); - p = AppendCrlf(p); - if (logmessages) { - LOGF("%s sending %,d byte message\n%.*s", clientaddrstr, p - outbuf, - p - outbuf - 4, outbuf); - } - CHECK_LT(p, outbuf + sizeof(outbuf)); - iovlen = 0; - iov[iovlen].iov_base = outbuf; - iov[iovlen].iov_len = p - outbuf; - ++iovlen; - if (req.method != kHttpHead) { - if (gzipped) { - iov[iovlen].iov_base = kGzipHeader; - iov[iovlen].iov_len = sizeof(kGzipHeader); - ++iovlen; - } - iov[iovlen].iov_base = content; - iov[iovlen].iov_len = contentlength; - ++iovlen; - if (gzipped) { - iov[iovlen].iov_base = gzip_footer; - iov[iovlen].iov_len = sizeof(gzip_footer); - ++iovlen; - } - } - DCHECK_EQ(p - outbuf + actualcontentlength, GetIovSize(iov, iovlen)); - /* LogRequestLatency(); */ - if (WritevAll(client, iov, iovlen) == -1) { - VERBOSEF("%s send error %s", clientaddrstr, strerror(errno)); - terminated = true; + return ServeError(404, "Not Found"); } } -void ProcessRequests(void) { - size_t got; +static bool HandleRequest(void) { + int rc; + char *p; + int iovlen; + struct iovec iov[4]; + long actualcontentlength; + msgsize = 0; + if ((rc = ParseHttpRequest(&req, inbuf.p, amtread)) != -1) { + if (!rc) return false; + p = HandleMessage(rc); + } else { + httpversion = 101; + p = ServeError(400, "Bad Request"); + } + if (!msgsize) { + amtread = 0; + connectionclose = true; + DEBUGF("%s could not synchronize message stream", clientaddrstr); + } else if (msgsize < amtread) { + DEBUGF("%s has %,ld pipelined bytes", clientaddrstr, amtread - msgsize); + memmove(inbuf.p, inbuf.p + msgsize, amtread - msgsize); + amtread -= msgsize; + } else { + amtread = 0; + } + if (httpversion >= 100) { + p = AppendHeader(p, "Date", currentdate); + if (!branded) { + p = AppendHeader(p, "Server", serverheader); + } + if (connectionclose) { + p = AppendHeader(p, "Connection", "close"); + } + actualcontentlength = contentlength; + if (gzipped) { + actualcontentlength += sizeof(kGzipHeader) + sizeof(gzip_footer); + } + p = AppendContentLength(p, actualcontentlength); + p = AppendCrlf(p); + CHECK_LE(p - hdrbuf.p, hdrbuf.n); + if (logmessages) { + LOGF("%s sending %,d byte message\n%.*s", clientaddrstr, p - hdrbuf.p, + p - hdrbuf.p - 4, hdrbuf.p); + } + iov[0].iov_base = hdrbuf.p; + iov[0].iov_len = p - hdrbuf.p; + iovlen = 1; + if (!MustNotIncludeMessageBody()) { + if (gzipped) { + iov[iovlen].iov_base = kGzipHeader; + iov[iovlen].iov_len = sizeof(kGzipHeader); + ++iovlen; + } + iov[iovlen].iov_base = content; + iov[iovlen].iov_len = contentlength; + ++iovlen; + if (gzipped) { + iov[iovlen].iov_base = gzip_footer; + iov[iovlen].iov_len = sizeof(gzip_footer); + ++iovlen; + } + } + } else { + iov[0].iov_base = content; + iov[0].iov_len = contentlength; + iovlen = 1; + } + Send(iov, iovlen); + CollectGarbage(); + return true; +} + +static void SendString(const char *s) { + write(client, s, strlen(s)); +} + +static void SendTimeout(void) { + SendString("\ +HTTP/1.1 408 Request Timeout\r\n\ +Connection: close\r\n\ +Content-Length: 0\r\n\ +\r\n"); +} + +static void SendServiceUnavailable(void) { + SendString("\ +HTTP/1.1 503 Service Unavailable\r\n\ +Connection: close\r\n\ +Content-Length: 0\r\n\ +\r\n"); +} + +static void InitRequest(void) { + frags = 0; + content = NULL; + gzipped = false; + branded = false; + contentlength = 0; + InitHttpRequest(&req); +} + +static void ProcessRequests(void) { ssize_t rc; + size_t got; long double now; - do { - if ((rc = read(client, inbuf, sizeof(inbuf))) != -1) { - startrequest = now = nowl(); - if (now - nowish > 1) UpdateCurrentDate(now); - if (!(got = rc)) break; - HandleRequest(got); - } else if (errno == EINTR) { - continue; - } else if (errno == ECONNRESET) { - DEBUGF("%s reset", clientaddrstr); - break; - } else { - WARNF("%s recv error %s", clientaddrstr, strerror(errno)); - break; + for (;;) { + InitRequest(); + startread = nowl(); + for (;;) { + if (!req.i && amtread && HandleRequest()) break; + if ((rc = read(client, inbuf.p + amtread, inbuf.n - amtread)) != -1) { + startrequest = now = nowl(); + if (now - nowish > 1) UpdateCurrentDate(now); + got = rc; + amtread += got; + if (amtread) { + if (HandleRequest()) { + break; + } else if (got) { + if (++frags == 32) { + SendTimeout(); + LogClose("fragged"); + return; + } else { + DEBUGF("%s fragmented msg %,ld %,ld", clientaddrstr, amtread, + got); + } + } + } + if (!got) { + LogClose("disconnect"); + return; + } + } else if (errno == ECONNRESET) { + LogClose("reset"); + return; + } else if (errno != EINTR) { + WARNF("%s recv msg %s", clientaddrstr, strerror(errno)); + return; + } + if (killed || (terminated && !amtread) || + (meltdown && (!amtread || nowl() - startread > 1))) { + if (amtread) SendServiceUnavailable(); + LogClose(DescribeClose()); + return; + } } - } while (!terminated); - if (killed) { - WARNF("%s killed", clientaddrstr); - } else { - DEBUGF("%s terminated", clientaddrstr); - } -} - -void ProcessConnection(void) { - int pid; - clientaddrsize = sizeof(clientaddr); - client = accept(server, &clientaddr, &clientaddrsize); - if (client != -1) { - startconnection = nowl(); - if ((pid = uniprocess ? -1 : fork()) > 0) { - close(client); + if (connectionclose || killed || ((terminated || meltdown) && !amtread)) { + LogClose(DescribeClose()); return; } - if (pid == -1) terminated = true; + } +} + +static void ProcessConnection(void) { + int pid; + clientaddrsize = sizeof(clientaddr); + if ((client = accept(server, &clientaddr, &clientaddrsize)) != -1) { + startconnection = nowl(); + if (uniprocess) { + pid = -1; + connectionclose = true; + } else { + switch ((pid = fork())) { + case 0: + meltdown = false; + connectionclose = false; + break; + case -1: + WARNF("redbean is entering meltdown mode"); + LOGIFNEG1(kill(0, SIGUSR2)); + SendServiceUnavailable(); + /* fallthrough */ + default: + close(client); + return; + } + } DescribeAddress(clientaddrstr, &clientaddr); DEBUGF("%s accept", clientaddrstr); ProcessRequests(); - DEBUGF("%s close", clientaddrstr); LOGIFNEG1(close(client)); if (!pid) _exit(0); - terminated = false; } else if (errno != EINTR) { FATALF("%s accept error %s", serveraddrstr, strerror(errno)); } @@ -1207,14 +1970,17 @@ void RedBean(void) { gmtoff = GetGmtOffset(); programfile = (const char *)getauxval(AT_EXECFN); CHECK(OpenZip(programfile)); - xsigaction(SIGINT, OnTerminate, 0, 0, 0); - xsigaction(SIGHUP, daemonize ? OnHup : OnTerminate, 0, 0, 0); - xsigaction(SIGTERM, OnTerminate, 0, 0, 0); - xsigaction(SIGCHLD, SIG_IGN, 0, 0, 0); + xsigaction(SIGINT, OnInt, 0, 0, 0); + xsigaction(SIGHUP, OnHup, 0, 0, 0); + xsigaction(SIGTERM, OnTerm, 0, 0, 0); + xsigaction(SIGCHLD, OnChld, 0, 0, 0); + xsigaction(SIGUSR1, OnUsr1, 0, 0, 0); + xsigaction(SIGUSR2, OnUsr2, 0, 0, 0); + xsigaction(SIGALRM, OnAlrm, 0, 0, 0); xsigaction(SIGPIPE, SIG_IGN, 0, 0, 0); - xsigaction(SIGUSR1, OnHup, 0, 0, 0); - xsigaction(SIGALRM, OnAlarm, 0, 0, 0); - if (setitimer(ITIMER_REAL, &kHeartbeat, NULL) == -1) notimer = true; + if (setitimer(ITIMER_REAL, &kHeartbeat, NULL) == -1) { + heartless = true; + } CHECK_NE(-1, (server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP))); TuneServerSocket(); if (bind(server, &serveraddr, sizeof(serveraddr)) == -1) { @@ -1236,30 +2002,45 @@ void RedBean(void) { fflush(stdout); } LuaInit(); - heartbeat = true; + UpdateCurrentDate(nowl()); + inbuf.n = 64 * 1024; + inbuf.p = xvalloc(inbuf.n); + hdrbuf.n = 4 * 1024; + hdrbuf.p = xvalloc(hdrbuf.n); while (!terminated) { - if (invalidated) { + if (zombied) { + ReapZombies(); + } else if (invalidated) { if (OpenZip(programfile)) { LOGF("%s reindexed zip", serveraddrstr); } else { WARNF("%s reindexing failed", serveraddrstr); } invalidated = false; - } - if (heartbeat | notimer) { + } else if (heartbeat) { UpdateCurrentDate(nowl()); heartbeat = false; + } else { + if (heartless) { + UpdateCurrentDate(nowl()); + } + ProcessConnection(); } - ProcessConnection(); } VERBOSEF("%s terminated", serveraddrstr); LOGIFNEG1(close(server)); - KillAll(); + if (!keyboardinterrupt) { + if (!killed) { + terminated = false; + } + LOGIFNEG1(kill(0, SIGTERM)); + } WaitAll(); } int main(int argc, char *argv[]) { showcrashreports(); + SetDefaults(); GetOpts(argc, argv); RedBean(); return 0; diff --git a/tool/net/redbean.lua b/tool/net/redbean.lua index 061efd1b6..91b8ce757 100644 --- a/tool/net/redbean.lua +++ b/tool/net/redbean.lua @@ -1,7 +1,48 @@ -send('HTTP/1.1 200 OK\r\n'.. - 'Date: ' .. date() .. '\r\n'.. - 'Server: redbean/0.1\r\n'.. - 'Content-Type: text/plain\r\n'.. - 'Content-Length: 7\r\n'.. - '\r\n'.. - 'hello\r\n') +-- redbean lua server page demo + +local function main() + -- This check is optional. + -- We do this by default if you don't call GetMethod(). + if GetMethod() ~= 'GET' and GetMethod() ~= 'HEAD' then + ServeError(405) + SetHeader('Allow', 'GET, HEAD') + return + end + + -- These two lines are optional. + -- The default behavior is to do this if you don't. + SetStatus(200) -- Shorthand for SetStatus(200, "OK") + SetHeader('Content-Type', 'text/html; charset=utf-8') + + -- Response data is buffered until the script finishes running. + -- Compression is applied automatically, based on your headers. + Write('\n') + Write('
\n')
+ Write('none
\n')
+ Write('ProTip: Try clicking here!\n')
+ end
+end
+
+main()