Blob


1 /*
2 * Copyright (c) 2022 Omar Polo <op@omarpolo.com>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
17 #include <sys/tree.h>
19 #include <ctype.h>
20 #include <event.h>
21 #include <fnmatch.h>
22 #include <limits.h>
23 #include <signal.h>
24 #include <stdlib.h>
25 #include <string.h>
26 #include <unistd.h>
28 #include <sqlite3.h>
30 #include "log.h"
31 #include "pkg.h"
32 #include "tmpl.h"
34 #ifndef nitems
35 #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
36 #endif
38 char dbpath[PATH_MAX];
40 void server_sig_handler(int, short, void *);
41 void server_open_db(struct env *);
42 void server_close_db(struct env *);
43 __dead void server_shutdown(struct env *);
44 int server_reply(struct client *, int, const char *);
46 int route_dispatch(struct env *, struct client *);
47 int route_home(struct env *, struct client *);
48 int route_search(struct env *, struct client *);
49 int route_categories(struct env *, struct client *);
50 int route_listing(struct env *, struct client *);
51 int route_port(struct env *, struct client *);
53 typedef int (*route_t)(struct env *, struct client *);
55 static const struct route {
56 const char *r_path;
57 route_t r_fn;
58 } routes[] = {
59 { "/", route_home },
60 { "/search", route_search },
61 { "/all", route_categories },
62 { "/*", route_port },
63 };
65 void
66 server_sig_handler(int sig, short ev, void *arg)
67 {
68 struct env *env = arg;
70 /*
71 * Normal signal handler rules don't apply because libevent
72 * decouples for us.
73 */
75 switch (sig) {
76 case SIGHUP:
77 log_info("re-opening the db");
78 server_close_db(env);
79 server_open_db(env);
80 break;
81 case SIGTERM:
82 case SIGINT:
83 server_shutdown(env);
84 break;
85 default:
86 fatalx("unexpected signal %d", sig);
87 }
88 }
90 static inline void
91 loadstmt(sqlite3 *db, sqlite3_stmt **stmt, const char *sql)
92 {
93 int err;
95 err = sqlite3_prepare_v2(db, sql, -1, stmt, NULL);
96 if (err != SQLITE_OK)
97 fatalx("failed prepare statement \"%s\": %s",
98 sql, sqlite3_errstr(err));
99 }
101 void
102 server_open_db(struct env *env)
104 int err;
106 err = sqlite3_open_v2(dbpath, &env->env_db,
107 SQLITE_OPEN_READONLY, NULL);
108 if (err != SQLITE_OK)
109 fatalx("can't open database %s: %s", dbpath,
110 sqlite3_errmsg(env->env_db));
112 /* load prepared statements */
113 loadstmt(env->env_db, &env->env_qsearch,
114 "select webpkg_fts.pkgstem, webpkg_fts.comment, paths.fullpkgpath"
115 " from webpkg_fts"
116 " join _ports p on p.fullpkgpath = webpkg_fts.id"
117 " join _paths paths on paths.id = webpkg_fts.id"
118 " where webpkg_fts match ?"
119 " order by bm25(webpkg_fts)");
121 loadstmt(env->env_db, &env->env_qfullpkgpath,
122 "select p.fullpkgpath, pp.pkgstem, pp.comment, pp.pkgname,"
123 " d.value, e.value, r.value, pp.homepage"
124 " from _paths p"
125 " join _descr d on d.fullpkgpath = p.id"
126 " join _ports pp on pp.fullpkgpath = p.id"
127 " join _email e on e.keyref = pp.maintainer"
128 " left join _readme r on r.fullpkgpath = p.id"
129 " where p.fullpkgpath = ?");
131 loadstmt(env->env_db, &env->env_qcats,
132 "select distinct value from categories order by value");
134 loadstmt(env->env_db, &env->env_qbycat,
135 "select fullpkgpath from categories where value = ?"
136 " order by fullpkgpath");
139 void
140 server_close_db(struct env *env)
142 int err;
144 sqlite3_finalize(env->env_qsearch);
145 sqlite3_finalize(env->env_qfullpkgpath);
146 sqlite3_finalize(env->env_qcats);
147 sqlite3_finalize(env->env_qbycat);
149 if ((err = sqlite3_close(env->env_db)) != SQLITE_OK)
150 log_warnx("sqlite3_close %s", sqlite3_errstr(err));
153 int
154 server_main(const char *db)
156 struct env env;
157 struct event sighup;
158 struct event sigint;
159 struct event sigterm;
161 signal(SIGPIPE, SIG_IGN);
163 memset(&env, 0, sizeof(env));
165 if (pledge("stdio rpath flock unix", NULL) == -1)
166 fatal("pledge");
168 if (realpath(db, dbpath) == NULL)
169 fatal("realpath %s", db);
171 server_open_db(&env);
173 event_init();
175 env.env_sockfd = 3;
177 event_set(&env.env_sockev, env.env_sockfd, EV_READ | EV_PERSIST,
178 fcgi_accept, &env);
179 event_add(&env.env_sockev, NULL);
181 evtimer_set(&env.env_pausev, fcgi_accept, &env);
183 signal_set(&sighup, SIGHUP, server_sig_handler, &env);
184 signal_set(&sigint, SIGINT, server_sig_handler, &env);
185 signal_set(&sigterm, SIGTERM, server_sig_handler, &env);
187 signal_add(&sighup, NULL);
188 signal_add(&sigint, NULL);
189 signal_add(&sigterm, NULL);
191 log_info("ready");
192 event_dispatch();
194 server_shutdown(&env);
197 void __dead
198 server_shutdown(struct env *env)
200 log_info("shutting down");
201 server_close_db(env);
202 exit(0);
205 int
206 server_reply(struct client *clt, int status, const char *ctype)
208 if (clt_printf(clt, "%02d %s\r\n", status, ctype) == -1)
209 return (-1);
210 return (0);
213 int
214 server_handle(struct env *env, struct client *clt)
216 log_debug("SCRIPT_NAME %s", clt->clt_script_name);
217 log_debug("PATH_INFO %s", clt->clt_path_info);
218 return (route_dispatch(env, clt));
221 void
222 server_client_free(struct client *clt)
224 #if template
225 template_free(clt->clt_tp);
226 #endif
227 free(clt->clt_server_name);
228 free(clt->clt_script_name);
229 free(clt->clt_path_info);
230 free(clt->clt_query);
231 free(clt);
234 static inline int
235 unquote(char *str)
237 char *p, *q;
238 char hex[3];
239 unsigned long x;
241 hex[2] = '\0';
242 p = q = str;
243 while (*p) {
244 switch (*p) {
245 case '%':
246 if (!isxdigit((unsigned char)p[1]) ||
247 !isxdigit((unsigned char)p[2]) ||
248 (p[1] == '0' && p[2] == '0'))
249 return (-1);
251 hex[0] = p[1];
252 hex[1] = p[2];
254 x = strtoul(hex, NULL, 16);
255 *q++ = (char)x;
256 p += 3;
257 break;
258 default:
259 *q++ = *p++;
260 break;
263 *q = '\0';
264 return (0);
267 static inline int
268 fts_escape(const char *p, char *buf, size_t bufsize)
270 char *q;
272 /*
273 * split p into words and quote them into buf.
274 * quoting means wrapping each word into "..." and
275 * replace every " with "".
276 * i.e. 'C++ "framework"' -> '"C++" """framework"""'
277 * flatting all the whitespaces seems fine too.
278 */
280 q = buf;
281 while (bufsize != 0) {
282 p += strspn(p, " \f\n\r\t\v");
283 if (*p == '\0')
284 break;
286 *q++ = '"';
287 bufsize--;
288 while (*p && !isspace((unsigned char)*p) && bufsize != 0) {
289 if (*p == '"') { /* double the quote character */
290 *q++ = '"';
291 bufsize--;
292 if (bufsize == 0)
293 break;
295 *q++ = *p++;
296 bufsize--;
299 if (bufsize < 2)
300 break;
301 *q++ = '"';
302 *q++ = ' ';
303 bufsize -= 2;
305 if ((*p == '\0') && bufsize != 0) {
306 *q = '\0';
307 return (0);
310 return (-1);
313 int
314 route_dispatch(struct env *env, struct client *clt)
316 const struct route *r;
317 size_t i;
319 for (i = 0; i < nitems(routes); ++i) {
320 r = &routes[i];
322 if (fnmatch(r->r_path, clt->clt_path_info, 0) != 0)
323 continue;
324 return (r->r_fn(env, clt));
327 if (server_reply(clt, 51, "not found") == -1)
328 return (-1);
329 return (fcgi_end_request(clt, 0));
332 int
333 route_home(struct env *env, struct client *clt)
335 if (server_reply(clt, 20, "text/gemini") == -1)
336 return (-1);
338 #if 1
339 if (clt_printf(clt, "# pkg_fcgi\n\n") == -1)
340 return (-1);
341 if (clt_printf(clt, "Welcome to pkg_fcgi, the Gemini interface "
342 "for the OpenBSD ports collection.\n\n") == -1)
343 return (-1);
344 if (clt_printf(clt, "=> %s/search Search for a package\n",
345 clt->clt_script_name) == -1)
346 return (-1);
347 if (clt_printf(clt, "=> %s/all All categories\n",
348 clt->clt_script_name) == -1)
349 return (-1);
350 if (clt_printf(clt, "\n") == -1)
351 return (-1);
352 if (clt_printf(clt, "What you search will be matched against the "
353 "package name (pkgstem), comment, DESCR and maintainer.\n") == -1)
354 return (-1);
355 #else
356 if (tp_home(clt->clt_tp) == -1)
357 return (-1);
358 #endif
360 return (fcgi_end_request(clt, 0));
363 int
364 route_search(struct env *env, struct client *clt)
366 const char *stem, *comment, *fullpkgpath;
367 char *query = clt->clt_query;
368 char equery[1024];
369 int err;
370 int found = 0;
372 if (query == NULL || *query == '\0') {
373 if (server_reply(clt, 10, "search for a package") == -1)
374 return (-1);
375 return (fcgi_end_request(clt, 0));
378 if (unquote(query) == -1 ||
379 fts_escape(query, equery, sizeof(equery)) == -1) {
380 if (server_reply(clt, 59, "bad request") == -1)
381 return (-1);
382 return (fcgi_end_request(clt, 1));
385 log_debug("searching for %s", equery);
387 err = sqlite3_bind_text(env->env_qsearch, 1, equery, -1, NULL);
388 if (err != SQLITE_OK) {
389 log_warnx("%s: sqlite3_bind_text \"%s\": %s", __func__,
390 query, sqlite3_errstr(err));
391 sqlite3_reset(env->env_qsearch);
393 if (server_reply(clt, 42, "internal error") == -1)
394 return (-1);
395 return (fcgi_end_request(clt, 1));
398 if (server_reply(clt, 20, "text/gemini") == -1)
399 goto err;
401 if (clt_printf(clt, "# search results for %s\n\n", query) == -1)
402 goto err;
404 for (;;) {
405 err = sqlite3_step(env->env_qsearch);
406 if (err == SQLITE_DONE)
407 break;
408 if (err != SQLITE_ROW) {
409 log_warnx("%s: sqlite3_step %s", __func__,
410 sqlite3_errstr(err));
411 break;
413 found = 1;
415 stem = sqlite3_column_text(env->env_qsearch, 0);
416 comment = sqlite3_column_text(env->env_qsearch, 1);
417 fullpkgpath = sqlite3_column_text(env->env_qsearch, 2);
419 if (clt_printf(clt, "=> %s/%s %s: %s\n", clt->clt_script_name,
420 fullpkgpath, stem, comment) == -1)
421 goto err;
424 sqlite3_reset(env->env_qsearch);
426 if (!found && clt_printf(clt, "No ports found\n") == -1)
427 return (-1);
429 return (fcgi_end_request(clt, 0));
431 err:
432 sqlite3_reset(env->env_qsearch);
433 return (-1);
436 int
437 route_categories(struct env *env, struct client *clt)
439 const char *fullpkgpath;
440 int err;
442 if (server_reply(clt, 20, "text/gemini") == -1)
443 return (-1);
444 if (clt_printf(clt, "# list of all categories\n") == -1)
445 return (-1);
447 if (clt_puts(clt, "\n") == -1)
448 return (-1);
450 for (;;) {
451 err = sqlite3_step(env->env_qcats);
452 if (err == SQLITE_DONE)
453 break;
454 if (err != SQLITE_ROW) {
455 log_warnx("%s: sqlite3_step %s", __func__,
456 sqlite3_errstr(err));
457 break;
460 fullpkgpath = sqlite3_column_text(env->env_qcats, 0);
462 if (clt_printf(clt, "=> %s/%s %s\n", clt->clt_script_name,
463 fullpkgpath, fullpkgpath) == -1) {
464 sqlite3_reset(env->env_qcats);
465 return (-1);
469 sqlite3_reset(env->env_qcats);
470 return (fcgi_end_request(clt, 0));
473 int
474 route_listing(struct env *env, struct client *clt)
476 char buf[128], *s;
477 const char *path = clt->clt_path_info + 1;
478 const char *fullpkgpath;
479 int err;
481 strlcpy(buf, path, sizeof(buf));
482 while ((s = strrchr(buf, '/')) != NULL)
483 *s = '\0';
485 err = sqlite3_bind_text(env->env_qbycat, 1, buf, -1, NULL);
486 if (err != SQLITE_OK) {
487 log_warnx("%s: sqlite3_bind_text \"%s\": %s", __func__,
488 path, sqlite3_errstr(err));
489 sqlite3_reset(env->env_qbycat);
491 if (server_reply(clt, 42, "internal error") == -1)
492 return (-1);
493 return (fcgi_end_request(clt, 1));
496 if (server_reply(clt, 20, "text/gemini") == -1)
497 goto err;
499 if (clt_printf(clt, "# port(s) under %s\n\n", path) == -1)
500 goto err;
502 for (;;) {
503 err = sqlite3_step(env->env_qbycat);
504 if (err == SQLITE_DONE)
505 break;
506 if (err != SQLITE_ROW) {
507 log_warnx("%s: sqlite3_step %s", __func__,
508 sqlite3_errstr(err));
509 break;
512 fullpkgpath = sqlite3_column_text(env->env_qbycat, 0);
514 if (clt_printf(clt, "=> %s/%s %s\n", clt->clt_script_name,
515 fullpkgpath, fullpkgpath) == -1) {
516 sqlite3_reset(env->env_qbycat);
517 return (-1);
521 sqlite3_reset(env->env_qbycat);
522 return (fcgi_end_request(clt, 0));
524 err:
525 sqlite3_reset(env->env_qbycat);
526 return (-1);
529 static int
530 print_maintainer(struct client *clt, const char *mail)
532 int r, in_addr;
534 for (in_addr = 0; *mail != '\0'; ++mail) {
535 if (!in_addr) {
536 if (clt_putc(clt, *mail) == -1)
537 return (-1);
538 if (*mail == '<')
539 in_addr = 1;
540 continue;
543 switch (*mail) {
544 case '@':
545 r = clt_puts(clt, " at ");
546 break;
547 case '.':
548 r = clt_puts(clt, " dot ");
549 break;
550 case '>':
551 in_addr = 0;
552 /* fallthrough */
553 default:
554 r = clt_putc(clt, *mail);
555 break;
557 if (r == -1)
558 return (-1);
561 return (0);
564 int
565 route_port(struct env *env, struct client *clt)
567 const char *path = clt->clt_path_info + 1;
568 const char *fullpkgpath, *stem, *pkgname, *descr;
569 const char *comment, *maintainer, *readme, *www;
570 const char *version;
571 int err;
573 err = sqlite3_bind_text(env->env_qfullpkgpath, 1, path, -1, NULL);
574 if (err != SQLITE_OK) {
575 log_warnx("%s: sqlite3_bind_text \"%s\": %s", __func__,
576 path, sqlite3_errstr(err));
577 sqlite3_reset(env->env_qfullpkgpath);
579 if (server_reply(clt, 42, "internal error") == -1)
580 return (-1);
581 return (fcgi_end_request(clt, 1));
584 err = sqlite3_step(env->env_qfullpkgpath);
585 if (err == SQLITE_DONE) {
586 /* No rows, retry as a category */
587 sqlite3_reset(env->env_qfullpkgpath);
588 return (route_listing(env, clt));
591 if (err != SQLITE_ROW) {
592 log_warnx("%s: sqlite3_step %s", __func__,
593 sqlite3_errstr(err));
594 if (server_reply(clt, 42, "internal error") == -1)
595 goto err;
596 goto done;
599 fullpkgpath = sqlite3_column_text(env->env_qfullpkgpath, 0);
600 stem = sqlite3_column_text(env->env_qfullpkgpath, 1);
601 comment = sqlite3_column_text(env->env_qfullpkgpath, 2);
602 pkgname = sqlite3_column_text(env->env_qfullpkgpath, 3);
603 descr = sqlite3_column_text(env->env_qfullpkgpath, 4);
604 maintainer = sqlite3_column_text(env->env_qfullpkgpath, 5);
605 readme = sqlite3_column_text(env->env_qfullpkgpath, 6);
606 www = sqlite3_column_text(env->env_qfullpkgpath, 7);
608 if ((version = strrchr(pkgname, '-')) != NULL)
609 version++;
610 else
611 version = " unknown";
613 if (server_reply(clt, 20, "text/gemini") == -1)
614 goto err;
616 if (clt_printf(clt, "# %s v%s\n", path, version) == -1 ||
617 clt_puts(clt, "\n") == -1 ||
618 clt_printf(clt, "``` Command to install the package %s\n",
619 stem) == -1 ||
620 clt_printf(clt, "# pkg_add %s\n", stem) == -1 ||
621 clt_printf(clt, "```\n") == -1 ||
622 clt_printf(clt, "\n") == -1 ||
623 clt_printf(clt, "> %s\n", comment) == -1 ||
624 clt_printf(clt, "\n") == -1 ||
625 clt_printf(clt, "=> https://cvsweb.openbsd.org/ports/%s "
626 "CVS Web\n", fullpkgpath) == -1)
627 goto err;
629 if (www && *www != '\0' &&
630 clt_printf(clt, "=> %s Port Homepage (WWW)\n", www) == -1)
631 goto err;
633 if (clt_printf(clt, "\n") == -1 ||
634 clt_printf(clt, "Maintainer: ") == -1 ||
635 print_maintainer(clt, maintainer) == -1 ||
636 clt_puts(clt, "\n\n") == -1 ||
637 clt_printf(clt, "## Description\n\n") == -1 ||
638 clt_printf(clt, "``` %s description\n", stem) == -1 ||
639 clt_puts(clt, descr) == -1 ||
640 clt_puts(clt, "```\n") == -1 ||
641 clt_puts(clt, "\n") == -1)
642 goto err;
644 if (readme && *readme != '\0') {
645 if (clt_puts(clt, "## Readme\n\n") == -1 ||
646 clt_puts(clt, "\n") == -1 ||
647 clt_printf(clt, "``` README for %s\n", stem) == -1 ||
648 clt_puts(clt, readme) == -1 ||
649 clt_puts(clt, "\n") == -1)
650 goto err;
653 done:
654 sqlite3_reset(env->env_qfullpkgpath);
655 return (fcgi_end_request(clt, 0));
657 err:
658 sqlite3_reset(env->env_qfullpkgpath);
659 return (-1);