shithub: moonfish

Download patch

ref: c5dca58113d2570c32918c1b823a2f7eb3bce713
parent: f76d40073233fa36c4ed23e113fa4769d2f1f658
author: zamfofex <zamfofex@twdb.moe>
date: Mon Oct 9 12:13:02 EDT 2023

add Lichess integration

--- a/.build.yml
+++ b/.build.yml
@@ -9,7 +9,7 @@
   - build: |
       cp sunfish/nnue/models/tanh.pickle moonfish
       cd moonfish
-      make LDFLAGS=-static inbuilt_network=yes all
+      make LDFLAGS=-static inbuilt_network=yes moonfish play
 artifacts:
   - moonfish/moonfish
   - moonfish/play
--- a/main.c
+++ b/main.c
@@ -23,18 +23,28 @@
 	
 	if (argc < 1) return 1;
 	
-	if (argc != 2 && moonfish_network == NULL)
+#ifdef MOONFISH_INBUILT_NET
+	
+	if (argc > 2)
 	{
-		fprintf(stderr, "usage: %s <file-name>\n", argv[0]);
+		fprintf(stderr, "usage: %s [<file-name>]\n", argv[0]);
 		return 1;
 	}
 	
-	if (argc > 2)
+	if (argc >= 2) file = fopen(argv[1], "rb");
+	else file = fmemopen(moonfish_network, 1139, "rb");
+	
+#else
+	if (argc != 2)
 	{
-		fprintf(stderr, "usage: %s [<file-name>]\n", argv[0]);
+		fprintf(stderr, "usage: %s <file-name>\n", argv[0]);
 		return 1;
 	}
 	
+	file = fopen(argv[1], "rb");
+	
+#endif
+	
 	ctx = malloc(sizeof *ctx);
 	if (ctx == NULL)
 	{
@@ -41,9 +51,6 @@
 		perror(argv[0]);
 		return 1;
 	}
-	
-	if (argc >= 2) file = fopen(argv[1], "rb");
-	else file = fmemopen(moonfish_network, 1139, "rb");
 	
 	if (file == NULL)
 	{
--- a/makefile
+++ b/makefile
@@ -5,7 +5,7 @@
 ifeq ($(inbuilt_network),yes)
 
 moonfish: moonfish.h *.c net/tanh.c tanh.o
-	$(cc) -D_POSIX_C_SOURCE=200809L -o moonfish net/tanh.c tanh.o *.c
+	$(cc) -D_POSIX_C_SOURCE=200809L -DMOONFISH_INBUILT_NET -o moonfish net/tanh.c tanh.o *.c
 
 tanh.o: tanh.moon
 	$(LD) -r -b binary -o tanh.o tanh.moon
@@ -20,12 +20,15 @@
 
 endif
 
-play: moonfish.h tools/play.c chess.c
-	$(cc) -pthread -D_POSIX_C_SOURCE=200809L -o play tools/play.c chess.c
+play: moonfish.h tools/tools.h tools/play.c tools/utils.c chess.c
+	$(cc) -pthread -D_POSIX_C_SOURCE=200809L -o play tools/play.c tools/utils.c chess.c
 
+lichess: tools/tools.h tools/lichess.c tools/utils.c tools/play.c
+	$(cc) -pthread -D_POSIX_C_SOURCE=200809L -std=c99 -o lichess tools/lichess.c tools/utils.c -lbearssl -lcjson
+
 .PHONY: all clean
 
-all: moonfish play
+all: moonfish play lichess
 
 clean:
 	$(RM) moonfish play tanh.moon tanh.o
--- /dev/null
+++ b/tools/lichess.c
@@ -1,0 +1,931 @@
+#include <netdb.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <pthread.h>
+#include <signal.h>
+
+#include <bearssl.h>
+#include <cjson/cJSON.h>
+
+#include "tools.h"
+
+#define moonfish_write_text(io_ctx, text) br_sslio_write_all(io_ctx, text, strlen(text))
+#define moonfish_write_string(io_ctx, text) br_sslio_write_all(io_ctx, text, sizeof text - 1)
+
+struct moonfish_game
+{
+	char *argv0;
+	char *name;
+	char *port;
+	char *token;
+	char id[512];
+	char **argv;
+	char *username;
+};
+
+static int moonfish_tcp(char *argv0, char *name, char *port)
+{
+	int fd;
+	int error;
+	struct addrinfo hints = {0};
+	struct addrinfo *infos, *info;
+	
+	hints.ai_family = AF_UNSPEC;
+	hints.ai_socktype = SOCK_STREAM;
+	hints.ai_flags = 0;
+	hints.ai_protocol = 0;
+	
+	error = getaddrinfo(name, port, &hints, &infos);
+	if (error)
+	{
+		fprintf(stderr, "%s: %s\n", argv0, gai_strerror(error));
+		exit(1);
+	}
+	
+	for (info = infos ; info != NULL ; info = info->ai_next)
+	{
+		fd = socket(info->ai_family, info->ai_socktype, info->ai_protocol);
+		if (fd == -1) continue;
+		if (connect(fd, info->ai_addr, info->ai_addrlen) != -1) break;
+		close(fd);
+	}
+	
+	freeaddrinfo(infos);
+	
+	if (info == NULL)
+	{
+		fprintf(stderr, "%s: could not connect to [%s]:%s\n", argv0, name, port);
+		exit(1);
+	}
+	
+	return fd;
+}
+
+static int moonfish_read(void *data, unsigned char *buffer, size_t length)
+{
+	int fd, *fds;
+	ssize_t read_length;
+	
+	fds = data;
+	fd = *fds;
+	
+	for (;;)
+	{
+		read_length = read(fd, buffer, length);
+		if (read_length <= 0)
+		{
+			if (read_length < 0 && errno == EINTR) continue;
+			return -1;
+		}
+		return read_length;
+	}
+}
+
+static int moonfish_write(void *data, const unsigned char *buffer, size_t length)
+{
+	int fd, *fds;
+	ssize_t write_length;
+	
+	fds = data;
+	fd = *fds;
+	
+	for (;;)
+	{
+		write_length = write(fd, buffer, length);
+		if (write_length <= 0) {
+			if (write_length < 0 && errno == EINTR) continue;
+			return -1;
+		}
+		return write_length;
+	}
+}
+
+static void moonfish_pem_data(void *data, const void *buffer, size_t length)
+{
+	br_x509_decoder_context *x509_ctx;
+	
+	if (length == 0) return;
+	
+	x509_ctx = data;
+	br_x509_decoder_push(x509_ctx, buffer, length);
+}
+
+static void moonfish_x509_data(void *data, const void *buffer, size_t length)
+{
+	br_x500_name *dn;
+	
+	if (length == 0) return;
+	
+	dn = data;
+	dn->len += length;
+	dn->data = realloc(dn->data, dn->len);
+	
+	if (dn->data == NULL)
+	{
+		perror(NULL);
+		exit(1);
+	}
+	
+	memcpy(dn->data + dn->len - length, buffer, length);
+}
+
+static void moonfish_copy_key(br_x509_pkey *to, br_x509_pkey *from)
+{
+	to->key_type = from->key_type;
+	
+	if (from->key_type == BR_KEYTYPE_RSA)
+	{
+		to->key.rsa.nlen = from->key.rsa.nlen;
+		to->key.rsa.n = malloc(to->key.rsa.nlen);
+		if (to->key.rsa.n == NULL)
+		{
+			perror(NULL);
+			exit(1);
+		}
+		memcpy(to->key.rsa.n, from->key.rsa.n, to->key.rsa.nlen);
+		
+		to->key.rsa.elen = from->key.rsa.elen;
+		to->key.rsa.e = malloc(to->key.rsa.elen);
+		if (to->key.rsa.e == NULL)
+		{
+			perror(NULL);
+			exit(1);
+		}
+		memcpy(to->key.rsa.e, from->key.rsa.e, to->key.rsa.elen);
+	}
+	
+	if (from->key_type == BR_KEYTYPE_EC)
+	{
+		to->key.ec.curve = from->key.ec.curve;
+		to->key.ec.qlen = from->key.ec.qlen;
+		to->key.ec.q = malloc(to->key.ec.qlen);
+		if (to->key.ec.q == NULL)
+		{
+			perror(NULL);
+			exit(1);
+		}
+		memcpy(to->key.ec.q, from->key.ec.q, to->key.ec.qlen);
+	}
+}
+
+static void moonfish_load_pem(char *argv0, FILE *file, br_x509_trust_anchor **tas, size_t *count)
+{
+	size_t read_length, n;
+	unsigned char buffer0[1024], *buffer;
+	br_pem_decoder_context pem_ctx;
+	br_x509_decoder_context x509_ctx;
+	br_x509_trust_anchor *trust_anchors, *trust_anchor;
+	size_t trust_anchor_count;
+	br_x509_pkey *pkey;
+	
+	br_pem_decoder_init(&pem_ctx);
+	
+	read_length = 0;
+	trust_anchors = NULL;
+	trust_anchor_count = 0;
+	
+	for (;;)
+	{
+		if (read_length == 0)
+		{
+			if (feof(file)) break;
+			read_length = fread(buffer0, 1, sizeof buffer0, file);
+			if (ferror(file))
+			{
+				fprintf(stderr, "%s: could not read certificat file\n", argv0);
+				exit(1);
+			}
+			buffer = buffer0;
+		}
+		
+		n = br_pem_decoder_push(&pem_ctx, buffer, read_length);
+		buffer += n;
+		read_length -= n;
+		
+		switch (br_pem_decoder_event(&pem_ctx))
+		{
+		case 0:
+			break;
+		default:
+			fprintf(stderr, "%s: PEM decoding failed\n", argv0);
+			exit(1);
+		case BR_PEM_BEGIN_OBJ:
+			trust_anchors = realloc(trust_anchors, sizeof *trust_anchors * (trust_anchor_count + 1));
+			if (trust_anchors == NULL)
+			{
+				perror(argv0);
+				exit(1);
+			}
+			trust_anchor = trust_anchors + trust_anchor_count;
+			trust_anchor->dn.len = 0;
+			trust_anchor->dn.data = NULL;
+			br_x509_decoder_init(&x509_ctx, &moonfish_x509_data, &trust_anchor->dn);
+			br_pem_decoder_setdest(&pem_ctx, &moonfish_pem_data, &x509_ctx);
+			break;
+		case BR_PEM_END_OBJ:
+			trust_anchor = trust_anchors + trust_anchor_count;
+			trust_anchor_count++;
+			pkey = br_x509_decoder_get_pkey(&x509_ctx);
+			if (pkey == NULL)
+			{
+				fprintf(stderr, "%s: X509 decoding failed: %d\n", argv0, br_x509_decoder_last_error(&x509_ctx));
+				exit(-1);
+			}
+			moonfish_copy_key(&trust_anchor->pkey, pkey);
+			trust_anchor->flags = 0;
+			if (br_x509_decoder_isCA(&x509_ctx)) trust_anchor->flags |= BR_X509_TA_CA;
+			break;
+		}
+	}
+	
+	*tas = trust_anchors;
+	*count = trust_anchor_count;
+}
+
+static void moonfish_request(
+	br_ssl_client_context *ctx,
+	br_sslio_context *io_ctx,
+	br_x509_minimal_context *min_ctx,
+	void *buffer,
+	size_t size,
+	char *argv0,
+	char *name,
+	char *port,
+	char *token,
+	char *request,
+	char *type,
+	int length,
+	int *fd
+)
+{
+	char length_string[64];
+	
+	static FILE *certs;
+	static br_x509_trust_anchor *tas = NULL;
+	static size_t count;
+	
+	if (tas == NULL)
+	{
+		certs = fopen("/etc/ssl/certs/ca-certificates.crt", "rb");
+		if (certs == NULL)
+		{
+			perror(argv0);
+			exit(1);
+		}
+		
+		moonfish_load_pem(argv0, certs, &tas, &count);
+		fclose(certs);
+	}
+	
+	br_ssl_client_init_full(ctx, min_ctx, tas, count);
+	br_ssl_engine_set_buffer(&ctx->eng, buffer, size, 1);
+	br_ssl_client_reset(ctx, name, 0);
+	
+	*fd = moonfish_tcp(argv0, name, port);
+	br_sslio_init(io_ctx, &ctx->eng, &moonfish_read, fd, &moonfish_write, fd);
+	
+	moonfish_write_text(io_ctx, request);
+	moonfish_write_string(io_ctx, " HTTP/1.0\r\n");
+	
+	moonfish_write_string(io_ctx, "Authorization: Bearer ");
+	moonfish_write_text(io_ctx, token);
+	moonfish_write_string(io_ctx, "\r\n");
+	
+	moonfish_write_string(io_ctx, "Connection: close\r\n");
+	
+	moonfish_write_string(io_ctx, "Host: ");
+	moonfish_write_text(io_ctx, name);
+	moonfish_write_string(io_ctx, "\r\n");
+	
+	if (type[0])
+	{
+		moonfish_write_string(io_ctx, "Content-Type: ");
+		moonfish_write_text(io_ctx, type);
+		moonfish_write_string(io_ctx, "\r\n");
+		
+		sprintf(length_string, "%d", length);
+		
+		moonfish_write_string(io_ctx, "Content-Length: ");
+		moonfish_write_text(io_ctx, length_string);
+		moonfish_write_string(io_ctx, "\r\n");
+	}
+	
+	moonfish_write_string(io_ctx, "User-Agent: moonfish/0\r\n\r\n");
+}
+
+static int moonfish_response(br_ssl_engine_context *ctx, br_sslio_context *io_ctx, char *argv0)
+{
+	static char success[] = "HTTP/1.0 2";
+	
+	char line[sizeof success];
+	char prev, cur;
+	int error;
+	
+	if (br_sslio_read_all(io_ctx, line, sizeof line - 1))
+	{
+		fprintf(stderr, "%s: malformed HTTP status\n", argv0);
+		exit(1);
+	}
+	
+	if (strcmp(line, success)) return 1;
+	
+	prev = 0;
+	for (;;)
+	{
+		if (br_sslio_read_all(io_ctx, &cur, 1))
+		{
+			fprintf(stderr, "%s: malformed HTTP response\n", argv0);
+			exit(1);
+		}
+		if (prev == '\n' && cur == '\r') break;
+		if (prev == '\r' && cur == '\r') break;
+		if (prev == '\n' && cur == '\n') return 0;
+		prev = cur;
+	}
+	
+	if (br_sslio_read_all(io_ctx, &cur, 1) || cur != '\n')
+	{
+		fprintf(stderr, "%s: malformed HTTP header separator\n", argv0);
+		exit(1);
+	}
+	
+	error = br_ssl_engine_last_error(ctx);
+	if (error)
+	{
+		fprintf(stderr, "%s: BearSSL error: %d\n", argv0, error);
+		exit(1);
+	}
+	
+	return 0;
+}
+
+static int moonfish_basic_request(char *argv0, char *name, char *port, char *token, char *request, char *type, char *body)
+{
+	br_ssl_client_context ctx;
+	br_sslio_context io_ctx;
+	br_x509_minimal_context min_ctx;
+	void *buffer;
+	int fd;
+	int status;
+	
+	buffer = malloc(BR_SSL_BUFSIZE_BIDI);
+	
+	if (type[0] == 0 && body[0] != 0) type = "application/x-www-form-urlencoded";
+	
+	moonfish_request(
+		&ctx, &io_ctx, &min_ctx,
+		buffer, BR_SSL_BUFSIZE_BIDI,
+		argv0,
+		name, port,
+		token, request, type,
+		strlen(body),
+		&fd
+	);
+	
+	moonfish_write_text(&io_ctx, body);
+	br_sslio_flush(&io_ctx);
+	
+	status = moonfish_response(&ctx.eng, &io_ctx, argv0);
+	
+	close(fd);
+	free(buffer);
+	
+	return status;
+}
+
+static int moonfish_tls_line(br_ssl_engine_context *ctx, br_sslio_context *io_ctx, char *argv0, char *line, int length)
+{
+	int error;
+	
+	if (length-- == 0) return 0;
+	
+	for (;;)
+	{
+		if (!length--)
+		{
+			fprintf(stderr, "%s: line too long\n", argv0);
+			exit(1);
+		}
+		if (br_sslio_read_all(io_ctx, line, 1))
+		{
+			error = br_ssl_engine_last_error(ctx);
+			if (error)
+			{
+				fprintf(stderr, "%s: BearSSL error: %d\n", argv0, error);
+				exit(1);
+			}
+			*line = 0;
+			return 1;
+		}
+		if (*line == '\n') break;
+		line++;
+	}
+	
+	*line = 0;
+	
+	return 0;
+}
+
+static void moonfish_json_error(char *argv0)
+{
+	fprintf(stderr, "%s: malformed JSON\n", argv0);
+	exit(1);
+}
+
+static void moonfish_handle_game_events(br_ssl_engine_context *ctx, br_sslio_context *io_ctx, struct moonfish_game *game, FILE *in, FILE *out)
+{
+	char line[4096];
+	cJSON *root, *type, *state, *white_player, *id, *moves;
+	cJSON *wtime, *btime, *winc, *binc;
+	const char *end;
+	int white;
+	int move_count, count;
+	int i;
+	char *move;
+	int done;
+	
+	fprintf(in, "uci\n");
+	moonfish_wait(out, "uciok");
+	
+	fprintf(in, "isready\n");
+	moonfish_wait(out, "readyok");
+	
+	fprintf(in, "ucinewgame\n");
+	
+	fprintf(in, "isready\n");
+	moonfish_wait(out, "readyok");
+	
+	root = NULL;
+	white = -1;
+	move_count = -1;
+	done = 0;
+	
+	while (!done)
+	{
+		if (root != NULL)
+		{
+			cJSON_Delete(root);
+			root = NULL;
+		}
+		
+		done = moonfish_tls_line(ctx, io_ctx, game->argv0, line, sizeof line);
+		if (line[0] == 0) continue;
+		
+		end = NULL;
+		root = cJSON_ParseWithOpts(line, &end, 1);
+		if (end != line + strlen(line)) moonfish_json_error(game->argv0);
+		
+		if (!cJSON_IsObject(root)) moonfish_json_error(game->argv0);
+		
+		type = cJSON_GetObjectItem(root, "type");
+		if (!cJSON_IsString(type)) moonfish_json_error(game->argv0);
+		
+		if (!strcmp(type->valuestring, "gameState"))
+		{
+			state = root;
+		}
+		else if (!strcmp(type->valuestring, "gameFull"))
+		{
+			state = cJSON_GetObjectItem(root, "state");
+			if (!cJSON_IsObject(state)) moonfish_json_error(game->argv0);
+			
+			white_player = cJSON_GetObjectItem(root, "white");
+			if (!cJSON_IsObject(white_player)) moonfish_json_error(game->argv0);
+			
+			id = cJSON_GetObjectItem(white_player, "id");
+			if (!cJSON_IsString(id)) moonfish_json_error(game->argv0);
+			
+			if (!strcmp(id->valuestring, game->username)) white = 1;
+			else white = 0;
+		}
+		else
+		{
+			continue;
+		}
+		
+		if (white == -1)
+		{
+			fprintf(stderr, "%s: 'gameState' event received prior to 'gameFull' event\n", game->argv0);
+			exit(1);
+		}
+		
+		moves = cJSON_GetObjectItem(state, "moves");
+		if (!cJSON_IsString(moves)) moonfish_json_error(game->argv0);
+		
+		count = 0;
+		if (moves->valuestring[0] != 0)
+		{
+			count = 1;
+			for (i = 0 ; moves->valuestring[i] != 0 ; i++)
+				if (moves->valuestring[i] == ' ')
+					count++;
+		}
+		
+		if (count <= move_count) continue;
+		
+		move_count = count;
+		
+		if (count % 2 == white) continue;
+		
+		wtime = cJSON_GetObjectItem(state, "wtime");
+		btime = cJSON_GetObjectItem(state, "btime");
+		winc = cJSON_GetObjectItem(state, "winc");
+		binc = cJSON_GetObjectItem(state, "binc");
+		
+		if (!cJSON_IsNumber(wtime)) moonfish_json_error(game->argv0);
+		if (!cJSON_IsNumber(btime)) moonfish_json_error(game->argv0);
+		if (!cJSON_IsNumber(winc)) moonfish_json_error(game->argv0);
+		if (!cJSON_IsNumber(binc)) moonfish_json_error(game->argv0);
+		
+		fprintf(in, "isready\n");
+		moonfish_wait(out, "readyok");
+		
+		fprintf(in, "position startpos");
+		if (count > 0) fprintf(in, " moves %s", moves->valuestring);
+		fprintf(in, "\n");
+		
+		fprintf(in, "isready\n");
+		moonfish_wait(out, "readyok");
+		
+		fprintf(in, "go wtime %d btime %d", wtime->valueint, btime->valueint);
+		if (winc->valueint > 0) fprintf(in, " winc %d", winc->valueint);
+		if (binc->valueint > 0) fprintf(in, " binc %d", binc->valueint);
+		fprintf(in, "\n");
+		
+		move = moonfish_wait(out, "bestmove");
+		if (move == NULL)
+		{
+			fprintf(stderr, "%s: could not find 'bestmove' command\n", game->argv0);
+			exit(1);
+		}
+		
+		snprintf(line, sizeof line, "POST /api/bot/game/%s/move/%s", game->id, move);
+		if (moonfish_basic_request(game->argv0, game->name, game->port, game->token, line, "", ""))
+		{
+			fprintf(stderr, "%s: could not make move '%s' in game '%s'\n", game->argv0, move, game->id);
+			snprintf(line, sizeof line, "POST /api/bot/game/%s/resign", game->id);
+			if (moonfish_basic_request(game->argv0, game->name, game->port, game->token, line, "", ""))
+				fprintf(stderr, "%s: could not make resign game '%s'\n", game->argv0, game->id);
+			break;
+		}
+	}
+	
+	fprintf(in, "isready\n");
+	moonfish_wait(out, "readyok");
+	fprintf(in, "quit\n");
+}
+
+static void *moonfish_handle_game(void *data)
+{
+	char request[4096];
+	struct moonfish_game *game;
+	int in_fd, out_fd;
+	FILE *in, *out;
+	br_ssl_client_context ctx;
+	br_sslio_context io_ctx;
+	br_x509_minimal_context min_ctx;
+	void *buffer;
+	int fd;
+	
+	game = data;
+	
+	if (moonfish_spawn(game->argv0, game->argv, &in_fd, &out_fd))
+	{
+		perror(game->argv0);
+		exit(1);
+	}
+	
+	in = fdopen(in_fd, "w");
+	if (in == NULL)
+	{
+		perror(game->argv0);
+		exit(1);
+	}
+	
+	out = fdopen(out_fd, "r");
+	if (out == NULL)
+	{
+		perror(game->argv0);
+		exit(1);
+	}
+	
+	errno = 0;
+	if (setvbuf(in, NULL, _IOLBF, 0))
+	{
+		if (errno) perror(game->argv0);
+		exit(1);
+	}
+	
+	errno = 0;
+	if (setvbuf(out, NULL, _IOLBF, 0))
+	{
+		if (errno) perror(game->argv0);
+		exit(1);
+	}
+	
+	buffer = malloc(BR_SSL_BUFSIZE_BIDI);
+	
+	snprintf(request, sizeof request, "GET /api/bot/game/stream/%s", game->id);
+	
+	moonfish_request(
+		&ctx, &io_ctx, &min_ctx,
+		buffer, BR_SSL_BUFSIZE_BIDI,
+		game->argv0,
+		game->name, game->port,
+		game->token, request, "",
+		0, &fd
+	);
+	
+	br_sslio_flush(&io_ctx);
+	
+	if (moonfish_response(&ctx.eng, &io_ctx, game->argv0))
+	{
+		fprintf(stderr, "%s: could not request game event stream\n", game->argv0);
+		exit(1);
+	}
+	
+	moonfish_handle_game_events(&ctx.eng, &io_ctx, game, in, out);
+	
+	free(game);
+	free(buffer);
+	close(fd);
+	
+	return NULL;
+}
+
+static void moonfish_handle_events(
+	br_ssl_engine_context *ctx,
+	br_sslio_context *io_ctx,
+	char *argv0,
+	char *name,
+	char *port,
+	char *token,
+	char **argv,
+	char *username
+)
+{
+	static char line[8192];
+	cJSON *root, *type, *challenge, *id, *variant, *speed;
+	const char *end;
+	struct moonfish_game *game;
+	pthread_t thread;
+	
+	game = malloc(sizeof *game);
+	if (game == NULL)
+	{
+		fprintf(stderr, "%s: could not allocate game\n", game->argv0);
+		exit(1);
+	}
+	
+	root = NULL;
+	
+	for (;;)
+	{
+		if (root != NULL)
+		{
+			cJSON_Delete(root);
+			root = NULL;
+		}
+		
+		if (moonfish_tls_line(ctx, io_ctx, argv0, line, sizeof line))
+		{
+			fprintf(stderr, "%s: connection with Lichess closed\n", argv0);
+			exit(1);
+		}
+		
+		if (line[0] == 0) continue;
+		
+		end = NULL;
+		root = cJSON_ParseWithOpts(line, &end, 1);
+		if (end != line + strlen(line)) moonfish_json_error(argv0);
+		
+		if (!cJSON_IsObject(root)) moonfish_json_error(argv0);
+		
+		type = cJSON_GetObjectItem(root, "type");
+		if (!cJSON_IsString(type)) moonfish_json_error(argv0);
+		
+		if (!strcmp(type->valuestring, "gameStart"))
+		{
+			challenge = cJSON_GetObjectItem(root, "game");
+			if (!cJSON_IsObject(challenge)) moonfish_json_error(argv0);
+			
+			id = cJSON_GetObjectItem(challenge, "id");
+			if (!cJSON_IsString(id)) moonfish_json_error(argv0);
+			
+			if (strlen(id->valuestring) > sizeof game->id - 1)
+			{
+				fprintf(stderr, "%s: game ID '%s' too long\n", argv0, id->valuestring);
+				exit(1);
+			}
+			
+			game->argv0 = argv0;
+			game->name = name;
+			game->port = port;
+			game->token = token;
+			strcpy(game->id, id->valuestring);
+			game->argv = argv;
+			game->username = username;
+			
+			pthread_create(&thread, NULL, &moonfish_handle_game, game);
+			
+			continue;
+		}
+		
+		if (strcmp(type->valuestring, "challenge")) continue;
+		
+		challenge = cJSON_GetObjectItem(root, "challenge");
+		if (!cJSON_IsObject(challenge)) moonfish_json_error(argv0);
+		
+		id = cJSON_GetObjectItem(challenge, "id");
+		if (!cJSON_IsString(id)) moonfish_json_error(argv0);
+		
+		variant = cJSON_GetObjectItem(challenge, "variant");
+		if (!cJSON_IsObject(variant)) moonfish_json_error(argv0);
+		
+		variant = cJSON_GetObjectItem(variant, "key");
+		if (!cJSON_IsString(variant)) moonfish_json_error(argv0);
+		
+		if (strcmp(variant->valuestring, "standard"))
+		{
+			snprintf(line, sizeof line, "POST /api/challenge/%s/decline", id->valuestring);
+			if (moonfish_basic_request(argv0, name, port, token, line, "", "reason=standard"))
+				fprintf(stderr, "%s: could not decline challenge '%s' (standard)\n", argv0, id->valuestring);
+			continue;
+		}
+		
+		speed = cJSON_GetObjectItem(challenge, "speed");
+		if (!cJSON_IsString(speed)) moonfish_json_error(argv0);
+		
+		if (!strcmp(speed->valuestring, "bullet"))
+		{
+			snprintf(line, sizeof line, "POST /api/challenge/%s/decline", id->valuestring);
+			if (moonfish_basic_request(argv0, name, port, token, line, "", "reason=tooFast"))
+				fprintf(stderr, "%s: could not decline challenge '%s' (too fast)\n", argv0, id->valuestring);
+			continue;
+		}
+		
+		if (!strcmp(speed->valuestring, "correspondence"))
+		{
+			snprintf(line, sizeof line, "POST /api/challenge/%s/decline", id->valuestring);
+			if (moonfish_basic_request(argv0, name, port, token, line, "", "reason=tooSlow"))
+				fprintf(stderr, "%s: could not decline challenge '%s' (too slow)\n", argv0, id->valuestring);
+			continue;
+		}
+		
+		snprintf(line, sizeof line, "POST /api/challenge/%s/accept", id->valuestring);
+		if (moonfish_basic_request(argv0, name, port, token, line, "", ""))
+			fprintf(stderr, "%s: could not accept challenge '%s'\n", argv0, id->valuestring);
+	}
+}
+
+static char *moonfish_username(char *argv0, char *name, char *port, char *token)
+{
+	static char line[8192];
+	static char username[512];
+	
+	br_ssl_client_context ctx;
+	br_sslio_context io_ctx;
+	br_x509_minimal_context min_ctx;
+	void *buffer;
+	int fd;
+	const char *end;
+	cJSON *root, *id;
+	
+	buffer = malloc(BR_SSL_BUFSIZE_BIDI);
+	
+	moonfish_request(
+		&ctx, &io_ctx, &min_ctx,
+		buffer, BR_SSL_BUFSIZE_BIDI,
+		argv0,
+		name, port,
+		token, "GET /api/account",
+		"", 0, &fd
+	);
+	
+	br_sslio_flush(&io_ctx);
+	
+	if (moonfish_response(&ctx.eng, &io_ctx, argv0))
+	{
+		fprintf(stderr, "%s: could not request the Lichess username\n", argv0);
+		exit(1);
+	}
+	
+	moonfish_tls_line(&ctx.eng, &io_ctx, argv0, line, sizeof line);
+	
+	end = NULL;
+	root = cJSON_ParseWithOpts(line, &end, 1);
+	if (end != line + strlen(line)) moonfish_json_error(argv0);
+	
+	if (!cJSON_IsObject(root)) moonfish_json_error(argv0);
+	
+	id = cJSON_GetObjectItem(root, "id");
+	if (!cJSON_IsString(id)) moonfish_json_error(argv0);
+	
+	strcpy(username, id->valuestring);
+	
+	br_sslio_close(&io_ctx);
+	
+	free(buffer);
+	close(fd);
+	
+	return username;
+}
+
+int main(int argc, char **argv)
+{
+	static unsigned char buffer[BR_SSL_BUFSIZE_BIDI];
+	
+	char *name, *port;
+	char *token;
+	br_ssl_client_context ctx;
+	br_sslio_context io_ctx;
+	br_x509_minimal_context min_ctx;
+	int fd;
+	int i;
+	int error;
+	char **command;
+	
+	if (argc < 1) return 1;
+	
+	if (argc < 3)
+	{
+		if (argc > 0)
+		{
+			fprintf(stderr, "usage: %s [<host-name>] [<host-port>] [--] <command>\n", argv[0]);
+			fprintf(stderr, "note: '--' is only optional when both '<host-name>' and '<host-port>' are specified\n");
+		}
+		return 1;
+	}
+	
+	name = "lichess.org";
+	port = "443";
+	
+	token = getenv("lichess_token");
+	if (token == NULL || token[0] == 0)
+	{
+		fprintf(stderr, "%s: Lichess token not provided\n", argv[0]);
+		return 1;
+	}
+	
+	for (i = 0 ; token[i] != 0 ; i++)
+	{
+		if (token[i] <= 0x20 || token[i] >= 0x7F)
+		{
+			fprintf(stderr, "%s: invalid token provided for Lichess\n", argv[0]);
+			return 1;
+		}
+	}
+	
+	if (!strcmp(argv[1], "--"))
+	{
+		command = argv + 2;
+	}
+	else
+	{
+		name = argv[1];
+		if (!strcmp(argv[2], "--"))
+		{
+			command = argv + 3;
+		}
+		else
+		{
+			name = argv[1];
+			if (!strcmp(argv[3], "--"))
+				command = argv + 4;
+			else
+				command = argv + 3;
+		}
+	}
+	
+	if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) return 1;
+	
+	moonfish_request(&ctx, &io_ctx, &min_ctx, buffer, sizeof buffer, argv[0], name, port, token, "GET /api/stream/event", "", 0, &fd);
+	br_sslio_flush(&io_ctx);
+	if (moonfish_response(&ctx.eng, &io_ctx, argv[0]))
+	{
+		fprintf(stderr, "%s: could not request event stream\n", argv[0]);
+		return 1;
+	}
+	
+	moonfish_handle_events(&ctx.eng, &io_ctx, argv[0], name, port, token, command, moonfish_username(argv[0], name, port, token));
+	
+	br_ssl_engine_close(&ctx.eng);
+	close(fd);
+	
+	if (br_ssl_engine_current_state(&ctx.eng) != BR_SSL_CLOSED)
+	{
+		fprintf(stderr,"%s: TLS connection closed improperly\n", argv[0]);
+		return 1;
+	}
+	
+	error = br_ssl_engine_last_error(&ctx.eng);
+	if (error)
+	{
+		fprintf(stderr, "%s: BearSSL error: %d\n", argv[0], error);
+		return 1;
+	}
+	
+	return 0;
+}
--- a/tools/play.c
+++ b/tools/play.c
@@ -9,6 +9,7 @@
 #include <signal.h>
 
 #include "../moonfish.h"
+#include "tools.h"
 
 struct moonfish_fancy
 {
@@ -24,68 +25,6 @@
 	int x, y;
 };
 
-static int moonfish_spawn(char *argv0, char **argv, int *in, int *out)
-{
-	int p1[2], p2[2];
-	int pid;
-	
-	if (pipe(p1) == -1) return 1;
-	if (pipe(p2) == -1) return 1;
-	
-	pid = fork();
-	if (pid == -1) return 1;
-	
-	if (pid)
-	{
-		*in = p1[1];
-		*out = p2[0];
-		close(p1[0]);
-		close(p2[1]);
-		return 0;
-	}
-	
-	dup2(p1[0], 0);
-	dup2(p2[1], 1);
-	
-	close(p1[0]);
-	close(p1[1]);
-	close(p2[0]);
-	close(p2[1]);
-	
-	execvp(argv[0], argv);
-	fprintf(stderr, "%s: %s: %s", argv0, argv[0], strerror(errno));
-	
-	exit(1);
-}
-
-static char *moonfish_next(FILE *file)
-{
-	static char line[2048];
-	
-	for (;;)
-	{
-		if (fgets(line, sizeof line, file) == NULL)
-			return NULL;
-		return line;
-	}
-}
-
-static char *moonfish_wait(FILE *file, char *name)
-{
-	char *line, *arg;
-	
-	for (;;)
-	{
-		line = moonfish_next(file);
-		if (line == NULL) exit(1);
-		
-		arg = strtok(line, "\r\n\t ");
-		if (arg == NULL) continue;
-		if (!strcmp(line, name))
-			return strtok(NULL, "\r\n\t ");
-	}
-}
-
 static void moonfish_fancy_square(struct moonfish_fancy *fancy, int x, int y)
 {
 	unsigned char piece;
@@ -350,7 +289,7 @@
 	
 	fprintf(in, "go wtime %d btime %d", white_time, black_time);
 	if (fancy->increment > 0)
-		fprintf(in, "winc %d binc %d", fancy->increment * 1000, fancy->increment * 1000);
+		fprintf(in, " winc %d binc %d", fancy->increment * 1000, fancy->increment * 1000);
 	fprintf(in, "\n");
 	
 	arg = moonfish_wait(out, "bestmove");
@@ -546,8 +485,8 @@
 		moonfish_go(fancy, names + 1, name, in, out);
 		name += strlen(name);
 		moonfish_reset_time(fancy);
-		pthread_mutex_unlock(fancy->mutex);
 		moonfish_fancy(fancy);
+		pthread_mutex_unlock(fancy->mutex);
 	}
 	
 	printf("\x1B[?1000h");
--- /dev/null
+++ b/tools/utils.c
@@ -1,0 +1,68 @@
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+
+#include "tools.h"
+
+int moonfish_spawn(char *argv0, char **argv, int *in, int *out)
+{
+	int p1[2], p2[2];
+	int pid;
+	
+	if (pipe(p1) == -1) return 1;
+	if (pipe(p2) == -1) return 1;
+	
+	pid = fork();
+	if (pid == -1) return 1;
+	
+	if (pid)
+	{
+		*in = p1[1];
+		*out = p2[0];
+		close(p1[0]);
+		close(p2[1]);
+		return 0;
+	}
+	
+	dup2(p1[0], 0);
+	dup2(p2[1], 1);
+	
+	close(p1[0]);
+	close(p1[1]);
+	close(p2[0]);
+	close(p2[1]);
+	
+	execvp(argv[0], argv);
+	fprintf(stderr, "%s: %s: %s", argv0, argv[0], strerror(errno));
+	
+	exit(1);
+}
+
+char *moonfish_next(FILE *file)
+{
+	static char line[2048];
+	
+	for (;;)
+	{
+		if (fgets(line, sizeof line, file) == NULL)
+			return NULL;
+		return line;
+	}
+}
+
+char *moonfish_wait(FILE *file, char *name)
+{
+	char *line, *arg;
+	
+	for (;;)
+	{
+		line = moonfish_next(file);
+		if (line == NULL) exit(1);
+		
+		arg = strtok(line, "\r\n\t ");
+		if (arg == NULL) continue;
+		if (!strcmp(line, name))
+			return strtok(NULL, "\r\n\t ");
+	}
+}
--