cosmopolitan/net/turfwar/turfwar.c

933 lines
30 KiB
C
Raw Normal View History

/*-*- 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("<!doctype html>\n"
"<title>The land at %s was claimed for %s.</title>\n"
"<meta name=\"viewport\" "
"content=\"width=device-width, initial-scale=1\">\n"
"The land at %s was claimed for <a "
"href=\"/user.html?name=%s\">%s</a>.\n"
"<p>\n<a href=/>Back to homepage</a>\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 = "<!doctype html>\r\n"
"<title>404 not found</title>\r\n"
"<h1>404 not found</h1>\r\n";
p = stpcpy(outbuf,
"HTTP/1.1 404 Not Found\r\n" STANDARD_RESPONSE_HEADERS
"Content-Type: text/html; charset=utf-8\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);
}
// if the client isn't pipelining and write() wrote the full
// amount, then since we sent the content length and checked
// that the client didn't attach a payload, we are so synced
// thus we can safely process more messages
} while (got == inmsglen && //
sent == outmsglen && //
++msgcount < MSG_MAX && //
!msg->headers[kHttpContentLength].a &&
!msg->headers[kHttpTransferEncoding].a &&
(msg->method == kHttpGet || msg->method == kHttpHead) &&
!nsync_note_is_notified(g_shutdown));
DestroyHttpMessage(msg);
close(client);
}
STRACE("HttpWorker #%d exiting", id);
close(server);
return 0;
}
struct Data Gzip(struct Data data) {
char *p;
void *tmp;
uint32_t crc;
char footer[8];
struct Data res;
z_stream zs = {0};
crc = crc32_z(0, data.p, data.n);
WRITE32LE(footer + 0, crc);
WRITE32LE(footer + 4, data.n);
CHECK_EQ(Z_OK, deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED,
-MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY));
zs.next_in = (const Bytef *)data.p;
zs.avail_in = data.n;
zs.avail_out = compressBound(data.n);
zs.next_out = tmp = xmalloc(zs.avail_out);
CHECK_EQ(Z_STREAM_END, deflate(&zs, Z_FINISH));
CHECK_EQ(Z_OK, deflateEnd(&zs));
res.n = sizeof(kGzipHeader) + zs.total_out + sizeof(footer);
p = res.p = xmalloc(res.n);
p = mempcpy(p, kGzipHeader, sizeof(kGzipHeader));
p = mempcpy(p, tmp, zs.total_out);
p = mempcpy(p, footer, sizeof(footer));
free(tmp);
return res;
}
struct Asset LoadAsset(const char *path, const char *type) {
struct stat st;
struct Asset a = {0};
CHECK_EQ(0, stat(path, &st));
CHECK_NOTNULL((a.data.p = xslurp(path, &a.data.n)));
a.type = type;
a.path = xstrdup(path);
a.mtim = st.st_mtim;
a.gzip = Gzip(a.data);
FormatUnixHttpDateTime(a.lastmod, a.mtim.tv_sec);
return a;
}
bool ReloadAsset(struct Asset *a) {
int fd;
void *f[2];
ssize_t rc;
struct stat st;
char lastmod[32];
struct Data data = {0};
struct Data gzip = {0};
CHECK_SYS((fd = open(a->path, O_RDONLY)));
CHECK_SYS(fstat(fd, &st));
if (_timespec_gt(st.st_mtim, a->mtim) && (data.p = malloc(st.st_size))) {
FormatUnixHttpDateTime(lastmod, st.st_mtim.tv_sec);
CHECK_SYS((rc = read(fd, data.p, st.st_size)));
if (rc != st.st_size) goto OnError;
gzip = Gzip(data);
//!//!//!//!//!//!//!//!//!//!//!//!//!/
nsync_mu_lock(&a->lock);
f[0] = a->data.p;
f[1] = a->gzip.p;
a->data = data;
a->gzip = gzip;
a->mtim = st.st_mtim;
memcpy(a->lastmod, lastmod, 32);
nsync_mu_unlock(&a->lock);
//!//!//!//!//!//!//!//!//!//!//!//!//!/
free(f[0]);
free(f[1]);
}
2022-10-03 15:34:04 +00:00
close(fd);
return true;
OnError:
free(data.p);
close(fd);
return false;
}
void FreeAsset(struct Asset *a) {
free(a->path);
free(a->data.p);
free(a->gzip.p);
}
static void GetOpts(int argc, char *argv[]) {
int opt;
while ((opt = getopt(argc, argv, GETOPTS)) != -1) {
switch (opt) {
case 'd':
g_daemonize = true;
break;
case 'p':
g_port = atoi(optarg);
break;
case 'w':
g_workers = atoi(optarg);
break;
case 'k':
g_keepalive = atoi(optarg);
break;
case 'v':
++__log_level;
break;
case '?':
write(1, USAGE, sizeof(USAGE) - 1);
exit(0);
default:
write(2, USAGE, sizeof(USAGE) - 1);
exit(64);
}
}
}
void OnCtrlC(int sig) {
nsync_note_notify(g_shutdown);
}
bool GenerateBoard(struct Asset *out) {
char *sb = 0;
sqlite3 *db = 0;
size_t sblen = 0;
struct Asset a = {0};
sqlite3_stmt *stmt = 0;
DEBUG("GenerateBoard\n");
a.type = "application/json";
CHECK_SYS(clock_gettime(CLOCK_REALTIME, &a.mtim));
FormatUnixHttpDateTime(a.lastmod, a.mtim.tv_sec);
CHECK_SYS(appends(&a.data.p, "{\"leaders\":[\n"));
CHECK_SQL(sqlite3_open("db.sqlite3", &db));
CHECK_SQL(sqlite3_exec(db, "PRAGMA journal_mode=WAL", 0, 0, 0));
CHECK_SQL(sqlite3_exec(db, "PRAGMA synchronous=NORMAL", 0, 0, 0));
CHECK_DB(sqlite3_prepare_v2(db,
"SELECT nick AS name, COUNT(ip) AS count\n"
"FROM land\n"
"WHERE ip >= ?1 AND ip <= ?2\n"
"GROUP BY nick\n"
"ORDER BY count DESC\n"
"LIMIT 1",
-1, &stmt, 0));
CHECK_SQL(sqlite3_exec(db, "BEGIN TRANSACTION", 0, 0, 0));
for (long i = 0; i < 256; ++i) {
if (i) CHECK_SYS(appends(&a.data.p, ",\n"));
CHECK_DB(sqlite3_bind_int64(stmt, 1, i * 0x1000000));
CHECK_DB(sqlite3_bind_int64(stmt, 2, i * 0x1000000 + 0xFFFFFF));
switch (sqlite3_step(stmt)) {
case SQLITE_ROW:
CHECK_SYS(appendf(
&a.data.p, "{\"name\":\"%s\",\"count\":%ld}",
EscapeJsStringLiteral(&sb, &sblen,
(void *)sqlite3_column_text(stmt, 0), -1, 0),
sqlite3_column_int64(stmt, 1)));
break;
case SQLITE_DONE:
CHECK_SYS(appends(&a.data.p, "false"));
break;
default:
kprintf("%s\n", sqlite3_errmsg(db));
abort();
}
CHECK_DB(sqlite3_reset(stmt));
}
CHECK_SQL(sqlite3_exec(db, "END TRANSACTION", 0, 0, 0));
CHECK_SYS(appends(&a.data.p, "\n]}\n"));
CHECK_DB(sqlite3_finalize(stmt));
CHECK_SQL(sqlite3_close(db));
a.data.n = appendz(a.data.p).i;
a.gzip = Gzip(a.data);
*out = a;
return true;
OnError:
sqlite3_finalize(stmt);
sqlite3_close(db);
free(a.data.p);
return false;
}
// single thread for regenerating the leaderboard json
void *BoardWorker(void *arg) {
int rc;
void *f[2];
struct Asset a;
LOG("BoardWorker started\n");
for (;;) {
nsync_mu_lock(&g_board.mu);
rc = nsync_cv_wait_with_deadline(&g_board.cv, &g_board.mu,
nsync_time_no_deadline, g_shutdown);
nsync_mu_unlock(&g_board.mu);
if (rc == ECANCELED) break;
if (GenerateBoard(&a)) {
//!//!//!//!//!//!//!//!//!//!//!//!//!/
nsync_mu_lock(&g_asset.board.lock);
f[0] = g_asset.board.data.p;
f[1] = g_asset.board.gzip.p;
g_asset.board.data = a.data;
g_asset.board.gzip = a.gzip;
g_asset.board.mtim = a.mtim;
memcpy(g_asset.board.lastmod, a.lastmod, 32);
nsync_mu_unlock(&g_asset.board.lock);
//!//!//!//!//!//!//!//!//!//!//!//!//!/
free(f[0]);
free(f[1]);
}
}
LOG("BoardWorker exiting\n");
return 0;
}
// single thread for inserting batched claims into the database
void *ClaimWorker(void *arg) {
int i, n, rc;
sqlite3 *db = 0;
struct Claim v[32];
sqlite3_stmt *stmt = 0;
StartOver:
CHECK_SQL(sqlite3_open("db.sqlite3", &db));
CHECK_SQL(sqlite3_exec(db, "PRAGMA journal_mode=WAL", 0, 0, 0));
CHECK_SQL(sqlite3_exec(db, "PRAGMA synchronous=NORMAL", 0, 0, 0));
CHECK_DB(sqlite3_prepare_v2(db,
"INSERT INTO land (ip, nick) VALUES (?1, ?2)\n"
"ON CONFLICT (ip) DO UPDATE SET (nick) = (?2)\n"
"WHERE nick != ?2",
-1, &stmt, 0));
LOG("ClaimWorker started\n");
while ((n = GetClaims(&g_claims, v, ARRAYLEN(v), nsync_time_no_deadline))) {
CHECK_SQL(sqlite3_exec(db, "BEGIN TRANSACTION", 0, 0, 0));
for (i = 0; i < n; ++i) {
CHECK_DB(sqlite3_bind_int64(stmt, 1, v[i].ip));
CHECK_DB(sqlite3_bind_text(stmt, 2, v[i].name, -1, SQLITE_TRANSIENT));
CHECK_DB((rc = sqlite3_step(stmt)) == SQLITE_DONE ? SQLITE_OK : rc);
CHECK_DB(sqlite3_reset(stmt));
}
CHECK_SQL(sqlite3_exec(db, "COMMIT TRANSACTION", 0, 0, 0));
DEBUG("Committed %d claims\n", n);
nsync_mu_lock(&g_board.mu);
nsync_cv_signal(&g_board.cv);
nsync_mu_unlock(&g_board.mu);
}
CHECK_DB(sqlite3_finalize(stmt));
CHECK_SQL(sqlite3_close(db));
return 0;
OnError:
sqlite3_finalize(stmt);
sqlite3_close(db);
stmt = 0;
db = 0;
usleep(1000 * 1000);
goto StartOver;
}
// single thread for computing HTTP Date header
void *NowWorker(void *arg) {
nsync_time deadline;
for (deadline = _timespec_real();;) {
deadline = _timespec_add(deadline, _timespec_frommillis(DATE_UPDATE_MS));
if (!nsync_note_wait(g_shutdown, deadline)) {
UpdateNow();
} else {
break;
}
}
return 0;
}
// single thread for monitoring assets on disk
void *AssetWorker(void *arg) {
nsync_time deadline;
for (deadline = _timespec_real();;) {
deadline = _timespec_add(deadline, _timespec_frommillis(POLL_ASSETS_MS));
if (!nsync_note_wait(g_shutdown, deadline)) {
ReloadAsset(&g_asset.index);
ReloadAsset(&g_asset.about);
ReloadAsset(&g_asset.user);
} else {
break;
}
}
return 0;
}
int main(int argc, char *argv[]) {
ShowCrashReports();
GetOpts(argc, argv);
__enable_threads();
sqlite3_initialize();
g_shutdown = nsync_note_new(0, nsync_time_no_deadline);
CHECK_EQ(0, chdir("/opt/turfwar"));
putenv("TMPDIR=/opt/turfwar/tmp");
g_asset.index = LoadAsset("index.html", "text/html; charset=utf-8");
g_asset.about = LoadAsset("about.html", "text/html; charset=utf-8");
g_asset.user = LoadAsset("user.html", "text/html; charset=utf-8");
CHECK_EQ(0, unveil("/opt/turfwar", "rwc"));
CHECK_EQ(0, unveil(0, 0));
__pledge_mode = PLEDGE_PENALTY_RETURN_EPERM;
CHECK_EQ(0, pledge("stdio flock rpath wpath cpath inet", 0));
// create threads
pthread_t boarder;
CHECK_EQ(1, GenerateBoard(&g_asset.board));
CHECK_EQ(0, pthread_create(&boarder, 0, BoardWorker, 0));
pthread_t claimer;
CHECK_EQ(0, pthread_create(&claimer, 0, ClaimWorker, 0));
pthread_t nower;
UpdateNow();
CHECK_EQ(0, pthread_create(&nower, 0, NowWorker, 0));
pthread_t *httper = _gc(xcalloc(g_workers, sizeof(pthread_t)));
for (intptr_t i = 0; i < g_workers; ++i) {
CHECK_EQ(0, pthread_create(httper + i, 0, HttpWorker, (void *)i));
}
// main thread activity
struct sigaction sa = {.sa_handler = OnCtrlC};
sigaction(SIGHUP, &sa, 0);
sigaction(SIGINT, &sa, 0);
sigaction(SIGTERM, &sa, 0);
AssetWorker(0);
// wait for threads to finish
for (int i = 0; i < g_workers; ++i) {
CHECK_EQ(0, pthread_join(httper[i], 0));
}
CHECK_EQ(0, pthread_join(claimer, 0));
CHECK_EQ(0, pthread_join(boarder, 0));
CHECK_EQ(0, pthread_join(nower, 0));
// free memory
FreeAsset(&g_asset.user);
FreeAsset(&g_asset.about);
FreeAsset(&g_asset.index);
nsync_note_free(g_shutdown);
// CheckForMemoryLeaks();
}