Blob


1 /*
2 * This file is in the public domain.
3 */
5 #include <sys/tree.h>
7 #include <ctype.h>
8 #include <event.h>
9 #include <libgen.h>
10 #include <limits.h>
11 #include <signal.h>
12 #include <stdlib.h>
13 #include <string.h>
14 #include <unistd.h>
16 #include <sqlite3.h>
18 #include "msearchd.h"
20 char dbpath[PATH_MAX];
22 void server_sig_handler(int, short, void *);
23 void server_open_db(struct env *);
24 void server_close_db(struct env *);
25 __dead void server_shutdown(struct env *);
26 int server_reply(struct client *, int, const char *);
27 int server_urldecode(char *);
28 char *server_getquery(struct client *);
30 void
31 server_sig_handler(int sig, short ev, void *arg)
32 {
33 struct env *env = arg;
35 /*
36 * Normal signal handler rules don't apply here because libevent
37 * decouples for us.
38 */
40 switch (sig) {
41 case SIGHUP:
42 log_info("re-opening the db");
43 server_close_db(env);
44 server_open_db(env);
45 break;
46 case SIGTERM:
47 case SIGINT:
48 server_shutdown(env);
49 break;
50 default:
51 fatalx("unexpected signal %d", sig);
52 }
53 }
55 static inline void
56 loadstmt(sqlite3 *db, sqlite3_stmt **stmt, const char *sql)
57 {
58 int err;
60 err = sqlite3_prepare_v2(db, sql, -1, stmt, NULL);
61 if (err != SQLITE_OK)
62 fatalx("failed to prepare statement \"%s\": %s",
63 sql, sqlite3_errstr(err));
64 }
66 void
67 server_open_db(struct env *env)
68 {
69 int err;
71 err = sqlite3_open_v2(dbpath, &env->env_db,
72 SQLITE_OPEN_READONLY, NULL);
73 if (err != SQLITE_OK)
74 fatalx("can't open database %s: %s", dbpath,
75 sqlite3_errmsg(env->env_db));
77 loadstmt(env->env_db, &env->env_query,
78 "select mid, \"from\", date, subj"
79 " from email"
80 " where email match ?"
81 " order by rank, date"
82 " limit 100");
83 }
85 void
86 server_close_db(struct env *env)
87 {
88 int err;
90 sqlite3_finalize(env->env_query);
92 if ((err = sqlite3_close(env->env_db)) != SQLITE_OK)
93 log_warnx("sqlite3_close %s", sqlite3_errstr(err));
94 }
96 int
97 server_main(const char *db)
98 {
99 char path[PATH_MAX], *parent;
100 struct env env;
101 struct event sighup;
102 struct event sigint;
103 struct event sigterm;
105 signal(SIGPIPE, SIG_IGN);
107 memset(&env, 0, sizeof(env));
109 if (realpath(db, dbpath) == NULL)
110 fatal("realpath %s", db);
112 strlcpy(path, dbpath, sizeof(path));
113 parent = dirname(path);
114 if (unveil(parent, "r") == -1)
115 fatal("unveil(%s, r)", parent);
117 /*
118 * rpath flock: sqlite3
119 * unix: accept(2)
120 */
121 if (pledge("stdio rpath flock unix", NULL) == -1)
122 fatal("pledge");
124 server_open_db(&env);
126 event_init();
128 env.env_sockfd = 3;
130 event_set(&env.env_sockev, env.env_sockfd, EV_READ|EV_PERSIST,
131 fcgi_accept, &env);
132 event_add(&env.env_sockev, NULL);
134 evtimer_set(&env.env_pausev, fcgi_accept, &env);
136 signal_set(&sighup, SIGHUP, server_sig_handler, &env);
137 signal_set(&sigint, SIGINT, server_sig_handler, &env);
138 signal_set(&sigterm, SIGTERM, server_sig_handler, &env);
140 signal_add(&sighup, NULL);
141 signal_add(&sigint, NULL);
142 signal_add(&sigterm, NULL);
144 log_info("ready");
145 event_dispatch();
147 server_shutdown(&env);
150 void __dead
151 server_shutdown(struct env *env)
153 log_info("shutting down");
154 server_close_db(env);
155 exit(0);
158 int
159 server_reply(struct client *clt, int status, const char *arg)
161 if (status != 200 &&
162 clt_printf(clt, "Status: %d\r\n", status) == -1)
163 return (-1);
165 if (status == 302) {
166 if (clt_printf(clt, "Location: %s\r\n", arg) == -1)
167 return (-1);
168 arg = NULL;
171 if (arg != NULL &&
172 clt_printf(clt, "Content-Type: %s\r\n", arg) == -1)
173 return (-1);
175 return (clt_puts(clt, "\r\n"));
178 int
179 server_urldecode(char *s)
181 unsigned int x;
182 char *q, code[3] = {0};
184 q = s;
185 for (;;) {
186 if (*s == '\0')
187 break;
189 if (*s == '+') {
190 *q++ = ' ';
191 s++;
192 continue;
195 if (*s != '%') {
196 *q++ = *s++;
197 continue;
200 if (!isxdigit((unsigned char)s[1]) ||
201 !isxdigit((unsigned char)s[2]))
202 return (-1);
203 code[0] = s[1];
204 code[1] = s[2];
205 x = strtoul(code, NULL, 16);
206 *q++ = (char)x;
207 s += 3;
209 *q = '\0';
210 return (0);
213 char *
214 server_getquery(struct client *clt)
216 char *tmp, *field;
218 tmp = clt->clt_query;
219 while ((field = strsep(&tmp, "&")) != NULL) {
220 if (server_urldecode(field) == -1)
221 continue;
223 if (!strncmp(field, "q=", 2))
224 return (field + 2);
225 log_info("unknown query param %s", field);
228 return (NULL);
231 static inline int
232 fts_escape(const char *p, char *buf, size_t bufsize)
234 char *q;
236 /*
237 * split p into words and quote them into buf.
238 * quoting means wrapping each word into "..." and
239 * replace every " with "".
240 * i.e. 'C++ "framework"' -> '"C++" """framework"""'
241 * flatting all the whitespaces seems fine too.
242 */
244 q = buf;
245 while (bufsize != 0) {
246 p += strspn(p, " \f\n\r\t\v");
247 if (*p == '\0')
248 break;
250 *q++ = '"';
251 bufsize--;
252 while (*p && !isspace((unsigned char)*p) && bufsize != 0) {
253 if (*p == '"') { /* double the quote character */
254 *q++ = '"';
255 if (--bufsize == 0)
256 break;
258 *q++ = *p++;
259 bufsize--;
262 if (bufsize < 2)
263 break;
264 *q++ = '"';
265 *q++ = ' ';
266 bufsize -= 2;
268 if ((*p == '\0') && bufsize != 0) {
269 *q = '\0';
270 return (0);
272 return (-1);
275 int
276 server_handle(struct env *env, struct client *clt)
278 char dbuf[64];
279 char esc[QUERY_MAXLEN];
280 char *query;
281 const char *mid, *from, *subj;
282 uint64_t date;
283 time_t d;
284 struct tm *tm;
285 int err;
287 if ((query = server_getquery(clt)) != NULL &&
288 fts_escape(query, esc, sizeof(esc)) != -1) {
289 log_debug("searching for %s", esc);
291 err = sqlite3_bind_text(env->env_query, 1, esc, -1, NULL);
292 if (err != SQLITE_OK) {
293 sqlite3_reset(env->env_query);
294 if (server_reply(clt, 500, "text/plain") == -1)
295 return (-1);
296 if (clt_puts(clt, "Internal server error\n") == -1)
297 return (-1);
298 return (fcgi_end_request(clt, 1));
302 if (server_reply(clt, 200, "text/html") == -1)
303 goto err;
305 if (clt_puts(clt, "<!doctype html>"
306 "<html>"
307 "<head>"
308 "<meta charset='utf-8'>"
309 "<meta name='viewport' content='width=device-width'>"
310 "<link rel='stylesheet' href='/style.css'>"
311 "<title>Game of Trees Mail Archive | Search</title>"
312 "</head>"
313 "<body>"
314 "<header class='index-header'>"
315 "<a href='https://gameoftrees.org' target='_blank'>"
316 "<img src='/got.png' srcset='/got.png, /got@2x.png 2x'"
317 " alt='\"GOT\" where the \"O\" is a cute, smiling pufferfish'"
318 " />"
319 "</a>"
320 "<h1>Game of Trees Mail Archive</h1>"
321 "</header>") == -1)
322 goto err;
324 if (clt_puts(clt, "<nav>"
325 "<a href='/'>Index</a>"
326 "</nav>"
327 "<form method='get'>"
328 "<label>Search: "
329 "<input type='search' name='q' value='") == -1 ||
330 clt_putsan(clt, query) == -1 ||
331 clt_puts(clt, "'/></label>"
332 " <button type='submit'>search</button>"
333 "</form>") == -1)
334 goto err;
336 if (query == NULL)
337 goto done;
339 if (clt_puts(clt, "<div class='thread'><ul>") == -1)
340 goto err;
342 for (;;) {
343 err = sqlite3_step(env->env_query);
344 if (err == SQLITE_DONE)
345 break;
346 if (err != SQLITE_ROW) {
347 log_warnx("%s: sqlite3_step %s", __func__,
348 sqlite3_errstr(err));
349 break;
352 mid = sqlite3_column_text(env->env_query, 0);
353 from = sqlite3_column_text(env->env_query, 1);
354 date = sqlite3_column_int64(env->env_query, 2);
355 subj = sqlite3_column_text(env->env_query, 3);
357 if ((sizeof(d) == 4) && date > UINT32_MAX) {
358 log_warnx("overflow of 32bit time value");
359 date = 0;
362 d = date;
363 if ((tm = gmtime(&d)) == NULL) {
364 log_warnx("gmtime failure");
365 continue;
368 if (strftime(dbuf, sizeof(dbuf), "%F %R", tm) == 0) {
369 log_warnx("strftime failure");
370 continue;
373 if (clt_puts(clt, "<li class='mail'>"
374 "<p class='mail-meta'><time>") == -1 ||
375 clt_putsan(clt, dbuf) == -1 ||
376 clt_puts(clt, "</time> <span class='from'>") == -1 ||
377 clt_putsan(clt, from) == -1 ||
378 clt_puts(clt, "</span><span class=colon>:</span>") == -1 ||
379 clt_puts(clt, "</p>"
380 "<p class='subject'>"
381 "<a href='/mail/") == -1 ||
382 clt_putsan(clt, mid) == -1 ||
383 clt_puts(clt, ".html'>") == -1 ||
384 clt_putsan(clt, subj) == -1 ||
385 clt_puts(clt, "</a></p></li>") == -1)
386 goto err;
389 if (clt_puts(clt, "</ul></div>") == -1)
390 goto err;
392 done:
393 if (clt_puts(clt, "</body></html>\n") == -1)
394 goto err;
396 sqlite3_reset(env->env_query);
397 return (fcgi_end_request(clt, 0));
398 err:
399 sqlite3_reset(env->env_query);
400 return (-1);
403 void
404 server_client_free(struct client *clt)
406 free(clt->clt_server_name);
407 free(clt->clt_script_name);
408 free(clt->clt_path_info);
409 free(clt->clt_query);
410 free(clt);