2020-09-21 00:31:33 +02:00
|
|
|
#include <assert.h>
|
|
|
|
#include <ctype.h>
|
|
|
|
#include <getopt.h>
|
|
|
|
#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"
|
|
|
|
#include "url.h"
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
FILE *tty;
|
2020-09-21 02:01:31 +02:00
|
|
|
char *plain_url;
|
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"
|
|
|
|
"\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
|
|
|
{
|
2020-09-21 04:34:27 +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));
|
2020-09-21 04:34:27 +02:00
|
|
|
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 02:01:31 +02:00
|
|
|
static enum prompt_result
|
|
|
|
do_prompts(const char *prompt, struct browser *browser)
|
|
|
|
{
|
2020-09-21 03:35:05 +02:00
|
|
|
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)) {
|
2020-09-21 03:35:05 +02:00
|
|
|
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':
|
2020-09-21 03:35:05 +02:00
|
|
|
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;
|
2020-09-21 03:35:05 +02:00
|
|
|
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");
|
2020-09-21 03:35:05 +02:00
|
|
|
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);
|
2020-09-21 03:35:05 +02:00
|
|
|
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");
|
2020-09-21 03:35:05 +02:00
|
|
|
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);
|
2020-09-21 03:35:05 +02:00
|
|
|
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;
|
2020-09-21 04:34:27 +02:00
|
|
|
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);
|
2020-09-21 04:11:13 +02:00
|
|
|
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);
|
2020-09-21 03:35:05 +02:00
|
|
|
result = PROMPT_ANSWERED;
|
|
|
|
goto exit;
|
2020-09-21 02:01:31 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-21 03:35:05 +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:
|
2020-09-21 03:35:05 +02:00
|
|
|
free(in);
|
|
|
|
return result;
|
2020-09-21 02:01:31 +02:00
|
|
|
}
|
|
|
|
|
2020-09-21 00:31:33 +02:00
|
|
|
static char *
|
|
|
|
trim_ws(char *in)
|
|
|
|
{
|
2020-09-21 03:22:40 +02:00
|
|
|
while (*in && isspace(*in)) ++in;
|
2020-09-21 00:31:33 +02:00
|
|
|
return in;
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
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 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"
|
2020-09-21 03:29:40 +02:00
|
|
|
"(more) => ", resp->meta, browser->plain_url,
|
2020-09-21 04:09:26 +02:00
|
|
|
browser->searching ? "[n]ext result; " : "",
|
2020-09-21 03:29:40 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
int
|
|
|
|
main(int argc, char *argv[])
|
|
|
|
{
|
|
|
|
struct browser browser = {
|
|
|
|
.pagination = true,
|
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 02:11:48 +02:00
|
|
|
while ((c = getopt(argc, argv, "hPU")) != -1) {
|
2020-09-21 00:31:33 +02:00
|
|
|
switch (c) {
|
|
|
|
case 'h':
|
|
|
|
usage(argv[0]);
|
|
|
|
return 0;
|
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) {
|
2020-09-21 01:33:43 +02:00
|
|
|
set_url(&browser, argv[optind], &browser.history);
|
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 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 : "",
|
2020-09-21 03:29:40 +02:00
|
|
|
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;
|
|
|
|
}
|