gmnlm.c (38462B)
1 #include <assert.h> 2 #include <bearssl.h> 3 #include <ctype.h> 4 #include <errno.h> 5 #include <fcntl.h> 6 #include <getopt.h> 7 #include <gmni/certs.h> 8 #include <gmni/gmni.h> 9 #include <gmni/tofu.h> 10 #include <gmni/url.h> 11 #include <libgen.h> 12 #include <limits.h> 13 #include <regex.h> 14 #include <stdbool.h> 15 #include <stdio.h> 16 #include <stdlib.h> 17 #include <string.h> 18 #include <sys/ioctl.h> 19 #include <sys/stat.h> 20 #include <sys/types.h> 21 #include <sys/wait.h> 22 #include <termios.h> 23 #include <unistd.h> 24 #include "util.h" 25 26 #define ANSI_COLOR_RED "\x1b[91m" 27 #define ANSI_COLOR_GREEN "\x1b[92m" 28 #define ANSI_COLOR_YELLOW "\x1b[93m" 29 #define ANSI_COLOR_BLUE "\x1b[94m" 30 #define ANSI_COLOR_MAGENTA "\x1b[35m" 31 #define ANSI_COLOR_LMAGENTA "\x1b[95m" 32 #define ANSI_COLOR_CYAN "\x1b[36m" 33 #define ANSI_COLOR_LCYAN "\x1b[96m" 34 #define ANSI_COLOR_GRAY "\x1b[37m" 35 #define ANSI_COLOR_RESET "\x1b[0m" 36 37 struct link { 38 char *url; 39 struct link *next; 40 }; 41 42 struct history { 43 char *url; 44 struct history *prev, *next; 45 }; 46 47 #define REDIRS_UNLIMITED -1 48 #define REDIRS_ASK -2 49 50 struct browser { 51 bool pagination, unicode, alttext, autoopen; 52 int max_width; 53 int max_redirs; 54 struct gemini_options opts; 55 struct gemini_tofu tofu; 56 enum tofu_action tofu_mode; 57 58 FILE *tty; 59 char *meta; 60 char *plain_url; 61 char *page_title; 62 struct Curl_URL *url; 63 struct link *links; 64 struct history *history; 65 bool running; 66 67 bool searching; 68 regex_t regex; 69 }; 70 71 enum prompt_result { 72 PROMPT_AGAIN, 73 PROMPT_MORE, 74 PROMPT_QUIT, 75 PROMPT_ANSWERED, 76 PROMPT_NEXT, 77 }; 78 79 const char *default_bookmarks = 80 "# Welcome to cgmnlm\n\n" 81 "Links:\n\n" 82 "=> gemini://gmn.clttr.info/cgmnln.gmi The colorful gemini line mode client\n" 83 "=> gemini://gemini.circumlunar.space The gemini protocol\n" 84 "=> gemini://geminispace.info/search/ search in geminispace\n\n" 85 "This file can be found at %s and may be edited at your pleasure.\n\n" 86 "Bookmarks:\n" 87 ; 88 89 const char *help_msg = 90 "The following commands are available:\n\n" 91 "<Enter>\t\tRead more lines (if available)\n" 92 "<url>\t\tGo to url\n" 93 "[N]\t\tFollow Nth link\n" 94 "p[N]\t\tPrint URL of Nth link\n" 95 "e[N]\t\tSend URL of current page or Nth link to external default program\n" 96 "t[N]\t\tDownload content of current page or Nth link to a temporary file\n" 97 "b[N]\t\tJump back N entries in history, default 1\n" 98 "f[N]\t\tJump forward N entries in history, default 1\n" 99 "u\t\tNavigate one path element up\n" 100 "i\r\t\tShow MIME type parameters\n" 101 "H\t\tView all page history\n" 102 "m [title]\t\tSave bookmark for current page (uses first header as name if title is omitted)\n" 103 "M\t\tBrowse bookmarks\n" 104 "K\t\tRemove bookmark for current page\n" 105 "r\t\tReload the page\n" 106 "s\t\tSearch via geminispace.info\n" 107 "l\t\tSearch backlinks to current page via geminispace.info\n" 108 "/<text>\t\tSearch for text (POSIX regular expression)\n" 109 "n\t\tJump to next search match\n" 110 "d[N] [path]\tDownload page, or Nth link, to path\n" 111 "[N]|<prog>\tPipe page, or Nth link, into program\n" 112 "a\t\tToggle usage of alt text instead of preformatted text\n" 113 "q\t\tQuit\n" 114 "\n" 115 "[N] must be replaced with a number >= 0\n" 116 "\n" 117 ; 118 119 static void 120 usage(const char *argv_0) 121 { 122 fprintf(stderr, "usage: %s [-PUAT] [-j mode] [-R redirs] [-W width] [gemini://...]\n", argv_0); 123 } 124 125 static void 126 history_free(struct history *history) 127 { 128 if (!history) { 129 return; 130 } 131 history_free(history->next); 132 free(history->url); 133 free(history); 134 } 135 136 static bool 137 set_url(struct browser *browser, char *new_url, struct history **history) 138 { 139 if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { 140 fprintf(stderr, "Error: invalid URL\n"); 141 return false; 142 } 143 if (browser->plain_url != NULL) { 144 free(browser->plain_url); 145 } 146 curl_url_get(browser->url, CURLUPART_URL, &browser->plain_url, 0); 147 if (history) { 148 struct history *next = calloc(1, sizeof(struct history)); 149 curl_url_get(browser->url, CURLUPART_URL, &next->url, 0); 150 next->prev = *history; 151 if (*history) { 152 if ((*history)->next) { 153 history_free((*history)->next); 154 } 155 (*history)->next = next; 156 } 157 *history = next; 158 } 159 return true; 160 } 161 162 static char * 163 get_data_pathfmt() 164 { 165 const struct pathspec paths[] = { 166 {.var = "GMNIDATA", .path = "/%s"}, 167 {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, 168 {.var = "HOME", .path = "/.local/share/gmni/%s"} 169 }; 170 return getpath(paths, sizeof(paths) / sizeof(paths[0])); 171 } 172 173 static char * 174 trim_ws(char *in) 175 { 176 while (*in && isspace(*in)) ++in; 177 return in; 178 } 179 180 static void 181 save_bookmark(struct browser *browser, const char *title) 182 { 183 char *path_fmt = get_data_pathfmt(); 184 static char path[PATH_MAX+1]; 185 static char dname[PATH_MAX+1]; 186 size_t n; 187 188 n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); 189 free(path_fmt); 190 assert(n < sizeof(path)); 191 posix_dirname(path, dname); 192 if (mkdirs(dname, 0755) != 0) { 193 fprintf(stderr, "Error creating directory %s: %s\n", 194 dname, strerror(errno)); 195 return; 196 } 197 198 FILE *f = fopen(path, "a"); 199 if (!f) { 200 fprintf(stderr, "Error opening %s for writing: %s\n", 201 path, strerror(errno)); 202 return; 203 } 204 205 fprintf(f, "=> %s%s%s\n", browser->plain_url, 206 title ? " " : "", title ? title : ""); 207 fclose(f); 208 209 fprintf(browser->tty, "Bookmark saved: %s\n", 210 title ? title : browser->plain_url); 211 } 212 213 static void 214 remove_bookmark(struct browser *browser) 215 { 216 char *path_fmt = get_data_pathfmt(); 217 static char path[PATH_MAX+1]; 218 snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); 219 free(path_fmt); 220 221 static char tempfile[PATH_MAX+2]; 222 snprintf(tempfile, sizeof(tempfile), "%s2", path); 223 FILE *fi = fopen(path, "r"); 224 FILE *fo = fopen(tempfile, "w"); 225 if(fi == NULL) { 226 fprintf(stderr, "Bookmark file not available!\n"); 227 return; 228 } 229 if(fo == NULL) { 230 fprintf(stderr, "tempfile not available!\n"); 231 return; 232 } 233 234 char *line = NULL; 235 size_t len = 0; 236 size_t n = 0; 237 238 static char url[1024]; 239 n = snprintf(url, sizeof(url), "=> %s ", browser->plain_url); 240 while(getline(&line, &len, fi) != -1) { 241 if (strncmp(line, url, n)==0) { 242 fprintf(browser->tty, "Bookmark removed!\n"); 243 } else { 244 fprintf(fo, "%s", line); 245 } 246 } 247 248 fclose(fi); 249 fclose(fo); 250 free(line); 251 if ( rename(tempfile, path) != 0) { 252 fprintf(browser->tty, "Failed to update bookmarks: %s\n", strerror(errno)); 253 } 254 } 255 256 static void 257 open_bookmarks(struct browser *browser) 258 { 259 char *path_fmt = get_data_pathfmt(); 260 static char path[PATH_MAX+1]; 261 static char dname[PATH_MAX+1]; 262 size_t n; 263 264 n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); 265 free(path_fmt); 266 267 assert(n < sizeof(path)); 268 posix_dirname(path, dname); 269 if (mkdirs(dname, 0755) != 0) { 270 fprintf(stderr, "Error creating directory %s: %s\n", 271 dname, strerror(errno)); 272 return; 273 } 274 275 struct stat buf; 276 if (stat(path, &buf) == -1 && errno == ENOENT) { 277 // TOCTOU, but we almost certainly don't care 278 FILE *f = fopen(path, "a"); 279 if (f == NULL) { 280 fprintf(stderr, "Error opening %s for writing: %s\n", 281 path, strerror(errno)); 282 return; 283 } 284 fprintf(f, default_bookmarks, path); 285 fclose(f); 286 } 287 288 static char url[PATH_MAX+1+7]; 289 snprintf(url, sizeof(url), "file://%s", path); 290 set_url(browser, url, &browser->history); 291 } 292 293 static void 294 print_media_parameters(FILE *out, char *params) 295 { 296 if (params == NULL) { 297 fprintf(out, "No media parameters\n"); 298 return; 299 } 300 for (char *param = strtok(params, ";"); param; 301 param = strtok(NULL, ";")) { 302 char *value = strchr(param, '='); 303 if (value == NULL) { 304 fprintf(out, "Invalid media type parameter '%s'\n", 305 trim_ws(param)); 306 continue; 307 } 308 *value = 0; 309 fprintf(out, "%s: ", trim_ws(param)); 310 *value++ = '='; 311 if (*value != '"') { 312 fprintf(out, "%s\n", value); 313 continue; 314 } 315 while (value++) { 316 switch (*value) { 317 case '\0': 318 if ((value = strtok(NULL, ";")) != NULL) { 319 fprintf(out, ";%c", *value); 320 } 321 break; 322 case '"': 323 value = NULL; 324 break; 325 case '\\': 326 if (value[1] == '\0') { 327 break; 328 } 329 value++; 330 /* fallthrough */ 331 default: 332 putc(*value, out); 333 } 334 } 335 putc('\n', out); 336 } 337 } 338 339 static char * 340 get_input(const struct gemini_response *resp, FILE *source) 341 { 342 int r = 0; 343 struct termios attrs; 344 bool tty = fileno(source) != -1 && isatty(fileno(source)); 345 char *input = NULL; 346 if (tty) { 347 fprintf(stderr, "%s: ", resp->meta); 348 if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) { 349 r = tcgetattr(fileno(source), &attrs); 350 struct termios new_attrs; 351 r = tcgetattr(fileno(source), &new_attrs); 352 if (r != -1) { 353 new_attrs.c_lflag &= ~ECHO; 354 tcsetattr(fileno(source), TCSANOW, &new_attrs); 355 } 356 } 357 } 358 size_t s = 0; 359 ssize_t n = getline(&input, &s, source); 360 if (n == -1) { 361 fprintf(stderr, "Error reading input: %s\n", 362 feof(source) ? "EOF" : strerror(ferror(source))); 363 return NULL; 364 } 365 input[n - 1] = '\0'; // Drop LF 366 if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) { 367 tcsetattr(fileno(source), TCSANOW, &attrs); 368 } 369 return input; 370 } 371 372 static bool 373 has_suffix(char *str, char *suff) 374 { 375 size_t suffl = strlen(suff); 376 size_t strl = strlen(str); 377 if (strl < suffl) { 378 return false; 379 } 380 return strcmp(&str[strl - suffl], suff) == 0; 381 } 382 383 static void 384 pipe_resp(FILE *out, struct gemini_response resp, char *cmd) { 385 char buf[BUFSIZ]; 386 int pfd[2]; 387 if (pipe(pfd) == -1) { 388 perror("pipe"); 389 return; 390 } 391 pid_t pid; 392 switch ((pid = fork())) { 393 case -1: 394 perror("fork"); 395 return; 396 case 0: 397 close(pfd[1]); 398 dup2(pfd[0], STDIN_FILENO); 399 close(pfd[0]); 400 execlp("sh", "sh", "-c", cmd, NULL); 401 perror("exec"); 402 _exit(1); 403 } 404 close(pfd[0]); 405 FILE *f = fdopen(pfd[1], "w"); 406 // XXX: may affect history, do we care? 407 for (int n = 1; n > 0;) { 408 if (resp.sc) { 409 n = br_sslio_read(&resp.body, buf, BUFSIZ); 410 } else { 411 n = read(resp.fd, buf, BUFSIZ); 412 } 413 if (n < 0) { 414 n = 0; 415 } 416 ssize_t w = 0; 417 while (w < (ssize_t)n) { 418 ssize_t x = fwrite(&buf[w], 1, n - w, f); 419 if (ferror(f)) { 420 fprintf(stderr, "Error: write: %s\n", 421 strerror(errno)); 422 return; 423 } 424 w += x; 425 } 426 } 427 fclose(f); 428 int status; 429 waitpid(pid, &status, 0); 430 if (status != 0) { 431 fprintf(out, "Command exited %d\n", status); 432 } 433 } 434 435 static enum gemini_result 436 do_requests(struct browser *browser, struct gemini_response *resp) 437 { 438 int nredir = 0; 439 bool requesting = true; 440 enum gemini_result res; 441 442 char *scheme; 443 CURLUcode uc = curl_url_get(browser->url, 444 CURLUPART_SCHEME, &scheme, 0); 445 assert(uc == CURLUE_OK); // Invariant 446 447 char *host = NULL; 448 struct gmni_client_certificate client_cert = {0}; 449 const struct pathspec paths[] = { 450 {.var = "GMNIDATA", .path = "/certs/%s.%s"}, 451 {.var = "XDG_DATA_HOME", .path = "/gmni/certs/%s.%s"}, 452 {.var = "HOME", .path = "/.local/share/gmni/certs/%s.%s"} 453 }; 454 char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); 455 char certpath[PATH_MAX+1], keypath[PATH_MAX+1]; 456 size_t n = 0; 457 458 if (strcmp(scheme, "gemini") == 0) { 459 CURLUcode uc = curl_url_get(browser->url, 460 CURLUPART_HOST, &host, 0); 461 assert(uc == CURLUE_OK); 462 463 n = snprintf(certpath, sizeof(certpath), path_fmt, host, "crt"); 464 assert(n < sizeof(certpath)); 465 FILE *certin = fopen(certpath, "r"); 466 if (certin) { 467 n = snprintf(keypath, sizeof(keypath), path_fmt, host, "key"); 468 assert(n < sizeof(keypath)); 469 470 FILE *skin = fopen(keypath, "r"); 471 if (gmni_ccert_load(&client_cert, certin, skin)) { 472 browser->opts.client_cert = NULL; 473 fprintf(stderr, "Unable to load client certificate for host %s", host); 474 } else { 475 browser->opts.client_cert = &client_cert; 476 } 477 } else { 478 browser->opts.client_cert = NULL; 479 } 480 } 481 482 while (requesting) { 483 if (strcmp(scheme, "file") == 0) { 484 requesting = false; 485 486 char *path; 487 uc = curl_url_get(browser->url, 488 CURLUPART_PATH, &path, 0); 489 if (uc != CURLUE_OK) { 490 resp->status = GEMINI_STATUS_BAD_REQUEST; 491 break; 492 } 493 494 int fd = open(path, O_RDONLY); 495 if (fd < 0) { 496 resp->status = GEMINI_STATUS_NOT_FOUND; 497 // Make sure members of resp evaluate to false, 498 // so that gemini_response_finish does not try 499 // to free them. 500 resp->sc = NULL; 501 resp->meta = NULL; 502 resp->fd = -1; 503 free(path); 504 break; 505 } 506 507 if (has_suffix(path, ".gmi") || has_suffix(path, ".gemini")) { 508 resp->meta = strdup("text/gemini"); 509 } else if (has_suffix(path, ".txt")) { 510 resp->meta = strdup("text/plain"); 511 } else { 512 resp->meta = strdup("application/x-octet-stream"); 513 } 514 free(path); 515 resp->status = GEMINI_STATUS_SUCCESS; 516 resp->fd = fd; 517 resp->sc = NULL; 518 res = GEMINI_OK; 519 goto out; 520 } 521 522 res = gemini_request(browser->plain_url, &browser->opts, 523 &browser->tofu, resp); 524 if (res != GEMINI_OK) { 525 fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); 526 requesting = false; 527 resp->status = 70 + res; 528 break; 529 } 530 531 char *input; 532 switch (gemini_response_class(resp->status)) { 533 case GEMINI_STATUS_CLASS_INPUT: 534 input = get_input(resp, browser->tty); 535 if (!input) { 536 requesting = false; 537 break; 538 } 539 if (input[0] == '\0' && browser->history->prev) { 540 free(input); 541 browser->history = browser->history->prev; 542 set_url(browser, browser->history->url, NULL); 543 break; 544 } 545 546 char *new_url = gemini_input_url( 547 browser->plain_url, input); 548 free(input); 549 assert(new_url); 550 set_url(browser, new_url, 551 resp->status == GEMINI_STATUS_SENSITIVE_INPUT ? 552 NULL : &browser->history); 553 free(new_url); 554 break; 555 case GEMINI_STATUS_CLASS_REDIRECT: 556 if (browser->max_redirs == REDIRS_ASK) { 557 again: 558 fprintf(browser->tty, 559 "The host %s is redirecting to:\n" 560 "%s\n\n" 561 "[F]ollow redirect; [a]bort\n" 562 "=> ", host, resp->meta); 563 564 size_t sz = 0; 565 char *line = NULL; 566 if (getline(&line, &sz, browser->tty) == -1) { 567 free(line); 568 requesting = false; 569 break; 570 } 571 if (line[0] != '\n' && line[1] != '\n') { 572 free(line); 573 goto again; 574 } 575 576 char c = line[0]; 577 free(line); 578 579 if (c == 'a') { 580 requesting = false; 581 break; 582 } else if (c != 'f' && c != '\n') { 583 goto again; 584 } 585 } else if (browser->max_redirs != REDIRS_UNLIMITED 586 && ++nredir >= browser->max_redirs) { 587 requesting = false; 588 fprintf(stderr, "Error: maximum redirects (%d) exceeded\n", 589 browser->max_redirs); 590 break; 591 } 592 set_url(browser, resp->meta, NULL); 593 break; 594 case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: 595 requesting = false; 596 assert(host); 597 n = snprintf(certpath, sizeof(certpath), path_fmt, host, "crt"); 598 assert(n < sizeof(certpath)); 599 n = snprintf(keypath, sizeof(keypath), path_fmt, host, "key"); 600 char dname[PATH_MAX + 1]; 601 posix_dirname(certpath, dname); 602 if (mkdirs(dname, 0755) != 0) { 603 fprintf(stderr, "Error creating directory %s: %s\n", 604 dname, strerror(errno)); 605 break; 606 } 607 assert(n < sizeof(keypath)); 608 fprintf(stderr, "The server requested a client certificate.\n" 609 "Presently, this process is not automated.\n" 610 "The following OpenSSL command will generate a certificate for this host:\n\n" 611 "openssl req -x509 -newkey rsa:4096 \\\n\t-keyout %s \\\n\t-out %s \\\n\t-days 36500 -nodes\n\n" 612 "Use the 'r' command to reload the page after creating this certificate.\n", 613 keypath, certpath); 614 break; 615 case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: 616 case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: 617 requesting = false; 618 fprintf(stderr, "Server returned %s %d %s\n", 619 resp->status / 10 == 4 ? 620 "TEMPORARY FAILURE" : "PERMANENT FAILURE", 621 resp->status, resp->meta); 622 break; 623 case GEMINI_STATUS_CLASS_SUCCESS: 624 goto out; 625 } 626 627 if (requesting) { 628 gemini_response_finish(resp); 629 } 630 } 631 632 out: 633 if (client_cert.key) { 634 free(client_cert.key); 635 } 636 free(path_fmt); 637 free(scheme); 638 free(host); 639 return res; 640 } 641 642 static enum prompt_result 643 do_prompts(const char *prompt, struct browser *browser) 644 { 645 enum prompt_result result = PROMPT_AGAIN; 646 fprintf(browser->tty, "%s", prompt); 647 648 size_t l = 0; 649 650 char curr_url[1024] = {0}; 651 char save_url[1024] = {0}; 652 653 char *in = NULL; 654 ssize_t n = getline(&in, &l, browser->tty); 655 if (n == -1 && feof(browser->tty)) { 656 fputc('\n', browser->tty); 657 result = PROMPT_QUIT; 658 goto exit; 659 } 660 661 in[n - 1] = 0; // Remove LF 662 char *endptr; 663 664 CURLU *url = curl_url(); 665 bool isurl = curl_url_set(url, CURLUPART_URL, in, 0) == CURLUE_OK; 666 curl_url_cleanup(url); 667 if (isurl) { 668 set_url(browser, in, &browser->history); 669 result = PROMPT_ANSWERED; 670 goto exit; 671 } 672 673 int historyhops = 1; 674 int r; 675 switch (in[0]) { 676 case '\0': 677 result = PROMPT_MORE; 678 goto exit; 679 case '.': 680 break; 681 case 'q': 682 if (in[1]) { 683 fprintf(stderr, "Error: unrecognized command.\n"); 684 goto exit; 685 } 686 result = PROMPT_QUIT; 687 goto exit; 688 case 'b': 689 if (in[1] && isdigit(in[1])) { 690 historyhops =(int)strtol(in+1, &endptr, 10); 691 if (endptr[0]) { 692 fprintf(stderr, "Error: invalid argument.\n"); 693 goto exit; 694 } 695 } else if (in[1]) { 696 fprintf(stderr, "Error: invalid argument.\n"); 697 goto exit; 698 } 699 while (historyhops > 0) { 700 if (browser->history->prev) { 701 browser->history = browser->history->prev; 702 } 703 historyhops--; 704 } 705 set_url(browser, browser->history->url, NULL); 706 result = PROMPT_ANSWERED; 707 goto exit; 708 case 's': 709 if (in[1]) break; 710 set_url(browser, "gemini://geminispace.info/search", &browser->history); 711 result = PROMPT_ANSWERED; 712 goto exit; 713 case 'a': 714 browser->alttext = !browser->alttext; 715 fprintf(browser->tty, "Alttext instead of preformatted block is now %s\n\n", browser->alttext ? "ENABLED" : "DISABLED"); 716 result = PROMPT_AGAIN; 717 goto exit; 718 case 'l': 719 snprintf(curr_url, sizeof(curr_url), "gemini://geminispace.info/backlinks?%s", browser->plain_url); 720 set_url(browser, curr_url, &browser->history); 721 result = PROMPT_ANSWERED; 722 goto exit; 723 case 'f': 724 if (in[1] && isdigit(in[1])) { 725 historyhops =(int)strtol(in+1, &endptr, 10); 726 if (endptr[0]) { 727 fprintf(stderr, "Error: invalid argument.\n"); 728 goto exit; 729 } 730 } else if (in[1]) { 731 fprintf(stderr, "Error: invalid argument.\n"); 732 goto exit; 733 } 734 while (historyhops > 0) { 735 if (browser->history->next) { 736 browser->history = browser->history->next; 737 } 738 historyhops--; 739 } 740 set_url(browser, browser->history->url, NULL); 741 result = PROMPT_ANSWERED; 742 goto exit; 743 case 'u':; 744 int keep = 0; 745 int len = strlen(browser->plain_url); 746 for (int i=0; i<len; i++) 747 { 748 // ignore trailing / on uri path 749 if (browser->plain_url[i] == '/' && i != len-1) { 750 keep = i; 751 } 752 } 753 if (keep > 9) { 754 strncpy(curr_url , browser->plain_url, keep+1); 755 set_url(browser, curr_url, &browser->history); 756 } 757 result = PROMPT_ANSWERED; 758 goto exit; 759 case 'H': 760 if (in[1]) { 761 fprintf(stderr, "Error: unrecognized command.\n"); 762 goto exit; 763 } 764 struct history *cur = browser->history; 765 int hist_count = 0; 766 while (cur->prev) { 767 cur = cur->prev; 768 hist_count++; 769 } 770 while (cur != browser->history) { 771 fprintf(browser->tty, "b%-3i %s\n", hist_count--, cur->url); 772 cur = cur->next; 773 } 774 fprintf(browser->tty, "* %s\n", cur->url); 775 cur = cur->next; 776 while (cur) { 777 fprintf(browser->tty, "f%-3i %s\n", ++hist_count, cur->url); 778 cur = cur->next; 779 } 780 goto exit; 781 case 'm': 782 if (in[1] != '\0' && !isspace(in[1])) { 783 fprintf(stderr, "Error: unrecognized command.\n"); 784 goto exit; 785 } 786 char *title = in[1] ? &in[1] : browser->page_title; 787 save_bookmark(browser, title ? trim_ws(title) : title); 788 goto exit; 789 case 'M': 790 if (in[1]) { 791 fprintf(stderr, "Error: unrecognized command.\n"); 792 goto exit; 793 } 794 open_bookmarks(browser); 795 result = PROMPT_ANSWERED; 796 goto exit; 797 case 'K': 798 if (in[1]) break; 799 remove_bookmark(browser); 800 result = PROMPT_AGAIN; 801 goto exit; 802 case 'e': 803 case 't': 804 strncpy(&save_url[0], browser->plain_url, sizeof(save_url)-1); 805 if (!in[1]) { 806 strncpy(&curr_url[0], browser->plain_url, sizeof(curr_url)-1); 807 } else { 808 struct link *link = browser->links; 809 int linksel = (int)strtol(in+1, &endptr, 10); 810 if (!endptr[0] && linksel >= 0) { 811 while (linksel > 0 && link) { 812 link = link->next; 813 --linksel; 814 } 815 if (!link) { 816 fprintf(stderr, "Error: no such link.\n"); 817 } else { 818 strncpy(&curr_url[0], link->url, sizeof(curr_url)-1); 819 } 820 } 821 } 822 if (curr_url[0]) { 823 fprintf(browser->tty, "=> %s\n", curr_url); 824 char *tempfile; 825 tempfile = tmpnam(NULL); 826 if (in[0] == 't') { 827 struct gemini_response resp; 828 set_url(browser, curr_url, NULL); 829 enum gemini_result res = do_requests(browser, &resp); 830 if (res != GEMINI_OK) { 831 fprintf(stderr, "Error: %s\n", 832 gemini_strerr(res, &resp)); 833 } else { 834 download_resp(browser->tty, resp, tempfile, curr_url); 835 } 836 gemini_response_finish(&resp); 837 set_url(browser, save_url, NULL); 838 } 839 if (in[0] == 'e' || browser->autoopen) { 840 char target[1050]; 841 snprintf(target, sizeof(target), "xdg-open %s >/dev/null 2>&1", in[0] == 't' ? tempfile : curr_url); 842 if ( !system(target) ) fprintf(browser->tty, "Link send to xdg-open\n"); 843 } 844 fprintf(browser->tty, "\n"); 845 } 846 result = PROMPT_AGAIN; 847 goto exit; 848 case '/': 849 if (!in[1]) break; 850 if ((r = regcomp(&browser->regex, &in[1], REG_EXTENDED)) != 0) { 851 static char buf[1024]; 852 r = regerror(r, &browser->regex, buf, sizeof(buf)); 853 assert(r < (int)sizeof(buf)); 854 fprintf(stderr, "Error: %s\n", buf); 855 } else { 856 browser->searching = true; 857 result = PROMPT_ANSWERED; 858 } 859 goto exit_re; 860 case 'n': 861 if (in[1]) { 862 fprintf(stderr, "Error: unrecognized command.\n"); 863 goto exit; 864 } 865 if (browser->searching) { 866 result = PROMPT_NEXT; 867 goto exit_re; 868 } else { 869 fprintf(stderr, "Cannot move to next result; we are not searching for anything\n"); 870 goto exit; 871 } 872 case 'p': 873 if (!in[1]) { 874 fprintf(stderr, "Error: missing argument.\n"); 875 goto exit; 876 } else if (!isdigit(in[1])) { 877 fprintf(stderr, "Error: invalid argument.\n"); 878 goto exit; 879 } 880 struct link *link = browser->links; 881 int linksel = (int)strtol(in+1, &endptr, 10); 882 if (!endptr[0] && linksel >= 0) { 883 while (linksel > 0 && link) { 884 link = link->next; 885 --linksel; 886 } 887 888 if (!link) { 889 fprintf(stderr, "Error: no such link.\n"); 890 } else { 891 fprintf(browser->tty, "=> %s\n", link->url); 892 goto exit; 893 } 894 } else { 895 fprintf(stderr, "Error: invalid argument.\n"); 896 } 897 goto exit; 898 case 'r': 899 if (in[1]) { 900 fprintf(stderr, "Error: unrecognized command.\n"); 901 goto exit; 902 } 903 result = PROMPT_ANSWERED; 904 goto exit; 905 case 'i': 906 if (in[1]) { 907 fprintf(stderr, "Error: unrecognized command.\n"); 908 goto exit; 909 } 910 print_media_parameters(browser->tty, browser->meta 911 ? strchr(browser->meta, ';') : NULL); 912 goto exit; 913 case 'd': 914 endptr = &in[1]; 915 char *d_url = browser->plain_url; 916 if (in[1] != '\0' && !isspace(in[1])) { 917 struct link *link = browser->links; 918 int linksel = (int)strtol(in+1, &endptr, 10); 919 while (linksel > 0 && link) { 920 link = link->next; 921 --linksel; 922 } 923 924 if (!link) { 925 fprintf(stderr, "Error: no such link.\n"); 926 goto exit; 927 } else { 928 d_url = link->url; 929 } 930 } 931 struct gemini_response resp; 932 strncpy(&save_url[0], browser->plain_url, sizeof(url)-1); 933 strncpy(&curr_url[0], d_url, sizeof(url)-1); 934 // XXX: may affect history, do we care? 935 set_url(browser, curr_url, NULL); 936 enum gemini_result res = do_requests(browser, &resp); 937 if (res != GEMINI_OK) { 938 fprintf(stderr, "Error: %s\n", 939 gemini_strerr(res, &resp)); 940 set_url(browser, save_url, NULL); 941 goto exit; 942 } 943 download_resp(browser->tty, resp, trim_ws(endptr), curr_url); 944 gemini_response_finish(&resp); 945 set_url(browser, save_url, NULL); 946 goto exit; 947 case '|': 948 strncpy(&curr_url[0], browser->plain_url, sizeof(url)-1); 949 res = do_requests(browser, &resp); 950 if (res != GEMINI_OK) { 951 fprintf(stderr, "Error: %s\n", 952 gemini_strerr(res, &resp)); 953 goto exit; 954 } 955 pipe_resp(browser->tty, resp, &in[1]); 956 gemini_response_finish(&resp); 957 set_url(browser, curr_url, NULL); 958 goto exit; 959 case '?': 960 if (in[1]) { 961 fprintf(stderr, "Error: unrecognized command.\n"); 962 goto exit; 963 } 964 fprintf(browser->tty, "%s", help_msg); 965 goto exit; 966 default: 967 if (isdigit(in[0])) break; 968 fprintf(stderr, "Error: unrecognized command.\n"); 969 goto exit; 970 } 971 972 if (isdigit(in[0])) { 973 struct link *link = browser->links; 974 int linksel = (int)strtol(in, &endptr, 10); 975 if ((endptr[0] && endptr[0] != '|') || linksel < 0) { 976 fprintf(stderr, "Error: no such link.\n"); 977 goto exit; 978 } 979 980 while (linksel > 0 && link) { 981 link = link->next; 982 --linksel; 983 } 984 985 if (!link) { 986 fprintf(stderr, "Error: no such link.\n"); 987 goto exit; 988 } else if (endptr[0] == '|') { 989 struct gemini_response resp; 990 strncpy(curr_url, browser->plain_url, sizeof(curr_url) - 1); 991 set_url(browser, link->url, &browser->history); 992 enum gemini_result res = do_requests(browser, &resp); 993 if (res != GEMINI_OK) { 994 fprintf(stderr, "Error: %s\n", 995 gemini_strerr(res, &resp)); 996 set_url(browser, curr_url, NULL); 997 goto exit; 998 } 999 pipe_resp(browser->tty, resp, &endptr[1]); 1000 gemini_response_finish(&resp); 1001 set_url(browser, curr_url, NULL); 1002 goto exit; 1003 } else { 1004 assert(endptr[0] == '\0'); 1005 set_url(browser, link->url, &browser->history); 1006 result = PROMPT_ANSWERED; 1007 goto exit; 1008 } 1009 } 1010 1011 set_url(browser, in, &browser->history); 1012 result = PROMPT_ANSWERED; 1013 exit: 1014 if (browser->searching) { 1015 browser->searching = false; 1016 regfree(&browser->regex); 1017 } 1018 exit_re: 1019 free(in); 1020 return result; 1021 } 1022 1023 static int 1024 wrap(FILE *f, char *s, struct winsize *ws, int *row, int *col) 1025 { 1026 if (!s[0]) { 1027 fprintf(f, "\n"); 1028 return 0; 1029 } 1030 for (int i = 0; s[i]; ++i) { 1031 switch (s[i]) { 1032 case '\n': 1033 assert(0); // Not supposed to happen 1034 case '\t': 1035 *col = *col + (8 - *col % 8); 1036 break; 1037 case '\r': 1038 if (!s[i+1]) break; 1039 /* fallthrough */ 1040 default: 1041 // skip unicode continuation bytes 1042 if ((s[i] & 0xc0) == 0x80) break; 1043 1044 if (iscntrl(s[i])) s[i] = '.'; 1045 *col += 1; 1046 break; 1047 } 1048 1049 if (*col >= ws->ws_col - 4) { 1050 int j = i--; 1051 while (&s[i] != s && !isspace(s[i])) --i; 1052 if (&s[i] == s) i = j; 1053 char c = s[i]; 1054 s[i] = 0; 1055 int n = fprintf(f, "%s\n", s) - (isspace(c) ? 0 : 1); 1056 s[i] = c; 1057 *row += 1; 1058 *col = 0; 1059 return n; 1060 } 1061 } 1062 return fprintf(f, "%s\n", s) - 1; 1063 } 1064 1065 static int 1066 resp_read(void *state, void *buf, size_t nbyte) 1067 { 1068 struct gemini_response *resp = state; 1069 if (resp->sc) { 1070 return br_sslio_read(&resp->body, buf, nbyte); 1071 } else { 1072 return read(resp->fd, buf, nbyte); 1073 } 1074 } 1075 1076 static bool 1077 display_gemini(struct browser *browser, struct gemini_response *resp) 1078 { 1079 int nlinks = 0; 1080 struct gemini_parser p; 1081 gemini_parser_init(&p, &resp_read, resp); 1082 free(browser->page_title); 1083 browser->page_title = NULL; 1084 1085 FILE *out = browser->tty; 1086 bool searching = browser->searching; 1087 if (searching) { 1088 out = fopen("/dev/null", "w+"); 1089 } 1090 bool alttext_printed = false; 1091 1092 fprintf(out, "\n"); 1093 char *text = NULL; 1094 int row = 0, col = 0; 1095 struct gemini_token tok; 1096 struct link **next = &browser->links; 1097 1098 char prompt[4096]; 1099 int info_rows = 0; 1100 struct winsize ws; 1101 bool first_screen = 1; 1102 while (text != NULL || gemini_parser_next(&p, &tok) == 0) { 1103 repeat: 1104 if (!row) { 1105 ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); 1106 if (browser->max_width != 0 && ws.ws_col > browser->max_width) { 1107 ws.ws_col = browser->max_width; 1108 } 1109 1110 char *end = NULL; 1111 if (browser->meta && (end = strchr(resp->meta, ';')) != NULL) { 1112 *end = 0; 1113 } 1114 snprintf(prompt, sizeof(prompt), "\n%s at %s\n" 1115 "[Enter]: read more; %s[N]: =follow Nth link; %s%s[q]uit; [?]; or type a URL\n" 1116 "(more) => ", resp->meta, browser->plain_url, 1117 browser->searching ? "[n]ext result; " : "", 1118 browser->history->prev ? "[b]ack; " : "", 1119 browser->history->next ? "[f]orward; " : ""); 1120 if (end != NULL) { 1121 *end = ';'; 1122 } 1123 1124 info_rows = 0; 1125 for (char *ln = prompt; (end = strchr(ln, '\n')); ln = end + 1) { 1126 *end = '\0'; 1127 int len = strlen(ln); 1128 info_rows += len / ws.ws_col + (len % ws.ws_col != 0); 1129 if (!*ln) info_rows++; // empty line 1130 *end = '\n'; 1131 } 1132 // if not first screen, text is preceded by an empty line, 1133 // and help text is followed by a prompt line 1134 info_rows += !first_screen + 1; 1135 } 1136 1137 switch (tok.token) { 1138 case GEMINI_TEXT: 1139 col += fprintf(out, " "); 1140 if (text == NULL) { 1141 text = tok.text; 1142 } 1143 break; 1144 case GEMINI_LINK: 1145 if (text == NULL) { 1146 col += fprintf(out, "%3d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9)) ? ANSI_COLOR_CYAN : ((strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_LCYAN : ANSI_COLOR_LMAGENTA)); 1147 text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); 1148 *next = calloc(1, sizeof(struct link)); 1149 (*next)->url = strdup(trim_ws(tok.link.url)); 1150 next = &(*next)->next; 1151 } else { 1152 col += fprintf(out, " "); 1153 } 1154 break; 1155 case GEMINI_PREFORMATTED_BEGIN: 1156 alttext_printed = false; 1157 if (text == NULL && browser->alttext && tok.preformatted != NULL) { 1158 fprintf(out, " A %s", ANSI_COLOR_GRAY); 1159 text = trim_ws(tok.preformatted); 1160 alttext_printed = true; 1161 } 1162 break; 1163 /* fallthrough */ 1164 case GEMINI_PREFORMATTED_END: 1165 continue; // Not used 1166 case GEMINI_PREFORMATTED_TEXT: 1167 if (alttext_printed) continue; 1168 if (text == NULL) { 1169 fprintf(out, " P %s", ANSI_COLOR_GRAY); 1170 text = tok.preformatted; 1171 } 1172 break; 1173 case GEMINI_HEADING: 1174 if (!browser->page_title) { 1175 browser->page_title = strdup(tok.heading.title); 1176 } 1177 if (text == NULL) { 1178 switch (tok.heading.level) { 1179 case 1: 1180 col += fprintf(out, " # %s", ANSI_COLOR_RED); 1181 break; 1182 case 2: 1183 col += fprintf(out, " ## %s", ANSI_COLOR_YELLOW); 1184 break; 1185 case 3: 1186 col += fprintf(out, " ### %s", ANSI_COLOR_GREEN); 1187 break; 1188 } 1189 text = trim_ws(tok.heading.title); 1190 } else { 1191 col += fprintf(out, " "); 1192 } 1193 break; 1194 case GEMINI_LIST_ITEM: 1195 if (text == NULL) { 1196 col += fprintf(out, " %s ", 1197 browser->unicode ? "•" : "*"); 1198 text = trim_ws(tok.list_item); 1199 } else { 1200 col += fprintf(out, " "); 1201 } 1202 break; 1203 case GEMINI_QUOTE: 1204 col += fprintf(out, " %s ", browser->unicode ? "┃" : ">"); 1205 if (text == NULL) { 1206 text = trim_ws(tok.quote_text); 1207 } 1208 break; 1209 } 1210 1211 if (text && searching) { 1212 int r = regexec(&browser->regex, text, 0, NULL, 0); 1213 if (r != 0) { 1214 text = NULL; 1215 continue; 1216 } else { 1217 fclose(out); 1218 row = col = 0; 1219 out = browser->tty; 1220 text = NULL; 1221 searching = false; 1222 goto repeat; 1223 } 1224 } 1225 1226 if (text) { 1227 int w = wrap(out, text, &ws, &row, &col); 1228 text += w; 1229 if (text[0] && row < ws.ws_row - info_rows) { 1230 continue; 1231 } 1232 1233 if (!text[0]) { 1234 text = NULL; 1235 } 1236 } 1237 if (text == NULL) { 1238 gemini_token_finish(&tok); 1239 } 1240 1241 while (col >= ws.ws_col) { 1242 col -= ws.ws_col; 1243 ++row; 1244 } 1245 fprintf(out, ANSI_COLOR_RESET); 1246 ++row; col = 0; 1247 1248 if (browser->pagination && row >= ws.ws_row - info_rows) { 1249 first_screen = 0; 1250 enum prompt_result result = PROMPT_AGAIN; 1251 while (result == PROMPT_AGAIN) { 1252 result = do_prompts(prompt, browser); 1253 } 1254 1255 switch (result) { 1256 case PROMPT_AGAIN: 1257 case PROMPT_MORE: 1258 printf("\n"); 1259 break; 1260 case PROMPT_QUIT: 1261 browser->running = false; 1262 if (text != NULL) { 1263 gemini_token_finish(&tok); 1264 } 1265 gemini_parser_finish(&p); 1266 return true; 1267 case PROMPT_ANSWERED: 1268 if (text != NULL) { 1269 gemini_token_finish(&tok); 1270 } 1271 gemini_parser_finish(&p); 1272 return true; 1273 case PROMPT_NEXT: 1274 searching = true; 1275 out = fopen("/dev/null", "w"); 1276 break; 1277 } 1278 1279 row = col = 0; 1280 } 1281 } 1282 1283 gemini_token_finish(&tok); 1284 gemini_parser_finish(&p); 1285 return false; 1286 } 1287 1288 static bool 1289 display_plaintext(struct browser *browser, struct gemini_response *resp) 1290 { 1291 struct winsize ws; 1292 int row = 0, col = 0; 1293 ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); 1294 1295 char buf[BUFSIZ]; 1296 for (int n = 1; n > 0;) { 1297 if (resp->sc) { 1298 n = br_sslio_read(&resp->body, buf, BUFSIZ); 1299 } else { 1300 n = read(resp->fd, buf, BUFSIZ); 1301 } 1302 if (n < 0) { 1303 n = 0; 1304 } 1305 for (int i = 0; i < n; i++) { 1306 if (iscntrl(buf[i]) && (buf[i] < '\t' || buf[i] > '\v')) { 1307 buf[i] = '.'; 1308 } 1309 } 1310 ssize_t w = 0; 1311 while (w < (ssize_t)n) { 1312 ssize_t x = fwrite(&buf[w], 1, n - w, browser->tty); 1313 if (ferror(browser->tty)) { 1314 fprintf(stderr, "Error: write: %s\n", 1315 strerror(errno)); 1316 return 1; 1317 } 1318 w += x; 1319 } 1320 } 1321 1322 (void)row; (void)col; // TODO: generalize pagination 1323 return false; 1324 } 1325 1326 static bool 1327 display_response(struct browser *browser, struct gemini_response *resp) 1328 { 1329 if (gemini_response_class(resp->status) != GEMINI_STATUS_CLASS_SUCCESS) { 1330 return false; 1331 } 1332 printf("%c]0;%s%s%c", '\033', browser->plain_url, " - cgmnlm", '\007'); 1333 if (strcmp(resp->meta, "text/gemini") == 0 1334 || strncmp(resp->meta, "text/gemini;", 12) == 0) { 1335 return display_gemini(browser, resp); 1336 } 1337 if (strncmp(resp->meta, "text/", 5) == 0) { 1338 return display_plaintext(browser, resp); 1339 } 1340 fprintf(stderr, "Media type %s is unsupported, use \"d [path]\" to download this page\n", 1341 resp->meta); 1342 return false; 1343 } 1344 1345 static enum tofu_action 1346 tofu_callback(enum tofu_error error, const char *fingerprint, 1347 struct known_host *khost, void *data) 1348 { 1349 struct browser *browser = data; 1350 if (browser->tofu_mode != TOFU_ASK) { 1351 return browser->tofu_mode; 1352 } 1353 1354 char *host; 1355 if (curl_url_get(browser->url, CURLUPART_HOST, &host, 0) != CURLUE_OK) { 1356 fprintf(stderr, "Error: invalid URL %s\n", 1357 browser->plain_url); 1358 return TOFU_FAIL; 1359 } 1360 static char prompt[8192]; 1361 switch (error) { 1362 case TOFU_VALID: 1363 assert(0); // Invariant 1364 case TOFU_INVALID_CERT: 1365 snprintf(prompt, sizeof(prompt), 1366 "The certificate offered by %s IS INVALID.\n" 1367 "/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n" 1368 "If you choose to proceed, you should not disclose personal information or trust " 1369 "the contents of the page.\n" 1370 "[A]bort; trust [o]nce\n" 1371 "=> ", host); 1372 break; 1373 case TOFU_UNTRUSTED_CERT:; 1374 snprintf(prompt, sizeof(prompt), 1375 "The certificate offered by %s is of unknown trust. " 1376 "Its fingerprint is: \n" 1377 "%s\n\n" 1378 "If you knew the fingerprint to expect in advance, verify that this matches.\n" 1379 "Otherwise, it should be safe to trust this certificate.\n\n" 1380 "[T]rust always; trust [o]nce; [a]bort\n" 1381 "=> ", host, fingerprint); 1382 free(host); 1383 break; 1384 case TOFU_FINGERPRINT_MISMATCH:; 1385 snprintf(prompt, sizeof(prompt), 1386 "The certificate offered by %s DOES NOT MATCH the one we have on file.\n" 1387 "/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n" 1388 "The unknown certificate's fingerprint is:\n" 1389 "%s\n\n" 1390 "The expected fingerprint is:\n" 1391 "%s\n\n" 1392 "If you choose to proceed, you should not disclose personal information or trust " 1393 "the contents of the page.\n" 1394 "[A]bort; trust [o]nce; [t]rust anyway\n" 1395 "=> ", host, fingerprint, khost->fingerprint); 1396 break; 1397 } 1398 1399 bool prompting = true; 1400 while (prompting) { 1401 fprintf(browser->tty, "%s", prompt); 1402 1403 size_t sz = 0; 1404 char *line = NULL; 1405 if (getline(&line, &sz, browser->tty) == -1) { 1406 free(line); 1407 return TOFU_FAIL; 1408 } 1409 1410 char c = line[0]; 1411 if (c == '\n') { 1412 return error == TOFU_UNTRUSTED_CERT ? 1413 TOFU_TRUST_ALWAYS : TOFU_FAIL; 1414 } else if (line[1] != '\n') { 1415 free(line); 1416 continue; 1417 } 1418 free(line); 1419 1420 switch (c) { 1421 case 't': 1422 if (error == TOFU_INVALID_CERT) { 1423 break; 1424 } 1425 return TOFU_TRUST_ALWAYS; 1426 case 'o': 1427 return TOFU_TRUST_ONCE; 1428 case 'a': 1429 return TOFU_FAIL; 1430 } 1431 } 1432 1433 return TOFU_FAIL; 1434 } 1435 1436 int 1437 main(int argc, char *argv[]) 1438 { 1439 struct browser browser = { 1440 .pagination = true, 1441 .alttext = false, 1442 .autoopen = false, 1443 .tofu_mode = TOFU_ASK, 1444 .unicode = true, 1445 .url = curl_url(), 1446 .tty = fopen("/dev/tty", "w+"), 1447 .meta = NULL, 1448 .max_redirs = REDIRS_ASK, 1449 }; 1450 1451 int c; 1452 while ((c = getopt(argc, argv, "hj:PR:UW:TA")) != -1) { 1453 switch (c) { 1454 case 'h': 1455 usage(argv[0]); 1456 return 0; 1457 case 'j': 1458 if (strcmp(optarg, "fail") == 0) { 1459 browser.tofu_mode = TOFU_FAIL; 1460 } else if (strcmp(optarg, "once") == 0) { 1461 browser.tofu_mode = TOFU_TRUST_ONCE; 1462 } else if (strcmp(optarg, "always") == 0) { 1463 browser.tofu_mode = TOFU_TRUST_ALWAYS; 1464 } else { 1465 usage(argv[0]); 1466 return 1; 1467 } 1468 break; 1469 case 'T': 1470 browser.autoopen = true; 1471 break; 1472 case 'A': 1473 browser.alttext = true; 1474 break; 1475 case 'P': 1476 browser.pagination = false; 1477 break; 1478 case 'R':; 1479 int mr = strtol(optarg, NULL, 10); 1480 browser.max_redirs = mr < 0 ? REDIRS_UNLIMITED : mr; 1481 break; 1482 case 'U': 1483 browser.unicode = false; 1484 break; 1485 case 'W': 1486 browser.max_width = strtoul(optarg, NULL, 10); 1487 break; 1488 default: 1489 fprintf(stderr, "fatal: unknown flag %c\n", c); 1490 curl_url_cleanup(browser.url); 1491 return 1; 1492 } 1493 } 1494 1495 if (optind == argc - 1) { 1496 if (!set_url(&browser, argv[optind], &browser.history)) { 1497 return 1; 1498 } 1499 } else { 1500 open_bookmarks(&browser); 1501 } 1502 1503 gemini_tofu_init(&browser.tofu, &tofu_callback, &browser); 1504 1505 struct gemini_response resp; 1506 browser.running = true; 1507 while (browser.running) { 1508 static char prompt[4096]; 1509 bool skip_prompt = do_requests(&browser, &resp) == GEMINI_OK 1510 && display_response(&browser, &resp); 1511 if (browser.meta) { 1512 free(browser.meta); 1513 } 1514 browser.meta = resp.status == GEMINI_STATUS_SUCCESS 1515 ? strdup(resp.meta) : NULL; 1516 gemini_response_finish(&resp); 1517 if (!skip_prompt) { 1518 char *end = NULL; 1519 if (browser.meta && (end = strchr(browser.meta, ';')) != NULL) { 1520 *end = 0; 1521 } 1522 snprintf(prompt, sizeof(prompt), "\n%s at %s\n" 1523 "[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" 1524 "=> ", browser.meta ? browser.meta 1525 : "[request failed]", browser.plain_url, 1526 browser.history->prev ? "[b]ack; " : "", 1527 browser.history->next ? "[f]orward; " : ""); 1528 if (end != NULL) { 1529 *end = ';'; 1530 } 1531 1532 enum prompt_result result = PROMPT_AGAIN; 1533 while (result == PROMPT_AGAIN || result == PROMPT_MORE) { 1534 result = do_prompts(prompt, &browser); 1535 } 1536 switch (result) { 1537 case PROMPT_AGAIN: 1538 case PROMPT_MORE: 1539 assert(0); 1540 case PROMPT_QUIT: 1541 browser.running = false; 1542 break; 1543 case PROMPT_ANSWERED: 1544 case PROMPT_NEXT: 1545 break; 1546 } 1547 } 1548 1549 struct link *link = browser.links; 1550 while (link) { 1551 struct link *next = link->next; 1552 free(link->url); 1553 free(link); 1554 link = next; 1555 } 1556 browser.links = NULL; 1557 } 1558 1559 gemini_tofu_finish(&browser.tofu); 1560 struct history *hist = browser.history; 1561 while (hist && hist->prev) { 1562 hist = hist->prev; 1563 } 1564 history_free(hist); 1565 curl_url_cleanup(browser.url); 1566 free(browser.page_title); 1567 free(browser.plain_url); 1568 if (browser.meta != NULL) { 1569 free(browser.meta); 1570 } 1571 fclose(browser.tty); 1572 return 0; 1573 }