commit - 4ad9fe9bbeb113cab0fddb1d670252ad6192d589
commit + 50794f47b0bc631e4e0940016fc52a3e82cda62f
blob - /dev/null
blob + 4794a2f4ca164921ce63a6bdb23016d6fbd74264 (mode 755)
--- /dev/null
+++ mimport
+#!/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
margin: 0 0 1rem 0;
}
+form {
+ text-align: center;
+}
+
main {
padding: 5px;
}
blob - /dev/null
blob + 9865fec98a708439ee382b55d9176a607ff0f523 (mode 644)
--- /dev/null
+++ msearchd/Makefile
+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
+/*
+ * 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, "<");
+ break;
+ case '>':
+ r = clt_puts(clt, ">");
+ break;
+ case '&':
+ r = clt_puts(clt, "&");
+ break;
+ case '"':
+ r = clt_puts(clt, """);
+ break;
+ case '\'':
+ r = clt_puts(clt, "'");
+ 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
+.\" 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
+/*
+ * 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
+/*
+ * 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
+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
+/*
+ * 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);
+}