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 "log.h"
19 #include "msearchd.h"
21 char dbpath[PATH_MAX];
23 void server_sig_handler(int, short, void *);
24 void server_open_db(struct env *);
25 void server_close_db(struct env *);
26 __dead void server_shutdown(struct env *);
27 int server_reply(struct client *, int, const char *);
28 int server_urldecode(char *);
29 char *server_getquery(struct client *);
31 void
32 server_sig_handler(int sig, short ev, void *arg)
33 {
34 struct env *env = arg;
36 /*
37 * Normal signal handler rules don't apply here because libevent
38 * decouples for us.
39 */
41 switch (sig) {
42 case SIGHUP:
43 log_info("re-opening the db");
44 server_close_db(env);
45 server_open_db(env);
46 break;
47 case SIGTERM:
48 case SIGINT:
49 server_shutdown(env);
50 break;
51 default:
52 fatalx("unexpected signal %d", sig);
53 }
54 }
56 static inline void
57 loadstmt(sqlite3 *db, sqlite3_stmt **stmt, const char *sql)
58 {
59 int err;
61 err = sqlite3_prepare_v2(db, sql, -1, stmt, NULL);
62 if (err != SQLITE_OK)
63 fatalx("failed to prepare statement \"%s\": %s",
64 sql, sqlite3_errstr(err));
65 }
67 void
68 server_open_db(struct env *env)
69 {
70 int err;
72 err = sqlite3_open_v2(dbpath, &env->env_db,
73 SQLITE_OPEN_READONLY, NULL);
74 if (err != SQLITE_OK)
75 fatalx("can't open database %s: %s", dbpath,
76 sqlite3_errmsg(env->env_db));
78 loadstmt(env->env_db, &env->env_query,
79 "select mid, \"from\", date, subj,"
80 " snippet(email, 4, '<strong>', '</strong>', '...', 32)"
81 " from email"
82 " where email match ?"
83 " order by rank, date"
84 " limit 100");
85 }
87 void
88 server_close_db(struct env *env)
89 {
90 int err;
92 sqlite3_finalize(env->env_query);
94 if ((err = sqlite3_close(env->env_db)) != SQLITE_OK)
95 log_warnx("sqlite3_close %s", sqlite3_errstr(err));
96 }
98 int
99 server_main(const char *db)
101 char path[PATH_MAX], *parent;
102 struct env env;
103 struct event sighup;
104 struct event sigint;
105 struct event sigterm;
107 signal(SIGPIPE, SIG_IGN);
109 memset(&env, 0, sizeof(env));
111 if (realpath(db, dbpath) == NULL)
112 fatal("realpath %s", db);
114 strlcpy(path, dbpath, sizeof(path));
115 parent = dirname(path);
116 if (unveil(parent, "r") == -1)
117 fatal("unveil(%s, r)", parent);
119 /*
120 * rpath flock: sqlite3
121 * unix: accept(2)
122 */
123 if (pledge("stdio rpath flock unix", NULL) == -1)
124 fatal("pledge");
126 server_open_db(&env);
128 event_init();
130 env.env_sockfd = 3;
132 event_set(&env.env_sockev, env.env_sockfd, EV_READ|EV_PERSIST,
133 fcgi_accept, &env);
134 event_add(&env.env_sockev, NULL);
136 evtimer_set(&env.env_pausev, fcgi_accept, &env);
138 signal_set(&sighup, SIGHUP, server_sig_handler, &env);
139 signal_set(&sigint, SIGINT, server_sig_handler, &env);
140 signal_set(&sigterm, SIGTERM, server_sig_handler, &env);
142 signal_add(&sighup, NULL);
143 signal_add(&sigint, NULL);
144 signal_add(&sigterm, NULL);
146 log_info("ready");
147 event_dispatch();
149 server_shutdown(&env);
152 void __dead
153 server_shutdown(struct env *env)
155 log_info("shutting down");
156 server_close_db(env);
157 exit(0);
160 int
161 server_reply(struct client *clt, int status, const char *arg)
163 const char *cps;
165 if (status != 200 &&
166 clt_printf(clt, "Status: %d\r\n", status) == -1)
167 return (-1);
169 cps = "Content-Security-Policy: default-src 'self'; "
170 "script-src 'none'; object-src 'none';\r\n";
171 if (clt_puts(clt, cps) == -1)
172 return (-1);
174 if (status == 302) {
175 if (clt_printf(clt, "Location: %s\r\n", arg) == -1)
176 return (-1);
177 arg = NULL;
180 if (arg != NULL &&
181 clt_printf(clt, "Content-Type: %s\r\n", arg) == -1)
182 return (-1);
184 return (clt_puts(clt, "\r\n"));
187 int
188 server_urldecode(char *s)
190 unsigned int x;
191 char *q, code[3] = {0};
193 q = s;
194 for (;;) {
195 if (*s == '\0')
196 break;
198 if (*s == '+') {
199 *q++ = ' ';
200 s++;
201 continue;
204 if (*s != '%') {
205 *q++ = *s++;
206 continue;
209 if (!isxdigit((unsigned char)s[1]) ||
210 !isxdigit((unsigned char)s[2]))
211 return (-1);
212 code[0] = s[1];
213 code[1] = s[2];
214 x = strtoul(code, NULL, 16);
215 *q++ = (char)x;
216 s += 3;
218 *q = '\0';
219 return (0);
222 char *
223 server_getquery(struct client *clt)
225 char *tmp, *field;
227 tmp = clt->clt_query;
228 while ((field = strsep(&tmp, "&")) != NULL) {
229 if (server_urldecode(field) == -1)
230 continue;
232 if (!strncmp(field, "q=", 2))
233 return (field + 2);
234 log_info("unknown query param %s", field);
237 return (NULL);
240 static inline int
241 fts_escape(const char *p, char *buf, size_t bufsize)
243 char *q;
245 /*
246 * split p into words and quote them into buf.
247 * quoting means wrapping each word into "..." and
248 * replace every " with "".
249 * i.e. 'C++ "framework"' -> '"C++" """framework"""'
250 * flatting all the whitespaces seems fine too.
251 */
253 q = buf;
254 while (bufsize != 0) {
255 p += strspn(p, " \f\n\r\t\v");
256 if (*p == '\0')
257 break;
259 *q++ = '"';
260 bufsize--;
261 while (*p && !isspace((unsigned char)*p) && bufsize != 0) {
262 if (*p == '"') { /* double the quote character */
263 *q++ = '"';
264 if (--bufsize == 0)
265 break;
267 *q++ = *p++;
268 bufsize--;
271 if (bufsize < 2)
272 break;
273 *q++ = '"';
274 *q++ = ' ';
275 bufsize -= 2;
277 if ((*p == '\0') && bufsize != 0) {
278 *q = '\0';
279 return (0);
281 return (-1);
284 static int
285 render_tmpl(struct client *clt, const char *tmpl,
286 const char *var, const char *val)
288 const char *t;
289 size_t vlen;
291 if (var == NULL)
292 return (clt_puts(clt, tmpl));
294 vlen = strlen(var);
295 while ((t = strstr(tmpl, var)) != NULL) {
296 if (clt_write(clt, tmpl, t - tmpl) == -1 ||
297 clt_putsan(clt, val) == -1)
298 return (-1);
299 tmpl = t + vlen;
302 return (clt_puts(clt, tmpl));
305 int
306 server_handle(struct env *env, struct client *clt)
308 char dbuf[64];
309 char esc[QUERY_MAXLEN];
310 char *query;
311 const char *mid, *from, *subj, *snip;
312 uint64_t date;
313 time_t d;
314 struct tm *tm;
315 int err, have_results = 0;
317 if ((query = server_getquery(clt)) != NULL &&
318 fts_escape(query, esc, sizeof(esc)) != -1 &&
319 *esc != '\0') {
320 log_debug("searching for %s", esc);
322 err = sqlite3_bind_text(env->env_query, 1, esc, -1, NULL);
323 if (err != SQLITE_OK) {
324 sqlite3_reset(env->env_query);
325 if (server_reply(clt, 500, "text/plain") == -1)
326 return (-1);
327 if (clt_puts(clt, "Internal server error\n") == -1)
328 return (-1);
329 return (fcgi_end_request(clt, 1));
331 } else
332 query = NULL;
334 if (server_reply(clt, 200, "text/html") == -1)
335 goto err;
337 if (render_tmpl(clt, tmpl_head, "TITLE", "Search") == -1 ||
338 render_tmpl(clt, tmpl_search_header, NULL, NULL) == -1 ||
339 render_tmpl(clt, tmpl_search, "QUERY", query) == -1)
340 goto err;
342 if (query == NULL)
343 goto done;
345 if (clt_puts(clt, "<div class='thread'><ul>") == -1)
346 goto err;
348 for (;;) {
349 err = sqlite3_step(env->env_query);
350 if (err == SQLITE_DONE)
351 break;
352 if (err != SQLITE_ROW) {
353 log_warnx("%s: sqlite3_step %s", __func__,
354 sqlite3_errstr(err));
355 break;
358 have_results = 1;
360 mid = sqlite3_column_text(env->env_query, 0);
361 from = sqlite3_column_text(env->env_query, 1);
362 date = sqlite3_column_int64(env->env_query, 2);
363 subj = sqlite3_column_text(env->env_query, 3);
364 snip = sqlite3_column_text(env->env_query, 4);
366 if ((sizeof(d) == 4) && date > UINT32_MAX) {
367 log_warnx("overflow of 32bit time value");
368 date = 0;
371 d = date;
372 if ((tm = gmtime(&d)) == NULL) {
373 log_warnx("gmtime failure");
374 continue;
377 if (strftime(dbuf, sizeof(dbuf), "%F %R", tm) == 0) {
378 log_warnx("strftime failure");
379 continue;
382 if (clt_puts(clt, "<li class='mail'>"
383 "<p class='mail-meta'><time>") == -1 ||
384 clt_putsan(clt, dbuf) == -1 ||
385 clt_puts(clt, "</time> <span class='from'>") == -1 ||
386 clt_putsan(clt, from) == -1 ||
387 clt_puts(clt, "</span><span class=colon>:</span>") == -1 ||
388 clt_puts(clt, "</p>"
389 "<p class='subject'>"
390 "<a href='/mail/") == -1 ||
391 clt_putsan(clt, mid) == -1 ||
392 clt_puts(clt, ".html'>") == -1 ||
393 clt_putsan(clt, subj) == -1 ||
394 clt_puts(clt, "</a></p><p class=excerpt>") == -1 ||
395 clt_putmatch(clt, snip) == -1 ||
396 clt_puts(clt, "</p></li>") == -1)
397 goto err;
400 if (clt_puts(clt, "</ul></div>") == -1)
401 goto err;
403 if (!have_results &&
404 clt_puts(clt, "<p class='notice'>No mail found.</p>") == -1)
405 goto err;
407 done:
408 if (render_tmpl(clt, tmpl_foot, NULL, NULL) == -1)
409 goto err;
411 sqlite3_reset(env->env_query);
412 return (fcgi_end_request(clt, 0));
413 err:
414 sqlite3_reset(env->env_query);
415 return (-1);
418 void
419 server_client_free(struct client *clt)
421 free(clt->clt_server_name);
422 free(clt->clt_script_name);
423 free(clt->clt_path_info);
424 free(clt->clt_query);
425 free(clt);