Blob


1 /*
2 * Copyright (c) 2021 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 /*
18 * Handles the data in ~/.telescope
19 *
20 * TODO: add some form of locking on the files
21 */
23 #include "compat.h"
25 #include <sys/stat.h>
26 #include <sys/types.h>
28 #include <dirent.h>
29 #include <errno.h>
30 #include <fcntl.h>
31 #include <limits.h>
32 #include <stdio.h>
33 #include <stdlib.h>
34 #include <string.h>
35 #include <unistd.h>
37 #include "pages.h"
38 #include "telescope.h"
40 static void die(void) __attribute__((__noreturn__));
41 static void send_file(uint32_t, FILE *);
42 static void handle_get(struct imsg*, size_t);
43 static int select_non_dot(const struct dirent *);
44 static int select_non_dotdot(const struct dirent *);
45 static void handle_get_file(struct imsg*, size_t);
46 static void handle_quit(struct imsg*, size_t);
47 static void handle_bookmark_page(struct imsg*, size_t);
48 static void handle_save_cert(struct imsg*, size_t);
49 static void handle_update_cert(struct imsg*, size_t);
50 static void handle_file_open(struct imsg*, size_t);
51 static void handle_session_start(struct imsg*, size_t);
52 static void handle_session_tab(struct imsg*, size_t);
53 static void handle_session_tab_title(struct imsg*, size_t);
54 static void handle_session_end(struct imsg*, size_t);
55 static void handle_dispatch_imsg(int, short, void*);
56 static int fs_send_ui(int, uint32_t, int, const void *, uint16_t);
58 static struct imsgev *iev_ui;
59 static FILE *session;
61 static char base_path[PATH_MAX];
62 static char lockfile_path[PATH_MAX];
63 static char bookmark_file[PATH_MAX];
64 static char known_hosts_file[PATH_MAX], known_hosts_tmp[PATH_MAX];
65 static char crashed_file[PATH_MAX];
67 char session_file[PATH_MAX];
69 static imsg_handlerfn *handlers[] = {
70 [IMSG_GET] = handle_get,
71 [IMSG_GET_FILE] = handle_get_file,
72 [IMSG_QUIT] = handle_quit,
73 [IMSG_BOOKMARK_PAGE] = handle_bookmark_page,
74 [IMSG_SAVE_CERT] = handle_save_cert,
75 [IMSG_UPDATE_CERT] = handle_update_cert,
76 [IMSG_FILE_OPEN] = handle_file_open,
77 [IMSG_SESSION_START] = handle_session_start,
78 [IMSG_SESSION_TAB] = handle_session_tab,
79 [IMSG_SESSION_TAB_TITLE] = handle_session_tab_title,
80 [IMSG_SESSION_END] = handle_session_end,
81 };
83 static void __attribute__((__noreturn__))
84 die(void)
85 {
86 abort(); /* TODO */
87 }
89 static void
90 send_file(uint32_t peerid, FILE *f)
91 {
92 ssize_t r;
93 char buf[BUFSIZ];
95 for (;;) {
96 r = fread(buf, 1, sizeof(buf), f);
97 if (r != 0)
98 fs_send_ui(IMSG_BUF, peerid, -1, buf, r);
99 if (r != sizeof(buf))
100 break;
102 fs_send_ui(IMSG_EOF, peerid, -1, NULL, 0);
103 fclose(f);
106 static void
107 handle_get(struct imsg *imsg, size_t datalen)
109 const char *bpath = "bookmarks.gmi";
110 char path[PATH_MAX];
111 FILE *f;
112 const char *data, *p;
113 size_t i;
114 struct page {
115 const char *name;
116 const char *path;
117 const uint8_t *data;
118 size_t len;
119 } pages[] = {
120 {"about", NULL, about_about, about_about_len},
121 {"blank", NULL, about_blank, about_blank_len},
122 {"bookmarks", bpath, bookmarks, bookmarks_len},
123 {"crash", NULL, about_crash, about_crash_len},
124 {"help", NULL, about_help, about_help_len},
125 {"license", NULL, about_license, about_license_len},
126 {"new", NULL, about_new, about_new_len},
127 }, *page = NULL;
129 data = imsg->data;
130 if (data[datalen-1] != '\0') /* make sure it's NUL-terminated */
131 die();
132 if ((data = strchr(data, ':')) == NULL)
133 goto notfound;
134 data++;
136 for (i = 0; i < sizeof(pages)/sizeof(pages[0]); ++i)
137 if (!strcmp(data, pages[i].name)) {
138 page = &pages[i];
139 break;
142 if (page == NULL)
143 goto notfound;
145 strlcpy(path, base_path, sizeof(path));
146 strlcat(path, "/", sizeof(path));
147 if (page->path != NULL)
148 strlcat(path, page->path, sizeof(path));
149 else {
150 strlcat(path, "pages/about_", sizeof(path));
151 strlcat(path, page->name, sizeof(path));
152 strlcat(path, ".gmi", sizeof(path));
155 if ((f = fopen(path, "r")) == NULL) {
156 fs_send_ui(IMSG_BUF, imsg->hdr.peerid, -1,
157 page->data, page->len);
158 fs_send_ui(IMSG_EOF, imsg->hdr.peerid, -1,
159 NULL, 0);
160 return;
163 send_file(imsg->hdr.peerid, f);
164 return;
166 notfound:
167 p = "# not found!\n";
168 fs_send_ui(IMSG_BUF, imsg->hdr.peerid, -1, p, strlen(p));
169 fs_send_ui(IMSG_EOF, imsg->hdr.peerid, -1, NULL, 0);
172 static inline void
173 send_hdr(uint32_t peerid, int code, const char *meta)
175 fs_send_ui(IMSG_GOT_CODE, peerid, -1, &code, sizeof(code));
176 fs_send_ui(IMSG_GOT_META, peerid, -1, meta, strlen(meta)+1);
179 static inline void
180 send_errno(uint32_t peerid, int code, const char *str, int no)
182 char *s;
184 if (asprintf(&s, "%s: %s", str, strerror(no)) == -1)
185 s = NULL;
187 send_hdr(peerid, code, s == NULL ? str : s);
188 free(s);
191 static inline const char *
192 file_type(const char *path)
194 struct mapping {
195 const char *ext;
196 const char *mime;
197 } ms[] = {
198 {"diff", "text/x-patch"},
199 {"gemini", "text/gemini"},
200 {"gmi", "text/gemini"},
201 {"markdown", "text/plain"},
202 {"md", "text/plain"},
203 {"patch", "text/x-patch"},
204 {"txt", "text/plain"},
205 {NULL, NULL},
206 }, *m;
207 char *dot;
209 if ((dot = strrchr(path, '.')) == NULL)
210 return NULL;
212 dot++;
214 for (m = ms; m->ext != NULL; ++m)
215 if (!strcmp(m->ext, dot))
216 return m->mime;
218 return NULL;
221 static int
222 select_non_dot(const struct dirent *d)
224 return strcmp(d->d_name, ".");
227 static int
228 select_non_dotdot(const struct dirent *d)
230 return strcmp(d->d_name, ".") && strcmp(d->d_name, "..");
233 static inline void
234 send_dir(uint32_t peerid, const char *path)
236 struct dirent **names;
237 struct evbuffer *ev;
238 char *s;
239 int (*selector)(const struct dirent *) = select_non_dot;
240 int i, len, no;
242 if (!has_suffix(path, "/")) {
243 if (asprintf(&s, "%s/", path) == -1)
244 die();
245 send_hdr(peerid, 30, s);
246 free(s);
247 return;
250 if (!strcmp(path, "/"))
251 selector = select_non_dotdot;
253 if ((ev = evbuffer_new()) == NULL ||
254 (len = scandir(path, &names, selector, alphasort)) == -1) {
255 no = errno;
256 evbuffer_free(ev);
257 send_errno(peerid, 40, "failure reading the directory", no);
258 return;
261 evbuffer_add_printf(ev, "# Index of %s\n\n", path);
262 for (i = 0; i < len; ++i) {
263 evbuffer_add_printf(ev, "=> %s", names[i]->d_name);
264 if (names[i]->d_type == DT_DIR)
265 evbuffer_add(ev, "/", 1);
266 evbuffer_add(ev, "\n", 1);
269 send_hdr(peerid, 20, "text/gemini");
270 fs_send_ui(IMSG_BUF, peerid, -1,
271 EVBUFFER_DATA(ev), EVBUFFER_LENGTH(ev));
272 fs_send_ui(IMSG_EOF, peerid, -1, NULL, 0);
274 evbuffer_free(ev);
275 free(names);
278 static void
279 handle_get_file(struct imsg *imsg, size_t datalen)
281 struct stat sb;
282 FILE *f;
283 char *data;
284 const char *meta = NULL;
286 data = imsg->data;
287 data[datalen-1] = '\0';
289 if ((f = fopen(data, "r")) == NULL) {
290 send_errno(imsg->hdr.peerid, 51, "can't open", errno);
291 return;
294 if (fstat(fileno(f), &sb) == -1) {
295 send_errno(imsg->hdr.peerid, 40, "fstat", errno);
296 return;
299 if (S_ISDIR(sb.st_mode)) {
300 fclose(f);
301 send_dir(imsg->hdr.peerid, data);
302 return;
305 if ((meta = file_type(data)) == NULL) {
306 fclose(f);
307 send_hdr(imsg->hdr.peerid, 51,
308 "don't know how to visualize this file");
309 return;
312 send_hdr(imsg->hdr.peerid, 20, meta);
313 send_file(imsg->hdr.peerid, f);
316 static void
317 handle_quit(struct imsg *imsg, size_t datalen)
319 if (!safe_mode)
320 unlink(crashed_file);
322 event_loopbreak();
325 static void
326 handle_bookmark_page(struct imsg *imsg, size_t datalen)
328 char *data;
329 int res;
330 FILE *f;
332 data = imsg->data;
333 if (data[datalen-1] != '\0')
334 die();
336 if ((f = fopen(bookmark_file, "a")) == NULL) {
337 res = errno;
338 goto end;
340 fprintf(f, "=> %s\n", data);
341 fclose(f);
343 res = 0;
344 end:
345 fs_send_ui(IMSG_BOOKMARK_OK, 0, -1, &res, sizeof(res));
348 static void
349 handle_save_cert(struct imsg *imsg, size_t datalen)
351 struct tofu_entry e;
352 FILE *f;
353 int res;
355 /* TODO: traverse the file to avoid duplications? */
357 if (datalen != sizeof(e))
358 die();
359 memcpy(&e, imsg->data, datalen);
361 if ((f = fopen(known_hosts_file, "a")) == NULL) {
362 res = errno;
363 goto end;
365 fprintf(f, "%s %s %d\n", e.domain, e.hash, e.verified);
366 fclose(f);
368 res = 0;
369 end:
370 fs_send_ui(IMSG_SAVE_CERT_OK, imsg->hdr.peerid, -1,
371 &res, sizeof(res));
374 static void
375 handle_update_cert(struct imsg *imsg, size_t datalen)
377 FILE *tmp, *f;
378 struct tofu_entry entry;
379 char sfn[PATH_MAX], *line = NULL, *t;
380 size_t l, linesize = 0;
381 ssize_t linelen;
382 int fd, e, res = 0;
384 if (datalen != sizeof(entry))
385 die();
386 memcpy(&entry, imsg->data, datalen);
388 strlcpy(sfn, known_hosts_tmp, sizeof(sfn));
389 if ((fd = mkstemp(sfn)) == -1 ||
390 (tmp = fdopen(fd, "w")) == NULL) {
391 if (fd != -1) {
392 unlink(sfn);
393 close(fd);
395 res = 0;
396 goto end;
399 if ((f = fopen(known_hosts_file, "r")) == NULL) {
400 unlink(sfn);
401 fclose(tmp);
402 res = 0;
403 goto end;
406 l = strlen(entry.domain);
407 while ((linelen = getline(&line, &linesize, f)) != -1) {
408 if ((t = strstr(line, entry.domain)) != NULL &&
409 (line[l] == ' ' || line[l] == '\t'))
410 continue;
411 /* line has a trailing \n */
412 fprintf(tmp, "%s", line);
414 fprintf(tmp, "%s %s %d\n", entry.domain, entry.hash, entry.verified);
416 free(line);
417 e = ferror(tmp);
419 fclose(tmp);
420 fclose(f);
422 if (e) {
423 unlink(sfn);
424 res = 0;
425 goto end;
428 res = rename(sfn, known_hosts_file) != -1;
430 end:
431 fs_send_ui(IMSG_UPDATE_CERT_OK, imsg->hdr.peerid, -1,
432 &res, sizeof(res));
435 static void
436 handle_file_open(struct imsg *imsg, size_t datalen)
438 char *path, *e;
439 int fd;
441 path = imsg->data;
442 if (path[datalen-1] != '\0')
443 die();
445 if ((fd = open(path, O_WRONLY | O_TRUNC | O_CREAT, 0644)) == -1) {
446 e = strerror(errno);
447 fs_send_ui(IMSG_FILE_OPENED, imsg->hdr.peerid, -1,
448 e, strlen(e)+1);
449 } else
450 fs_send_ui(IMSG_FILE_OPENED, imsg->hdr.peerid, fd,
451 NULL, 0);
454 static void
455 handle_session_start(struct imsg *imsg, size_t datalen)
457 if (datalen != 0)
458 die();
460 if ((session = fopen(session_file, "w")) == NULL)
461 die();
464 static void
465 handle_session_tab(struct imsg *imsg, size_t datalen)
467 char *url;
468 uint32_t flags;
470 if (session == NULL)
471 die();
473 flags = imsg->hdr.peerid;
474 url = imsg->data;
475 if (datalen == 0 || url[datalen-1] != '\0')
476 die();
477 fprintf(session, "%s", url);
479 if (flags & TAB_CURRENT)
480 fprintf(session, " current ");
481 else
482 fprintf(session, " - ");
485 static void
486 handle_session_tab_title(struct imsg *imsg, size_t datalen)
488 const char *title;
490 title = imsg->data;
491 if (title == NULL) {
492 datalen = 1;
493 title = "";
496 if (title[datalen-1] != '\0')
497 die();
499 fprintf(session, "%s\n", title);
502 static void
503 handle_session_end(struct imsg *imsg, size_t datalen)
505 if (session == NULL)
506 die();
507 fclose(session);
508 session = NULL;
511 static void
512 handle_dispatch_imsg(int fd, short ev, void *d)
514 struct imsgev *iev = d;
515 int e;
517 if (dispatch_imsg(iev, ev, handlers, sizeof(handlers)) == -1) {
518 /*
519 * This should leave a ~/.telescope/crashed file to
520 * trigger about:crash on next run. Unfortunately, if
521 * the main process dies the fs sticks around and
522 * doesn't notice that the fd was closed. Why EV_READ
523 * is not triggered when a fd is closed on the other end?
524 */
525 e = errno;
526 if ((fd = open(crashed_file, O_CREAT|O_TRUNC|O_WRONLY, 0600))
527 == -1)
528 err(1, "open");
529 close(fd);
530 errx(1, "connection closed: %s", strerror(e));
534 static int
535 fs_send_ui(int type, uint32_t peerid, int fd, const void *data,
536 uint16_t datalen)
538 return imsg_compose_event(iev_ui, type, peerid, 0, fd,
539 data, datalen);
542 int
543 fs_init(void)
545 strlcpy(base_path, getenv("HOME"), sizeof(base_path));
546 strlcat(base_path, "/.telescope", sizeof(base_path));
547 mkdir(base_path, 0700);
549 strlcpy(lockfile_path, base_path, sizeof(lockfile_path));
550 strlcat(lockfile_path, "/lock", sizeof(lockfile_path));
552 strlcpy(bookmark_file, base_path, sizeof(bookmark_file));
553 strlcat(bookmark_file, "/bookmarks.gmi", sizeof(bookmark_file));
555 strlcpy(known_hosts_file, base_path, sizeof(known_hosts_file));
556 strlcat(known_hosts_file, "/known_hosts", sizeof(known_hosts_file));
558 strlcpy(known_hosts_tmp, base_path, sizeof(known_hosts_tmp));
559 strlcat(known_hosts_tmp, "/known_hosts.tmp.XXXXXXXXXX",
560 sizeof(known_hosts_file));
562 strlcpy(session_file, base_path, sizeof(session_file));
563 strlcat(session_file, "/session", sizeof(session_file));
565 strlcpy(crashed_file, base_path, sizeof(crashed_file));
566 strlcat(crashed_file, "/crashed", sizeof(crashed_file));
568 return 1;
571 int
572 fs_main(void)
574 setproctitle("fs");
576 fs_init();
578 event_init();
580 /* Setup pipe and event handler to the main process */
581 if ((iev_ui = malloc(sizeof(*iev_ui))) == NULL)
582 die();
583 imsg_init(&iev_ui->ibuf, 3);
584 iev_ui->handler = handle_dispatch_imsg;
585 iev_ui->events = EV_READ;
586 event_set(&iev_ui->ev, iev_ui->ibuf.fd, iev_ui->events,
587 iev_ui->handler, iev_ui);
588 event_add(&iev_ui->ev, NULL);
590 sandbox_fs_process();
592 event_dispatch();
593 return 0;
598 /*
599 * Check if the last time telescope crashed. The check is done by
600 * looking at `crashed_file': if it exists then last time we crashed.
601 * Then, while here, touch the file too. During IMSG_QUIT we'll
602 * remove it.
603 */
604 int
605 last_time_crashed(void)
607 int fd, crashed = 1;
609 if (safe_mode)
610 return 0;
612 if (unlink(crashed_file) == -1 && errno == ENOENT)
613 crashed = 0;
615 if ((fd = open(crashed_file, O_CREAT|O_WRONLY, 0600)) == -1)
616 return crashed;
617 close(fd);
619 return crashed;
622 int
623 lock_session(void)
625 struct flock lock;
626 int fd;
628 if ((fd = open(lockfile_path, O_WRONLY|O_CREAT, 0600)) == -1)
629 return -1;
631 lock.l_start = 0;
632 lock.l_len = 0;
633 lock.l_type = F_WRLCK;
634 lock.l_whence = SEEK_SET;
636 if (fcntl(fd, F_SETLK, &lock) == -1) {
637 close(fd);
638 return -1;
641 return fd;
644 static int
645 parse_khost_line(char *line, char *tmp[3])
647 char **ap;
649 for (ap = tmp; ap < &tmp[3] &&
650 (*ap = strsep(&line, " \t\n")) != NULL;) {
651 if (**ap != '\0')
652 ap++;
655 return ap == &tmp[3] && *line == '\0';
658 int
659 load_certs(struct ohash *h)
661 char *tmp[3], *line = NULL;
662 const char *errstr;
663 size_t lineno = 0, linesize = 0;
664 ssize_t linelen;
665 FILE *f;
666 struct tofu_entry *e;
668 if ((f = fopen(known_hosts_file, "r")) == NULL)
669 return 0;
671 while ((linelen = getline(&line, &linesize, f)) != -1) {
672 if ((e = calloc(1, sizeof(*e))) == NULL)
673 abort();
675 lineno++;
677 if (parse_khost_line(line, tmp)) {
678 strlcpy(e->domain, tmp[0], sizeof(e->domain));
679 strlcpy(e->hash, tmp[1], sizeof(e->hash));
681 e->verified = strtonum(tmp[2], 0, 1, &errstr);
682 if (errstr != NULL)
683 errx(1, "%s:%zu verification for %s is %s: %s",
684 known_hosts_file, lineno,
685 e->domain, errstr, tmp[2]);
686 tofu_add(h, e);
687 } else {
688 warnx("%s:%zu invalid entry",
689 known_hosts_file, lineno);
690 free(e);
694 free(line);
695 return ferror(f);