1
0
Fork 0
mirror of https://git.sr.ht/~sircmpwn/gmni synced 2024-06-03 11:06:03 +02:00
gmni/src/gmnlm.c

864 lines
19 KiB
C
Raw Normal View History

2020-09-21 00:31:33 +02:00
#include <assert.h>
#include <ctype.h>
#include <getopt.h>
#include <libgen.h>
#include <limits.h>
2020-09-21 00:31:33 +02:00
#include <openssl/bio.h>
#include <openssl/err.h>
2020-09-21 04:09:26 +02:00
#include <regex.h>
2020-09-21 00:31:33 +02:00
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
#include "gmni.h"
2020-09-21 21:37:24 +02:00
#include "tofu.h"
2020-09-21 00:31:33 +02:00
#include "url.h"
2020-09-21 05:50:50 +02:00
#include "util.h"
2020-09-21 00:31:33 +02:00
struct link {
char *url;
struct link *next;
};
2020-09-21 01:01:14 +02:00
struct history {
char *url;
struct history *prev, *next;
};
2020-09-21 01:33:43 +02:00
struct browser {
2020-09-21 02:11:48 +02:00
bool pagination, unicode;
2020-09-21 01:33:43 +02:00
struct gemini_options opts;
2020-09-21 21:37:24 +02:00
struct gemini_tofu tofu;
enum tofu_action tofu_mode;
2020-09-21 01:33:43 +02:00
FILE *tty;
2020-09-21 02:01:31 +02:00
char *plain_url;
2020-09-21 05:50:50 +02:00
char *page_title;
2020-09-21 01:33:43 +02:00
struct Curl_URL *url;
struct link *links;
struct history *history;
2020-09-21 02:01:31 +02:00
bool running;
2020-09-21 04:09:26 +02:00
bool searching;
regex_t regex;
2020-09-21 02:01:31 +02:00
};
enum prompt_result {
PROMPT_AGAIN,
PROMPT_MORE,
PROMPT_QUIT,
PROMPT_ANSWERED,
2020-09-21 04:09:26 +02:00
PROMPT_NEXT,
2020-09-21 01:33:43 +02:00
};
2020-09-21 03:42:27 +02:00
const char *help_msg =
"The following commands are available:\n\n"
2020-09-21 04:09:26 +02:00
"q\tQuit\n"
"N\tFollow Nth link (where N is a number)\n"
"b\tBack (in the page history)\n"
"f\tForward (in the page history)\n"
"H\tView all page history\n"
2020-09-21 05:50:50 +02:00
"m\tSave bookmark\n"
"M\tBrowse bookmarks\n"
2020-09-21 04:09:26 +02:00
"\n"
"Other commands include:\n\n"
"<Enter>\tread more lines\n"
"<url>\tgo to url\n"
"/<text>\tsearch for text (POSIX regular expression)\n"
2020-09-21 03:42:27 +02:00
;
2020-09-21 00:31:33 +02:00
static void
usage(const char *argv_0)
{
fprintf(stderr, "usage: %s [gemini://...]\n", argv_0);
}
2020-09-21 01:01:14 +02:00
static void
history_free(struct history *history)
{
if (!history) {
return;
}
history_free(history->next);
free(history);
}
2020-09-21 00:31:33 +02:00
static bool
2020-09-21 01:33:43 +02:00
set_url(struct browser *browser, char *new_url, struct history **history)
2020-09-21 00:31:33 +02:00
{
if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) {
fprintf(stderr, "Error: invalid URL\n");
return false;
}
2020-09-21 05:33:32 +02:00
curl_url_get(browser->url, CURLUPART_URL, &browser->plain_url, 0);
2020-09-21 01:01:14 +02:00
if (history) {
struct history *next = calloc(1, sizeof(struct history));
curl_url_get(browser->url, CURLUPART_URL, &next->url, 0);
2020-09-21 01:01:14 +02:00
next->prev = *history;
if (*history) {
if ((*history)->next) {
history_free((*history)->next);
}
(*history)->next = next;
}
*history = next;
}
2020-09-21 00:31:33 +02:00
return true;
}
2020-09-21 05:50:50 +02:00
static char *
get_data_pathfmt()
{
const struct pathspec paths[] = {
{.var = "GMNIDATA", .path = "/%s"},
{.var = "XDG_DATA_HOME", .path = "/gmni/%s"},
{.var = "HOME", .path = "/.local/share/gmni/%s"}
};
return getpath(paths, sizeof(paths) / sizeof(paths[0]));
}
static char *
trim_ws(char *in)
{
while (*in && isspace(*in)) ++in;
return in;
}
static void
save_bookmark(struct browser *browser)
{
const char *path_fmt = get_data_pathfmt();
static char path[PATH_MAX+1];
snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi");
if (mkdirs(dirname(path), 0755) != 0) {
snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi");
fprintf(stderr, "Error creating directory %s: %s\n",
dirname(path), strerror(errno));
return;
}
2020-09-21 05:50:50 +02:00
snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi");
2020-09-21 05:50:50 +02:00
FILE *f = fopen(path, "a");
if (!f) {
fprintf(stderr, "Error opening %s for writing: %s\n",
path, strerror(errno));
return;
}
char *title = browser->page_title;
if (title) {
title = trim_ws(browser->page_title);
}
fprintf(f, "=> %s%s%s\n", browser->plain_url,
title ? " " : "", title ? title : "");
fclose(f);
fprintf(browser->tty, "Bookmark saved: %s\n",
title ? title : browser->plain_url);
}
static void
open_bookmarks(struct browser *browser)
{
const char *path_fmt = get_data_pathfmt();
static char path[PATH_MAX+1];
snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi");
static char url[PATH_MAX+1+7];
snprintf(url, sizeof(url), "file://%s", path);
set_url(browser, url, &browser->history);
}
2020-09-21 02:01:31 +02:00
static enum prompt_result
do_prompts(const char *prompt, struct browser *browser)
{
enum prompt_result result;
2020-09-21 02:01:31 +02:00
fprintf(browser->tty, "%s", prompt);
size_t l = 0;
char *in = NULL;
ssize_t n = getline(&in, &l, browser->tty);
if (n == -1 && feof(browser->tty)) {
result = PROMPT_QUIT;
goto exit;
2020-09-21 02:01:31 +02:00
}
2020-09-21 04:09:26 +02:00
in[n - 1] = 0; // Remove LF
int r;
switch (in[0]) {
case '\0':
result = PROMPT_MORE;
goto exit;
2020-09-21 04:09:26 +02:00
case 'q':
2020-09-21 05:33:32 +02:00
if (in[1]) break;
result = PROMPT_QUIT;
goto exit;
2020-09-21 04:09:26 +02:00
case 'b':
2020-09-21 05:33:32 +02:00
if (in[1]) break;
2020-09-21 02:01:31 +02:00
if (!browser->history->prev) {
fprintf(stderr, "At beginning of history\n");
result = PROMPT_AGAIN;
goto exit;
2020-09-21 02:01:31 +02:00
}
2020-09-21 05:33:32 +02:00
if (in[1]) break;
2020-09-21 02:01:31 +02:00
browser->history = browser->history->prev;
set_url(browser, browser->history->url, NULL);
result = PROMPT_ANSWERED;
goto exit;
2020-09-21 04:09:26 +02:00
case 'f':
2020-09-21 05:33:32 +02:00
if (in[1]) break;
2020-09-21 02:01:31 +02:00
if (!browser->history->next) {
fprintf(stderr, "At end of history\n");
result = PROMPT_AGAIN;
goto exit;
2020-09-21 02:01:31 +02:00
}
browser->history = browser->history->next;
set_url(browser, browser->history->url, NULL);
result = PROMPT_ANSWERED;
goto exit;
case 'H':
if (in[1]) break;
struct history *cur = browser->history;
while (cur->prev) cur = cur->prev;
while (cur != browser->history) {
fprintf(browser->tty, " %s\n", cur->url);
cur = cur->next;
}
fprintf(browser->tty, "* %s\n", cur->url);
cur = cur->next;
while (cur) {
fprintf(browser->tty, " %s\n", cur->url);
cur = cur->next;
}
result = PROMPT_AGAIN;
goto exit;
case 'm':
if (in[1]) break;
save_bookmark(browser);
result = PROMPT_AGAIN;
goto exit;
case 'M':
if (in[1]) break;
open_bookmarks(browser);
result = PROMPT_ANSWERED;
goto exit;
2020-09-21 04:09:26 +02:00
case '/':
2020-09-21 05:33:32 +02:00
if (in[1]) break;
2020-09-21 04:09:26 +02:00
if ((r = regcomp(&browser->regex, &in[1], REG_EXTENDED)) != 0) {
static char buf[1024];
r = regerror(r, &browser->regex, buf, sizeof(buf));
assert(r < (int)sizeof(buf));
fprintf(stderr, "Error: %s\n", buf);
result = PROMPT_AGAIN;
} else {
browser->searching = true;
result = PROMPT_ANSWERED;
}
goto exit_re;
case 'n':
2020-09-21 05:33:32 +02:00
if (in[1]) break;
if (browser->searching) {
result = PROMPT_NEXT;
goto exit_re;
} else {
fprintf(stderr, "Cannot move to next result; we are not searching for anything\n");
result = PROMPT_AGAIN;
goto exit;
}
2020-09-21 04:09:26 +02:00
case '?':
2020-09-21 05:33:32 +02:00
if (in[1]) break;
2020-09-21 04:09:26 +02:00
fprintf(browser->tty, "%s", help_msg);
result = PROMPT_AGAIN;
goto exit;
2020-09-21 02:01:31 +02:00
}
struct link *link = browser->links;
char *endptr;
int linksel = (int)strtol(in, &endptr, 10);
if (!endptr[0] && linksel >= 0) {
2020-09-21 02:01:31 +02:00
while (linksel > 0 && link) {
link = link->next;
--linksel;
}
if (!link) {
fprintf(stderr, "Error: no such link.\n");
} else {
set_url(browser, link->url, &browser->history);
result = PROMPT_ANSWERED;
goto exit;
2020-09-21 02:01:31 +02:00
}
}
set_url(browser, in, &browser->history);
result = PROMPT_ANSWERED;
exit:
2020-09-21 04:09:26 +02:00
if (browser->searching) {
browser->searching = false;
regfree(&browser->regex);
}
exit_re:
free(in);
return result;
2020-09-21 02:01:31 +02:00
}
2020-09-21 02:52:18 +02:00
static int
wrap(FILE *f, char *s, struct winsize *ws, int *row, int *col)
{
if (!s[0]) {
fprintf(f, "\n");
return 0;
}
for (int i = 0; s[i]; ++i) {
switch (s[i]) {
case '\n':
assert(0); // Not supposed to happen
case '\t':
*col = *col + (8 - *col % 8);
break;
default:
2020-09-21 03:02:17 +02:00
if (iscntrl(s[i])) {
s[i] = '.';
}
2020-09-21 02:52:18 +02:00
*col += 1;
break;
}
if (*col >= ws->ws_col) {
int j = i--;
while (&s[i] != s && !isspace(s[i])) --i;
if (&s[i] == s) {
i = j;
}
char c = s[i];
s[i] = 0;
int n = fprintf(f, "%s\n", s);
s[i] = c;
*row += 1;
*col = 0;
return n;
}
}
return fprintf(f, "%s\n", s) - 1;
}
2020-09-21 02:01:31 +02:00
static bool
2020-09-21 01:33:43 +02:00
display_gemini(struct browser *browser, struct gemini_response *resp)
2020-09-21 00:31:33 +02:00
{
int nlinks = 0;
struct gemini_parser p;
gemini_parser_init(&p, resp->bio);
2020-09-21 05:50:50 +02:00
free(browser->page_title);
browser->page_title = NULL;
2020-09-21 00:31:33 +02:00
struct winsize ws;
2020-09-21 01:33:43 +02:00
ioctl(fileno(browser->tty), TIOCGWINSZ, &ws);
2020-09-21 00:31:33 +02:00
2020-09-21 04:09:26 +02:00
FILE *out = browser->tty;
bool searching = browser->searching;
if (searching) {
out = fopen("/dev/null", "w+");
}
2020-09-21 02:52:18 +02:00
char *text = NULL;
2020-09-21 00:31:33 +02:00
int row = 0, col = 0;
struct gemini_token tok;
2020-09-21 01:33:43 +02:00
struct link **next = &browser->links;
2020-09-21 02:52:18 +02:00
while (text != NULL || gemini_parser_next(&p, &tok) == 0) {
2020-09-21 04:09:26 +02:00
repeat:
2020-09-21 00:31:33 +02:00
switch (tok.token) {
case GEMINI_TEXT:
2020-09-21 04:09:26 +02:00
col += fprintf(out, " ");
2020-09-21 02:52:18 +02:00
if (text == NULL) {
text = tok.text;
}
2020-09-21 00:31:33 +02:00
break;
case GEMINI_LINK:
2020-09-21 03:22:40 +02:00
if (text == NULL) {
2020-09-21 04:09:26 +02:00
col += fprintf(out, "%d) ", nlinks++);
2020-09-21 03:22:40 +02:00
text = trim_ws(tok.link.text ? tok.link.text : tok.link.url);
*next = calloc(1, sizeof(struct link));
(*next)->url = strdup(trim_ws(tok.link.url));
next = &(*next)->next;
} else {
2020-09-21 04:09:26 +02:00
col += fprintf(out, " ");
2020-09-21 03:22:40 +02:00
}
2020-09-21 00:31:33 +02:00
break;
2020-09-21 04:28:45 +02:00
case GEMINI_PREFORMATTED_BEGIN:
case GEMINI_PREFORMATTED_END:
continue; // Not used
case GEMINI_PREFORMATTED_TEXT:
col += fprintf(out, "` ");
if (text == NULL) {
text = tok.text;
}
break;
2020-09-21 00:31:33 +02:00
case GEMINI_HEADING:
2020-09-21 05:50:50 +02:00
if (!browser->page_title) {
browser->page_title = strdup(tok.heading.title);
}
2020-09-21 03:22:40 +02:00
if (text == NULL) {
for (int n = tok.heading.level; n; --n) {
2020-09-21 04:09:26 +02:00
col += fprintf(out, "#");
2020-09-21 03:22:40 +02:00
}
switch (tok.heading.level) {
case 1:
2020-09-21 04:09:26 +02:00
col += fprintf(out, " ");
2020-09-21 03:22:40 +02:00
break;
case 2:
case 3:
2020-09-21 04:09:26 +02:00
col += fprintf(out, " ");
2020-09-21 03:22:40 +02:00
break;
}
text = trim_ws(tok.heading.title);
} else {
2020-09-21 04:09:26 +02:00
col += fprintf(out, " ");
2020-09-21 02:42:46 +02:00
}
2020-09-21 00:31:33 +02:00
break;
case GEMINI_LIST_ITEM:
2020-09-21 03:22:40 +02:00
if (text == NULL) {
2020-09-21 04:09:26 +02:00
col += fprintf(out, " %s ",
2020-09-21 03:22:40 +02:00
browser->unicode ? "•" : "*");
text = trim_ws(tok.list_item);
} else {
2020-09-21 04:09:26 +02:00
col += fprintf(out, " ");
2020-09-21 03:22:40 +02:00
}
2020-09-21 00:31:33 +02:00
break;
case GEMINI_QUOTE:
2020-09-21 05:13:30 +02:00
col += fprintf(out, "> ");
2020-09-21 03:22:40 +02:00
if (text == NULL) {
text = trim_ws(tok.quote_text);
}
2020-09-21 00:31:33 +02:00
break;
}
2020-09-21 04:09:26 +02:00
if (text && searching) {
int r = regexec(&browser->regex, text, 0, NULL, 0);
if (r != 0) {
text = NULL;
continue;
} else {
fclose(out);
row = col = 0;
out = browser->tty;
text = NULL;
searching = false;
goto repeat;
}
}
2020-09-21 03:22:40 +02:00
if (text) {
2020-09-21 04:09:26 +02:00
int w = wrap(out, text, &ws, &row, &col);
2020-09-21 03:22:40 +02:00
text += w;
if (text[0] && row < ws.ws_row - 4) {
continue;
}
if (!text[0]) {
text = NULL;
}
}
2020-09-21 00:31:33 +02:00
while (col >= ws.ws_col) {
col -= ws.ws_col;
++row;
}
2020-09-21 03:22:40 +02:00
++row; col = 0;
2020-09-21 00:31:33 +02:00
2020-09-21 02:01:31 +02:00
if (browser->pagination && row >= ws.ws_row - 4) {
char prompt[4096];
snprintf(prompt, sizeof(prompt), "\n%s at %s\n"
2020-09-21 04:09:26 +02:00
"[Enter]: read more; %s[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n"
"(more) => ", resp->meta, browser->plain_url,
2020-09-21 04:09:26 +02:00
browser->searching ? "[n]ext result; " : "",
browser->history->prev ? "[b]ack; " : "",
browser->history->next ? "[f]orward; " : "");
2020-09-21 02:01:31 +02:00
enum prompt_result result = PROMPT_AGAIN;
while (result == PROMPT_AGAIN) {
result = do_prompts(prompt, browser);
2020-09-21 00:31:33 +02:00
}
2020-09-21 02:01:31 +02:00
switch (result) {
case PROMPT_AGAIN:
case PROMPT_MORE:
break;
case PROMPT_QUIT:
browser->running = false;
return true;
case PROMPT_ANSWERED:
return true;
2020-09-21 04:09:26 +02:00
case PROMPT_NEXT:
searching = true;
out = fopen("/dev/null", "w");
break;
2020-09-21 00:31:33 +02:00
}
row = col = 0;
}
}
gemini_parser_finish(&p);
2020-09-21 02:01:31 +02:00
return false;
2020-09-21 00:31:33 +02:00
}
2020-09-21 02:01:31 +02:00
static bool
2020-09-21 01:33:43 +02:00
display_plaintext(struct browser *browser, struct gemini_response *resp)
2020-09-21 01:09:55 +02:00
{
2020-09-21 01:33:43 +02:00
// TODO: Strip ANSI escape sequences
2020-09-21 01:09:55 +02:00
struct winsize ws;
int row = 0, col = 0;
2020-09-21 01:33:43 +02:00
ioctl(fileno(browser->tty), TIOCGWINSZ, &ws);
2020-09-21 01:09:55 +02:00
char buf[BUFSIZ];
int n;
while ((n = BIO_read(resp->bio, buf, sizeof(buf)) != 0)) {
while (n) {
2020-09-21 01:33:43 +02:00
n -= fwrite(buf, 1, n, browser->tty);
2020-09-21 01:09:55 +02:00
}
}
2020-09-21 01:33:43 +02:00
(void)row; (void)col; // TODO: generalize pagination
2020-09-21 02:01:31 +02:00
return false;
2020-09-21 01:09:55 +02:00
}
2020-09-21 02:01:31 +02:00
static bool
2020-09-21 01:33:43 +02:00
display_response(struct browser *browser, struct gemini_response *resp)
2020-09-21 01:09:55 +02:00
{
if (strcmp(resp->meta, "text/gemini") == 0
|| strncmp(resp->meta, "text/gemini;", 12) == 0) {
2020-09-21 02:01:31 +02:00
return display_gemini(browser, resp);
2020-09-21 01:09:55 +02:00
}
if (strncmp(resp->meta, "text/", 5) == 0) {
2020-09-21 02:01:31 +02:00
return display_plaintext(browser, resp);
2020-09-21 01:09:55 +02:00
}
2020-09-21 02:01:31 +02:00
assert(0); // TODO: Deal with other mimetypes
2020-09-21 01:09:55 +02:00
}
2020-09-21 01:40:42 +02:00
static char *
get_input(const struct gemini_response *resp, FILE *source)
{
int r = 0;
struct termios attrs;
bool tty = fileno(source) != -1 && isatty(fileno(source));
char *input = NULL;
if (tty) {
fprintf(stderr, "%s: ", resp->meta);
if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) {
r = tcgetattr(fileno(source), &attrs);
struct termios new_attrs;
r = tcgetattr(fileno(source), &new_attrs);
if (r != -1) {
new_attrs.c_lflag &= ~ECHO;
tcsetattr(fileno(source), TCSANOW, &new_attrs);
}
}
}
size_t s = 0;
ssize_t n = getline(&input, &s, source);
if (n == -1) {
fprintf(stderr, "Error reading input: %s\n",
feof(source) ? "EOF" : strerror(ferror(source)));
return NULL;
}
input[n - 1] = '\0'; // Drop LF
if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) {
attrs.c_lflag &= ~ECHO;
tcsetattr(fileno(source), TCSANOW, &attrs);
}
return input;
}
2020-09-21 05:33:32 +02:00
static bool
has_suffix(char *str, char *suff)
{
size_t suffl = strlen(suff);
size_t strl = strlen(str);
if (strl < suffl) {
return false;
}
return strcmp(&str[strl - suffl], suff) == 0;
}
2020-09-21 02:01:31 +02:00
// Returns true to skip prompting
static bool
2020-09-21 01:33:43 +02:00
do_requests(struct browser *browser, struct gemini_response *resp)
2020-09-21 00:31:33 +02:00
{
2020-09-21 01:33:43 +02:00
int nredir = 0;
bool requesting = true;
while (requesting) {
2020-09-21 05:33:32 +02:00
char *scheme;
2020-09-21 01:33:43 +02:00
CURLUcode uc = curl_url_get(browser->url,
2020-09-21 05:33:32 +02:00
CURLUPART_SCHEME, &scheme, 0);
2020-09-21 01:33:43 +02:00
assert(uc == CURLUE_OK); // Invariant
2020-09-21 05:33:32 +02:00
if (strcmp(scheme, "file") == 0) {
requesting = false;
char *path;
uc = curl_url_get(browser->url,
CURLUPART_PATH, &path, 0);
if (uc != CURLUE_OK) {
resp->status = GEMINI_STATUS_BAD_REQUEST;
break;
}
FILE *fp = fopen(path, "r");
if (!fp) {
resp->status = GEMINI_STATUS_NOT_FOUND;
break;
}
BIO *file = BIO_new_fp(fp, BIO_CLOSE);
resp->bio = BIO_new(BIO_f_buffer());
BIO_push(resp->bio, file);
if (has_suffix(path, ".gmi") || has_suffix(path, ".gemini")) {
resp->meta = strdup("text/gemini");
} else if (has_suffix(path, ".txt")) {
resp->meta = strdup("text/plain");
} else {
resp->meta = strdup("application/x-octet-stream");
}
resp->status = GEMINI_STATUS_SUCCESS;
return display_response(browser, resp);
}
2020-09-21 01:33:43 +02:00
2020-09-21 02:01:31 +02:00
enum gemini_result res = gemini_request(browser->plain_url,
&browser->opts, resp);
2020-09-21 01:33:43 +02:00
if (res != GEMINI_OK) {
fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp));
requesting = false;
break;
}
2020-09-21 00:31:33 +02:00
2020-09-21 01:40:42 +02:00
char *input;
2020-09-21 01:33:43 +02:00
switch (gemini_response_class(resp->status)) {
case GEMINI_STATUS_CLASS_INPUT:
2020-09-21 01:40:42 +02:00
input = get_input(resp, browser->tty);
if (!input) {
requesting = false;
break;
}
2020-09-21 02:01:31 +02:00
char *new_url = gemini_input_url(
browser->plain_url, input);
2020-09-21 01:40:42 +02:00
assert(new_url);
set_url(browser, new_url, NULL);
break;
2020-09-21 01:33:43 +02:00
case GEMINI_STATUS_CLASS_REDIRECT:
if (++nredir >= 5) {
requesting = false;
fprintf(stderr, "Error: maximum redirects (5) exceeded");
break;
}
fprintf(stderr, "Following redirect to %s\n", resp->meta);
set_url(browser, resp->meta, NULL);
break;
case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED:
assert(0); // TODO
case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE:
case GEMINI_STATUS_CLASS_PERMANENT_FAILURE:
requesting = false;
fprintf(stderr, "Server returned %s %d %s\n",
resp->status / 10 == 4 ?
"TEMPORARY FAILURE" : "PERMANENT FALIURE",
resp->status, resp->meta);
break;
case GEMINI_STATUS_CLASS_SUCCESS:
requesting = false;
2020-09-21 02:01:31 +02:00
return display_response(browser, resp);
2020-09-21 01:33:43 +02:00
}
2020-09-21 00:31:33 +02:00
2020-09-21 01:33:43 +02:00
if (requesting) {
gemini_response_finish(resp);
}
}
2020-09-21 02:01:31 +02:00
return false;
2020-09-21 01:33:43 +02:00
}
2020-09-21 21:37:24 +02:00
static enum tofu_action
tofu_callback(enum tofu_error error, const char *fingerprint,
struct known_host *host, void *data)
{
struct browser *browser = data;
if (browser->tofu_mode != TOFU_ASK) {
return browser->tofu_mode;
}
static char prompt[8192];
switch (error) {
case TOFU_VALID:
assert(0); // Invariant
case TOFU_INVALID_CERT:
snprintf(prompt, sizeof(prompt),
"The server presented an invalid certificate. If you choose to proceed, "
"you should not disclose personal information or trust the contents of the page.\n"
"trust [o]nce; [a]bort\n"
"=> ");
break;
case TOFU_UNTRUSTED_CERT:
snprintf(prompt, sizeof(prompt),
"The certificate offered by this server is of unknown trust. "
"Its fingerprint is: \n"
"%s\n\n"
"If you knew the fingerprint to expect in advance, verify that this matches.\n"
"Otherwise, it should be safe to trust this certificate.\n\n"
"[t]rust always; trust [o]nce; [a]bort\n"
"=> ", fingerprint);
break;
case TOFU_FINGERPRINT_MISMATCH:
snprintf(prompt, sizeof(prompt),
"The certificate offered by this server DOES NOT MATCH the one we have on file.\n"
"/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n"
"The unknown certificate's fingerprint is:\n"
"%s\n\n"
"The expected fingerprint is:\n"
"%s\n\n"
"If you're certain that this is correct, edit %s:%d\n",
fingerprint, host->fingerprint,
browser->tofu.known_hosts_path, host->lineno);
return TOFU_FAIL;
}
bool prompting = true;
while (prompting) {
fprintf(browser->tty, "%s", prompt);
size_t sz = 0;
char *line = NULL;
if (getline(&line, &sz, browser->tty) == -1) {
free(line);
return TOFU_FAIL;
}
if (line[1] != '\n') {
free(line);
continue;
}
char c = line[0];
free(line);
switch (c) {
case 't':
if (error == TOFU_INVALID_CERT) {
break;
}
return TOFU_TRUST_ALWAYS;
case 'o':
return TOFU_TRUST_ONCE;
case 'a':
return TOFU_FAIL;
}
}
return TOFU_FAIL;
}
2020-09-21 01:33:43 +02:00
int
main(int argc, char *argv[])
{
struct browser browser = {
.pagination = true,
2020-09-21 21:37:24 +02:00
.tofu_mode = TOFU_ASK,
2020-09-21 02:11:48 +02:00
.unicode = true,
2020-09-21 01:33:43 +02:00
.url = curl_url(),
.tty = fopen("/dev/tty", "w+"),
};
2020-09-21 00:31:33 +02:00
int c;
2020-09-21 21:37:24 +02:00
while ((c = getopt(argc, argv, "hj:PU")) != -1) {
2020-09-21 00:31:33 +02:00
switch (c) {
case 'h':
usage(argv[0]);
return 0;
2020-09-21 21:37:24 +02:00
case 'j':
if (strcmp(optarg, "fail") == 0) {
browser.tofu_mode = TOFU_FAIL;
} else if (strcmp(optarg, "once") == 0) {
browser.tofu_mode = TOFU_TRUST_ONCE;
} else if (strcmp(optarg, "always") == 0) {
browser.tofu_mode = TOFU_TRUST_ALWAYS;
} else {
usage(argv[0]);
return 1;
}
break;
2020-09-21 02:11:48 +02:00
case 'P':
browser.pagination = false;
break;
case 'U':
browser.unicode = false;
break;
2020-09-21 00:31:33 +02:00
default:
fprintf(stderr, "fatal: unknown flag %c\n", c);
return 1;
}
}
if (optind == argc - 1) {
if (!set_url(&browser, argv[optind], &browser.history)) {
return 1;
}
2020-09-21 00:41:30 +02:00
} else {
2020-09-21 00:31:33 +02:00
usage(argv[0]);
return 1;
}
SSL_load_error_strings();
ERR_load_crypto_strings();
2020-09-21 01:33:43 +02:00
browser.opts.ssl_ctx = SSL_CTX_new(TLS_method());
2020-09-21 21:37:24 +02:00
gemini_tofu_init(&browser.tofu, browser.opts.ssl_ctx,
&tofu_callback, &browser);
2020-09-21 00:31:33 +02:00
struct gemini_response resp;
2020-09-21 02:01:31 +02:00
browser.running = true;
while (browser.running) {
2020-09-21 00:31:33 +02:00
static char prompt[4096];
2020-09-21 02:01:31 +02:00
if (do_requests(&browser, &resp)) {
// Skip prompts
goto next;
}
2020-09-21 00:31:33 +02:00
2020-09-21 02:01:31 +02:00
snprintf(prompt, sizeof(prompt), "\n%s at %s\n"
2020-09-21 03:42:27 +02:00
"[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n"
2020-09-21 01:09:55 +02:00
"=> ",
resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "",
browser.plain_url,
browser.history->prev ? "[b]ack; " : "",
browser.history->next ? "[f]orward; " : "");
2020-09-21 00:31:33 +02:00
gemini_response_finish(&resp);
2020-09-21 02:01:31 +02:00
enum prompt_result result = PROMPT_AGAIN;
while (result == PROMPT_AGAIN || result == PROMPT_MORE) {
result = do_prompts(prompt, &browser);
}
switch (result) {
case PROMPT_AGAIN:
case PROMPT_MORE:
assert(0);
case PROMPT_QUIT:
browser.running = false;
break;
case PROMPT_ANSWERED:
2020-09-21 04:09:26 +02:00
case PROMPT_NEXT:
2020-09-21 02:01:31 +02:00
break;
}
2020-09-21 01:01:14 +02:00
2020-09-21 02:01:31 +02:00
next:;
2020-09-21 01:33:43 +02:00
struct link *link = browser.links;
2020-09-21 01:01:14 +02:00
while (link) {
struct link *next = link->next;
free(link->url);
free(link);
link = next;
}
2020-09-21 01:33:43 +02:00
browser.links = NULL;
2020-09-21 00:31:33 +02:00
}
2020-09-21 01:33:43 +02:00
history_free(browser.history);
SSL_CTX_free(browser.opts.ssl_ctx);
curl_url_cleanup(browser.url);
2020-09-21 00:31:33 +02:00
return 0;
}