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 " snippet(email, 4, '<strong>', '</strong>', '...', 32)"
80 " from email"
81 " where email match ?"
82 " order by rank, date"
83 " limit 100");
84 }
86 void
87 server_close_db(struct env *env)
88 {
89 int err;
91 sqlite3_finalize(env->env_query);
93 if ((err = sqlite3_close(env->env_db)) != SQLITE_OK)
94 log_warnx("sqlite3_close %s", sqlite3_errstr(err));
95 }
97 int
98 server_main(const char *db)
99 {
100 char path[PATH_MAX], *parent;
101 struct env env;
102 struct event sighup;
103 struct event sigint;
104 struct event sigterm;
106 signal(SIGPIPE, SIG_IGN);
108 memset(&env, 0, sizeof(env));
110 if (realpath(db, dbpath) == NULL)
111 fatal("realpath %s", db);
113 strlcpy(path, dbpath, sizeof(path));
114 parent = dirname(path);
115 if (unveil(parent, "r") == -1)
116 fatal("unveil(%s, r)", parent);
118 /*
119 * rpath flock: sqlite3
120 * unix: accept(2)
121 */
122 if (pledge("stdio rpath flock unix", NULL) == -1)
123 fatal("pledge");
125 server_open_db(&env);
127 event_init();
129 env.env_sockfd = 3;
131 event_set(&env.env_sockev, env.env_sockfd, EV_READ|EV_PERSIST,
132 fcgi_accept, &env);
133 event_add(&env.env_sockev, NULL);
135 evtimer_set(&env.env_pausev, fcgi_accept, &env);
137 signal_set(&sighup, SIGHUP, server_sig_handler, &env);
138 signal_set(&sigint, SIGINT, server_sig_handler, &env);
139 signal_set(&sigterm, SIGTERM, server_sig_handler, &env);
141 signal_add(&sighup, NULL);
142 signal_add(&sigint, NULL);
143 signal_add(&sigterm, NULL);
145 log_info("ready");
146 event_dispatch();
148 server_shutdown(&env);
151 void __dead
152 server_shutdown(struct env *env)
154 log_info("shutting down");
155 server_close_db(env);
156 exit(0);
159 int
160 server_reply(struct client *clt, int status, const char *arg)
162 const char *cps;
164 if (status != 200 &&
165 clt_printf(clt, "Status: %d\r\n", status) == -1)
166 return (-1);
168 cps = "Content-Security-Policy: default-src 'self'; "
169 "script-src 'none'; object-src 'none';\r\n";
170 if (clt_puts(clt, cps) == -1)
171 return (-1);
173 if (status == 302) {
174 if (clt_printf(clt, "Location: %s\r\n", arg) == -1)
175 return (-1);
176 arg = NULL;
179 if (arg != NULL &&
180 clt_printf(clt, "Content-Type: %s\r\n", arg) == -1)
181 return (-1);
183 return (clt_puts(clt, "\r\n"));
186 int
187 server_urldecode(char *s)
189 unsigned int x;
190 char *q, code[3] = {0};
192 q = s;
193 for (;;) {
194 if (*s == '\0')
195 break;
197 if (*s == '+') {
198 *q++ = ' ';
199 s++;
200 continue;
203 if (*s != '%') {
204 *q++ = *s++;
205 continue;
208 if (!isxdigit((unsigned char)s[1]) ||
209 !isxdigit((unsigned char)s[2]))
210 return (-1);
211 code[0] = s[1];
212 code[1] = s[2];
213 x = strtoul(code, NULL, 16);
214 *q++ = (char)x;
215 s += 3;
217 *q = '\0';
218 return (0);
221 char *
222 server_getquery(struct client *clt)
224 char *tmp, *field;
226 tmp = clt->clt_query;
227 while ((field = strsep(&tmp, "&")) != NULL) {
228 if (server_urldecode(field) == -1)
229 continue;
231 if (!strncmp(field, "q=", 2))
232 return (field + 2);
233 log_info("unknown query param %s", field);
236 return (NULL);
239 static inline int
240 fts_escape(const char *p, char *buf, size_t bufsize)
242 char *q;
244 /*
245 * split p into words and quote them into buf.
246 * quoting means wrapping each word into "..." and
247 * replace every " with "".
248 * i.e. 'C++ "framework"' -> '"C++" """framework"""'
249 * flatting all the whitespaces seems fine too.
250 */
252 q = buf;
253 while (bufsize != 0) {
254 p += strspn(p, " \f\n\r\t\v");
255 if (*p == '\0')
256 break;
258 *q++ = '"';
259 bufsize--;
260 while (*p && !isspace((unsigned char)*p) && bufsize != 0) {
261 if (*p == '"') { /* double the quote character */
262 *q++ = '"';
263 if (--bufsize == 0)
264 break;
266 *q++ = *p++;
267 bufsize--;
270 if (bufsize < 2)
271 break;
272 *q++ = '"';
273 *q++ = ' ';
274 bufsize -= 2;
276 if ((*p == '\0') && bufsize != 0) {
277 *q = '\0';
278 return (0);
280 return (-1);
283 static int
284 render_tmpl(struct client *clt, const char *tmpl,
285 const char *var, const char *val)
287 const char *t;
288 size_t vlen;
290 if (var == NULL)
291 return (clt_puts(clt, tmpl));
293 vlen = strlen(var);
294 while ((t = strstr(tmpl, var)) != NULL) {
295 if (clt_write(clt, tmpl, t - tmpl) == -1 ||
296 clt_putsan(clt, val) == -1)
297 return (-1);
298 tmpl = t + vlen;
301 return (clt_puts(clt, tmpl));
304 int
305 server_handle(struct env *env, struct client *clt)
307 char dbuf[64];
308 char esc[QUERY_MAXLEN];
309 char *query;
310 const char *mid, *from, *subj, *snip;
311 uint64_t date;
312 time_t d;
313 struct tm *tm;
314 int err, have_results = 0;
316 if ((query = server_getquery(clt)) != NULL &&
317 fts_escape(query, esc, sizeof(esc)) != -1 &&
318 *esc != '\0') {
319 log_debug("searching for %s", esc);
321 err = sqlite3_bind_text(env->env_query, 1, esc, -1, NULL);
322 if (err != SQLITE_OK) {
323 sqlite3_reset(env->env_query);
324 if (server_reply(clt, 500, "text/plain") == -1)
325 return (-1);
326 if (clt_puts(clt, "Internal server error\n") == -1)
327 return (-1);
328 return (fcgi_end_request(clt, 1));
330 } else
331 query = NULL;
333 if (server_reply(clt, 200, "text/html") == -1)
334 goto err;
336 if (render_tmpl(clt, tmpl_head, "TITLE", "Search") == -1 ||
337 render_tmpl(clt, tmpl_search_header, NULL, NULL) == -1 ||
338 render_tmpl(clt, tmpl_search, "QUERY", query) == -1)
339 goto err;
341 if (query == NULL)
342 goto done;
344 if (clt_puts(clt, "<div class='thread'><ul>") == -1)
345 goto err;
347 for (;;) {
348 err = sqlite3_step(env->env_query);
349 if (err == SQLITE_DONE)
350 break;
351 if (err != SQLITE_ROW) {
352 log_warnx("%s: sqlite3_step %s", __func__,
353 sqlite3_errstr(err));
354 break;
357 have_results = 1;
359 mid = sqlite3_column_text(env->env_query, 0);
360 from = sqlite3_column_text(env->env_query, 1);
361 date = sqlite3_column_int64(env->env_query, 2);
362 subj = sqlite3_column_text(env->env_query, 3);
363 snip = sqlite3_column_text(env->env_query, 4);
365 if ((sizeof(d) == 4) && date > UINT32_MAX) {
366 log_warnx("overflow of 32bit time value");
367 date = 0;
370 d = date;
371 if ((tm = gmtime(&d)) == NULL) {
372 log_warnx("gmtime failure");
373 continue;
376 if (strftime(dbuf, sizeof(dbuf), "%F %R", tm) == 0) {
377 log_warnx("strftime failure");
378 continue;
381 if (clt_puts(clt, "<li class='mail'>"
382 "<p class='mail-meta'><time>") == -1 ||
383 clt_putsan(clt, dbuf) == -1 ||
384 clt_puts(clt, "</time> <span class='from'>") == -1 ||
385 clt_putsan(clt, from) == -1 ||
386 clt_puts(clt, "</span><span class=colon>:</span>") == -1 ||
387 clt_puts(clt, "</p>"
388 "<p class='subject'>"
389 "<a href='/mail/") == -1 ||
390 clt_putsan(clt, mid) == -1 ||
391 clt_puts(clt, ".html'>") == -1 ||
392 clt_putsan(clt, subj) == -1 ||
393 clt_puts(clt, "</a></p><p class=excerpt>") == -1 ||
394 clt_putmatch(clt, snip) == -1 ||
395 clt_puts(clt, "</p></li>") == -1)
396 goto err;
399 if (clt_puts(clt, "</ul></div>") == -1)
400 goto err;
402 if (!have_results &&
403 clt_puts(clt, "<p class='notice'>No mail found.</p>") == -1)
404 goto err;
406 done:
407 if (render_tmpl(clt, tmpl_foot, NULL, NULL) == -1)
408 goto err;
410 sqlite3_reset(env->env_query);
411 return (fcgi_end_request(clt, 0));
412 err:
413 sqlite3_reset(env->env_query);
414 return (-1);
417 void
418 server_client_free(struct client *clt)
420 free(clt->clt_server_name);
421 free(clt->clt_script_name);
422 free(clt->clt_path_info);
423 free(clt->clt_query);
424 free(clt);