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"
33 #if template
34 #include "tmpl.h"
35 #endif
37 #ifndef nitems
38 #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
39 #endif
41 char dbpath[PATH_MAX];
43 void server_sig_handler(int, short, void *);
44 void server_open_db(struct env *);
45 void server_close_db(struct env *);
46 __dead void server_shutdown(struct env *);
47 int server_reply(struct client *, int, const char *);
49 int route_dispatch(struct env *, struct client *);
50 int route_home(struct env *, struct client *);
51 int route_search(struct env *, struct client *);
52 int route_categories(struct env *, struct client *);
53 int route_listing(struct env *, struct client *);
54 int route_port(struct env *, struct client *);
56 typedef int (*route_t)(struct env *, struct client *);
58 static const struct route {
59 const char *r_path;
60 route_t r_fn;
61 } routes[] = {
62 { "/", route_home },
63 { "/search", route_search },
64 { "/all", route_categories },
65 { "/*", route_port },
66 };
68 void
69 server_sig_handler(int sig, short ev, void *arg)
70 {
71 struct env *env = arg;
73 /*
74 * Normal signal handler rules don't apply because libevent
75 * decouples for us.
76 */
78 switch (sig) {
79 case SIGHUP:
80 log_info("re-opening the db");
81 server_close_db(env);
82 server_open_db(env);
83 break;
84 case SIGTERM:
85 case SIGINT:
86 server_shutdown(env);
87 break;
88 default:
89 fatalx("unexpected signal %d", sig);
90 }
91 }
93 static inline void
94 loadstmt(sqlite3 *db, sqlite3_stmt **stmt, const char *sql)
95 {
96 int err;
98 err = sqlite3_prepare_v2(db, sql, -1, stmt, NULL);
99 if (err != SQLITE_OK)
100 fatalx("failed prepare statement \"%s\": %s",
101 sql, sqlite3_errstr(err));
104 void
105 server_open_db(struct env *env)
107 int err;
109 err = sqlite3_open_v2(dbpath, &env->env_db,
110 SQLITE_OPEN_READONLY, NULL);
111 if (err != SQLITE_OK)
112 fatalx("can't open database %s: %s", dbpath,
113 sqlite3_errmsg(env->env_db));
115 /* load prepared statements */
116 loadstmt(env->env_db, &env->env_qsearch,
117 "select webpkg_fts.pkgstem, webpkg_fts.comment, paths.fullpkgpath"
118 " from webpkg_fts"
119 " join _ports p on p.fullpkgpath = webpkg_fts.id"
120 " join _paths paths on paths.id = webpkg_fts.id"
121 " where webpkg_fts match ?"
122 " order by bm25(webpkg_fts)");
124 loadstmt(env->env_db, &env->env_qfullpkgpath,
125 "select p.fullpkgpath, pp.pkgstem, pp.comment, pp.pkgname,"
126 " d.value, e.value, r.value, pp.homepage"
127 " from _paths p"
128 " join _descr d on d.fullpkgpath = p.id"
129 " join _ports pp on pp.fullpkgpath = p.id"
130 " join _email e on e.keyref = pp.maintainer"
131 " left join _readme r on r.fullpkgpath = p.id"
132 " where p.fullpkgpath = ?");
134 loadstmt(env->env_db, &env->env_qcats,
135 "select distinct value from categories order by value");
137 loadstmt(env->env_db, &env->env_qbycat,
138 "select fullpkgpath from categories where value = ?"
139 " order by fullpkgpath");
142 void
143 server_close_db(struct env *env)
145 int err;
147 sqlite3_finalize(env->env_qsearch);
148 sqlite3_finalize(env->env_qfullpkgpath);
149 sqlite3_finalize(env->env_qcats);
150 sqlite3_finalize(env->env_qbycat);
152 if ((err = sqlite3_close(env->env_db)) != SQLITE_OK)
153 log_warnx("sqlite3_close %s", sqlite3_errstr(err));
156 int
157 server_main(const char *db)
159 struct env env;
160 struct event sighup;
161 struct event sigint;
162 struct event sigterm;
164 signal(SIGPIPE, SIG_IGN);
166 memset(&env, 0, sizeof(env));
168 if (pledge("stdio rpath flock unix", NULL) == -1)
169 fatal("pledge");
171 if (realpath(db, dbpath) == NULL)
172 fatal("realpath %s", db);
174 server_open_db(&env);
176 event_init();
178 env.env_sockfd = 3;
180 event_set(&env.env_sockev, env.env_sockfd, EV_READ | EV_PERSIST,
181 fcgi_accept, &env);
182 event_add(&env.env_sockev, NULL);
184 evtimer_set(&env.env_pausev, fcgi_accept, &env);
186 signal_set(&sighup, SIGHUP, server_sig_handler, &env);
187 signal_set(&sigint, SIGINT, server_sig_handler, &env);
188 signal_set(&sigterm, SIGTERM, server_sig_handler, &env);
190 signal_add(&sighup, NULL);
191 signal_add(&sigint, NULL);
192 signal_add(&sigterm, NULL);
194 log_info("ready");
195 event_dispatch();
197 server_shutdown(&env);
200 void __dead
201 server_shutdown(struct env *env)
203 log_info("shutting down");
204 server_close_db(env);
205 exit(0);
208 int
209 server_reply(struct client *clt, int status, const char *ctype)
211 if (clt_printf(clt, "%02d %s\r\n", status, ctype) == -1)
212 return (-1);
213 return (0);
216 int
217 server_handle(struct env *env, struct client *clt)
219 log_debug("SCRIPT_NAME %s", clt->clt_script_name);
220 log_debug("PATH_INFO %s", clt->clt_path_info);
221 return (route_dispatch(env, clt));
224 void
225 server_client_free(struct client *clt)
227 #if template
228 template_free(clt->clt_tp);
229 #endif
230 free(clt->clt_server_name);
231 free(clt->clt_script_name);
232 free(clt->clt_path_info);
233 free(clt->clt_query);
234 free(clt);
237 static inline int
238 unquote(char *str)
240 char *p, *q;
241 char hex[3];
242 unsigned long x;
244 hex[2] = '\0';
245 p = q = str;
246 while (*p) {
247 switch (*p) {
248 case '%':
249 if (!isxdigit((unsigned char)p[1]) ||
250 !isxdigit((unsigned char)p[2]) ||
251 (p[1] == '0' && p[2] == '0'))
252 return (-1);
254 hex[0] = p[1];
255 hex[1] = p[2];
257 x = strtoul(hex, NULL, 16);
258 *q++ = (char)x;
259 p += 3;
260 break;
261 default:
262 *q++ = *p++;
263 break;
266 *q = '\0';
267 return (0);
270 static inline int
271 fts_escape(const char *p, char *buf, size_t bufsize)
273 char *q;
275 /*
276 * split p into words and quote them into buf.
277 * quoting means wrapping each word into "..." and
278 * replace every " with "".
279 * i.e. 'C++ "framework"' -> '"C++" """framework"""'
280 * flatting all the whitespaces seems fine too.
281 */
283 q = buf;
284 while (bufsize != 0) {
285 p += strspn(p, " \f\n\r\t\v");
286 if (*p == '\0')
287 break;
289 *q++ = '"';
290 bufsize--;
291 while (*p && !isspace((unsigned char)*p) && bufsize != 0) {
292 if (*p == '"') { /* double the quote character */
293 *q++ = '"';
294 bufsize--;
295 if (bufsize == 0)
296 break;
298 *q++ = *p++;
299 bufsize--;
302 if (bufsize < 2)
303 break;
304 *q++ = '"';
305 *q++ = ' ';
306 bufsize -= 2;
308 if ((*p == '\0') && bufsize != 0) {
309 *q = '\0';
310 return (0);
313 return (-1);
316 int
317 route_dispatch(struct env *env, struct client *clt)
319 const struct route *r;
320 size_t i;
322 for (i = 0; i < nitems(routes); ++i) {
323 r = &routes[i];
325 if (fnmatch(r->r_path, clt->clt_path_info, 0) != 0)
326 continue;
327 return (r->r_fn(env, clt));
330 if (server_reply(clt, 51, "not found") == -1)
331 return (-1);
332 return (fcgi_end_request(clt, 0));
335 int
336 route_home(struct env *env, struct client *clt)
338 if (server_reply(clt, 20, "text/gemini") == -1)
339 return (-1);
341 #if 1
342 if (clt_printf(clt, "# pkg_fcgi\n\n") == -1)
343 return (-1);
344 if (clt_printf(clt, "Welcome to pkg_fcgi, the Gemini interface "
345 "for the OpenBSD ports collection.\n\n") == -1)
346 return (-1);
347 if (clt_printf(clt, "=> %s/search Search for a package\n",
348 clt->clt_script_name) == -1)
349 return (-1);
350 if (clt_printf(clt, "=> %s/all All categories\n",
351 clt->clt_script_name) == -1)
352 return (-1);
353 if (clt_printf(clt, "\n") == -1)
354 return (-1);
355 if (clt_printf(clt, "What you search will be matched against the "
356 "package name (pkgstem), comment, DESCR and maintainer.\n") == -1)
357 return (-1);
358 #else
359 if (tp_home(clt->clt_tp) == -1)
360 return (-1);
361 #endif
363 return (fcgi_end_request(clt, 0));
366 int
367 route_search(struct env *env, struct client *clt)
369 const char *stem, *comment, *fullpkgpath;
370 char *query = clt->clt_query;
371 char equery[1024];
372 int err;
373 int found = 0;
375 if (query == NULL || *query == '\0') {
376 if (server_reply(clt, 10, "search for a package") == -1)
377 return (-1);
378 return (fcgi_end_request(clt, 0));
381 if (unquote(query) == -1 ||
382 fts_escape(query, equery, sizeof(equery)) == -1) {
383 if (server_reply(clt, 59, "bad request") == -1)
384 return (-1);
385 return (fcgi_end_request(clt, 1));
388 log_debug("searching for %s", equery);
390 err = sqlite3_bind_text(env->env_qsearch, 1, equery, -1, NULL);
391 if (err != SQLITE_OK) {
392 log_warnx("%s: sqlite3_bind_text \"%s\": %s", __func__,
393 query, sqlite3_errstr(err));
394 sqlite3_reset(env->env_qsearch);
396 if (server_reply(clt, 42, "internal error") == -1)
397 return (-1);
398 return (fcgi_end_request(clt, 1));
401 if (server_reply(clt, 20, "text/gemini") == -1)
402 goto err;
404 if (clt_printf(clt, "# search results for %s\n\n", query) == -1)
405 goto err;
407 for (;;) {
408 err = sqlite3_step(env->env_qsearch);
409 if (err == SQLITE_DONE)
410 break;
411 if (err != SQLITE_ROW) {
412 log_warnx("%s: sqlite3_step %s", __func__,
413 sqlite3_errstr(err));
414 break;
416 found = 1;
418 stem = sqlite3_column_text(env->env_qsearch, 0);
419 comment = sqlite3_column_text(env->env_qsearch, 1);
420 fullpkgpath = sqlite3_column_text(env->env_qsearch, 2);
422 if (clt_printf(clt, "=> %s/%s %s: %s\n", clt->clt_script_name,
423 fullpkgpath, stem, comment) == -1)
424 goto err;
427 sqlite3_reset(env->env_qsearch);
429 if (!found && clt_printf(clt, "No ports found\n") == -1)
430 return (-1);
432 return (fcgi_end_request(clt, 0));
434 err:
435 sqlite3_reset(env->env_qsearch);
436 return (-1);
439 int
440 route_categories(struct env *env, struct client *clt)
442 const char *fullpkgpath;
443 int err;
445 if (server_reply(clt, 20, "text/gemini") == -1)
446 return (-1);
447 if (clt_printf(clt, "# list of all categories\n") == -1)
448 return (-1);
450 if (clt_puts(clt, "\n") == -1)
451 return (-1);
453 for (;;) {
454 err = sqlite3_step(env->env_qcats);
455 if (err == SQLITE_DONE)
456 break;
457 if (err != SQLITE_ROW) {
458 log_warnx("%s: sqlite3_step %s", __func__,
459 sqlite3_errstr(err));
460 break;
463 fullpkgpath = sqlite3_column_text(env->env_qcats, 0);
465 if (clt_printf(clt, "=> %s/%s %s\n", clt->clt_script_name,
466 fullpkgpath, fullpkgpath) == -1) {
467 sqlite3_reset(env->env_qcats);
468 return (-1);
472 sqlite3_reset(env->env_qcats);
473 return (fcgi_end_request(clt, 0));
476 int
477 route_listing(struct env *env, struct client *clt)
479 char buf[128], *s;
480 const char *path = clt->clt_path_info + 1;
481 const char *fullpkgpath;
482 int err;
484 strlcpy(buf, path, sizeof(buf));
485 while ((s = strrchr(buf, '/')) != NULL)
486 *s = '\0';
488 err = sqlite3_bind_text(env->env_qbycat, 1, buf, -1, NULL);
489 if (err != SQLITE_OK) {
490 log_warnx("%s: sqlite3_bind_text \"%s\": %s", __func__,
491 path, sqlite3_errstr(err));
492 sqlite3_reset(env->env_qbycat);
494 if (server_reply(clt, 42, "internal error") == -1)
495 return (-1);
496 return (fcgi_end_request(clt, 1));
499 if (server_reply(clt, 20, "text/gemini") == -1)
500 goto err;
502 if (clt_printf(clt, "# port(s) under %s\n\n", path) == -1)
503 goto err;
505 for (;;) {
506 err = sqlite3_step(env->env_qbycat);
507 if (err == SQLITE_DONE)
508 break;
509 if (err != SQLITE_ROW) {
510 log_warnx("%s: sqlite3_step %s", __func__,
511 sqlite3_errstr(err));
512 break;
515 fullpkgpath = sqlite3_column_text(env->env_qbycat, 0);
517 if (clt_printf(clt, "=> %s/%s %s\n", clt->clt_script_name,
518 fullpkgpath, fullpkgpath) == -1) {
519 sqlite3_reset(env->env_qbycat);
520 return (-1);
524 sqlite3_reset(env->env_qbycat);
525 return (fcgi_end_request(clt, 0));
527 err:
528 sqlite3_reset(env->env_qbycat);
529 return (-1);
532 static int
533 print_maintainer(struct client *clt, const char *mail)
535 int r, in_addr;
537 for (in_addr = 0; *mail != '\0'; ++mail) {
538 if (!in_addr) {
539 if (clt_putc(clt, *mail) == -1)
540 return (-1);
541 if (*mail == '<')
542 in_addr = 1;
543 continue;
546 switch (*mail) {
547 case '@':
548 r = clt_puts(clt, " at ");
549 break;
550 case '.':
551 r = clt_puts(clt, " dot ");
552 break;
553 case '>':
554 in_addr = 0;
555 /* fallthrough */
556 default:
557 r = clt_putc(clt, *mail);
558 break;
560 if (r == -1)
561 return (-1);
564 return (0);
567 int
568 route_port(struct env *env, struct client *clt)
570 const char *path = clt->clt_path_info + 1;
571 const char *fullpkgpath, *stem, *pkgname, *descr;
572 const char *comment, *maintainer, *readme, *www;
573 const char *version;
574 int err;
576 err = sqlite3_bind_text(env->env_qfullpkgpath, 1, path, -1, NULL);
577 if (err != SQLITE_OK) {
578 log_warnx("%s: sqlite3_bind_text \"%s\": %s", __func__,
579 path, sqlite3_errstr(err));
580 sqlite3_reset(env->env_qfullpkgpath);
582 if (server_reply(clt, 42, "internal error") == -1)
583 return (-1);
584 return (fcgi_end_request(clt, 1));
587 err = sqlite3_step(env->env_qfullpkgpath);
588 if (err == SQLITE_DONE) {
589 /* No rows, retry as a category */
590 sqlite3_reset(env->env_qfullpkgpath);
591 return (route_listing(env, clt));
594 if (err != SQLITE_ROW) {
595 log_warnx("%s: sqlite3_step %s", __func__,
596 sqlite3_errstr(err));
597 if (server_reply(clt, 42, "internal error") == -1)
598 goto err;
599 goto done;
602 fullpkgpath = sqlite3_column_text(env->env_qfullpkgpath, 0);
603 stem = sqlite3_column_text(env->env_qfullpkgpath, 1);
604 comment = sqlite3_column_text(env->env_qfullpkgpath, 2);
605 pkgname = sqlite3_column_text(env->env_qfullpkgpath, 3);
606 descr = sqlite3_column_text(env->env_qfullpkgpath, 4);
607 maintainer = sqlite3_column_text(env->env_qfullpkgpath, 5);
608 readme = sqlite3_column_text(env->env_qfullpkgpath, 6);
609 www = sqlite3_column_text(env->env_qfullpkgpath, 7);
611 if ((version = strrchr(pkgname, '-')) != NULL)
612 version++;
613 else
614 version = " unknown";
616 if (server_reply(clt, 20, "text/gemini") == -1)
617 goto err;
619 if (clt_printf(clt, "# %s v%s\n", path, version) == -1 ||
620 clt_puts(clt, "\n") == -1 ||
621 clt_printf(clt, "``` Command to install the package %s\n",
622 stem) == -1 ||
623 clt_printf(clt, "# pkg_add %s\n", stem) == -1 ||
624 clt_printf(clt, "```\n") == -1 ||
625 clt_printf(clt, "\n") == -1 ||
626 clt_printf(clt, "> %s\n", comment) == -1 ||
627 clt_printf(clt, "\n") == -1 ||
628 clt_printf(clt, "=> https://cvsweb.openbsd.org/ports/%s "
629 "CVS Web\n", fullpkgpath) == -1)
630 goto err;
632 if (www && *www != '\0' &&
633 clt_printf(clt, "=> %s Port Homepage (WWW)\n", www) == -1)
634 goto err;
636 if (clt_printf(clt, "\n") == -1 ||
637 clt_printf(clt, "Maintainer: ") == -1 ||
638 print_maintainer(clt, maintainer) == -1 ||
639 clt_puts(clt, "\n\n") == -1 ||
640 clt_printf(clt, "## Description\n\n") == -1 ||
641 clt_printf(clt, "``` %s description\n", stem) == -1 ||
642 clt_puts(clt, descr) == -1 ||
643 clt_puts(clt, "```\n") == -1 ||
644 clt_puts(clt, "\n") == -1)
645 goto err;
647 if (readme && *readme != '\0') {
648 if (clt_puts(clt, "## Readme\n\n") == -1 ||
649 clt_puts(clt, "\n") == -1 ||
650 clt_printf(clt, "``` README for %s\n", stem) == -1 ||
651 clt_puts(clt, readme) == -1 ||
652 clt_puts(clt, "\n") == -1)
653 goto err;
656 done:
657 sqlite3_reset(env->env_qfullpkgpath);
658 return (fcgi_end_request(clt, 0));
660 err:
661 sqlite3_reset(env->env_qfullpkgpath);
662 return (-1);