commit 50794f47b0bc631e4e0940016fc52a3e82cda62f from: Omar Polo date: Tue Apr 04 10:12:18 2023 UTC import msearchd: the fastcgi search server 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 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 blob - /dev/null blob + 6389a88ec1ab83623efd4b625340b0837fbb7b2b (mode 644) --- /dev/null +++ msearchd/fcgi.c @@ -0,0 +1,776 @@ +/* + * This file is in the public domain. + */ + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 @@ -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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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, "" + "" + "" + "" + "" + "" + "Game of Trees Mail Archive | Search" + "" + "" + "
" + "" + "\"GOT\" where the \"O\" is a cute, smiling pufferfish" + "" + "

Game of Trees Mail Archive

" + "
") == -1) + goto err; + + if (clt_puts(clt, "
" + "" + " " + "
") == -1) + goto err; + + if (query == NULL) + goto done; + + if (clt_puts(clt, "
    ") == -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, "
  • " + "

    ") == -1 || + clt_putsan(clt, from) == -1 || + clt_puts(clt, ":") == -1 || + clt_puts(clt, "

    " + "

    " + "") == -1 || + clt_putsan(clt, subj) == -1 || + clt_puts(clt, "

  • ") == -1) + goto err; + } + + if (clt_puts(clt, "
") == -1) + goto err; + +done: + if (clt_puts(clt, "\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); +}