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 config and runtime files
19 */
21 #include "compat.h"
23 #include <sys/stat.h>
24 #include <sys/types.h>
26 #include <dirent.h>
27 #include <errno.h>
28 #include <fcntl.h>
29 #include <limits.h>
30 #include <libgen.h>
31 #include <stdio.h>
32 #include <stdlib.h>
33 #include <string.h>
34 #include <unistd.h>
36 #include "pages.h"
37 #include "parser.h"
38 #include "session.h"
39 #include "telescope.h"
40 #include "utils.h"
42 #include "fs.h"
44 #ifndef nitems
45 #define nitems(x) (sizeof(x) / sizeof(x[0]))
46 #endif
48 static void die(void) __attribute__((__noreturn__));
49 static int select_non_dot(const struct dirent *);
50 static int select_non_dotdot(const struct dirent *);
51 static size_t join_path(char*, const char*, const char*, size_t);
52 static void getenv_default(char*, const char*, const char*, size_t);
53 static void mkdirs(const char*, mode_t);
54 static void init_paths(void);
55 static void load_last_session(void);
56 static void load_hist(void);
57 static int last_time_crashed(void);
58 static void load_certs(struct ohash *);
60 /*
61 * Where to store user data. These are all equal to ~/.telescope if
62 * it exists.
63 */
64 char config_path_base[PATH_MAX];
65 char data_path_base[PATH_MAX];
66 char cache_path_base[PATH_MAX];
68 char ctlsock_path[PATH_MAX];
69 char config_path[PATH_MAX];
70 char lockfile_path[PATH_MAX];
71 char bookmark_file[PATH_MAX];
72 char known_hosts_file[PATH_MAX], known_hosts_tmp[PATH_MAX];
73 char crashed_file[PATH_MAX];
74 char session_file[PATH_MAX], session_file_tmp[PATH_MAX];
75 char history_file[PATH_MAX], history_file_tmp[PATH_MAX];
77 static void __attribute__((__noreturn__))
78 die(void)
79 {
80 abort(); /* TODO */
81 }
83 static int
84 select_non_dot(const struct dirent *d)
85 {
86 return strcmp(d->d_name, ".");
87 }
89 static int
90 select_non_dotdot(const struct dirent *d)
91 {
92 return strcmp(d->d_name, ".") && strcmp(d->d_name, "..");
93 }
95 static void
96 send_dir(struct tab *tab, const char *path)
97 {
98 struct dirent **names;
99 int (*selector)(const struct dirent *) = select_non_dot;
100 int i, len;
102 #if notyet
103 /*
104 * need something to fake a redirect
105 */
107 if (!has_suffix(path, "/")) {
108 if (asprintf(&s, "%s/", path) == -1)
109 die();
110 send_hdr(peerid, 30, s);
111 free(s);
112 return;
114 #endif
116 if (!strcmp(path, "/"))
117 selector = select_non_dotdot;
119 if ((len = scandir(path, &names, selector, alphasort)) == -1) {
120 load_page_from_str(tab, "# failure reading the directory\n");
121 return;
124 parser_init(tab, gemtext_initparser);
125 parser_parsef(tab, "# Index of %s\n\n", path);
127 for (i = 0; i < len; ++i) {
128 const char *sufx = "";
130 if (names[i]->d_type == DT_DIR)
131 sufx = "/";
133 parser_parsef(tab, "=> %s%s\n", names[i]->d_name, sufx);
136 parser_free(tab);
137 free(names);
140 static int
141 is_dir(FILE *fp)
143 struct stat sb;
145 if (fstat(fileno(fp), &sb) == -1)
146 return 0;
148 return S_ISDIR(sb.st_mode);
151 static parserinit
152 file_type(const char *path)
154 struct mapping {
155 const char *ext;
156 parserinit fn;
157 } ms[] = {
158 {"diff", textpatch_initparser},
159 {"gemini", gemtext_initparser},
160 {"gmi", gemtext_initparser},
161 {"markdown", textplain_initparser},
162 {"md", textplain_initparser},
163 {"patch", gemtext_initparser},
164 {NULL, NULL},
165 }, *m;
166 char *dot;
168 if ((dot = strrchr(path, '.')) == NULL)
169 return textplain_initparser;
171 dot++;
173 for (m = ms; m->ext != NULL; ++m)
174 if (!strcmp(m->ext, dot))
175 return m->fn;
177 return textplain_initparser;
180 void
181 fs_load_url(struct tab *tab, const char *url)
183 const char *bpath = "bookmarks.gmi", *fallback = "# Not found\n";
184 parserinit initfn = gemtext_initparser;
185 char path[PATH_MAX];
186 FILE *fp = NULL;
187 size_t i;
188 char buf[BUFSIZ];
189 struct page {
190 const char *name;
191 const char *path;
192 const uint8_t *data;
193 size_t len;
194 } pages[] = {
195 {"about", NULL, about_about, about_about_len},
196 {"blank", NULL, about_blank, about_blank_len},
197 {"bookmarks", bpath, bookmarks, bookmarks_len},
198 {"crash", NULL, about_crash, about_crash_len},
199 {"help", NULL, about_help, about_help_len},
200 {"license", NULL, about_license, about_license_len},
201 {"new", NULL, about_new, about_new_len},
202 }, *page = NULL;
204 if (!strncmp(url, "about:", 6)) {
205 url += 6;
207 for (i = 0; page == NULL && i < nitems(pages); ++i) {
208 if (!strcmp(url, pages[i].name))
209 page = &pages[i];
212 if (page == NULL)
213 goto done;
215 strlcpy(path, data_path_base, sizeof(path));
216 strlcat(path, "/", sizeof(path));
217 if (page->path != NULL)
218 strlcat(path, page->path, sizeof(path));
219 else {
220 strlcat(path, "page/about_", sizeof(path));
221 strlcat(path, page->name, sizeof(path));
222 strlcat(path, ".gmi", sizeof(path));
225 fallback = page->data;
226 } else if (!strncmp(url, "file://", 7)) {
227 url += 7;
228 strlcpy(path, url, sizeof(path));
229 initfn = file_type(url);
230 } else
231 goto done;
233 if ((fp = fopen(path, "r")) == NULL)
234 goto done;
236 if (is_dir(fp)) {
237 fclose(fp);
238 send_dir(tab, path);
239 goto done;
242 parser_init(tab, initfn);
243 for (;;) {
244 size_t r;
246 r = fread(buf, 1, sizeof(buf), fp);
247 if (!parser_parse(tab, buf, r))
248 break;
249 if (r != sizeof(buf))
250 break;
252 parser_free(tab);
254 done:
255 if (fp != NULL)
256 fclose(fp);
257 else
258 load_page_from_str(tab, fallback);
261 int
262 bookmark_page(const char *url)
264 FILE *f;
266 if ((f = fopen(bookmark_file, "a")) == NULL)
267 return -1;
268 fprintf(f, "=> %s\n", url);
269 fclose(f);
270 return 0;
273 int
274 save_cert(const struct tofu_entry *e)
276 FILE *f;
278 if ((f = fopen(known_hosts_file, "a")) == NULL)
279 return -1;
280 fprintf(f, "%s %s %d\n", e->domain, e->hash, e->verified);
281 fclose(f);
282 return 0;
285 int
286 update_cert(const struct tofu_entry *e)
288 FILE *tmp, *f;
289 char sfn[PATH_MAX], *line = NULL, *t;
290 size_t l, linesize = 0;
291 ssize_t linelen;
292 int fd, err;
294 strlcpy(sfn, known_hosts_tmp, sizeof(sfn));
295 if ((fd = mkstemp(sfn)) == -1 ||
296 (tmp = fdopen(fd, "w")) == NULL) {
297 if (fd != -1) {
298 unlink(sfn);
299 close(fd);
301 return -1;
304 if ((f = fopen(known_hosts_file, "r")) == NULL) {
305 unlink(sfn);
306 fclose(tmp);
307 return -1;
310 l = strlen(e->domain);
311 while ((linelen = getline(&line, &linesize, f)) != -1) {
312 if ((t = strstr(line, e->domain)) != NULL &&
313 (line[l] == ' ' || line[l] == '\t'))
314 continue;
315 /* line has a trailing \n */
316 fprintf(tmp, "%s", line);
318 fprintf(tmp, "%s %s %d\n", e->domain, e->hash, e->verified);
320 free(line);
321 err = ferror(tmp);
323 fclose(tmp);
324 fclose(f);
326 if (err) {
327 unlink(sfn);
328 return -1;
331 if (rename(sfn, known_hosts_file))
332 return -1;
333 return 0;
336 static size_t
337 join_path(char *buf, const char *lhs, const char *rhs, size_t buflen)
339 strlcpy(buf, lhs, buflen);
340 return strlcat(buf, rhs, buflen);
343 static void
344 getenv_default(char *buf, const char *name, const char *def, size_t buflen)
346 size_t ret;
347 char *home, *env;
349 if ((home = getenv("HOME")) == NULL)
350 errx(1, "HOME is not defined");
352 if ((env = getenv(name)) != NULL)
353 ret = strlcpy(buf, env, buflen);
354 else
355 ret = join_path(buf, home, def, buflen);
357 if (ret >= buflen)
358 errx(1, "buffer too small for %s", name);
361 static void
362 mkdirs(const char *path, mode_t mode)
364 char copy[PATH_MAX+1], orig[PATH_MAX+1], *parent;
366 strlcpy(copy, path, sizeof(copy));
367 strlcpy(orig, path, sizeof(orig));
368 parent = dirname(copy);
369 if (!strcmp(parent, "/"))
370 return;
371 mkdirs(parent, mode);
373 if (mkdir(orig, mode) != 0) {
374 if (errno == EEXIST)
375 return;
376 err(1, "can't mkdir %s", orig);
380 static void
381 init_paths(void)
383 char xdg_config_base[PATH_MAX];
384 char xdg_data_base[PATH_MAX];
385 char xdg_cache_base[PATH_MAX];
386 char old_path[PATH_MAX];
387 char *home;
388 struct stat info;
390 /* old path */
391 if ((home = getenv("HOME")) == NULL)
392 errx(1, "HOME is not defined");
393 join_path(old_path, home, "/.telescope", sizeof(old_path));
395 /* if ~/.telescope exists, use that instead of xdg dirs */
396 if (stat(old_path, &info) == 0 && S_ISDIR(info.st_mode)) {
397 join_path(config_path_base, home, "/.telescope",
398 sizeof(config_path_base));
399 join_path(data_path_base, home, "/.telescope",
400 sizeof(data_path_base));
401 join_path(cache_path_base, home, "/.telescope",
402 sizeof(cache_path_base));
403 return;
406 /* xdg paths */
407 getenv_default(xdg_config_base, "XDG_CONFIG_HOME", "/.config",
408 sizeof(xdg_config_base));
409 getenv_default(xdg_data_base, "XDG_DATA_HOME", "/.local/share",
410 sizeof(xdg_data_base));
411 getenv_default(xdg_cache_base, "XDG_CACHE_HOME", "/.cache",
412 sizeof(xdg_cache_base));
414 join_path(config_path_base, xdg_config_base, "/telescope",
415 sizeof(config_path_base));
416 join_path(data_path_base, xdg_data_base, "/telescope",
417 sizeof(data_path_base));
418 join_path(cache_path_base, xdg_cache_base, "/telescope",
419 sizeof(cache_path_base));
421 mkdirs(xdg_config_base, S_IRWXU);
422 mkdirs(xdg_data_base, S_IRWXU);
423 mkdirs(xdg_cache_base, S_IRWXU);
425 mkdirs(config_path_base, S_IRWXU);
426 mkdirs(data_path_base, S_IRWXU);
427 mkdirs(cache_path_base, S_IRWXU);
430 int
431 fs_init(void)
433 init_paths();
435 join_path(ctlsock_path, cache_path_base, "/ctl",
436 sizeof(ctlsock_path));
437 join_path(config_path, config_path_base, "/config",
438 sizeof(config_path));
439 join_path(lockfile_path, cache_path_base, "/lock",
440 sizeof(lockfile_path));
441 join_path(bookmark_file, data_path_base, "/bookmarks.gmi",
442 sizeof(bookmark_file));
443 join_path(known_hosts_file, data_path_base, "/known_hosts",
444 sizeof(known_hosts_file));
445 join_path(known_hosts_tmp, cache_path_base,
446 "/known_hosts.tmp.XXXXXXXXXX", sizeof(known_hosts_tmp));
447 join_path(session_file, cache_path_base, "/session",
448 sizeof(session_file));
449 join_path(session_file_tmp, cache_path_base, "/session.XXXXXXXXXX",
450 sizeof(session_file));
451 join_path(history_file, cache_path_base, "/history",
452 sizeof(history_file));
453 join_path(history_file_tmp, cache_path_base, "/history.XXXXXXXXXX",
454 sizeof(history_file));
455 join_path(crashed_file, cache_path_base, "/crashed",
456 sizeof(crashed_file));
458 return 1;
461 /*
462 * Parse a line of the session file and restores it. The format is:
464 * URL [flags,...] [title]\n
465 */
466 static inline struct tab *
467 parse_session_line(char *line, struct tab **ct)
469 struct tab *tab;
470 char *s, *t, *ap;
471 const char *uri, *title = "";
472 int current = 0, killed = 0;
473 size_t top_line = 0, current_line = 0;
475 uri = line;
476 if ((s = strchr(line, ' ')) == NULL)
477 return NULL;
479 *s++ = '\0';
481 if ((t = strchr(s, ' ')) != NULL) {
482 *t++ = '\0';
483 title = t;
486 while ((ap = strsep(&s, ",")) != NULL) {
487 if (!strcmp(ap, "current"))
488 current = 1;
489 else if (!strcmp(ap, "killed"))
490 killed = 1;
491 else if (has_prefix(ap, "top="))
492 top_line = strtonum(ap+4, 0, UINT32_MAX, NULL);
493 else if (has_prefix(ap, "cur="))
494 current_line = strtonum(ap+4, 0, UINT32_MAX, NULL);
497 if (top_line > current_line) {
498 top_line = 0;
499 current_line = 0;
502 if ((tab = new_tab(uri, NULL, NULL)) == NULL)
503 die();
504 tab->hist_cur->line_off = top_line;
505 tab->hist_cur->current_off = current_line;
506 strlcpy(tab->buffer.page.title, title, sizeof(tab->buffer.page.title));
508 if (current)
509 *ct = tab;
510 else if (killed)
511 kill_tab(tab, 1);
513 return tab;
516 static inline void
517 sendhist(struct tab *tab, const char *uri, int future)
519 struct hist *h;
521 if ((h = calloc(1, sizeof(*h))) == NULL)
522 die();
523 strlcpy(h->h, uri, sizeof(h->h));
525 if (future)
526 hist_push(&tab->hist, h);
527 else
528 hist_add_before(&tab->hist, tab->hist_cur, h);
531 static void
532 load_last_session(void)
534 struct tab *tab = NULL, *ct = NULL;
535 FILE *session;
536 size_t linesize = 0;
537 ssize_t linelen;
538 int future;
539 char *nl, *s, *line = NULL;
541 if ((session = fopen(session_file, "r")) == NULL) {
542 new_tab("about:new", NULL, NULL);
543 switch_to_tab(new_tab("about:help", NULL, NULL));
544 return;
547 while ((linelen = getline(&line, &linesize, session)) != -1) {
548 if ((nl = strchr(line, '\n')) != NULL)
549 *nl = '\0';
551 if (*line == '<' || *line == '>') {
552 future = *line == '>';
553 s = line+1;
554 if (*s != ' ' || tab == NULL)
555 continue;
556 sendhist(tab, ++s, future);
557 } else {
558 tab = parse_session_line(line, &ct);
562 fclose(session);
563 free(line);
565 if (ct != NULL)
566 switch_to_tab(ct);
568 if (last_time_crashed())
569 switch_to_tab(new_tab("about:crash", NULL, NULL));
572 static void
573 load_hist(void)
575 FILE *hist;
576 size_t linesize = 0;
577 ssize_t linelen;
578 char *nl, *spc, *line = NULL;
579 const char *errstr;
580 struct histitem hi;
582 if ((hist = fopen(history_file, "r")) == NULL)
583 return;
585 while ((linelen = getline(&line, &linesize, hist)) != -1) {
586 if ((nl = strchr(line, '\n')) != NULL)
587 *nl = '\0';
588 if ((spc = strchr(line, ' ')) == NULL)
589 continue;
590 *spc = '\0';
591 spc++;
593 memset(&hi, 0, sizeof(hi));
594 hi.ts = strtonum(line, INT64_MIN, INT64_MAX, &errstr);
595 if (errstr != NULL)
596 continue;
597 if (strlcpy(hi.uri, spc, sizeof(hi.uri)) >= sizeof(hi.uri))
598 continue;
600 history_push(&hi);
603 fclose(hist);
604 free(line);
606 history_sort();
609 int
610 fs_load_state(struct ohash *certs)
612 load_certs(certs);
613 load_hist();
614 load_last_session();
615 return 0;
618 /*
619 * Check if the last time telescope crashed. The check is done by
620 * looking at `crashed_file': if it exists then last time we crashed.
621 * Then, while here, touch the file too. During IMSG_QUIT we'll
622 * remove it.
623 */
624 static int
625 last_time_crashed(void)
627 int fd, crashed = 1;
629 if (safe_mode)
630 return 0;
632 if (unlink(crashed_file) == -1 && errno == ENOENT)
633 crashed = 0;
635 if ((fd = open(crashed_file, O_CREAT|O_WRONLY, 0600)) == -1)
636 return crashed;
637 close(fd);
639 return crashed;
642 int
643 lock_session(void)
645 struct flock lock;
646 int fd;
648 if ((fd = open(lockfile_path, O_WRONLY|O_CREAT, 0600)) == -1)
649 return -1;
651 lock.l_start = 0;
652 lock.l_len = 0;
653 lock.l_type = F_WRLCK;
654 lock.l_whence = SEEK_SET;
656 if (fcntl(fd, F_SETLK, &lock) == -1) {
657 close(fd);
658 return -1;
661 return fd;
664 static inline int
665 parse_khost_line(char *line, char *tmp[3])
667 char **ap;
669 for (ap = tmp; ap < &tmp[3] &&
670 (*ap = strsep(&line, " \t\n")) != NULL;) {
671 if (**ap != '\0')
672 ap++;
675 return ap == &tmp[3] && *line == '\0';
678 static void
679 load_certs(struct ohash *certs)
681 char *tmp[3], *line = NULL;
682 const char *errstr;
683 size_t lineno = 0, linesize = 0;
684 ssize_t linelen;
685 FILE *f;
686 struct tofu_entry *e;
688 if ((f = fopen(known_hosts_file, "r")) == NULL)
689 return;
691 if ((e = calloc(1, sizeof(*e))) == NULL) {
692 fclose(f);
693 return;
696 while ((linelen = getline(&line, &linesize, f)) != -1) {
697 lineno++;
699 if (parse_khost_line(line, tmp)) {
700 strlcpy(e->domain, tmp[0], sizeof(e->domain));
701 strlcpy(e->hash, tmp[1], sizeof(e->hash));
703 e->verified = strtonum(tmp[2], 0, 1, &errstr);
704 if (errstr != NULL)
705 errx(1, "%s:%zu verification for %s is %s: %s",
706 known_hosts_file, lineno,
707 e->domain, errstr, tmp[2]);
709 tofu_add(certs, e);
710 } else {
711 warnx("%s:%zu invalid entry",
712 known_hosts_file, lineno);
716 free(line);
717 fclose(f);
718 return;