2020-09-20 19:06:34 +02:00
|
|
|
#include <assert.h>
|
2020-09-20 19:30:28 +02:00
|
|
|
#include <errno.h>
|
2020-09-20 19:06:34 +02:00
|
|
|
#include <getopt.h>
|
2020-09-20 20:21:03 +02:00
|
|
|
#include <netdb.h>
|
2020-09-20 19:30:28 +02:00
|
|
|
#include <openssl/bio.h>
|
2020-09-20 19:06:34 +02:00
|
|
|
#include <openssl/err.h>
|
|
|
|
#include <stdbool.h>
|
2020-09-20 16:17:39 +02:00
|
|
|
#include <stdio.h>
|
2020-09-20 19:06:34 +02:00
|
|
|
#include <stdlib.h>
|
2020-09-20 19:30:28 +02:00
|
|
|
#include <string.h>
|
2020-09-20 20:21:03 +02:00
|
|
|
#include <sys/socket.h>
|
|
|
|
#include <sys/types.h>
|
2020-09-20 19:30:28 +02:00
|
|
|
#include <unistd.h>
|
2020-09-20 19:06:34 +02:00
|
|
|
#include "client.h"
|
|
|
|
|
|
|
|
static void
|
|
|
|
usage(char *argv_0)
|
|
|
|
{
|
|
|
|
fprintf(stderr,
|
2020-09-20 20:41:03 +02:00
|
|
|
"usage: %s [-46lLiIN] [-E cert] [-d input] [-D path] gemini://...\n",
|
2020-09-20 19:06:34 +02:00
|
|
|
argv_0);
|
|
|
|
}
|
2020-09-20 16:17:39 +02:00
|
|
|
|
|
|
|
int
|
2020-09-20 19:06:34 +02:00
|
|
|
main(int argc, char *argv[])
|
|
|
|
{
|
2020-09-20 19:30:28 +02:00
|
|
|
enum header_mode {
|
|
|
|
OMIT_HEADERS,
|
|
|
|
SHOW_HEADERS,
|
|
|
|
ONLY_HEADERS,
|
|
|
|
};
|
2020-09-20 20:09:45 +02:00
|
|
|
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;
|
2020-09-20 20:18:40 +02:00
|
|
|
bool follow_redirects = false, linefeed = true;
|
2020-09-20 20:21:03 +02:00
|
|
|
struct addrinfo hints = {0};
|
|
|
|
struct gemini_options opts = {
|
|
|
|
.hints = &hints,
|
|
|
|
};
|
2020-09-20 19:06:34 +02:00
|
|
|
|
|
|
|
int c;
|
2020-09-20 20:41:03 +02:00
|
|
|
while ((c = getopt(argc, argv, "46d:D:E:hlLiIN")) != -1) {
|
2020-09-20 19:06:34 +02:00
|
|
|
switch (c) {
|
|
|
|
case '4':
|
2020-09-20 20:21:03 +02:00
|
|
|
hints.ai_family = AF_INET;
|
2020-09-20 19:06:34 +02:00
|
|
|
break;
|
|
|
|
case '6':
|
2020-09-20 20:21:03 +02:00
|
|
|
hints.ai_family = AF_INET6;
|
2020-09-20 19:06:34 +02:00
|
|
|
break;
|
|
|
|
case 'd':
|
2020-09-20 20:09:45 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2020-09-20 19:06:34 +02:00
|
|
|
break;
|
2020-09-20 20:41:03 +02:00
|
|
|
case 'E':
|
|
|
|
assert(0); // TODO: Client certificates
|
|
|
|
break;
|
2020-09-20 19:06:34 +02:00
|
|
|
case 'h':
|
|
|
|
usage(argv[0]);
|
|
|
|
return 0;
|
2020-09-20 20:13:18 +02:00
|
|
|
case 'l':
|
|
|
|
linefeed = false;
|
|
|
|
break;
|
2020-09-20 19:06:34 +02:00
|
|
|
case 'L':
|
2020-09-20 20:18:40 +02:00
|
|
|
follow_redirects = true;
|
2020-09-20 19:30:28 +02:00
|
|
|
break;
|
|
|
|
case 'i':
|
2020-09-20 20:09:45 +02:00
|
|
|
header_mode = SHOW_HEADERS;
|
2020-09-20 19:06:34 +02:00
|
|
|
break;
|
|
|
|
case 'I':
|
2020-09-20 20:09:45 +02:00
|
|
|
header_mode = ONLY_HEADERS;
|
|
|
|
input_mode = INPUT_SUPPRESS;
|
|
|
|
break;
|
|
|
|
case 'N':
|
|
|
|
input_mode = INPUT_SUPPRESS;
|
2020-09-20 19:06:34 +02:00
|
|
|
break;
|
|
|
|
default:
|
2020-09-20 20:13:18 +02:00
|
|
|
fprintf(stderr, "fatal: unknown flag %c\n", c);
|
2020-09-20 19:06:34 +02:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
}
|
2020-09-20 19:30:28 +02:00
|
|
|
|
2020-09-20 19:06:34 +02:00
|
|
|
if (optind != argc - 1) {
|
|
|
|
usage(argv[0]);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
SSL_load_error_strings();
|
|
|
|
ERR_load_crypto_strings();
|
|
|
|
|
2020-09-20 20:09:45 +02:00
|
|
|
bool exit = false;
|
|
|
|
char *url = strdup(argv[optind]);
|
|
|
|
|
|
|
|
int ret = 0;
|
|
|
|
while (!exit) {
|
|
|
|
struct gemini_response resp;
|
2020-09-20 20:21:03 +02:00
|
|
|
enum gemini_result r = gemini_request(url, &opts, &resp);
|
2020-09-20 20:09:45 +02:00
|
|
|
if (r != GEMINI_OK) {
|
|
|
|
fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp));
|
|
|
|
ret = (int)r;
|
|
|
|
exit = true;
|
|
|
|
goto next;
|
|
|
|
}
|
|
|
|
|
|
|
|
char *new_url, *input = NULL;
|
|
|
|
switch (resp.status / 10) {
|
|
|
|
case 1: // INPUT
|
|
|
|
if (input_mode == INPUT_SUPPRESS) {
|
|
|
|
exit = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (fileno(input_source) != -1 &&
|
|
|
|
isatty(fileno(input_source))) {
|
|
|
|
fprintf(stderr, "%s: ", resp.meta);
|
|
|
|
}
|
2020-09-20 19:30:28 +02:00
|
|
|
|
2020-09-20 20:09:45 +02:00
|
|
|
size_t s = 0;
|
|
|
|
ssize_t n = getline(&input, &s, input_source);
|
2020-09-20 19:30:28 +02:00
|
|
|
if (n == -1) {
|
2020-09-20 20:09:45 +02:00
|
|
|
fprintf(stderr, "Error reading input: %s\n",
|
|
|
|
feof(input_source) ? "EOF" :
|
|
|
|
strerror(ferror(input_source)));
|
|
|
|
r = 1;
|
|
|
|
exit = true;
|
|
|
|
break;
|
2020-09-20 19:30:28 +02:00
|
|
|
}
|
2020-09-20 20:09:45 +02:00
|
|
|
input[n - 1] = '\0'; // Drop LF
|
|
|
|
|
|
|
|
new_url = gemini_input_url(url, input);
|
|
|
|
free(url);
|
|
|
|
url = new_url;
|
2020-09-20 21:14:54 +02:00
|
|
|
assert(url);
|
2020-09-20 20:09:45 +02:00
|
|
|
goto next;
|
|
|
|
case 3: // REDIRECT
|
2020-09-20 20:18:40 +02:00
|
|
|
free(url);
|
|
|
|
url = strdup(resp.meta);
|
|
|
|
if (!follow_redirects) {
|
|
|
|
if (header_mode == OMIT_HEADERS) {
|
|
|
|
fprintf(stderr, "REDIRECT: %d %s\n",
|
|
|
|
resp.status, resp.meta);
|
|
|
|
}
|
|
|
|
exit = true;
|
|
|
|
}
|
|
|
|
goto next;
|
2020-09-20 20:09:45 +02:00
|
|
|
case 6: // CLIENT CERTIFICATE REQUIRED
|
|
|
|
assert(0); // TODO
|
|
|
|
case 4: // TEMPORARY FAILURE
|
|
|
|
case 5: // PERMANENT FAILURE
|
|
|
|
if (header_mode == OMIT_HEADERS) {
|
|
|
|
fprintf(stderr, "%s: %d %s\n",
|
|
|
|
resp.status / 10 == 4 ?
|
|
|
|
"TEMPORARY FAILURE" : "PERMANENT FALIURE",
|
|
|
|
resp.status, resp.meta);
|
|
|
|
}
|
|
|
|
exit = true;
|
|
|
|
break;
|
|
|
|
case 2: // 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 (resp.status / 10 != 2) {
|
|
|
|
break;
|
|
|
|
}
|
2020-09-20 20:18:26 +02:00
|
|
|
char last;
|
2020-09-20 20:13:18 +02:00
|
|
|
char buf[BUFSIZ];
|
2020-09-20 20:18:26 +02:00
|
|
|
for (int n = 1; n > 0;) {
|
2020-09-20 20:09:45 +02:00
|
|
|
n = BIO_read(resp.bio, buf, BUFSIZ);
|
|
|
|
if (n == -1) {
|
|
|
|
fprintf(stderr, "Error: read\n");
|
2020-09-20 19:30:28 +02:00
|
|
|
return 1;
|
2020-09-20 20:18:26 +02:00
|
|
|
} else if (n != 0) {
|
|
|
|
last = buf[n - 1];
|
2020-09-20 19:30:28 +02:00
|
|
|
}
|
2020-09-20 20:09:45 +02:00
|
|
|
ssize_t w = 0;
|
|
|
|
while (w < (ssize_t)n) {
|
|
|
|
ssize_t x = write(STDOUT_FILENO, &buf[w], n - w);
|
|
|
|
if (x == -1) {
|
|
|
|
fprintf(stderr, "Error: write: %s\n",
|
|
|
|
strerror(errno));
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
w += x;
|
|
|
|
}
|
2020-09-20 19:30:28 +02:00
|
|
|
}
|
2020-09-20 20:13:18 +02:00
|
|
|
if (strncmp(resp.meta, "text/", 5) == 0
|
2020-09-20 20:37:43 +02:00
|
|
|
&& linefeed && last != '\n'
|
|
|
|
&& isatty(STDOUT_FILENO)) {
|
2020-09-20 20:13:18 +02:00
|
|
|
printf("\n");
|
|
|
|
}
|
2020-09-20 20:09:45 +02:00
|
|
|
break;
|
2020-09-20 19:30:28 +02:00
|
|
|
}
|
2020-09-20 20:09:45 +02:00
|
|
|
|
|
|
|
next:
|
|
|
|
gemini_response_finish(&resp);
|
2020-09-20 19:06:34 +02:00
|
|
|
}
|
|
|
|
|
2020-09-20 20:09:45 +02:00
|
|
|
free(url);
|
|
|
|
return ret;
|
2020-09-20 16:17:39 +02:00
|
|
|
}
|