1
0
Fork 0
mirror of https://git.sr.ht/~sircmpwn/gmni synced 2024-05-03 22:28:57 +02:00
gmni/src/gmni.c
2021-03-04 10:59:37 -05:00

371 lines
8.0 KiB
C

#include <assert.h>
#include <bearssl_ssl.h>
#include <errno.h>
#include <getopt.h>
#include <netdb.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>
#include <gmni/gmni.h>
#include <gmni/tofu.h>
#include <gmni/url.h>
#include "util.h"
static void
usage(const char *argv_0)
{
fprintf(stderr,
"usage: %s [-46lLiIN] [-j mode] [-E cert] [-d input] [-D path] gemini://...\n",
argv_0);
}
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;
}
struct tofu_config {
struct gemini_tofu tofu;
enum tofu_action action;
};
static enum tofu_action
tofu_callback(enum tofu_error error, const char *fingerprint,
struct known_host *host, void *data)
{
struct tofu_config *cfg = (struct tofu_config *)data;
enum tofu_action action = cfg->action;
switch (error) {
case TOFU_VALID:
assert(0); // Invariant
case TOFU_INVALID_CERT:
fprintf(stderr,
"The server presented an invalid certificate with fingerprint %s.\n",
fingerprint);
if (action == TOFU_TRUST_ALWAYS) {
action = TOFU_TRUST_ONCE;
}
break;
case TOFU_UNTRUSTED_CERT:
fprintf(stderr,
"The certificate offered by this server is of unknown trust. "
"Its fingerprint is: \n"
"%s\n\n"
"Use '-j once' to trust temporarily, or '-j always' to add to the trust store.\n", fingerprint);
break;
case TOFU_FINGERPRINT_MISMATCH:
fprintf(stderr,
"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,
cfg->tofu.known_hosts_path, host->lineno);
return TOFU_FAIL;
}
if (action == TOFU_ASK) {
return TOFU_FAIL;
}
return action;
}
int
main(int argc, char *argv[])
{
enum header_mode {
OMIT_HEADERS,
SHOW_HEADERS,
ONLY_HEADERS,
};
enum header_mode header_mode = OMIT_HEADERS;
enum input_mode {
INPUT_READ,
INPUT_SUPPRESS,
};
enum input_mode input_mode = INPUT_READ;
FILE *input_source = stdin;
char *output_file = NULL;
bool follow_redirects = false, linefeed = true;
int max_redirect = 5;
struct addrinfo hints = {0};
struct gemini_options opts = {
.hints = &hints,
};
struct tofu_config cfg;
cfg.action = TOFU_ASK;
int c;
while ((c = getopt(argc, argv, "46d:D:E:hj:lLiINR:o:")) != -1) {
switch (c) {
case '4':
hints.ai_family = AF_INET;
break;
case '6':
hints.ai_family = AF_INET6;
break;
case 'd':
input_mode = INPUT_READ;
input_source = fmemopen(optarg, strlen(optarg), "r");
break;
case 'D':
input_mode = INPUT_READ;
if (strcmp(optarg, "-") == 0) {
input_source = stdin;
} else {
input_source = fopen(optarg, "r");
if (!input_source) {
fprintf(stderr, "Error: open %s: %s",
optarg, strerror(errno));
return 1;
}
}
break;
case 'E':
assert(0); // TODO: Client certificates
break;
case 'h':
usage(argv[0]);
return 0;
case 'j':
if (strcmp(optarg, "fail") == 0) {
cfg.action = TOFU_FAIL;
} else if (strcmp(optarg, "once") == 0) {
cfg.action = TOFU_TRUST_ONCE;
} else if (strcmp(optarg, "always") == 0) {
cfg.action = TOFU_TRUST_ALWAYS;
} else {
usage(argv[0]);
return 1;
}
break;
case 'l':
linefeed = false;
break;
case 'L':
follow_redirects = true;
break;
case 'i':
header_mode = SHOW_HEADERS;
break;
case 'I':
header_mode = ONLY_HEADERS;
input_mode = INPUT_SUPPRESS;
break;
case 'N':
input_mode = INPUT_SUPPRESS;
break;
case 'R':;
char *endptr;
errno = 0;
max_redirect = strtoul(optarg, &endptr, 10);
if (*endptr || errno != 0) {
fprintf(stderr, "Error: -R expects numeric argument\n");
return 1;
}
break;
case 'o':
output_file = optarg;
break;
default:
fprintf(stderr, "fatal: unknown flag %c\n", c);
return 1;
}
}
if (optind != argc - 1) {
usage(argv[0]);
return 1;
}
gemini_tofu_init(&cfg.tofu, &tofu_callback, &cfg);
bool exit = false;
struct Curl_URL *url = curl_url();
if(curl_url_set(url, CURLUPART_URL, argv[optind], 0) != CURLUE_OK) {
// TODO: Better error
fprintf(stderr, "Error: invalid URL\n");
return 1;
}
int ret = 0, nredir = 0;
while (!exit) {
char *buf;
curl_url_get(url, CURLUPART_URL, &buf, 0);
struct gemini_response resp;
enum gemini_result r = gemini_request(
buf, &opts, &cfg.tofu, &resp);
free(buf);
if (r != GEMINI_OK) {
fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp));
ret = (int)r;
exit = true;
goto next;
}
switch (gemini_response_class(resp.status)) {
case GEMINI_STATUS_CLASS_INPUT:
if (input_mode == INPUT_SUPPRESS) {
exit = true;
break;
}
char *input = get_input(&resp, input_source);
if (!input) {
r = 1;
exit = true;
break;
}
char *buf;
curl_url_get(url, CURLUPART_URL, &buf, 0);
char *new_url = gemini_input_url(buf, input);
assert(new_url);
free(input);
free(buf);
curl_url_set(url, CURLUPART_URL, new_url, 0);
goto next;
case GEMINI_STATUS_CLASS_REDIRECT:
if (++nredir >= max_redirect) {
fprintf(stderr,
"Error: maximum redirects (%d) exceeded",
max_redirect);
exit = true;
goto next;
}
curl_url_set(url, CURLUPART_URL, resp.meta, 0);
if (!follow_redirects) {
if (header_mode == OMIT_HEADERS) {
fprintf(stderr, "REDIRECT: %d %s\n",
resp.status, resp.meta);
}
exit = true;
}
goto next;
case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED:
assert(0); // TODO
case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE:
case GEMINI_STATUS_CLASS_PERMANENT_FAILURE:
if (header_mode == OMIT_HEADERS) {
fprintf(stderr, "%s: %d %s\n",
resp.status / 10 == 4 ?
"TEMPORARY FAILURE" : "PERMANENT FAILURE",
resp.status, resp.meta);
}
exit = true;
break;
case GEMINI_STATUS_CLASS_SUCCESS:
exit = true;
break;
}
switch (header_mode) {
case ONLY_HEADERS:
printf("%d %s\n", resp.status, resp.meta);
break;
case SHOW_HEADERS:
printf("%d %s\n", resp.status, resp.meta);
/* fallthrough */
case OMIT_HEADERS:
if (gemini_response_class(resp.status) !=
GEMINI_STATUS_CLASS_SUCCESS) {
break;
}
if (output_file != NULL) {
char *buf;
curl_url_get(url, CURLUPART_URL, &buf, 0);
ret = download_resp(stderr, resp, output_file, buf);
free(buf);
break;
}
char last = 0;
char buf[BUFSIZ];
for (int n = 1; n > 0;) {
n = br_sslio_read(&resp.body, buf, BUFSIZ);
if (n > 0) {
last = buf[n - 1];
}
ssize_t w = 0;
while (w < (ssize_t)n) {
ssize_t x = fwrite(&buf[w], 1, n - w, stdout);
if (ferror(stdout)) {
fprintf(stderr, "Error: write: %s\n",
strerror(errno));
return 1;
}
w += x;
}
}
if (strncmp(resp.meta, "text/", 5) == 0
&& linefeed && last != '\n'
&& isatty(STDOUT_FILENO)) {
printf("\n");
}
break;
}
next:
gemini_response_finish(&resp);
}
curl_url_cleanup(url);
gemini_tofu_finish(&cfg.tofu);
return ret;
}