Commit Diff


commit - 4ad9fe9bbeb113cab0fddb1d670252ad6192d589
commit + 50794f47b0bc631e4e0940016fc52a3e82cda62f
blob - /dev/null
blob + 4794a2f4ca164921ce63a6bdb23016d6fbd74264 (mode 755)
--- /dev/null
+++ mimport
@@ -0,0 +1,66 @@
+#!/usr/bin/env perl
+#
+# mimport was written by Omar Polo <op@openbsd.org> and is placed in the
+# public domain.  The author hereby disclaims copyright to this source
+# code.
+
+use strict;
+use warnings;
+use v5.32;
+use utf8;
+
+use Date::Parse;
+use File::Basename;
+
+use OpenBSD::Pledge;
+use OpenBSD::Unveil;
+
+die "usage: $0 dbpath\n" if @ARGV != 1;
+my $dbpath = shift @ARGV;
+
+open(my $sqlite, "|-", "/usr/local/bin/sqlite3", "mails.sqlite")
+    or die "can't spawn sqlite3";
+
+unveil("/usr/local/bin/mshow", "rx") or die "unveil mshow: $!";
+pledge("stdio proc exec") or die "pledge: $!";
+
+say $sqlite ".import --csv /dev/stdin email"
+    or die "can't speak to sqlite: $!";
+
+while (<>) {
+	chomp;
+
+	open(my $fh, "-|", "/usr/local/bin/mshow", "-Atext/plain", "-NF", $_)
+	    or die "can't run mshow $_: $!";
+
+	my $f = $_;
+	my ($time, $id) = split /\./, basename $_;
+	my $mid = "$time.$id";
+	$mid =~ s/"/""/g;
+
+	my ($from, $subj, $date) = ('', '', undef);
+	while (<$fh>) {
+		chomp;
+		last if /^$/;
+		s/"/""/g;
+		$from = s/.*?: //r if /^From:/;
+		$subj = s/.*?: //r if /^Subject:/;
+		$date = str2time(s/.*?: //r) if /^Date:/;
+	}
+	$date //= time;
+	$from =~ s/ +<.*>//;
+
+	# leave open for the body
+	print $sqlite "\"$mid\",\"$from\",\"$date\",\"$subj\",\"";
+
+	while (<$fh>) {
+		s/"/""/g;
+		print $sqlite $_;
+	}
+	say $sqlite '"';
+
+	close $fh;
+}
+
+close $sqlite;
+die "sqlite3 exited with $?\n" unless $? == 0;
blob - 2cfb773abc7b84351ea5161e21fc798c1e6e54f4
blob + 62afc1864a231ce0459c290e24327f06d8b34a78
--- style.css
+++ style.css
@@ -49,6 +49,10 @@ h1 {
     margin: 0 0 1rem 0;
 }
 
+form {
+    text-align: center;
+}
+
 main {
     padding: 5px;
 }
blob - /dev/null
blob + 9865fec98a708439ee382b55d9176a607ff0f523 (mode 644)
--- /dev/null
+++ msearchd/Makefile
@@ -0,0 +1,19 @@
+PROG =		msearchd
+SRCS =		msearchd.c fcgi.c server.c
+
+DEBUG =		-O0 -g -DDEBUG
+
+CDIAGFLAGS =	-Wall -Wuninitialized -Wshadow -Wunused
+CDIAGFLAGS +=	-Wmissing-prototypes -Wstrict-prototypes
+CDIAGFLAGS +=	-Werror
+
+CPPFLAGS +=	-I${.CURDIR}/template
+CPPFLAGS +=	-I/usr/local/include
+
+LIBSQLITE3 =	/usr/local/lib/libsqlite3.a
+
+LDADD = -levent -lsqlite3
+DPADD = ${LIBEVENT} ${LIBSQLITE3}
+LDFLAGS = -L/usr/local/lib
+
+.include <bsd.prog.mk>
blob - /dev/null
blob + 6389a88ec1ab83623efd4b625340b0837fbb7b2b (mode 644)
--- /dev/null
+++ msearchd/fcgi.c
@@ -0,0 +1,776 @@
+/*
+ * This file is in the public domain.
+ */
+
+#include <sys/queue.h>
+#include <sys/tree.h>
+#include <sys/socket.h>
+
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <errno.h>
+#include <event.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "msearchd.h"
+
+#define MIN(a, b)	((a) < (b) ? (a) : (b))
+
+struct fcgi_header {
+	unsigned char version;
+	unsigned char type;
+	unsigned char req_id1;
+	unsigned char req_id0;
+	unsigned char content_len1;
+	unsigned char content_len0;
+	unsigned char padding;
+	unsigned char reserved;
+} __attribute__((packed));
+
+/*
+ * number of bytes in a FCGI_HEADER.  Future version of the protocol
+ * will not reduce this number.
+ */
+#define FCGI_HEADER_LEN	8
+
+/*
+ * values for the version component
+ */
+#define FCGI_VERSION_1	1
+
+/*
+ * values for the type component
+ */
+#define FCGI_BEGIN_REQUEST	 1
+#define FCGI_ABORT_REQUEST	 2
+#define FCGI_END_REQUEST	 3
+#define FCGI_PARAMS		 4
+#define FCGI_STDIN		 5
+#define FCGI_STDOUT		 6
+#define FCGI_STDERR		 7
+#define FCGI_DATA		 8
+#define FCGI_GET_VALUES		 9
+#define FCGI_GET_VALUES_RESULT	10
+#define FCGI_UNKNOWN_TYPE	11
+#define FCGI_MAXTYPE		(FCGI_UNKNOWN_TYPE)
+
+struct fcgi_begin_req {
+	unsigned char role1;
+	unsigned char role0;
+	unsigned char flags;
+	unsigned char reserved[5];
+};
+
+struct fcgi_begin_req_record {
+	struct fcgi_header	header;
+	struct fcgi_begin_req	body;
+};
+
+/*
+ * mask for flags;
+ */
+#define FCGI_KEEP_CONN		1
+
+/*
+ * values for the role
+ */
+#define FCGI_RESPONDER	1
+#define FCGI_AUTHORIZER	2
+#define FCGI_FILTER	3
+
+struct fcgi_end_req_body {
+	unsigned char app_status3;
+	unsigned char app_status2;
+	unsigned char app_status1;
+	unsigned char app_status0;
+	unsigned char proto_status;
+	unsigned char reserved[3];
+};
+
+/*
+ * values for proto_status
+ */
+#define FCGI_REQUEST_COMPLETE	0
+#define FCGI_CANT_MPX_CONN	1
+#define FCGI_OVERLOADED		2
+#define FCGI_UNKNOWN_ROLE	3
+
+/*
+ * Variable names for FCGI_GET_VALUES / FCGI_GET_VALUES_RESULT
+ * records.
+ */
+#define FCGI_MAX_CONNS	"FCGI_MAX_CONNS"
+#define FCGI_MAX_REQS	"FCGI_MAX_REQS"
+#define FCGI_MPXS_CONNS	"FCGI_MPXS_CONNS"
+
+#define CAT(f0, f1)	((f0) + ((f1) << 8))
+
+enum {
+	FCGI_RECORD_HEADER,
+	FCGI_RECORD_BODY,
+};
+
+volatile int	fcgi_inflight;
+int32_t		fcgi_id;
+
+int	accept_reserve(int, struct sockaddr *, socklen_t *, int,
+    volatile int *);
+
+static int
+fcgi_send_end_req(struct fcgi *fcgi, int id, int as, int ps)
+{
+	struct bufferevent	*bev = fcgi->fcg_bev;
+	struct fcgi_header	 hdr;
+	struct fcgi_end_req_body end;
+
+	memset(&hdr, 0, sizeof(hdr));
+	memset(&end, 0, sizeof(end));
+
+	hdr.version = FCGI_VERSION_1;
+	hdr.type = FCGI_END_REQUEST;
+	hdr.req_id0 = (id & 0xFF);
+	hdr.req_id1 = (id >> 8);
+	hdr.content_len0 = sizeof(end);
+
+	end.app_status0 = (unsigned char)as;
+	end.proto_status = (unsigned char)ps;
+
+	if (bufferevent_write(bev, &hdr, sizeof(hdr)) == -1)
+		return (-1);
+	if (bufferevent_write(bev, &end, sizeof(end)) == -1)
+		return (-1);
+	return (0);
+}
+
+static int
+end_request(struct client *clt, int status, int proto_status)
+{
+	struct fcgi		*fcgi = clt->clt_fcgi;
+	int			 r;
+
+	if (clt_flush(clt) == -1)
+		return (-1);
+
+	r = fcgi_send_end_req(fcgi, clt->clt_id, status,
+	    proto_status);
+	if (r == -1) {
+		fcgi_error(fcgi->fcg_bev, EV_WRITE, fcgi);
+		return (-1);
+	}
+
+	SPLAY_REMOVE(client_tree, &fcgi->fcg_clients, clt);
+	server_client_free(clt);
+
+	if (!fcgi->fcg_keep_conn)
+		fcgi->fcg_done = 1;
+
+	return (0);
+}
+
+int
+fcgi_end_request(struct client *clt, int status)
+{
+	return (end_request(clt, status, FCGI_REQUEST_COMPLETE));
+}
+
+int
+fcgi_abort_request(struct client *clt)
+{
+	return (end_request(clt, 1, FCGI_OVERLOADED));
+}
+
+static void
+fcgi_inflight_dec(const char *why)
+{
+	fcgi_inflight--;
+	log_debug("%s: fcgi inflight decremented, now %d, %s",
+	    __func__, fcgi_inflight, why);
+}
+
+void
+fcgi_accept(int fd, short event, void *arg)
+{
+	struct env		*env = arg;
+	struct fcgi		*fcgi = NULL;
+	socklen_t		 slen;
+	struct sockaddr_storage	 ss;
+	int			 s = -1;
+
+	event_add(&env->env_pausev, NULL);
+	if ((event & EV_TIMEOUT))
+		return;
+
+	slen = sizeof(ss);
+	if ((s = accept_reserve(env->env_sockfd, (struct sockaddr *)&ss,
+	    &slen, FD_RESERVE, &fcgi_inflight)) == -1) {
+		/*
+		 * Pause accept if we are out of file descriptors, or
+		 * libevent will haunt us here too.
+		 */
+		if (errno == ENFILE || errno == EMFILE) {
+			struct timeval evtpause = { 1, 0 };
+
+			event_del(&env->env_sockev);
+			evtimer_add(&env->env_pausev, &evtpause);
+			log_debug("%s: deferring connections", __func__);
+		}
+		return;
+	}
+
+	if ((fcgi = calloc(1, sizeof(*fcgi))) == NULL)
+		goto err;
+
+	fcgi->fcg_id = ++fcgi_id;
+	fcgi->fcg_s = s;
+	fcgi->fcg_env = env;
+	fcgi->fcg_want = FCGI_RECORD_HEADER;
+	fcgi->fcg_toread = sizeof(struct fcgi_header);
+	SPLAY_INIT(&fcgi->fcg_clients);
+
+	/* assume it's enabled until we get a FCGI_BEGIN_REQUEST */
+	fcgi->fcg_keep_conn = 1;
+
+	fcgi->fcg_bev = bufferevent_new(fcgi->fcg_s, fcgi_read, fcgi_write,
+	    fcgi_error, fcgi);
+	if (fcgi->fcg_bev == NULL)
+		goto err;
+
+	bufferevent_enable(fcgi->fcg_bev, EV_READ | EV_WRITE);
+	return;
+
+err:
+	if (s != -1) {
+		close(s);
+		free(fcgi);
+		fcgi_inflight_dec(__func__);
+	}
+}
+
+static int
+parse_len(struct fcgi *fcgi, struct evbuffer *src)
+{
+	unsigned char		 c, x[3];
+
+	fcgi->fcg_toread--;
+	evbuffer_remove(src, &c, 1);
+	if (c >> 7 == 0)
+		return (c);
+
+	if (fcgi->fcg_toread < 3)
+		return (-1);
+
+	fcgi->fcg_toread -= 3;
+	evbuffer_remove(src, x, sizeof(x));
+	return (((c & 0x7F) << 24) | (x[0] << 16) | (x[1] << 8) | x[2]);
+}
+
+static int
+fcgi_parse_params(struct fcgi *fcgi, struct evbuffer *src, struct client *clt)
+{
+	char			 pname[32];
+	char			 server[HOST_NAME_MAX + 1];
+	char			 path[PATH_MAX];
+	char			 query[QUERY_MAXLEN];
+	char			 method[8];
+	int			 nlen, vlen;
+
+	while (fcgi->fcg_toread > 0) {
+		if ((nlen = parse_len(fcgi, src)) < 0 ||
+		    (vlen = parse_len(fcgi, src)) < 0)
+			return (-1);
+
+		if (fcgi->fcg_toread < nlen + vlen)
+			return (-1);
+
+		if ((size_t)nlen > sizeof(pname) - 1) {
+			/* ignore this parameter */
+			fcgi->fcg_toread -= nlen - vlen;
+			evbuffer_drain(src, nlen + vlen);
+			continue;
+		}
+
+		fcgi->fcg_toread -= nlen;
+		evbuffer_remove(src, &pname, nlen);
+		pname[nlen] = '\0';
+
+		if (!strcmp(pname, "SERVER_NAME") &&
+		    (size_t)vlen < sizeof(server)) {
+			fcgi->fcg_toread -= vlen;
+			evbuffer_remove(src, &server, vlen);
+			server[vlen] = '\0';
+
+			free(clt->clt_server_name);
+			if ((clt->clt_server_name = strdup(server)) == NULL)
+				return (-1);
+			DPRINTF("clt %d: server_name: %s", clt->clt_id,
+			    clt->clt_server_name);
+			continue;
+		}
+
+		if (!strcmp(pname, "SCRIPT_NAME") &&
+		    (size_t)vlen < sizeof(path)) {
+			fcgi->fcg_toread -= vlen;
+			evbuffer_remove(src, &path, vlen);
+			path[vlen] = '\0';
+
+			free(clt->clt_script_name);
+			clt->clt_script_name = NULL;
+
+			if (vlen == 0 || path[vlen - 1] != '/')
+				asprintf(&clt->clt_script_name, "%s/", path);
+			else
+				clt->clt_script_name = strdup(path);
+
+			if (clt->clt_script_name == NULL)
+				return (-1);
+
+			DPRINTF("clt %d: script_name: %s", clt->clt_id,
+			    clt->clt_script_name);
+			continue;
+		}
+
+		if (!strcmp(pname, "PATH_INFO") &&
+		    (size_t)vlen < sizeof(path)) {
+			fcgi->fcg_toread -= vlen;
+			evbuffer_remove(src, &path, vlen);
+			path[vlen] = '\0';
+
+			free(clt->clt_path_info);
+			clt->clt_path_info = NULL;
+
+			if (*path != '/')
+				asprintf(&clt->clt_path_info, "/%s", path);
+			else
+				clt->clt_path_info = strdup(path);
+
+			if (clt->clt_path_info == NULL)
+				return (-1);
+
+			DPRINTF("clt %d: path_info: %s", clt->clt_id,
+			    clt->clt_path_info);
+			continue;
+		}
+
+		if (!strcmp(pname, "QUERY_STRING") &&
+		    (size_t)vlen < sizeof(query) &&
+		    vlen > 0) {
+			fcgi->fcg_toread -= vlen;
+			evbuffer_remove(src, &query, vlen);
+			query[vlen] = '\0';
+
+			free(clt->clt_query);
+			if ((clt->clt_query = strdup(query)) == NULL)
+				return (-1);
+
+			DPRINTF("clt %d: query: %s", clt->clt_id,
+			    clt->clt_query);
+			continue;
+		}
+
+		if (!strcmp(pname, "REQUEST_METHOD") &&
+		    (size_t)vlen < sizeof(method)) {
+			fcgi->fcg_toread -= vlen;
+			evbuffer_remove(src, &method, vlen);
+			method[vlen] = '\0';
+
+			if (!strcasecmp(method, "GET"))
+				clt->clt_method = METHOD_GET;
+			if (!strcasecmp(method, "POST"))
+				clt->clt_method = METHOD_POST;
+
+			continue;
+		}
+
+		fcgi->fcg_toread -= vlen;
+		evbuffer_drain(src, vlen);
+	}
+
+	return (0);
+}
+
+void
+fcgi_read(struct bufferevent *bev, void *d)
+{
+	struct fcgi		*fcgi = d;
+	struct env		*env = fcgi->fcg_env;
+	struct evbuffer		*src = EVBUFFER_INPUT(bev);
+	struct fcgi_header	 hdr;
+	struct fcgi_begin_req	 breq;
+	struct client		*clt, q;
+	int			 role;
+
+	memset(&q, 0, sizeof(q));
+
+	for (;;) {
+		if (EVBUFFER_LENGTH(src) < (size_t)fcgi->fcg_toread)
+			return;
+
+		if (fcgi->fcg_want == FCGI_RECORD_HEADER) {
+			fcgi->fcg_want = FCGI_RECORD_BODY;
+			bufferevent_read(bev, &hdr, sizeof(hdr));
+
+			DPRINTF("header: v=%d t=%d id=%d len=%d p=%d",
+			    hdr.version, hdr.type,
+			    CAT(hdr.req_id0, hdr.req_id1),
+			    CAT(hdr.content_len0, hdr.content_len1),
+			    hdr.padding);
+
+			if (hdr.version != FCGI_VERSION_1) {
+				log_warnx("unknown fastcgi version: %d",
+				    hdr.version);
+				fcgi_error(bev, EV_READ, d);
+				return;
+			}
+
+			fcgi->fcg_toread = CAT(hdr.content_len0,
+			    hdr.content_len1);
+			if (fcgi->fcg_toread < 0) {
+				log_warnx("invalid record length: %d",
+				    fcgi->fcg_toread);
+				fcgi_error(bev, EV_READ, d);
+				return;
+			}
+
+			fcgi->fcg_padding = hdr.padding;
+			if (fcgi->fcg_padding < 0) {
+				log_warnx("invalid padding: %d",
+				    fcgi->fcg_padding);
+				fcgi_error(bev, EV_READ, d);
+				return;
+			}
+
+			fcgi->fcg_type = hdr.type;
+			fcgi->fcg_rec_id = CAT(hdr.req_id0, hdr.req_id1);
+			continue;
+		}
+
+		q.clt_id = fcgi->fcg_rec_id;
+		clt = SPLAY_FIND(client_tree, &fcgi->fcg_clients, &q);
+
+		switch (fcgi->fcg_type) {
+		case FCGI_BEGIN_REQUEST:
+			if (sizeof(breq) != fcgi->fcg_toread) {
+				log_warnx("unexpected size for "
+				    "FCGI_BEGIN_REQUEST");
+				fcgi_error(bev, EV_READ, d);
+				return;
+			}
+
+			evbuffer_remove(src, &breq, sizeof(breq));
+
+			role = CAT(breq.role0, breq.role1);
+			if (role != FCGI_RESPONDER) {
+				log_warnx("unknown fastcgi role: %d",
+				    role);
+				if (fcgi_send_end_req(fcgi, fcgi->fcg_rec_id,
+				    1, FCGI_UNKNOWN_ROLE) == -1) {
+					fcgi_error(bev, EV_READ, d);
+					return;
+				}
+				break;
+			}
+
+			if (!fcgi->fcg_keep_conn) {
+				log_warnx("trying to reuse the fastcgi "
+				    "socket without marking it as so.");
+				fcgi_error(bev, EV_READ, d);
+				return;
+			}
+			fcgi->fcg_keep_conn = breq.flags & FCGI_KEEP_CONN;
+
+			if (clt != NULL) {
+				log_warnx("ignoring attemp to re-use an "
+				    "active request id (%d)",
+				    fcgi->fcg_rec_id);
+				break;
+			}
+
+			if ((clt = calloc(1, sizeof(*clt))) == NULL) {
+				log_warnx("calloc");
+				break;
+			}
+
+			clt->clt_id = fcgi->fcg_rec_id;
+			clt->clt_fd = -1;
+			clt->clt_fcgi = fcgi;
+			SPLAY_INSERT(client_tree, &fcgi->fcg_clients, clt);
+			break;
+		case FCGI_PARAMS:
+			if (clt == NULL) {
+				log_warnx("got FCGI_PARAMS for inactive id "
+				    "(%d)", fcgi->fcg_rec_id);
+				evbuffer_drain(src, fcgi->fcg_toread);
+				break;
+			}
+			if (fcgi->fcg_toread == 0) {
+				evbuffer_drain(src, fcgi->fcg_toread);
+				if (server_handle(env, clt) == -1)
+					return;
+				break;
+			}
+			if (fcgi_parse_params(fcgi, src, clt) == -1) {
+				log_warnx("fcgi_parse_params failed");
+				fcgi_error(bev, EV_READ, d);
+				return;
+			}
+			break;
+		case FCGI_STDIN:
+			/* not interested in reading stdin */
+			evbuffer_drain(src, fcgi->fcg_toread);
+			break;
+		case FCGI_ABORT_REQUEST:
+			if (clt == NULL) {
+				log_warnx("got FCGI_ABORT_REQUEST for inactive"
+				    " id (%d)", fcgi->fcg_rec_id);
+				evbuffer_drain(src, fcgi->fcg_toread);
+				break;
+			}
+			if (fcgi_end_request(clt, 1) == -1) {
+				/* calls fcgi_error on failure */
+				return;
+			}
+			break;
+		default:
+			log_warnx("unknown fastcgi record type %d",
+			    fcgi->fcg_type);
+			evbuffer_drain(src, fcgi->fcg_toread);
+			break;
+		}
+
+		/* Prepare for the next record. */
+		evbuffer_drain(src, fcgi->fcg_padding);
+		fcgi->fcg_want = FCGI_RECORD_HEADER;
+		fcgi->fcg_toread = sizeof(struct fcgi_header);
+	}
+}
+
+void
+fcgi_write(struct bufferevent *bev, void *d)
+{
+	struct fcgi		*fcgi = d;
+	struct evbuffer		*out = EVBUFFER_OUTPUT(bev);
+
+	if (fcgi->fcg_done && EVBUFFER_LENGTH(out) == 0)
+		fcgi_error(bev, EVBUFFER_EOF, fcgi);
+}
+
+void
+fcgi_error(struct bufferevent *bev, short event, void *d)
+{
+	struct fcgi		*fcgi = d;
+	struct env		*env = fcgi->fcg_env;
+	struct client		*clt;
+
+	log_debug("fcgi failure, shutting down connection (ev: %x)",
+	    event);
+	fcgi_inflight_dec(__func__);
+
+	while ((clt = SPLAY_MIN(client_tree, &fcgi->fcg_clients)) != NULL) {
+		SPLAY_REMOVE(client_tree, &fcgi->fcg_clients, clt);
+		server_client_free(clt);
+	}
+
+	SPLAY_REMOVE(fcgi_tree, &env->env_fcgi_socks, fcgi);
+	fcgi_free(fcgi);
+
+	return;
+}
+
+void
+fcgi_free(struct fcgi *fcgi)
+{
+	close(fcgi->fcg_s);
+	bufferevent_free(fcgi->fcg_bev);
+	free(fcgi);
+}
+
+int
+clt_flush(struct client *clt)
+{
+	struct fcgi		*fcgi = clt->clt_fcgi;
+	struct bufferevent	*bev = fcgi->fcg_bev;
+	struct fcgi_header	 hdr;
+
+	if (clt->clt_buflen == 0)
+		return (0);
+
+	memset(&hdr, 0, sizeof(hdr));
+	hdr.version = FCGI_VERSION_1;
+	hdr.type = FCGI_STDOUT;
+	hdr.req_id0 = (clt->clt_id & 0xFF);
+	hdr.req_id1 = (clt->clt_id >> 8);
+	hdr.content_len0 = (clt->clt_buflen & 0xFF);
+	hdr.content_len1 = (clt->clt_buflen >> 8);
+
+	if (bufferevent_write(bev, &hdr, sizeof(hdr)) == -1 ||
+	    bufferevent_write(bev, clt->clt_buf, clt->clt_buflen) == -1) {
+		fcgi_error(bev, EV_WRITE, fcgi);
+		return (-1);
+	}
+
+	clt->clt_buflen = 0;
+
+	return (0);
+}
+
+int
+clt_write(struct client *clt, const uint8_t *buf, size_t len)
+{
+	size_t			 left, copy;
+
+	while (len > 0) {
+		left = sizeof(clt->clt_buf) - clt->clt_buflen;
+		if (left == 0) {
+			if (clt_flush(clt) == -1)
+				return (-1);
+			left = sizeof(clt->clt_buf);
+		}
+
+		copy = MIN(left, len);
+
+		memcpy(&clt->clt_buf[clt->clt_buflen], buf, copy);
+		clt->clt_buflen += copy;
+		buf += copy;
+		len -= copy;
+	}
+
+	return (0);
+}
+
+int
+clt_putc(struct client *clt, char ch)
+{
+	return (clt_write(clt, &ch, 1));
+}
+
+int
+clt_puts(struct client *clt, const char *str)
+{
+	return (clt_write(clt, str, strlen(str)));
+}
+
+int
+clt_write_bufferevent(struct client *clt, struct bufferevent *bev)
+{
+	struct evbuffer		*src = EVBUFFER_INPUT(bev);
+	size_t			 len, left, copy;
+
+	len = EVBUFFER_LENGTH(src);
+	while (len > 0) {
+		left = sizeof(clt->clt_buf) - clt->clt_buflen;
+		if (left == 0) {
+			if (clt_flush(clt) == -1)
+				return (-1);
+			left = sizeof(clt->clt_buf);
+		}
+
+		copy = bufferevent_read(bev, &clt->clt_buf[clt->clt_buflen],
+		    MIN(left, len));
+		clt->clt_buflen += copy;
+
+		len = EVBUFFER_LENGTH(src);
+	}
+
+	return (0);
+}
+
+int
+clt_putsan(struct client *clt, const char *s)
+{
+	int	r;
+
+	if (s == NULL)
+		return (0);
+
+	for (; *s; ++s) {
+		switch (*s) {
+		case '<':
+			r = clt_puts(clt, "&lt;");
+			break;
+		case '>':
+			r = clt_puts(clt, "&gt;");
+			break;
+		case '&':
+			r = clt_puts(clt, "&amp;");
+			break;
+		case '"':
+			r = clt_puts(clt, "&quot;");
+			break;
+		case '\'':
+			r = clt_puts(clt, "&apos;");
+			break;
+		default:
+			r = clt_putc(clt, *s);
+			break;
+		}
+
+		if (r == -1)
+			return (-1);
+	}
+
+	return (0);
+}
+
+int
+clt_printf(struct client *clt, const char *fmt, ...)
+{
+	struct fcgi		*fcgi = clt->clt_fcgi;
+	struct bufferevent	*bev = fcgi->fcg_bev;
+	char			*str;
+	va_list			 ap;
+	int			 r;
+
+	va_start(ap, fmt);
+	r = vasprintf(&str, fmt, ap);
+	va_end(ap);
+	if (r == -1) {
+		fcgi_error(bev, EV_WRITE, fcgi);
+		return (-1);
+	}
+
+	r = clt_write(clt, str, r);
+	free(str);
+	return (r);
+}
+
+int
+fcgi_cmp(struct fcgi *a, struct fcgi *b)
+{
+	return ((int)a->fcg_id - b->fcg_id);
+}
+
+int
+fcgi_client_cmp(struct client *a, struct client *b)
+{
+	return ((int)a->clt_id - b->clt_id);
+}
+
+int
+accept_reserve(int sockfd, struct sockaddr *addr, socklen_t *addrlen,
+    int reserve, volatile int *counter)
+{
+	int ret;
+
+	if (getdtablecount() + reserve + *counter >= getdtablesize()) {
+		errno = EMFILE;
+		return (-1);
+	}
+
+	if ((ret = accept4(sockfd, addr, addrlen, SOCK_NONBLOCK)) > -1) {
+		(*counter)++;
+		log_debug("%s: inflight incremented, now %d", __func__,
+		    *counter);
+	}
+
+	return (ret);
+}
+
+SPLAY_GENERATE(fcgi_tree, fcgi, fcg_nodes, fcgi_cmp);
+SPLAY_GENERATE(client_tree, client, clt_nodes, fcgi_client_cmp);
blob - /dev/null
blob + b130fd67c91acce9727918c7af0be8d4aa4787ba (mode 644)
--- /dev/null
+++ msearchd/msearchd.8
@@ -0,0 +1,96 @@
+.\" This file is in the public domain.
+.Dd April 4, 2023
+.Dt MSEARCHD 8
+.Os
+.Sh NAME
+.Nm msearchd
+.Nd FastCGI mail archive query server
+.Sh SYNOPSIS
+.Nm
+.Op Fl dv
+.Op Fl j Ar n
+.Op Fl p Ar path
+.Op Fl s Ar socket
+.Op Fl u Ar user
+.Op Ar db
+.Sh DESCRIPTION
+.Nm
+is a server which implements the FastCGI Protocol to provide search
+facilities for the mail archive.
+.Pp
+It opens a socket at
+.Pa /var/www/run/msearchd.sock ,
+owned by www:www with permissions 0660.
+It will then
+.Xr chroot 8
+to
+.Pa /var/www
+and drop privileges to user
+.Dq www .
+Three child processes are ran to handle the incoming traffic on the
+FastCGI socket.
+Upon
+.Dv SIGHUP
+the database is closed and re-opened.
+The default database used is at
+.Pa /msearchd/mails.sqlite3
+inside the chroot.
+.Pp
+The options are as follows:
+.Bl -tag -width Ds
+.It Fl d
+Do not daemonize.
+If this option is specified,
+.Nm
+will run in the foreground and log to standard error.
+.It Fl j Ar n
+Run
+.Ar n
+child processes.
+.It Fl p Ar path
+.Xr chroot 2
+to
+.Ar path .
+A
+.Ar path
+of
+.Pa /
+effectively disables the chroot.
+.It Fl s Ar socket
+Create an bind to the local socket at
+.Ar socket .
+.It Fl u Ar user
+Drop privileges to
+.Ar user
+instead of default user www and
+.Xr chroot 8
+to their home directory.
+.It Fl v
+Enable more verbose (debug) logging.
+Multiple
+.Fl v
+options increase the verbosity.
+.El
+.Sh FILES
+.Bl -tag -width /var/www/msearchd/mails.sqlite3 -compact
+.It Pa /var/www/msearchd/mails.sqite3
+Default database.
+.It Pa /var/www/run/msearchd.sock
+.Ux Ns -domain socket.
+.El
+.Sh EXAMPLES
+Example configuration for
+.Xr httpd.conf 5 :
+.Bd -literal -offset -indent
+server "localhost" {
+	listen on * port 80
+	root "/marc"
+	location "/search" {
+		fastcgi socket "/run/msearchd.sock"
+	}
+}
+.Ed
+.Sh SEE ALSO
+.Xr httpd 8
+.Sh AUTHORS
+.An Omar Polo Aq Mt op@openbsd.org
blob - /dev/null
blob + 654882d205924e3b21e6fb2e329f47612220a99a (mode 644)
--- /dev/null
+++ msearchd/msearchd.c
@@ -0,0 +1,462 @@
+/*
+ * This file is in the public domain.
+ */
+
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/tree.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <sys/wait.h>
+
+#include <err.h>
+#include <errno.h>
+#include <event.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <pwd.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#include "msearchd.h"
+
+#ifndef MSEARCHD_DB
+#define MSEARCHD_DB "/msearchd/mails.sqlite3"
+#endif
+
+#ifndef MSEARCHD_SOCK
+#define MSEARCHD_SOCK "/run/msearchd.sock"
+#endif
+
+#ifndef MSEARCHD_USER
+#define MSEARCHD_USER "www"
+#endif
+
+#define MAX_CHILDREN 32
+
+int	debug;
+int	verbose;
+int	children = 3;
+pid_t	pids[MAX_CHILDREN];
+
+__dead void	srch_syslog_fatal(int, const char *, ...);
+__dead void	srch_syslog_fatalx(int, const char *, ...);
+void		srch_syslog_warn(const char *, ...);
+void		srch_syslog_warnx(const char *, ...);
+void		srch_syslog_info(const char *, ...);
+void		srch_syslog_debug(const char *, ...);
+
+const struct logger syslogger = {
+	.fatal =	&srch_syslog_fatal,
+	.fatalx =	&srch_syslog_fatalx,
+	.warn =		&srch_syslog_warn,
+	.warnx =	&srch_syslog_warnx,
+	.info =		&srch_syslog_info,
+	.debug =	&srch_syslog_debug,
+};
+
+const struct logger dbglogger = {
+	.fatal =	&err,
+	.fatalx =	&errx,
+	.warn =		&warn,
+	.warnx =	&warnx,
+	.info =		&warnx,
+	.debug =	&warnx,
+};
+
+const struct logger *logger = &dbglogger;
+
+static void
+handle_sigchld(int sig)
+{
+	static volatile sig_atomic_t got_sigchld;
+	int	i, save_errno;
+
+	if (got_sigchld)
+		return;
+	got_sigchld = -1;
+
+	save_errno = errno;
+	for (i = 0; i < children; ++i)
+		(void)kill(pids[i], SIGTERM);
+	errno = save_errno;
+}
+
+static int
+bind_socket(const char *path, struct passwd *pw)
+{
+	struct sockaddr_un	 sun;
+	int			 fd, old_umask;
+
+	if ((fd = socket(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK, 0)) == -1) {
+		log_warn("%s: socket", __func__);
+		return (-1);
+	}
+
+	memset(&sun, 0, sizeof(sun));
+	sun.sun_family = AF_UNIX;
+
+	if (strlcpy(sun.sun_path, path, sizeof(sun.sun_path)) >=
+	    sizeof(sun.sun_path)) {
+		log_warnx("%s: path too long: %s", __func__, path);
+		close(fd);
+		return (-1);
+	}
+
+	if (unlink(path) == -1 && errno != ENOENT) {
+		log_warn("%s: unlink %s", __func__, path);
+		close(fd);
+		return (-1);
+	}
+
+	old_umask = umask(0117);
+	if (bind(fd, (struct sockaddr *)&sun, sizeof(sun)) == -1) {
+		log_warn("%s: bind: %s (%d)", __func__, path, geteuid());
+		close(fd);
+		umask(old_umask);
+		return (-1);
+	}
+	umask(old_umask);
+
+	if (chmod(path, 0660) == -1) {
+		log_warn("%s: chmod 0660 %s", __func__, path);
+		close(fd);
+		(void)unlink(path);
+		return (-1);
+	}
+
+	if (chown(path, pw->pw_uid, pw->pw_gid) == -1) {
+		log_warn("%s: chown %s %s", __func__, pw->pw_name, path);
+		close(fd);
+		(void)unlink(path);
+		return (-1);
+	}
+
+	if (listen(fd, 5) == -1) {
+		log_warn("%s: listen", __func__);
+		close(fd);
+		(void)unlink(path);
+		return (-1);
+	}
+
+	return (fd);
+}
+
+static pid_t
+start_child(const char *argv0, const char *root, const char *user,
+    const char *db, int debug, int verobes, int fd)
+{
+	const char	*argv[11];
+	int		 argc = 0;
+	pid_t		 pid;
+
+	switch (pid = fork()) {
+	case -1:
+		fatal("cannot fork");
+	case 0:
+		break;
+	default:
+		close(fd);
+		return (pid);
+	}
+
+	if (fd != 3) {
+		if (dup2(fd, 3) == -1)
+			fatal("cannot setup socket fd");
+	} else if (fcntl(fd, F_SETFD, 0) == -1)
+		fatal("cannot setup socket fd");
+
+	argv[argc++] = argv0;
+	argv[argc++] = "-S";
+	argv[argc++] = "-p"; argv[argc++] = root;
+	argv[argc++] = "-u"; argv[argc++] = user;
+	if (debug)
+		argv[argc++] = "-d";
+	if (verbose--)
+		argv[argc++] = "-v";
+	if (verbose--)
+		argv[argc++] = "-v";
+	argv[argc++] = db;
+	argv[argc++] = NULL;
+
+	/* obnoxious cast */
+	execvp(argv0, (char * const *) argv);
+	fatal("execvp %s", argv0);
+}
+
+static void __dead
+usage(void)
+{
+	fprintf(stderr,
+	    "usage: %s [-dv] [-j n] [-p path] [-s socket] [-u user] [db]\n",
+	    getprogname());
+	exit(1);
+}
+
+int
+main(int argc, char **argv)
+{
+	struct stat	 sb;
+	struct passwd	*pw;
+	char		 sockp[PATH_MAX];
+	const char	*sock = MSEARCHD_SOCK;
+	const char	*user = MSEARCHD_USER;
+	const char	*root = NULL;
+	const char	*db = MSEARCHD_DB;
+	const char	*errstr, *cause, *argv0;
+	pid_t		 pid;
+	int		 ch, i, fd, ret, status, server = 0;
+
+	/*
+	 * Ensure we have fds 0-2 open so that we have no issue with
+	 * calling bind_socket before daemon(3).
+	 */
+	for (i = 0; i < 3; ++i) {
+		if (fstat(i, &sb) == -1) {
+			if ((fd = open("/dev/null", O_RDWR)) != -1) {
+				if (dup2(fd, i) == -1)
+					exit(1);
+				if (fd > i)
+					close(fd);
+			} else
+				exit(1);
+		}
+	}
+
+	if ((argv0 = argv[0]) == NULL)
+		argv0 = "msearchd";
+
+	while ((ch = getopt(argc, argv, "dj:p:Ss:u:v")) != -1) {
+		switch (ch) {
+		case 'd':
+			debug = 1;
+			break;
+		case 'j':
+			children = strtonum(optarg, 1, MAX_CHILDREN, &errstr);
+			if (errstr)
+				fatalx("number of children is %s: %s",
+				    errstr, optarg);
+			break;
+		case 'p':
+			root = optarg;
+			break;
+		case 'S':
+			server = 1;
+			break;
+		case 's':
+			sock = optarg;
+			break;
+		case 'u':
+			user = optarg;
+			break;
+		case 'v':
+			verbose++;
+			break;
+		default:
+			usage();
+		}
+	}
+	argc -= optind;
+	argv += optind;
+
+	if (argc > 0) {
+		db = argv[0];
+		argv++;
+		argc--;
+	}
+	if (argc != 0)
+		usage();
+
+	if (geteuid())
+		fatalx("need root privileges");
+
+	pw = getpwnam(user);
+	if (pw == NULL)
+		fatalx("user %s not found", user);
+	if (pw->pw_uid == 0)
+		fatalx("cannot run as %s: must not be the superuser", user);
+
+	if (root == NULL)
+		root = pw->pw_dir;
+
+	if (!server) {
+		sigset_t set;
+
+		sigemptyset(&set);
+		sigaddset(&set, SIGCHLD);
+		sigprocmask(SIG_BLOCK, &set, NULL);
+
+		ret = snprintf(sockp, sizeof(sockp), "%s/%s", root, sock);
+		if (ret < 0 || (size_t)ret >= sizeof(sockp))
+			fatalx("socket path too long");
+		if ((fd = bind_socket(sockp, pw)) == -1)
+			fatalx("failed to open socket %s", sock);
+		for (i = 0; i < children; ++i) {
+			int d;
+
+			if ((d = dup(fd)) == -1)
+				fatalx("dup");
+			pids[i] = start_child(argv0, root, user, db, debug,
+			    verbose, d);
+			log_debug("forking child %d (pid %lld)", i,
+			    (long long)pids[i]);
+		}
+
+		signal(SIGCHLD, handle_sigchld);
+		signal(SIGHUP, SIG_IGN);
+
+		sigprocmask(SIG_UNBLOCK, &set, NULL);
+	}
+
+	if (chroot(root) == -1)
+		fatal("chroot %s", root);
+	if (chdir("/") == -1)
+		fatal("chdir /");
+
+	if (setgroups(1, &pw->pw_gid) == -1 ||
+	    setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) == -1 ||
+	    setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) == -1)
+		fatal("failed to drop privileges");
+
+	if (!debug)
+		logger = &syslogger;
+
+	if (server)
+		return (server_main(db));
+
+	if (!debug && daemon(1, 0) == -1)
+		fatal("daemon");
+
+	if (pledge("stdio proc", NULL) == -1)
+		fatal("pledge");
+
+	for (;;) {
+		do {
+			pid = waitpid(WAIT_ANY, &status, 0);
+		} while (pid != -1 || errno == EINTR);
+
+		if (pid == -1) {
+			if (errno == ECHILD)
+				break;
+			fatal("waitpid");
+		}
+
+		if (WIFSIGNALED(status))
+			cause = "was terminated";
+		else if (WIFEXITED(status)) {
+			if (WEXITSTATUS(status) != 0)
+				cause = "exited abnormally";
+			else
+				cause = "exited successfully";
+		} else
+			cause = "died";
+
+		log_warnx("child process %lld %s", (long long)pid, cause);
+	}
+
+	return (1);
+}
+
+__dead void
+srch_syslog_fatal(int eval, const char *fmt, ...)
+{
+	static char	 s[BUFSIZ];
+	va_list		 ap;
+	int		 r, save_errno;
+
+	save_errno = errno;
+
+	va_start(ap, fmt);
+	r = vsnprintf(s, sizeof(s), fmt, ap);
+	va_end(ap);
+
+	errno = save_errno;
+
+	if (r > 0 && (size_t)r <= sizeof(s))
+		syslog(LOG_DAEMON|LOG_CRIT, "%s: %s", s, strerror(errno));
+
+	exit(eval);
+}
+
+__dead void
+srch_syslog_fatalx(int eval, const char *fmt, ...)
+{
+	va_list		 ap;
+
+	va_start(ap, fmt);
+	vsyslog(LOG_DAEMON|LOG_CRIT, fmt, ap);
+	va_end(ap);
+
+	exit(eval);
+}
+
+void
+srch_syslog_warn(const char *fmt, ...)
+{
+	static char	 s[BUFSIZ];
+	va_list		 ap;
+	int		 r, save_errno;
+
+	save_errno = errno;
+
+	va_start(ap, fmt);
+	r = vsnprintf(s, sizeof(s), fmt, ap);
+	va_end(ap);
+
+	errno = save_errno;
+
+	if (r > 0 && (size_t)r <= sizeof(s))
+		syslog(LOG_DAEMON|LOG_ERR, "%s: %s", s, strerror(errno));
+
+	errno = save_errno;
+}
+
+void
+srch_syslog_warnx(const char *fmt, ...)
+{
+	va_list		 ap;
+	int		 save_errno;
+
+	save_errno = errno;
+	va_start(ap, fmt);
+	vsyslog(LOG_DAEMON|LOG_ERR, fmt, ap);
+	va_end(ap);
+	errno = save_errno;
+}
+
+void
+srch_syslog_info(const char *fmt, ...)
+{
+	va_list		 ap;
+	int		 save_errno;
+
+	if (verbose < 1)
+		return;
+
+	save_errno = errno;
+	va_start(ap, fmt);
+	vsyslog(LOG_DAEMON|LOG_INFO, fmt, ap);
+	va_end(ap);
+	errno = save_errno;
+}
+
+void
+srch_syslog_debug(const char *fmt, ...)
+{
+	va_list		 ap;
+	int		 save_errno;
+
+	if (verbose < 2)
+		return;
+
+	save_errno = errno;
+	va_start(ap, fmt);
+	vsyslog(LOG_DAEMON|LOG_DEBUG, fmt, ap);
+	va_end(ap);
+	errno = save_errno;
+}
blob - /dev/null
blob + 47503ed5c28b0c755cf05499e59e260a30933d5f (mode 644)
--- /dev/null
+++ msearchd/msearchd.h
@@ -0,0 +1,117 @@
+/*
+ * This file is in the public domain.
+ */
+
+#define FD_RESERVE	5
+#define QUERY_MAXLEN	1025	/* including NUL */
+
+struct bufferevent;
+struct event;
+struct fcgi;
+struct sqlite3;
+struct sqlite3_stmt;
+struct template;
+
+enum {
+	METHOD_UNKNOWN,
+	METHOD_GET,
+	METHOD_POST,
+};
+
+#define ATTR_PRINTF(A, B) __attribute__((__format__ (printf, A, B)))
+
+struct logger {
+	__dead void (*fatal)(int, const char *, ...)	ATTR_PRINTF(2, 3);
+	__dead void (*fatalx)(int, const char *, ...)	ATTR_PRINTF(2, 3);
+	void (*warn)(const char *, ...)			ATTR_PRINTF(1, 2);
+	void (*warnx)(const char *, ...)		ATTR_PRINTF(1, 2);
+	void (*info)(const char *, ...)			ATTR_PRINTF(1, 2);
+	void (*debug)(const char *, ...)		ATTR_PRINTF(1, 2);
+};
+
+extern const struct logger *logger;
+#define fatal(...)	logger->fatal(1, __VA_ARGS__)
+#define fatalx(...)	logger->fatalx(1, __VA_ARGS__)
+#define log_warn(...)	logger->warn(__VA_ARGS__)
+#define log_warnx(...)	logger->warnx(__VA_ARGS__)
+#define log_info(...)	logger->info(__VA_ARGS__)
+#define log_debug(...)	logger->debug(__VA_ARGS__)
+
+#ifdef DEBUG
+#define DPRINTF		log_debug
+#else
+#define DPRINTF(...)	do {} while (0)
+#endif
+
+struct client {
+	uint32_t		 clt_id;
+	int			 clt_fd;
+	struct fcgi		*clt_fcgi;
+	char			*clt_server_name;
+	char			*clt_script_name;
+	char			*clt_path_info;
+	char			*clt_query;
+	int			 clt_method;
+	char			 clt_buf[1024];
+	size_t			 clt_buflen;
+
+	SPLAY_ENTRY(client)	 clt_nodes;
+};
+SPLAY_HEAD(client_tree, client);
+
+struct fcgi {
+	uint32_t		 fcg_id;
+	int			 fcg_s;
+	struct client_tree	 fcg_clients;
+	struct bufferevent	*fcg_bev;
+	int			 fcg_toread;
+	int			 fcg_want;
+	int			 fcg_padding;
+	int			 fcg_type;
+	int			 fcg_rec_id;
+	int			 fcg_keep_conn;
+	int			 fcg_done;
+
+	struct env		*fcg_env;
+
+	SPLAY_ENTRY(fcgi)	 fcg_nodes;
+};
+SPLAY_HEAD(fcgi_tree, fcgi);
+
+struct env {
+	int			 env_sockfd;
+	struct event		 env_sockev;
+	struct event		 env_pausev;
+	struct fcgi_tree	 env_fcgi_socks;
+
+	struct sqlite3		*env_db;
+	struct sqlite3_stmt	*env_query;
+};
+
+/* fcgi.c */
+int	fcgi_end_request(struct client *, int);
+int	fcgi_abort_request(struct client *);
+void	fcgi_accept(int, short, void *);
+void	fcgi_read(struct bufferevent *, void *);
+void	fcgi_write(struct bufferevent *, void *);
+void	fcgi_error(struct bufferevent *, short, void *);
+void	fcgi_free(struct fcgi *);
+int	clt_putc(struct client *, char);
+int	clt_puts(struct client *, const char *);
+int	clt_putsan(struct client *, const char *);
+int	clt_write_bufferevent(struct client *, struct bufferevent *);
+int	clt_flush(struct client *);
+int	clt_write(struct client *, const uint8_t *, size_t);
+int	clt_printf(struct client *, const char *, ...)
+	    __attribute__((__format__(printf, 2, 3)))
+	    __attribute__((__nonnull__(2)));
+int	fcgi_cmp(struct fcgi *, struct fcgi *);
+int	fcgi_client_cmp(struct client *, struct client *);
+
+/* server.c */
+int	server_main(const char *);
+int	server_handle(struct env *, struct client *);
+void	server_client_free(struct client *);
+
+SPLAY_PROTOTYPE(client_tree, client, clt_nodes, fcgi_client_cmp);
+SPLAY_PROTOTYPE(fcgi_tree, fcgi, fcg_nodes, fcgi_cmp);
blob - /dev/null
blob + 89a4878f1341d8d9be52410cf061e772f75edb7c (mode 644)
--- /dev/null
+++ msearchd/schema.sql
@@ -0,0 +1,2 @@
+create virtual table email using fts5(mid UNINDEXED, from, date, subj, body,
+	tokenize = 'porter unicode61 remove_diacritics 2');
blob - /dev/null
blob + 944c33d5e60ba91bb6a19dda737ad757661877b1 (mode 644)
--- /dev/null
+++ msearchd/server.c
@@ -0,0 +1,408 @@
+/*
+ * This file is in the public domain.
+ */
+
+#include <sys/tree.h>
+
+#include <ctype.h>
+#include <event.h>
+#include <libgen.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sqlite3.h>
+
+#include "msearchd.h"
+
+char		dbpath[PATH_MAX];
+
+void		 server_sig_handler(int, short, void *);
+void		 server_open_db(struct env *);
+void		 server_close_db(struct env *);
+__dead void	 server_shutdown(struct env *);
+int		 server_reply(struct client *, int, const char *);
+char		*server_getquery(struct client *);
+
+void
+server_sig_handler(int sig, short ev, void *arg)
+{
+	struct env	*env = arg;
+
+	/*
+	 * Normal signal handler rules don't apply here because libevent
+	 * decouples for us.
+	 */
+
+	switch (sig) {
+	case SIGHUP:
+		log_info("re-opening the db");
+		server_close_db(env);
+		server_open_db(env);
+		break;
+	case SIGTERM:
+	case SIGINT:
+		server_shutdown(env);
+		break;
+	default:
+		fatalx("unexpected signal %d", sig);
+	}
+}
+
+static inline void
+loadstmt(sqlite3 *db, sqlite3_stmt **stmt, const char *sql)
+{
+	int	err;
+
+	err = sqlite3_prepare_v2(db, sql, -1, stmt, NULL);
+	if (err != SQLITE_OK)
+		fatalx("failed to prepare statement \"%s\": %s",
+		    sql, sqlite3_errstr(err));
+}
+
+void
+server_open_db(struct env *env)
+{
+	int	err;
+
+	err = sqlite3_open_v2(dbpath, &env->env_db,
+	    SQLITE_OPEN_READONLY, NULL);
+	if (err != SQLITE_OK)
+		fatalx("can't open database %s: %s", dbpath,
+		    sqlite3_errmsg(env->env_db));
+
+	loadstmt(env->env_db, &env->env_query,
+	    "select mid, \"from\", date, subj"
+	    " from email"
+	    " where email match ?"
+	    " order by rank, date"
+	    " limit 100");
+}
+
+void
+server_close_db(struct env *env)
+{
+	int	err;
+
+	sqlite3_finalize(env->env_query);
+
+	if ((err = sqlite3_close(env->env_db)) != SQLITE_OK)
+		log_warnx("sqlite3_close %s", sqlite3_errstr(err));
+}
+
+int
+server_main(const char *db)
+{
+	char		 path[PATH_MAX], *parent;
+	struct env	 env;
+	struct event	 sighup;
+	struct event	 sigint;
+	struct event	 sigterm;
+
+	signal(SIGPIPE, SIG_IGN);
+
+	memset(&env, 0, sizeof(env));
+
+	if (realpath(db, dbpath) == NULL)
+		fatal("realpath %s", db);
+
+	strlcpy(path, dbpath, sizeof(path));
+	parent = dirname(path);
+	if (unveil(parent, "r") == -1)
+		fatal("unveil(%s, r)", parent);
+
+	/*
+	 * rpath flock: sqlite3
+	 * unix: accept(2)
+	 */
+	if (pledge("stdio rpath flock unix", NULL) == -1)
+		fatal("pledge");
+
+	server_open_db(&env);
+
+	event_init();
+
+	env.env_sockfd = 3;
+
+	event_set(&env.env_sockev, env.env_sockfd, EV_READ|EV_PERSIST,
+	    fcgi_accept, &env);
+	event_add(&env.env_sockev, NULL);
+
+	evtimer_set(&env.env_pausev, fcgi_accept, &env);
+
+	signal_set(&sighup, SIGHUP, server_sig_handler, &env);
+	signal_set(&sigint, SIGINT, server_sig_handler, &env);
+	signal_set(&sigterm, SIGTERM, server_sig_handler, &env);
+
+	signal_add(&sighup, NULL);
+	signal_add(&sigint, NULL);
+	signal_add(&sigterm, NULL);
+
+	log_info("ready");
+	event_dispatch();
+
+	server_shutdown(&env);
+}
+
+void __dead
+server_shutdown(struct env *env)
+{
+	log_info("shutting down");
+	server_close_db(env);
+	exit(0);
+}
+
+int
+server_reply(struct client *clt, int status, const char *arg)
+{
+	if (status != 200 &&
+	    clt_printf(clt, "Status: %d\r\n", status) == -1)
+		return (-1);
+
+	if (status == 302) {
+		if (clt_printf(clt, "Location: %s\r\n", arg) == -1)
+			return (-1);
+		arg = NULL;
+	}
+
+	if (arg != NULL &&
+	    clt_printf(clt, "Content-Type: %s\r\n", arg) == -1)
+		return (-1);
+
+	return (clt_puts(clt, "\r\n"));
+}
+
+int
+server_urldecode(char *s)
+{
+	unsigned int	 x;
+	size_t		 n;
+	char		*q, code[3] = {0};
+
+	q = s;
+	for (;;) {
+		if (*s == '\0')
+			break;
+
+		if (*s == '+') {
+			*q++ = ' ';
+			s++;
+			continue;
+		}
+
+		if (*s != '%') {
+			*q++ = *s++;
+			continue;
+		}
+
+		if (!isxdigit((unsigned char)s[1]) ||
+		    !isxdigit((unsigned char)s[2]))
+			return (-1);
+		code[0] = s[1];
+		code[1] = s[2];
+		x = strtoul(code, NULL, 16);
+		*q++ = (char)x;
+		s += 3;
+	}
+	*q = '\0';
+	return (0);
+}
+
+char *
+server_getquery(struct client *clt)
+{
+	char	*tmp, *field;
+
+	tmp = clt->clt_query;
+	while ((field = strsep(&tmp, "&")) != NULL) {
+		if (server_urldecode(field) == -1)
+			continue;
+
+		if (!strncmp(field, "q=", 2))
+			return (field + 2);
+		log_info("unknown query param %s", field);
+	}
+
+	return (NULL);
+}
+
+static inline int
+fts_escape(const char *p, char *buf, size_t bufsize)
+{
+	char		*q;
+
+	/*
+	 * split p into words and quote them into buf.
+	 * quoting means wrapping each word into "..." and
+	 * replace every " with "".
+	 * i.e. 'C++ "framework"' -> '"C++" """framework"""'
+	 * flatting all the whitespaces seems fine too.
+	 */
+
+	q = buf;
+	while (bufsize != 0) {
+		p += strspn(p, " \f\n\r\t\v");
+		if (*p == '\0')
+			break;
+
+		*q++ = '"';
+		bufsize--;
+		while (*p && !isspace((unsigned char)*p) && bufsize != 0) {
+			if (*p == '"') { /* double the quote character */
+				*q++ = '"';
+				if (--bufsize == 0)
+					break;
+			}
+			*q++ = *p++;
+			bufsize--;
+		}
+
+		if (bufsize < 2)
+			break;
+		*q++ = '"';
+		*q++ = ' ';
+		bufsize -= 2;
+	}
+	if ((*p == '\0') && bufsize != 0) {
+		*q = '\0';
+		return (0);
+	}
+	return (-1);
+}
+
+int
+server_handle(struct env *env, struct client *clt)
+{
+	char		 dbuf[64];
+	char		 esc[QUERY_MAXLEN];
+	char		*query;
+	const char	*mid, *from, *subj;
+	uint64_t	 date;
+	time_t		 d;
+	struct tm	*tm;
+	int		 err;
+
+	if ((query = server_getquery(clt)) != NULL &&
+	    fts_escape(query, esc, sizeof(esc)) != -1) {
+		log_debug("searching for %s", esc);
+
+		err = sqlite3_bind_text(env->env_query, 1, esc, -1, NULL);
+		if (err != SQLITE_OK) {
+			sqlite3_reset(env->env_query);
+			if (server_reply(clt, 500, "text/plain") == -1)
+				return (-1);
+			if (clt_puts(clt, "Internal server error\n") == -1)
+				return (-1);
+			return (fcgi_end_request(clt, 1));
+		}
+	}
+
+	if (server_reply(clt, 200, "text/html") == -1)
+		goto err;
+
+	if (clt_puts(clt, "<!doctype html>"
+	    "<html>"
+	    "<head>"
+	    "<meta charset='utf-8'>"
+	    "<meta name='viewport' content='width=device-width'>"
+	    "<link rel='stylesheet' href='/style.css'>"
+	    "<title>Game of Trees Mail Archive | Search</title>"
+	    "</head>"
+	    "<body>"
+	    "<header class='index-header'>"
+	    "<a href='https://gameoftrees.org' target='_blank'>"
+	    "<img src='/got.png' srcset='/got.png, /got@2x.png 2x'"
+	    "     alt='\"GOT\" where the \"O\" is a cute, smiling pufferfish'"
+	    "     />"
+	    "</a>"
+	    "<h1>Game of Trees Mail Archive</h1>"
+	    "</header>") == -1)
+		goto err;
+
+	if (clt_puts(clt, "<form method='get'>"
+	    "<label>Search: "
+	    "<input type='search' name='q' value='") == -1 ||
+	    clt_putsan(clt, query) == -1 ||
+	    clt_puts(clt, "'/></label>"
+	    " <button type='submit'>search</button>"
+	    "</form>") == -1)
+		goto err;
+
+	if (query == NULL)
+		goto done;
+
+	if (clt_puts(clt, "<div class='thread'><ul>") == -1)
+		goto err;
+
+	for (;;) {
+		err = sqlite3_step(env->env_query);
+		if (err == SQLITE_DONE)
+			break;
+		if (err != SQLITE_ROW) {
+			log_warnx("%s: sqlite3_step %s", __func__,
+			    sqlite3_errstr(err));
+			break;
+		}
+
+		mid = sqlite3_column_text(env->env_query, 0);
+		from = sqlite3_column_text(env->env_query, 1);
+		date = sqlite3_column_int64(env->env_query, 2);
+		subj = sqlite3_column_text(env->env_query, 3);
+
+		if ((sizeof(d) == 4) && date > UINT32_MAX) {
+			log_warnx("overflow of 32bit time value");
+			date = 0;
+		}
+
+		d = date;
+		if ((tm = gmtime(&d)) == NULL) {
+			log_warnx("gmtime failure");
+			continue;
+		}
+
+		if (strftime(dbuf, sizeof(dbuf), "%F %R", tm) == 0) {
+			log_warnx("strftime failure");
+			continue;
+		}
+
+		if (clt_puts(clt, "<li class='mail'>"
+		    "<p class='mail-meta'><time>") == -1 ||
+		    clt_putsan(clt, dbuf) == -1 ||
+		    clt_puts(clt, "</time> <span class='from'>") == -1 ||
+		    clt_putsan(clt, from) == -1 ||
+		    clt_puts(clt, "</span><span class=colon>:</span>") == -1 ||
+		    clt_puts(clt, "</p>"
+			"<p class='subject'>"
+			"<a href='/mail/") == -1 ||
+		    clt_putsan(clt, mid) == -1 ||
+		    clt_puts(clt, ".html'>") == -1 ||
+		    clt_putsan(clt, subj) == -1 ||
+		    clt_puts(clt, "</a></p></li>") == -1)
+			goto err;
+	}
+
+	if (clt_puts(clt, "</ul></div>") == -1)
+		goto err;
+
+done:
+	if (clt_puts(clt, "</body></html>\n") == -1)
+		goto err;
+
+	sqlite3_reset(env->env_query);
+	return (fcgi_end_request(clt, 0));
+err:
+	sqlite3_reset(env->env_query);
+	return (-1);
+}
+
+void
+server_client_free(struct client *clt)
+{
+	free(clt->clt_server_name);
+	free(clt->clt_script_name);
+	free(clt->clt_path_info);
+	free(clt->clt_query);
+	free(clt);
+}