gmnlm.c (38016B)
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[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') { 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 struct winsize ws; 1086 ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); 1087 if (browser->max_width != 0 && ws.ws_col > browser->max_width) { 1088 ws.ws_col = browser->max_width; 1089 } 1090 1091 FILE *out = browser->tty; 1092 bool searching = browser->searching; 1093 if (searching) { 1094 out = fopen("/dev/null", "w+"); 1095 } 1096 1097 fprintf(out, "\n"); 1098 char *text = NULL; 1099 int row = 0, col = 0; 1100 bool alttext_printed = false; 1101 struct gemini_token tok; 1102 struct link **next = &browser->links; 1103 // When your screen is too narrow, more lines will be used for helptext and URL. 1104 // 87 is the maximum width of the prompt. 1105 int info_rows = (ws.ws_col >= 87) ? 4 : 6; 1106 1107 while (text != NULL || gemini_parser_next(&p, &tok) == 0) { 1108 repeat: 1109 switch (tok.token) { 1110 case GEMINI_TEXT: 1111 col += fprintf(out, " "); 1112 if (text == NULL) { 1113 text = tok.text; 1114 } 1115 break; 1116 case GEMINI_LINK: 1117 if (text == NULL) { 1118 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)); 1119 text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); 1120 *next = calloc(1, sizeof(struct link)); 1121 (*next)->url = strdup(trim_ws(tok.link.url)); 1122 next = &(*next)->next; 1123 } else { 1124 col += fprintf(out, " "); 1125 } 1126 break; 1127 case GEMINI_PREFORMATTED_BEGIN: 1128 alttext_printed = false; 1129 if (text == NULL && browser->alttext && tok.preformatted != NULL) { 1130 fprintf(out, " A %s", ANSI_COLOR_GRAY); 1131 text = trim_ws(tok.preformatted); 1132 alttext_printed = true; 1133 } 1134 break; 1135 /* fallthrough */ 1136 case GEMINI_PREFORMATTED_END: 1137 continue; // Not used 1138 case GEMINI_PREFORMATTED_TEXT: 1139 if (alttext_printed) continue; 1140 if (text == NULL) { 1141 fprintf(out, " P %s", ANSI_COLOR_GRAY); 1142 text = tok.preformatted; 1143 } 1144 break; 1145 case GEMINI_HEADING: 1146 if (!browser->page_title) { 1147 browser->page_title = strdup(tok.heading.title); 1148 } 1149 if (text == NULL) { 1150 switch (tok.heading.level) { 1151 case 1: 1152 col += fprintf(out, " # %s", ANSI_COLOR_RED); 1153 break; 1154 case 2: 1155 col += fprintf(out, " ## %s", ANSI_COLOR_YELLOW); 1156 break; 1157 case 3: 1158 col += fprintf(out, " ### %s", ANSI_COLOR_GREEN); 1159 break; 1160 } 1161 text = trim_ws(tok.heading.title); 1162 } else { 1163 col += fprintf(out, " "); 1164 } 1165 break; 1166 case GEMINI_LIST_ITEM: 1167 if (text == NULL) { 1168 col += fprintf(out, " %s ", 1169 browser->unicode ? "•" : "*"); 1170 text = trim_ws(tok.list_item); 1171 } else { 1172 col += fprintf(out, " "); 1173 } 1174 break; 1175 case GEMINI_QUOTE: 1176 col += fprintf(out, " %s ", browser->unicode ? "┃" : ">"); 1177 if (text == NULL) { 1178 text = trim_ws(tok.quote_text); 1179 } 1180 break; 1181 } 1182 1183 if (text && searching) { 1184 int r = regexec(&browser->regex, text, 0, NULL, 0); 1185 if (r != 0) { 1186 text = NULL; 1187 continue; 1188 } else { 1189 fclose(out); 1190 row = col = 0; 1191 out = browser->tty; 1192 text = NULL; 1193 searching = false; 1194 goto repeat; 1195 } 1196 } 1197 1198 if (text) { 1199 int w = wrap(out, text, &ws, &row, &col); 1200 text += w; 1201 if (text[0] && row < ws.ws_row - info_rows) { 1202 continue; 1203 } 1204 1205 if (!text[0]) { 1206 text = NULL; 1207 } 1208 } 1209 if (text == NULL) { 1210 gemini_token_finish(&tok); 1211 } 1212 1213 while (col >= ws.ws_col) { 1214 col -= ws.ws_col; 1215 ++row; 1216 } 1217 ++row; col = 0; 1218 1219 fprintf(out, ANSI_COLOR_RESET); 1220 if (browser->pagination && row >= ws.ws_row - info_rows) { 1221 char prompt[4096]; 1222 char *end = NULL; 1223 if (browser->meta && (end = strchr(resp->meta, ';')) != NULL) { 1224 *end = 0; 1225 } 1226 snprintf(prompt, sizeof(prompt), "%s at %s\n" 1227 "[Enter]: read more; %s[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" 1228 "(more) => ", resp->meta, browser->plain_url, 1229 browser->searching ? "[n]ext result; " : "", 1230 browser->history->prev ? "[b]ack; " : "", 1231 browser->history->next ? "[f]orward; " : ""); 1232 if (end != NULL) { 1233 *end = ';'; 1234 } 1235 enum prompt_result result = PROMPT_AGAIN; 1236 while (result == PROMPT_AGAIN) { 1237 result = do_prompts(prompt, browser); 1238 } 1239 1240 switch (result) { 1241 case PROMPT_AGAIN: 1242 case PROMPT_MORE: 1243 printf("\n"); 1244 break; 1245 case PROMPT_QUIT: 1246 browser->running = false; 1247 if (text != NULL) { 1248 gemini_token_finish(&tok); 1249 } 1250 gemini_parser_finish(&p); 1251 return true; 1252 case PROMPT_ANSWERED: 1253 if (text != NULL) { 1254 gemini_token_finish(&tok); 1255 } 1256 gemini_parser_finish(&p); 1257 return true; 1258 case PROMPT_NEXT: 1259 searching = true; 1260 out = fopen("/dev/null", "w"); 1261 break; 1262 } 1263 1264 row = col = 0; 1265 } 1266 } 1267 1268 gemini_token_finish(&tok); 1269 gemini_parser_finish(&p); 1270 return false; 1271 } 1272 1273 static bool 1274 display_plaintext(struct browser *browser, struct gemini_response *resp) 1275 { 1276 struct winsize ws; 1277 int row = 0, col = 0; 1278 ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); 1279 1280 char buf[BUFSIZ]; 1281 for (int n = 1; n > 0;) { 1282 if (resp->sc) { 1283 n = br_sslio_read(&resp->body, buf, BUFSIZ); 1284 } else { 1285 n = read(resp->fd, buf, BUFSIZ); 1286 } 1287 if (n < 0) { 1288 n = 0; 1289 } 1290 for (int i = 0; i < n; i++) { 1291 if (iscntrl(buf[i]) && (buf[i] < '\t' || buf[i] > '\v')) { 1292 buf[i] = '.'; 1293 } 1294 } 1295 ssize_t w = 0; 1296 while (w < (ssize_t)n) { 1297 ssize_t x = fwrite(&buf[w], 1, n - w, browser->tty); 1298 if (ferror(browser->tty)) { 1299 fprintf(stderr, "Error: write: %s\n", 1300 strerror(errno)); 1301 return 1; 1302 } 1303 w += x; 1304 } 1305 } 1306 1307 (void)row; (void)col; // TODO: generalize pagination 1308 return false; 1309 } 1310 1311 static bool 1312 display_response(struct browser *browser, struct gemini_response *resp) 1313 { 1314 if (gemini_response_class(resp->status) != GEMINI_STATUS_CLASS_SUCCESS) { 1315 return false; 1316 } 1317 printf("%c]0;%s%s%c", '\033', browser->plain_url, " - cgmnlm", '\007'); 1318 if (strcmp(resp->meta, "text/gemini") == 0 1319 || strncmp(resp->meta, "text/gemini;", 12) == 0) { 1320 return display_gemini(browser, resp); 1321 } 1322 if (strncmp(resp->meta, "text/", 5) == 0) { 1323 return display_plaintext(browser, resp); 1324 } 1325 fprintf(stderr, "Media type %s is unsupported, use \"d [path]\" to download this page\n", 1326 resp->meta); 1327 return false; 1328 } 1329 1330 static enum tofu_action 1331 tofu_callback(enum tofu_error error, const char *fingerprint, 1332 struct known_host *khost, void *data) 1333 { 1334 struct browser *browser = data; 1335 if (browser->tofu_mode != TOFU_ASK) { 1336 return browser->tofu_mode; 1337 } 1338 1339 static char prompt[8192]; 1340 switch (error) { 1341 case TOFU_VALID: 1342 assert(0); // Invariant 1343 case TOFU_INVALID_CERT: 1344 snprintf(prompt, sizeof(prompt), 1345 "The certificate offered by this server IS INVALID.\n" 1346 "/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n" 1347 "If you choose to proceed, you should not disclose personal information or trust " 1348 "the contents of the page.\n" 1349 "[a]bort; trust [o]nce\n" 1350 "=> "); 1351 break; 1352 case TOFU_UNTRUSTED_CERT:; 1353 char *host; 1354 if (curl_url_get(browser->url, CURLUPART_HOST, &host, 0) != CURLUE_OK) { 1355 fprintf(stderr, "Error: invalid URL %s\n", 1356 browser->plain_url); 1357 return TOFU_FAIL; 1358 } 1359 snprintf(prompt, sizeof(prompt), 1360 "The certificate offered by %s is of unknown trust. " 1361 "Its fingerprint is: \n" 1362 "%s\n\n" 1363 "If you knew the fingerprint to expect in advance, verify that this matches.\n" 1364 "Otherwise, it should be safe to trust this certificate.\n\n" 1365 "[t]rust always; trust [o]nce; [a]bort\n" 1366 "=> ", host, fingerprint); 1367 free(host); 1368 break; 1369 case TOFU_FINGERPRINT_MISMATCH: 1370 snprintf(prompt, sizeof(prompt), 1371 "The certificate offered by this server DOES NOT MATCH the one we have on file.\n" 1372 "/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n" 1373 "The unknown certificate's fingerprint is:\n" 1374 "%s\n\n" 1375 "The expected fingerprint is:\n" 1376 "%s\n\n" 1377 "If you choose to proceed, you should not disclose personal information or trust " 1378 "the contents of the page.\n" 1379 "[a]bort; trust [o]nce; [t]rust anyway\n" 1380 "=> ", fingerprint, khost->fingerprint); 1381 break; 1382 } 1383 1384 bool prompting = true; 1385 while (prompting) { 1386 fprintf(browser->tty, "%s", prompt); 1387 1388 size_t sz = 0; 1389 char *line = NULL; 1390 if (getline(&line, &sz, browser->tty) == -1) { 1391 free(line); 1392 return TOFU_FAIL; 1393 } 1394 if (line[1] != '\n') { 1395 free(line); 1396 continue; 1397 } 1398 1399 char c = line[0]; 1400 free(line); 1401 1402 switch (c) { 1403 case 't': 1404 if (error == TOFU_INVALID_CERT) { 1405 break; 1406 } 1407 return TOFU_TRUST_ALWAYS; 1408 case 'o': 1409 return TOFU_TRUST_ONCE; 1410 case 'a': 1411 return TOFU_FAIL; 1412 } 1413 } 1414 1415 return TOFU_FAIL; 1416 } 1417 1418 int 1419 main(int argc, char *argv[]) 1420 { 1421 struct browser browser = { 1422 .pagination = true, 1423 .alttext = false, 1424 .autoopen = false, 1425 .tofu_mode = TOFU_ASK, 1426 .unicode = true, 1427 .url = curl_url(), 1428 .tty = fopen("/dev/tty", "w+"), 1429 .meta = NULL, 1430 .max_redirs = REDIRS_ASK, 1431 }; 1432 1433 int c; 1434 while ((c = getopt(argc, argv, "hj:PR:UW:TA")) != -1) { 1435 switch (c) { 1436 case 'h': 1437 usage(argv[0]); 1438 return 0; 1439 case 'j': 1440 if (strcmp(optarg, "fail") == 0) { 1441 browser.tofu_mode = TOFU_FAIL; 1442 } else if (strcmp(optarg, "once") == 0) { 1443 browser.tofu_mode = TOFU_TRUST_ONCE; 1444 } else if (strcmp(optarg, "always") == 0) { 1445 browser.tofu_mode = TOFU_TRUST_ALWAYS; 1446 } else { 1447 usage(argv[0]); 1448 return 1; 1449 } 1450 break; 1451 case 'T': 1452 browser.autoopen = true; 1453 break; 1454 case 'A': 1455 browser.alttext = true; 1456 break; 1457 case 'P': 1458 browser.pagination = false; 1459 break; 1460 case 'R':; 1461 int mr = strtol(optarg, NULL, 10); 1462 browser.max_redirs = mr < 0 ? REDIRS_UNLIMITED : mr; 1463 break; 1464 case 'U': 1465 browser.unicode = false; 1466 break; 1467 case 'W': 1468 browser.max_width = strtoul(optarg, NULL, 10); 1469 break; 1470 default: 1471 fprintf(stderr, "fatal: unknown flag %c\n", c); 1472 curl_url_cleanup(browser.url); 1473 return 1; 1474 } 1475 } 1476 1477 if (optind == argc - 1) { 1478 if (!set_url(&browser, argv[optind], &browser.history)) { 1479 return 1; 1480 } 1481 } else { 1482 open_bookmarks(&browser); 1483 } 1484 1485 gemini_tofu_init(&browser.tofu, &tofu_callback, &browser); 1486 1487 struct gemini_response resp; 1488 browser.running = true; 1489 while (browser.running) { 1490 static char prompt[4096]; 1491 bool skip_prompt = do_requests(&browser, &resp) == GEMINI_OK 1492 && display_response(&browser, &resp); 1493 if (browser.meta) { 1494 free(browser.meta); 1495 } 1496 browser.meta = resp.status == GEMINI_STATUS_SUCCESS 1497 ? strdup(resp.meta) : NULL; 1498 gemini_response_finish(&resp); 1499 if (!skip_prompt) { 1500 char *end = NULL; 1501 if (browser.meta && (end = strchr(browser.meta, ';')) != NULL) { 1502 *end = 0; 1503 } 1504 snprintf(prompt, sizeof(prompt), "\n%s at %s\n" 1505 "[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" 1506 "=> ", browser.meta ? browser.meta 1507 : "[request failed]", browser.plain_url, 1508 browser.history->prev ? "[b]ack; " : "", 1509 browser.history->next ? "[f]orward; " : ""); 1510 if (end != NULL) { 1511 *end = ';'; 1512 } 1513 1514 enum prompt_result result = PROMPT_AGAIN; 1515 while (result == PROMPT_AGAIN || result == PROMPT_MORE) { 1516 result = do_prompts(prompt, &browser); 1517 } 1518 switch (result) { 1519 case PROMPT_AGAIN: 1520 case PROMPT_MORE: 1521 assert(0); 1522 case PROMPT_QUIT: 1523 browser.running = false; 1524 break; 1525 case PROMPT_ANSWERED: 1526 case PROMPT_NEXT: 1527 break; 1528 } 1529 } 1530 1531 struct link *link = browser.links; 1532 while (link) { 1533 struct link *next = link->next; 1534 free(link->url); 1535 free(link); 1536 link = next; 1537 } 1538 browser.links = NULL; 1539 } 1540 1541 gemini_tofu_finish(&browser.tofu); 1542 struct history *hist = browser.history; 1543 while (hist && hist->prev) { 1544 hist = hist->prev; 1545 } 1546 history_free(hist); 1547 curl_url_cleanup(browser.url); 1548 free(browser.page_title); 1549 free(browser.plain_url); 1550 if (browser.meta != NULL) { 1551 free(browser.meta); 1552 } 1553 fclose(browser.tty); 1554 return 0; 1555 }