/*-*- 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 2022 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/calls/calls.h" #include "libc/calls/pledge.h" #include "libc/calls/struct/iovec.h" #include "libc/calls/struct/sigaction.h" #include "libc/calls/struct/stat.h" #include "libc/calls/struct/timespec.h" #include "libc/calls/struct/timeval.h" #include "libc/errno.h" #include "libc/fmt/conv.h" #include "libc/fmt/itoa.h" #include "libc/intrin/bits.h" #include "libc/intrin/kprintf.h" #include "libc/intrin/strace.internal.h" #include "libc/log/check.h" #include "libc/log/log.h" #include "libc/macros.internal.h" #include "libc/mem/gc.h" #include "libc/mem/mem.h" #include "libc/nexgen32e/crc32.h" #include "libc/runtime/internal.h" #include "libc/runtime/runtime.h" #include "libc/sock/sock.h" #include "libc/sock/struct/pollfd.h" #include "libc/sock/struct/sockaddr.h" #include "libc/stdio/append.h" #include "libc/stdio/stdio.h" #include "libc/str/slice.h" #include "libc/str/str.h" #include "libc/sysv/consts/af.h" #include "libc/sysv/consts/clock.h" #include "libc/sysv/consts/o.h" #include "libc/sysv/consts/poll.h" #include "libc/sysv/consts/sig.h" #include "libc/sysv/consts/so.h" #include "libc/sysv/consts/sock.h" #include "libc/sysv/consts/sol.h" #include "libc/sysv/consts/tcp.h" #include "libc/thread/thread.h" #include "libc/time/struct/tm.h" #include "libc/x/x.h" #include "libc/x/xasprintf.h" #include "libc/zip.h" #include "net/http/escape.h" #include "net/http/http.h" #include "net/http/url.h" #include "third_party/getopt/getopt.h" #include "third_party/nsync/cv.h" #include "third_party/nsync/mu.h" #include "third_party/nsync/note.h" #include "third_party/sqlite3/sqlite3.h" #include "third_party/zlib/zconf.h" #include "third_party/zlib/zlib.h" #include "tool/net/lfuncs.h" /** * @fileoverview production webserver for turfwar online game */ #define PORT 8080 #define WORKERS 2000 #define HEARTBEAT 2000 #define KEEPALIVE_MS 2000 #define DATE_UPDATE_MS 500 #define POLL_ASSETS_MS 250 #define BOARD_GENERATE_MS 10000 #define CLAIM_DEADLINE_MS 1000 #define CLAIM_MAX 200 #define NICK_MAX 40 #define MSG_MAX 10 #define GETOPTS "dvp:w:k:" #define USAGE \ "\ Usage: turfwar.com [-dv] ARGS...\n\ -d daemonize\n\ -v verbosity\n\ -p INT port\n\ -w INT workers\n\ -k INT keepalive\n\ " #define STANDARD_RESPONSE_HEADERS \ "Server: turfwar\r\n" \ "Referrer-Policy: origin\r\n" \ "Access-Control-Allow-Origin: *\r\n" #define HasHeader(H) (!!msg->headers[H].a) #define HeaderData(H) (inbuf + msg->headers[H].a) #define HeaderLength(H) (msg->headers[H].b - msg->headers[H].a) #define HeaderEqual(H, S) \ SlicesEqual(S, strlen(S), HeaderData(H), HeaderLength(H)) #define HeaderEqualCase(H, S) \ SlicesEqualCase(S, strlen(S), HeaderData(H), HeaderLength(H)) #define UrlEqual(S) \ SlicesEqual(inbuf + msg->uri.a, msg->uri.b - msg->uri.a, S, strlen(S)) #if 1 #define LOG(...) kprintf(__VA_ARGS__) #else #define LOG(...) (void)0 #endif #if 0 #define DEBUG(...) kprintf(__VA_ARGS__) #else #define DEBUG(...) (void)0 #endif #define CHECK_SYS(x) \ do { \ if (!CheckSys(__FILE__, __LINE__, x)) { \ goto OnError; \ } \ } while (0) #define CHECK_SQL(x) \ do { \ int e = errno; \ if (!CheckSql(__FILE__, __LINE__, x)) { \ goto OnError; \ } \ errno = e; \ } while (0) #define CHECK_DB(x) \ do { \ int e = errno; \ if (!CheckDb(__FILE__, __LINE__, x, db)) { \ goto OnError; \ } \ errno = e; \ } while (0) static const uint8_t kGzipHeader[] = { 0x1F, // MAGNUM 0x8B, // MAGNUM 0x08, // CM: DEFLATE 0x00, // FLG: NONE 0x00, // MTIME: NONE 0x00, // 0x00, // 0x00, // 0x00, // XFL kZipOsUnix, // OS }; struct Data { char *p; size_t n; }; struct Asset { char *path; nsync_mu lock; const char *type; struct Data data; struct Data gzip; struct timespec mtim; char lastmod[32]; }; bool g_daemonize; int g_port = PORT; int g_workers = WORKERS; int g_keepalive = KEEPALIVE_MS; struct tm g_nowish; nsync_note g_shutdown; nsync_mu g_nowish_lock; struct Board { nsync_mu mu; nsync_cv cv; } g_board; struct Assets { struct Asset index; struct Asset about; struct Asset user; struct Asset board; } g_asset; struct Claims { int pos; int count; nsync_mu mu; nsync_cv non_full; nsync_cv non_empty; struct Claim { uint32_t ip; char name[NICK_MAX + 1]; } data[CLAIM_MAX]; } g_claims; bool CheckSys(const char *file, int line, long rc) { if (rc != -1) return true; kprintf("%s:%d: %s\n", file, line, strerror(errno)); return false; } bool CheckSql(const char *file, int line, int rc) { if (rc == SQLITE_OK) return true; kprintf("%s:%d: %s\n", file, line, sqlite3_errstr(rc)); return false; } bool CheckDb(const char *file, int line, int rc, sqlite3 *db) { if (rc == SQLITE_OK) return true; kprintf("%s:%d: %s: %s\n", file, line, sqlite3_errstr(rc), sqlite3_errmsg(db)); return false; } bool IsValidNick(const char *s, size_t n) { size_t i; if (n > NICK_MAX) return false; for (i = 0; i < n; ++i) { if (!(isalnum(s[i]) || // s[i] == '@' || // s[i] == '/' || // s[i] == ':' || // s[i] == '.' || // s[i] == '+' || // s[i] == '_' || // s[i] == '*')) { return false; } } return true; } char *FormatUnixHttpDateTime(char *s, int64_t t) { struct tm tm; gmtime_r(&t, &tm); FormatHttpDateTime(s, &tm); return s; } void UpdateNow(void) { int64_t secs; struct tm tm; struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); secs = ts.tv_sec; gmtime_r(&secs, &tm); //!//!//!//!//!//!//!//!//!//!//!//!//!/ nsync_mu_lock(&g_nowish_lock); g_nowish = tm; nsync_mu_unlock(&g_nowish_lock); //!//!//!//!//!//!//!//!//!//!//!//!//!/ } char *FormatDate(char *p) { //////////////////////////////////////// nsync_mu_rlock(&g_nowish_lock); p = FormatHttpDateTime(p, &g_nowish); nsync_mu_runlock(&g_nowish_lock); //////////////////////////////////////// return p; } bool AddClaim(struct Claims *q, const struct Claim *v, nsync_time dead) { bool wake = false; bool added = false; nsync_mu_lock(&q->mu); while (q->count == ARRAYLEN(q->data)) { if (nsync_cv_wait_with_deadline(&q->non_full, &q->mu, dead, g_shutdown)) { break; // must be ETIMEDOUT or ECANCELED } } if (q->count != ARRAYLEN(q->data)) { int i = q->pos + q->count; if (ARRAYLEN(q->data) <= i) i -= ARRAYLEN(q->data); memcpy(q->data + i, v, sizeof(*v)); if (!q->count) wake = true; q->count++; added = true; } nsync_mu_unlock(&q->mu); if (wake) { nsync_cv_broadcast(&q->non_empty); } return added; } int GetClaims(struct Claims *q, struct Claim *out, int len, nsync_time dead) { int got = 0; nsync_mu_lock(&q->mu); while (!q->count) { if (nsync_cv_wait_with_deadline(&q->non_empty, &q->mu, dead, g_shutdown)) { break; // must be ETIMEDOUT or ECANCELED } } while (got < len && q->count) { memcpy(out + got, q->data + q->pos, sizeof(*out)); if (q->count == ARRAYLEN(q->data)) { nsync_cv_broadcast(&q->non_full); } ++got; q->pos++; q->count--; if (q->pos == ARRAYLEN(q->data)) { q->pos = 0; } } nsync_mu_unlock(&q->mu); return got; } static bool GetNick(char *inbuf, struct HttpMessage *msg, struct Claim *v) { size_t i, n; struct Url url; void *f[2] = {0}; bool found = false; f[0] = ParseUrl(inbuf + msg->uri.a, msg->uri.b - msg->uri.a, &url, kUrlPlus | kUrlLatin1); f[1] = url.params.p; for (i = 0; i < url.params.n; ++i) { if (SlicesEqual("name", 4, url.params.p[i].key.p, url.params.p[i].key.n) && url.params.p[i].val.p && IsValidNick(url.params.p[i].val.p, url.params.p[i].val.n)) { memcpy(v->name, url.params.p[i].val.p, url.params.p[i].val.n); found = true; break; } } free(f[1]); free(f[0]); return found; } // thousands of threads for handling client connections void *HttpWorker(void *arg) { int server; int yes = 1; int id = (intptr_t)arg; struct HttpMessage *msg; STRACE("HttpWorker #%d started", id); // load balance incoming connections for port 8080 across all threads // hangup on any browser clients that lag for more than a few seconds struct timeval timeo = {g_keepalive / 1000, g_keepalive % 1000}; struct sockaddr_in addr = {.sin_family = AF_INET, .sin_port = htons(g_port)}; CHECK_NE(-1, (server = socket(AF_INET, SOCK_STREAM, 0))); setsockopt(server, SOL_SOCKET, SO_RCVTIMEO, &timeo, sizeof(timeo)); setsockopt(server, SOL_SOCKET, SO_SNDTIMEO, &timeo, sizeof(timeo)); setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); setsockopt(server, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes)); setsockopt(server, SOL_TCP, TCP_FASTOPEN, &yes, sizeof(yes)); setsockopt(server, SOL_TCP, TCP_QUICKACK, &yes, sizeof(yes)); errno = 0; CHECK_NE(-1, bind(server, &addr, sizeof(addr))); CHECK_NE(-1, listen(server, 1)); msg = _gc(xmalloc(sizeof(struct HttpMessage))); // connection loop while (!nsync_note_is_notified(g_shutdown)) { int msgcount; struct Data d; struct Url url; bool comp, ipv6; struct Asset *a; ssize_t got, sent; struct iovec iov[2]; uint32_t ip, clientip; uint32_t clientaddrsize; struct sockaddr_in clientaddr; int client, inmsglen, outmsglen; char inbuf[1500], outbuf[512], ipbuf[32], *p, *q; // this slows the server down a lot but is needed on non-Linux to // react to keyboard ctrl-c if (!IsLinux() && poll(&(struct pollfd){server, POLLIN}, 1, HEARTBEAT) < 1) { continue; } // wait for client connection clientaddrsize = sizeof(clientaddr); client = accept(server, (struct sockaddr *)&clientaddr, &clientaddrsize); if (client == -1) continue; ip = clientip = ntohl(clientaddr.sin_addr.s_addr); // strict message loop w/o pipelining msgcount = 0; do { InitHttpMessage(msg, kHttpRequest); if ((got = read(client, inbuf, sizeof(inbuf))) <= 0) break; if ((inmsglen = ParseHttpMessage(msg, inbuf, got)) <= 0) break; if (msg->version != 11) break; // cloudflare won't send 0.9 or 1.0 // get the ip address again // we assume a firewall only lets the frontend talk to this server ipv6 = false; if (HasHeader(kHttpXForwardedFor) && ParseForwarded(HeaderData(kHttpXForwardedFor), HeaderLength(kHttpXForwardedFor), &ip, 0) == -1) { ipv6 = true; } ksnprintf(ipbuf, sizeof(ipbuf), "%hhu.%hhu.%hhu.%hhu", ip >> 24, ip >> 16, ip >> 8, ip); if (UrlEqual("/") || UrlEqual("/index.html")) { a = &g_asset.index; } else if (UrlEqual("/about.html")) { a = &g_asset.about; } else if (UrlEqual("/user.html")) { a = &g_asset.user; } else if (UrlEqual("/board")) { a = &g_asset.board; } else { a = 0; } if (a) { comp = HeaderHas(msg, inbuf, kHttpAcceptEncoding, "gzip", 4); p = stpcpy(outbuf, "HTTP/1.1 200 OK\r\n" STANDARD_RESPONSE_HEADERS "Cache-Control: max-age=60, must-revalidate\r\n" "Vary: Accept-Encoding\r\n" "Date: "); p = FormatDate(p); //////////////////////////////////////// nsync_mu_rlock(&a->lock); p = stpcpy(p, "\r\nLast-Modified: "); p = stpcpy(p, a->lastmod); p = stpcpy(p, "\r\nContent-Type: "); p = stpcpy(p, a->type); if (comp) p = stpcpy(p, "\r\nContent-Encoding: gzip"); p = stpcpy(p, "\r\nContent-Length: "); d = comp ? a->gzip : a->data; p = FormatInt32(p, d.n); p = stpcpy(p, "\r\n\r\n"); iov[0].iov_base = outbuf; iov[0].iov_len = p - outbuf; iov[1].iov_base = d.p; iov[1].iov_len = d.n; sent = writev(client, iov, 2); outmsglen = iov[0].iov_len + iov[1].iov_len; nsync_mu_runlock(&a->lock); //////////////////////////////////////// } else if (UrlEqual("/ip")) { if (!ipv6) { p = stpcpy(outbuf, "HTTP/1.1 200 OK\r\n" STANDARD_RESPONSE_HEADERS "Content-Type: text/plain\r\n" "Cache-Control: private\r\n" "Date: "); p = FormatDate(p); p = stpcpy(p, "\r\nContent-Length: "); p = FormatInt32(p, strlen(ipbuf)); p = stpcpy(p, "\r\n\r\n"); p = stpcpy(p, ipbuf); outmsglen = p - outbuf; sent = write(client, outbuf, outmsglen); } else { Ipv6Warning: DEBUG("%.*s via %s: 400 Need IPv4\n", HeaderLength(kHttpXForwardedFor), HeaderData(kHttpXForwardedFor), ipbuf); q = "IPv4 Games only supports IPv4 right now"; p = stpcpy(outbuf, "HTTP/1.1 400 Need IPv4\r\n" STANDARD_RESPONSE_HEADERS "Content-Type: text/plain\r\n" "Cache-Control: private\r\n" "Connection: close\r\n" "Date: "); p = FormatDate(p); p = stpcpy(p, "\r\nContent-Length: "); p = FormatInt32(p, strlen(q)); p = stpcpy(p, "\r\n\r\n"); p = stpcpy(p, q); outmsglen = p - outbuf; sent = write(client, outbuf, p - outbuf); break; } } else if (msg->uri.b - msg->uri.a > 6 && !memcmp(inbuf + msg->uri.a, "/claim", 6)) { if (ipv6) goto Ipv6Warning; struct Claim v = {.ip = ip}; if (GetNick(inbuf, msg, &v)) { if (AddClaim( &g_claims, &v, _timespec_add(_timespec_real(), _timespec_frommillis(CLAIM_DEADLINE_MS)))) { LOG("%s claimed by %s\n", ipbuf, v.name); q = xasprintf("\n" "
\nBack to homepage\n", ipbuf, v.name, ipbuf, v.name, v.name); p = stpcpy(outbuf, "HTTP/1.1 200 OK\r\n" STANDARD_RESPONSE_HEADERS "Content-Type: text/html\r\n" "Cache-Control: private\r\n" "Date: "); p = FormatDate(p); p = stpcpy(p, "\r\nContent-Length: "); p = FormatInt32(p, strlen(q)); p = stpcpy(p, "\r\n\r\n"); p = stpcpy(p, q); outmsglen = p - outbuf; sent = write(client, outbuf, p - outbuf); free(q); } else { LOG("%s: 502 Claims Queue Full\n", ipbuf); q = "Claims Queue Full"; p = stpcpy( outbuf, "HTTP/1.1 502 Claims Queue Full\r\n" STANDARD_RESPONSE_HEADERS "Content-Type: text/plain\r\n" "Cache-Control: private\r\n" "Connection: close\r\n" "Date: "); p = FormatDate(p); p = stpcpy(p, "\r\nContent-Length: "); p = FormatInt32(p, strlen(q)); p = stpcpy(p, "\r\n\r\n"); p = stpcpy(p, q); outmsglen = p - outbuf; sent = write(client, outbuf, p - outbuf); break; } } else { LOG("%s: 400 invalid name\n", ipbuf); q = "invalid name"; p = stpcpy(outbuf, "HTTP/1.1 400 Invalid Name\r\n" STANDARD_RESPONSE_HEADERS "Content-Type: text/plain\r\n" "Cache-Control: private\r\n" "Connection: close\r\n" "Date: "); p = FormatDate(p); p = stpcpy(p, "\r\nContent-Length: "); p = FormatInt32(p, strlen(q)); p = stpcpy(p, "\r\n\r\n"); p = stpcpy(p, q); outmsglen = p - outbuf; sent = write(client, outbuf, p - outbuf); break; } } else { LOG("%s: 400 not found\n", ipbuf); q = "\r\n" "