Initial commit
This commit is contained in:
commit
f8f55b167c
|
@ -0,0 +1,19 @@
|
|||
.POSIX:
|
||||
default: all
|
||||
all: libgeminiclient.a gemini-cat
|
||||
libgeminiclient.a: libgeminiclient.c libgeminiclient.h
|
||||
gemini-cat: gemini-cat.c libgeminiclient.a
|
||||
${CC} -I. ${CFLAGS} ${LFLAGS} -ltls -o $@ gemini-cat.c libgeminiclient.a
|
||||
install: libgeminiclient.a
|
||||
install -m444 libgeminiclient.a \
|
||||
"$$DESTDIR/$${PREFIX:-/usr/local}/lib/"
|
||||
install -m444 libgeminiclient.h \
|
||||
"$$DESTDIR/$${PREFIX:-/usr/local}/include/"
|
||||
install-gemini-cat: gemini-cat
|
||||
install -m755 gemini-cat "$$DESTDIR/$${PREFIX:-/usr/local}/bin/"
|
||||
install -m444 gemini-cat.1 \
|
||||
"$$DESTDIR/$${PREFIX:-/usr/local}/$${MANDIR:-share/man}/man1/"
|
||||
linstall-gemini-cat: gemini-cat
|
||||
install -m755 gemini-cat "$$HOME/bin/"
|
||||
clean:
|
||||
rm -f libgeminiclient.a gemini-cat
|
|
@ -0,0 +1,55 @@
|
|||
.Dd Apr 24, 2020
|
||||
.Dt GEMINI-CAT 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm gemini-cat
|
||||
.Nd concatnate responses from a Gemini URLs
|
||||
.Sh SYNOPSIS
|
||||
.Nm
|
||||
.Op Fl RSisw
|
||||
.Op Fl c Ar cert-file
|
||||
.Op Fl k Ar key-file
|
||||
.Op Fl p Ar port
|
||||
.Op Fl r Ar max-redirects
|
||||
.Op Fl t Ar tofu-file
|
||||
.Ar URL ...
|
||||
.Sh DESCRIPTION
|
||||
Concatnate the responses from tha
|
||||
.Ar URL
|
||||
arguments obtained by using the Gemini portocol.
|
||||
The options are as follows:
|
||||
.Bl -tag -width Ds
|
||||
.It Fl R
|
||||
Output the raw file, the default for non-TTY outputs.
|
||||
.It Fl S
|
||||
Strip out non-space control-characters before writing, the default for
|
||||
TTY outputs.
|
||||
.It Fl c Ar cert-file
|
||||
The certificate file to use for TLS.
|
||||
.It Fl i
|
||||
Interactive mode (allow prompts).
|
||||
.It Fl k Ar key-file
|
||||
The key file to use for TLS.
|
||||
.It Fl p Ar port
|
||||
Use
|
||||
.Ar port
|
||||
instead of the default
|
||||
.Pq 1965 .
|
||||
.It Fl r Ar max-redirects
|
||||
Set the maximum number of redirects (5 by default).
|
||||
.It Fl s
|
||||
Secure interactive mode (allow prompts, but do not echo).
|
||||
.It Fl t Ar tofu-file
|
||||
The file to read host certificate hashes from.
|
||||
.It Fl w
|
||||
Write any new hashes to the
|
||||
.Ar tofu-file .
|
||||
.El
|
||||
.Sh EXAMPLES
|
||||
Fetch the
|
||||
.Lk tilde.black
|
||||
root index:
|
||||
.Dl gemini-cat gemini://tilde.black/ | more
|
||||
.Sh SEE ALSO
|
||||
.Xr tls_config_set_cert_file 3
|
||||
.Xr tls_config_set_key_file 3
|
|
@ -0,0 +1,132 @@
|
|||
#include <ctype.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <err.h>
|
||||
#include <sysexits.h>
|
||||
|
||||
#include <tls.h>
|
||||
|
||||
#include <libgeminiclient.h>
|
||||
|
||||
#ifndef BUFFER_SIZE
|
||||
#define BUFFER_SIZE 4096
|
||||
#endif
|
||||
|
||||
static ssize_t stripcntrl(char *, size_t, const char *, size_t);
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
char b[BUFFER_SIZE];
|
||||
struct gemini gemini = GEMINI_INITIALIZER;
|
||||
ssize_t r;
|
||||
size_t i;
|
||||
int rawout;
|
||||
int c;
|
||||
|
||||
rawout = !isatty(STDOUT_FILENO);
|
||||
#ifdef __OpenBSD__
|
||||
if (pledge("stdio cpath rpath wpath flock inet dns tty", NULL)
|
||||
!= 0)
|
||||
err(EX_NOPERM, "Failed pledge(2)");
|
||||
#endif
|
||||
while ((c = getopt(argc, argv, "RSc:ik:p:r:st:w")) != -1)
|
||||
switch (c) {
|
||||
case 'R':
|
||||
rawout = 1;
|
||||
break;
|
||||
case 'S':
|
||||
rawout = 0;
|
||||
break;
|
||||
case 'c':
|
||||
gemini.certfile = optarg;
|
||||
break;
|
||||
case 'i':
|
||||
gemini.flags |= GEMINI_PROMPT;
|
||||
break;
|
||||
case 'k':
|
||||
gemini.keyfile = optarg;
|
||||
break;
|
||||
case 'p':
|
||||
for (i = 0, gemini.port = 0;
|
||||
optarg[i] >= '0' && optarg[i] <= '9'; i++)
|
||||
gemini.port = gemini.port * 10 +
|
||||
optarg[i] - '0';
|
||||
break;
|
||||
case 'r':
|
||||
for (i = 0, gemini.maxredirects = 0;
|
||||
gemini.maxredirects <= (INT_MAX - 9) / 10 &&
|
||||
optarg[i] >= '0' && optarg[i] <= '9';
|
||||
gemini.maxredirects = gemini.maxredirects *
|
||||
10 + (optarg[i] - '0'), i++)
|
||||
/* do nothing */;
|
||||
if (i == 0 || optarg[i] != '\0')
|
||||
warnx("Invalid number: %s", optarg);
|
||||
break;
|
||||
case 's':
|
||||
gemini.flags |=
|
||||
GEMINI_PROMPT | GEMINI_SECPROMPT;
|
||||
break;
|
||||
case 't':
|
||||
gemini.tofufile = optarg;
|
||||
break;
|
||||
case 'w':
|
||||
gemini.flags |= GEMINI_TOFU_WRITE;
|
||||
break;
|
||||
default:
|
||||
goto usage;
|
||||
}
|
||||
argc -= optind;
|
||||
argv += optind;
|
||||
#ifdef __OpenBSD__
|
||||
if (!(gemini.flags & GEMINI_TOFU_WRITE) &&
|
||||
pledge("stdio rpath flock inet dns tty", NULL) != 0)
|
||||
err(EX_NOPERM, "Failed pledge(2)");
|
||||
#endif
|
||||
for (i = 0; i < argc; i++) {
|
||||
if (gemini_open(&gemini, argv[i]) != 0)
|
||||
goto err;
|
||||
#ifdef __OpenBSD__
|
||||
if (i == 0 &&
|
||||
pledge("stdio rpath flock inet dns tty", NULL) != 0)
|
||||
err(EX_NOPERM, "Failed pledge(2)");
|
||||
#endif
|
||||
while ((r = gemini_read(&gemini, b, sizeof(b))) > 0) {
|
||||
if (!rawout)
|
||||
r = stripcntrl(b, sizeof(b), b, r);
|
||||
(void)fwrite(b, 1, r, stdout);
|
||||
}
|
||||
if (r < 0)
|
||||
goto err;
|
||||
gemini_reset(&gemini);
|
||||
}
|
||||
gemini_close(&gemini);
|
||||
return (0);
|
||||
err:
|
||||
gemini_close(&gemini);
|
||||
return (EX_SOFTWARE);
|
||||
usage:
|
||||
(void)fprintf(stderr, "usage: gemini-cat [-RSisw] "
|
||||
"[-c cert-file] [-k key-file] [-p port]\n"
|
||||
" [-r max-redirects] [-t tofu-file] "
|
||||
"URL...\n");
|
||||
return (EX_USAGE);
|
||||
}
|
||||
|
||||
ssize_t
|
||||
stripcntrl(char *dst, size_t dl, const char *src, size_t sl)
|
||||
{
|
||||
size_t i;
|
||||
size_t j;
|
||||
|
||||
for (i = 0, j = 0; i < sl && j < dl; i++)
|
||||
if (!iscntrl(src[i]) || isspace(src[i]))
|
||||
dst[j++] = src[i];
|
||||
return (j);
|
||||
}
|
|
@ -0,0 +1,782 @@
|
|||
#include <assert.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <err.h>
|
||||
#include <readpassphrase.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <tls.h>
|
||||
|
||||
#include "libgeminiclient.h"
|
||||
|
||||
enum {
|
||||
GEMINI_STATE_INIT = 0,
|
||||
GEMINI_STATE_REQUEST,
|
||||
GEMINI_STATE_RESPONSE,
|
||||
GEMINI_STATE_RESPONSE_WHITESPACE,
|
||||
GEMINI_STATE_RESPONSE_META,
|
||||
GEMINI_STATE_READ,
|
||||
GEMINI_STATE_DONE,
|
||||
GEMINI_STATE_ERROR
|
||||
};
|
||||
|
||||
#ifndef GEMINI_DEFAULT_PORT
|
||||
#define GEMINI_DEFAULT_PORT 1965
|
||||
#endif
|
||||
|
||||
#define ISASCIILOWER(c) ((c) >= 'a' && (c) <= 'z')
|
||||
#define ISASCIIUPPER(c) ((c) >= 'A' && (c) <= 'Z')
|
||||
#define ISASCIIDIGIT(c) ((c) >= '0' && (c) <= '9')
|
||||
#define ISASCIIXDIGIT(c) (ISASCIIDIGIT(c) || \
|
||||
((c) >= 'A' && (c) <= 'F') || ((c) >= 'a' && (c) <= 'f'))
|
||||
#define ISASCIIALPHA(c) (ISASCIILOWER(c) || ISASCIIUPPER(c))
|
||||
#define ISASCIIALNUM(c) (ISASCIIALPHA(c) || ISASCIIDIGIT(c))
|
||||
|
||||
#define ISASCIILOWER(c) ((c) >= 'a' && (c) <= 'z')
|
||||
#define ISASCIIUPPER(c) ((c) >= 'A' && (c) <= 'Z')
|
||||
#define ISASCIIDIGIT(c) ((c) >= '0' && (c) <= '9')
|
||||
#define ISASCIIXDIGIT(c) (ISASCIIDIGIT(c) || \
|
||||
((c) >= 'A' && (c) <= 'F') || ((c) >= 'a' && (c) <= 'f'))
|
||||
#define ISASCIIALPHA(c) (ISASCIILOWER(c) || ISASCIIUPPER(c))
|
||||
#define ISASCIIALNUM(c) (ISASCIIALPHA(c) || ISASCIIDIGIT(c))
|
||||
|
||||
#define GET_HOSTNAME_PORT 0x01
|
||||
#define GET_HOSTNAME_VALID 0x02
|
||||
|
||||
static int gemini_bounce(struct gemini *, const char *);
|
||||
static int set_tofu(struct gemini_tofu **, const char *,
|
||||
const char *, time_t);
|
||||
static int load_tofu(struct gemini_tofu **, int);
|
||||
static int check_tofu(struct gemini_tofu *, const char *,
|
||||
const char *);
|
||||
static int write_tofu(struct gemini_tofu *, int);
|
||||
static void free_tofu(struct gemini_tofu *);
|
||||
static size_t escapequery(char *, size_t, const char *, size_t);
|
||||
static size_t get_hostname(char *, size_t, const char *, size_t, int);
|
||||
|
||||
void
|
||||
gemini_init(struct gemini *g)
|
||||
{
|
||||
|
||||
g->tls = NULL;
|
||||
g->tls_config = NULL;
|
||||
g->tofu = NULL;
|
||||
g->keymem = NULL;
|
||||
g->certmem = NULL;
|
||||
g->keyfile = NULL;
|
||||
g->certfile = NULL;
|
||||
g->metalen = 0;
|
||||
g->reqlen = 0;
|
||||
g->keylen = 0;
|
||||
g->certlen = 0;
|
||||
g->index = 0;
|
||||
g->tofufd = -1;
|
||||
g->tofumod = 0;
|
||||
g->flags = 0;
|
||||
g->state = GEMINI_STATE_INIT;
|
||||
g->redirects = 0;
|
||||
g->maxredirects = GEMINI_REDIRECT_MAX;
|
||||
g->status = 0;
|
||||
g->port = 0;
|
||||
}
|
||||
|
||||
void
|
||||
gemini_reset(struct gemini *g)
|
||||
{
|
||||
|
||||
if (g->tls_config != NULL) {
|
||||
tls_config_free(g->tls_config);
|
||||
g->tls_config = NULL;
|
||||
}
|
||||
if (g->tls) {
|
||||
tls_free(g->tls);
|
||||
g->tls = NULL;
|
||||
}
|
||||
g->tls = NULL;
|
||||
g->tls_config = NULL;
|
||||
g->metalen = 0;
|
||||
g->reqlen = 0;
|
||||
g->index = 0;
|
||||
g->state = GEMINI_STATE_INIT;
|
||||
g->redirects = 0;
|
||||
g->status = 0;
|
||||
}
|
||||
|
||||
struct gemini *
|
||||
gemini_create(void)
|
||||
{
|
||||
struct gemini *g;
|
||||
|
||||
if ((g = malloc(sizeof(*g))) == NULL)
|
||||
return (NULL);
|
||||
gemini_init(g);
|
||||
return (g);
|
||||
}
|
||||
|
||||
int
|
||||
gemini_open(struct gemini *g, const char *url)
|
||||
{
|
||||
char portstr[16];
|
||||
const char *hash;
|
||||
char *portptr;
|
||||
size_t urllen;
|
||||
size_t i;
|
||||
size_t j;
|
||||
int e;
|
||||
short port;
|
||||
|
||||
if (g->port == 0)
|
||||
g->port = GEMINI_DEFAULT_PORT;
|
||||
if (g->state != GEMINI_STATE_INIT || g->port < 0) {
|
||||
errno = EINVAL;
|
||||
return (-1);
|
||||
}
|
||||
if ((urllen = strlen(url)) > GEMINI_URL_MAX) {
|
||||
errno = ENAMETOOLONG;
|
||||
return (-1);
|
||||
}
|
||||
/* The request is simply: $URL\r\n */
|
||||
g->reqlen = urllen + 2;
|
||||
/* Create the `port' string */
|
||||
for (port = g->port, i = 14; port > 0; i--, port /= 10)
|
||||
portstr[i] = port % 10 + '0';
|
||||
portstr[15] = '\0';
|
||||
portptr = portstr + i + 1;
|
||||
/* Extract the `hostname' */
|
||||
if ((i = get_hostname(g->request, GEMINI_HOSTNAME_MAX + 1,
|
||||
url, urllen, GET_HOSTNAME_PORT | GET_HOSTNAME_VALID))
|
||||
== 0 || i > GEMINI_URL_MAX) {
|
||||
errno = EINVAL;
|
||||
return (-1);
|
||||
}
|
||||
/* Initialize TLS */
|
||||
if ((g->tls_config = tls_config_new()) == NULL)
|
||||
return (-1);
|
||||
tls_config_insecure_noverifycert(g->tls_config);
|
||||
tls_config_insecure_noverifyname(g->tls_config);
|
||||
tls_config_insecure_noverifytime(g->tls_config);
|
||||
if (g->keyfile != NULL && g->keymem == NULL)
|
||||
g->keymem = tls_load_file(g->keyfile, &g->keylen, NULL);
|
||||
if (g->certfile != NULL && g->certmem == NULL)
|
||||
g->certmem = tls_load_file(g->certfile, &g->certlen,
|
||||
NULL);
|
||||
if ((g->keymem != NULL && tls_config_set_key_mem(g->tls_config,
|
||||
g->keymem, g->keylen) != 0) || (g->certmem != NULL &&
|
||||
tls_config_set_cert_mem(g->tls_config, g->certmem,
|
||||
g->certlen) != 0))
|
||||
warnx("TLS-Config Error: %s",
|
||||
tls_config_error(g->tls_config));
|
||||
if ((g->tls = tls_client()) == NULL) {
|
||||
tls_config_free(g->tls_config);
|
||||
g->tls_config = NULL;
|
||||
return (-1);
|
||||
}
|
||||
/* Connect */
|
||||
if (tls_configure(g->tls, g->tls_config) != 0 ||
|
||||
tls_connect(g->tls, g->request, portptr) != 0 ||
|
||||
tls_handshake(g->tls)) {
|
||||
warnx("TLS Error: %s", tls_error(g->tls));
|
||||
goto err;
|
||||
}
|
||||
if (g->tofufile != NULL) {
|
||||
if ((hash = tls_peer_cert_hash(g->tls)) == NULL)
|
||||
goto err;
|
||||
if (g->tofufd < 0 && (g->tofufd = open(g->tofufile,
|
||||
((g->flags & GEMINI_TOFU_WRITE) ?
|
||||
(O_RDWR | O_CREAT) : O_RDONLY) |
|
||||
O_EXLOCK, 0600)) < 0 && errno != ENOENT) {
|
||||
warn("Could not open: %s", g->tofufile);
|
||||
goto err;
|
||||
}
|
||||
if (g->tofu == NULL && g->tofufd >= 0 &&
|
||||
load_tofu(&g->tofu, g->tofufd) < 0) {
|
||||
(void)close(g->tofufd);
|
||||
g->tofufd = -1;
|
||||
warn("Could not load: %s", g->tofufile);
|
||||
goto err;
|
||||
}
|
||||
if (g->tofu != NULL) {
|
||||
if (!check_tofu(g->tofu, g->request, hash))
|
||||
goto err;
|
||||
if ((e = set_tofu(&g->tofu, g->request, hash,
|
||||
tls_peer_cert_notafter(g->tls))) < 0)
|
||||
goto err;
|
||||
if (e > 0)
|
||||
g->tofumod = 1;
|
||||
}
|
||||
}
|
||||
/* Construct the request */
|
||||
(void)memcpy(g->request, url, urllen);
|
||||
g->request[g->reqlen - 2] = '\r';
|
||||
g->request[g->reqlen - 1] = '\n';
|
||||
g->state = GEMINI_STATE_REQUEST;
|
||||
return (0);
|
||||
err:
|
||||
gemini_reset(g);
|
||||
return (-1);
|
||||
}
|
||||
|
||||
void
|
||||
gemini_close(struct gemini *g)
|
||||
{
|
||||
|
||||
if ((g->flags & GEMINI_TOFU_WRITE) && g->tofufd >= 0 &&
|
||||
g->tofumod && (lseek(g->tofufd, 0, SEEK_SET) < 0 ||
|
||||
write_tofu(g->tofu, g->tofufd) != 0 ||
|
||||
ftruncate(g->tofufd, lseek(g->tofufd, 0, SEEK_CUR)) != 0))
|
||||
warn("Could not write: %s", g->tofufile);
|
||||
if (g->tofu != NULL) {
|
||||
free_tofu(g->tofu);
|
||||
g->tofu = NULL;
|
||||
}
|
||||
if (g->tofufd >= 0) {
|
||||
(void)close(g->tofufd);
|
||||
g->tofufd = -1;
|
||||
}
|
||||
g->tofumod = 0;
|
||||
if (g->keymem != NULL) {
|
||||
free(g->keymem);
|
||||
g->keymem = NULL;
|
||||
g->keylen = 0;
|
||||
}
|
||||
if (g->certmem != NULL) {
|
||||
free(g->certmem);
|
||||
g->certmem = NULL;
|
||||
g->certlen = 0;
|
||||
}
|
||||
gemini_reset(g);
|
||||
}
|
||||
|
||||
void
|
||||
gemini_destroy(struct gemini *g)
|
||||
{
|
||||
|
||||
gemini_destroy(g);
|
||||
free(g);
|
||||
}
|
||||
|
||||
ssize_t
|
||||
gemini_read(struct gemini *g, void *b, size_t bs)
|
||||
{
|
||||
char *cb;
|
||||
ssize_t r;
|
||||
size_t i = 0;
|
||||
size_t j;
|
||||
int c;
|
||||
|
||||
cb = b;
|
||||
if (g->state == GEMINI_STATE_ERROR)
|
||||
return (-1);
|
||||
if (g->state == GEMINI_STATE_DONE)
|
||||
return (0);
|
||||
if (g->state == GEMINI_STATE_REQUEST) {
|
||||
if (tls_write(g->tls, g->request, g->reqlen)
|
||||
!= g->reqlen)
|
||||
goto errt;
|
||||
g->status = 0;
|
||||
g->index = 0;
|
||||
g->state = GEMINI_STATE_RESPONSE;
|
||||
}
|
||||
for (;;) {
|
||||
i = i != r ? i : 0;
|
||||
if (i == 0 && (r = tls_read(g->tls, b, bs)) < 0)
|
||||
goto errt;
|
||||
if (r == 0)
|
||||
goto eof;
|
||||
switch (g->state) {
|
||||
case GEMINI_STATE_RESPONSE:
|
||||
/* This is (probably) over-engineered. */
|
||||
for (; i < r && g->index < 2; i++, g->index++) {
|
||||
if (!ISASCIIDIGIT(cb[i]))
|
||||
goto errinval;
|
||||
g->status =
|
||||
g->status * 10 + (cb[i] - '0');
|
||||
}
|
||||
if (g->index < 2)
|
||||
break;
|
||||
g->state = GEMINI_STATE_RESPONSE_WHITESPACE;
|
||||
break;
|
||||
case GEMINI_STATE_RESPONSE_WHITESPACE:
|
||||
/* The unspecified maximum is kind-of absurd. */
|
||||
for (; i < r &&
|
||||
(cb[i] == ' ' || cb[i] == '\t'); i++)
|
||||
/* do nothing */;
|
||||
if (i == r)
|
||||
break;
|
||||
g->metalen = 0;
|
||||
g->state = GEMINI_STATE_RESPONSE_META;
|
||||
break;
|
||||
case GEMINI_STATE_RESPONSE_META:
|
||||
/*
|
||||
* If the white-space had a reasonable maximum
|
||||
* then this would be easier.
|
||||
*/
|
||||
if (i == 0 && g->meta[g->metalen - 1] == '\r' &&
|
||||
cb[0] == '\n') {
|
||||
i++;
|
||||
g->meta[--g->metalen] = '\0';
|
||||
} else if (i == 0 && cb[0] == '\r' &&
|
||||
cb[1] == '\n') {
|
||||
i += 2;
|
||||
} else {
|
||||
for (j = i + (i == 0);
|
||||
j < r && (cb[j] != '\n' ||
|
||||
cb[j - 1] != '\r'); j++)
|
||||
/* do nothing */;
|
||||
j -= j < r;
|
||||
if (g->metalen + j - i >
|
||||
GEMINI_META_MAX + j == r)
|
||||
goto errinval;
|
||||
(void)memcpy(g->meta + g->metalen,
|
||||
cb + i, j - i);
|
||||
g->meta[g->metalen += j - i] = '\0';
|
||||
if (j == r) {
|
||||
i = j;
|
||||
break;
|
||||
}
|
||||
i = j + 2;
|
||||
}
|
||||
switch (g->status / 10) {
|
||||
case 0:
|
||||
goto errinval;
|
||||
case 1:
|
||||
r = -1 /* Do not refill the buffer */;
|
||||
if (g->flags & GEMINI_PROMPT)
|
||||
c = RPP_ECHO_ON;
|
||||
else if (g->flags & GEMINI_SECPROMPT)
|
||||
c = RPP_ECHO_OFF;
|
||||
else
|
||||
goto errunsup;
|
||||
/* Remove any control-characters */
|
||||
for (i = 0, j = 0; i < g->metalen;
|
||||
i++)
|
||||
if (!iscntrl(g->meta[i]))
|
||||
g->meta[j++] =
|
||||
g->meta[i];
|
||||
for (i = 0; i < g->reqlen &&
|
||||
g->request[i] != '?'; i++)
|
||||
/* do nothing */;
|
||||
if (i == g->reqlen)
|
||||
g->request[i -= 2] = '?';
|
||||
cb = g->request + ++i;
|
||||
j = GEMINI_URL_MAX - i;
|
||||
if (readpassphrase(g->meta, cb, j, c)
|
||||
== NULL)
|
||||
goto err;
|
||||
/*
|
||||
* readpassphrase(3) already truncates
|
||||
* the query, so there is no reason to
|
||||
* prevent the escape function from
|
||||
* doing so as well.
|
||||
*/
|
||||
(void)escapequery(cb, j, cb,
|
||||
strlen(cb));
|
||||
g->request[GEMINI_URL_MAX] = '\0';
|
||||
if (gemini_bounce(g, g->request) != 0)
|
||||
goto err;
|
||||
return (gemini_read(g, b, bs));
|
||||
case 2:
|
||||
g->state = GEMINI_STATE_READ;
|
||||
break;
|
||||
case 3:
|
||||
if (g->redirects >= g->maxredirects)
|
||||
goto errredir;
|
||||
g->redirects++;
|
||||
if (gemini_bounce(g, g->meta) != 0)
|
||||
goto err;
|
||||
return (gemini_read(g, b, bs));
|
||||
case 4:
|
||||
goto errg;
|
||||
case 5:
|
||||
goto errg;
|
||||
case 6:
|
||||
goto errcert;
|
||||
case 7:
|
||||
goto errinval;
|
||||
case 8:
|
||||
goto errinval;
|
||||
case 9:
|
||||
goto errinval;
|
||||
default:
|
||||
abort(); /* unreachable */
|
||||
}
|
||||
break;
|
||||
case GEMINI_STATE_READ:
|
||||
if (i > 0) {
|
||||
(void)memmove(cb, cb + i, r - i);
|
||||
r -= i;
|
||||
}
|
||||
return (r);
|
||||
default:
|
||||
abort(); /* unreachable */;
|
||||
}
|
||||
}
|
||||
eof:
|
||||
if (g->state != GEMINI_STATE_READ) {
|
||||
warnx("Unexpected EOF");
|
||||
goto err;
|
||||
}
|
||||
g->state = GEMINI_STATE_DONE;
|
||||
return (0);
|
||||
errt:
|
||||
warnx("TLS Error: %s", tls_error(g->tls));
|
||||
goto err;
|
||||
errg:
|
||||
warnx("Gemini Error %02hd: %s", g->status, g->meta);
|
||||
goto err;
|
||||
errr:
|
||||
warnx("Too many redirects");
|
||||
goto err;
|
||||
errcert:
|
||||
warnx("Certificate Error %02hd: %s", g->status, g->meta);
|
||||
goto err;
|
||||
errinval:
|
||||
warnx("Invalid response");
|
||||
goto err;
|
||||
errunsup:
|
||||
warnx("Unsupported status: %02hd", g->status);
|
||||
goto err;
|
||||
errredir:
|
||||
warnx("Redirect %02hd: %s", g->status, g->meta);
|
||||
goto err;
|
||||
err:
|
||||
g->state = GEMINI_STATE_ERROR;
|
||||
return (-1);
|
||||
}
|
||||
|
||||
int
|
||||
gemini_bounce(struct gemini *g, const char *url)
|
||||
{
|
||||
char b[GEMINI_URL_MAX + 1];
|
||||
size_t l;
|
||||
|
||||
gemini_reset(g);
|
||||
if ((l = strlen(url)) > GEMINI_URL_MAX) {
|
||||
errno = EINVAL;
|
||||
return (-1);
|
||||
}
|
||||
(void)memcpy(b, url, l);
|
||||
b[l] = '\0';
|
||||
return (gemini_open(g, b));
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
set_tofu(struct gemini_tofu **tofu, const char *host, const char *hash,
|
||||
time_t date)
|
||||
{
|
||||
struct gemini_tofu *prev;
|
||||
struct gemini_tofu *new;
|
||||
|
||||
for (prev = NULL, new = *tofu; new != NULL;)
|
||||
for (prev = NULL, new = *tofu; new != NULL;
|
||||
prev = new, new = new->next)
|
||||
if (strcasecmp(new->host, host) == 0) {
|
||||
if (strcasecmp(new->hash, hash) == 0 &&
|
||||
difftime(new->date, date) <= 0)
|
||||
return (1);
|
||||
free(new->host);
|
||||
free(new->hash);
|
||||
if (prev == NULL)
|
||||
*tofu = new->next;
|
||||
else
|
||||
prev->next = new->next;
|
||||
free(new);
|
||||
prev = NULL;
|
||||
new = *tofu;
|
||||
break;
|
||||
}
|
||||
/* Append the new entry */
|
||||
if ((new = malloc(sizeof(*new))) == NULL)
|
||||
return (-1);
|
||||
new->next = NULL;
|
||||
new->host = strdup(host);
|
||||
new->hash = strdup(hash);
|
||||
new->date = date;
|
||||
if (prev == NULL)
|
||||
*tofu = new;
|
||||
else
|
||||
prev->next = new;
|
||||
return (0);
|
||||
}
|
||||
|
||||
int
|
||||
load_tofu(struct gemini_tofu **tofup, int fd)
|
||||
{
|
||||
char b[4096];
|
||||
struct tm tm;
|
||||
struct gemini_tofu *tofu = NULL;
|
||||
const char *host;
|
||||
const char *hash;
|
||||
char *p;
|
||||
ssize_t r;
|
||||
size_t i;
|
||||
size_t e;
|
||||
size_t o;
|
||||
|
||||
for (o = 0; o < sizeof(b);) {
|
||||
if ((r = read(fd, b + o, sizeof(b) - o)) < 0)
|
||||
goto err;
|
||||
o += r;
|
||||
if (memchr(b, '\n', o) == NULL) {
|
||||
if (r == 0)
|
||||
break;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
/* Get the host entry */
|
||||
for (i = 0, e = 0; e < o && !isblank(b[e]); e++)
|
||||
/* do nothing */;
|
||||
if (e == o) { /* blank line */
|
||||
o = 0;
|
||||
continue;
|
||||
}
|
||||
b[e] = '\0';
|
||||
host = b + i;
|
||||
/* Get the hash entry */
|
||||
for (i = e + 1; i < o && isblank(b[i]); i++)
|
||||
/* do nothing */;
|
||||
for (e = i; e < o && !isblank(b[e]); e++)
|
||||
/* do nothing */;
|
||||
b[e] = '\0';
|
||||
hash = b + i;
|
||||
/* Get the date entry */
|
||||
for (i = e + 1; i < o && isblank(b[i]); i++)
|
||||
/* do nothing */;
|
||||
for (e = i; e < o && b[e] != '\n'; e++)
|
||||
/* do nothing */;
|
||||
b[e] = '\0';
|
||||
if ((p = strptime(b + i, "%s", &tm)) == NULL ||
|
||||
*p != '\0') {
|
||||
warnx("Invalid time: %s", b + i);
|
||||
goto err;
|
||||
}
|
||||
if (set_tofu(&tofu, host, hash, timegm(&tm)) != 0)
|
||||
goto err;
|
||||
if (++e < o)
|
||||
(void)memmove(b, b + e, o - e);
|
||||
o = e >= o ? 0 : o - e;
|
||||
}
|
||||
if (o > 0)
|
||||
goto err;
|
||||
*tofup = tofu;
|
||||
return (0);
|
||||
err:
|
||||
free_tofu(tofu);
|
||||
return (-1);
|
||||
}
|
||||
|
||||
int
|
||||
check_tofu(struct gemini_tofu *tofu, const char *host, const char *hash)
|
||||
{
|
||||
time_t date;
|
||||
int r = 1;
|
||||
|
||||
date = time(NULL);
|
||||
for (; tofu != NULL; tofu = tofu->next)
|
||||
if (strcasecmp(tofu->host, host) == 0 &&
|
||||
difftime(tofu->date, date) <= 0)
|
||||
r = strcasecmp(tofu->hash, hash) == 0;
|
||||
return (r);
|
||||
}
|
||||
|
||||
int
|
||||
write_tofu(struct gemini_tofu *tofu, int fd)
|
||||
{
|
||||
char b[4096];
|
||||
struct tm *tm;
|
||||
time_t t;
|
||||
ssize_t r;
|
||||
size_t i;
|
||||
size_t l;
|
||||
int c = 0;
|
||||
|
||||
for (; tofu != NULL; tofu = tofu->next) {
|
||||
if ((l = strlen(tofu->host)) >= sizeof(b)) {
|
||||
c = -1;
|
||||
continue;
|
||||
}
|
||||
(void)memcpy(b, tofu->host, l);
|
||||
b[l++] = ' ';
|
||||
i = l;
|
||||
if ((l = strlen(tofu->hash)) >= sizeof(b) - i) {
|
||||
c = -1;
|
||||
continue;
|
||||
}
|
||||
(void)memcpy(b + i, tofu->hash, l);
|
||||
b[i + l++] = ' ';
|
||||
i += l;
|
||||
t = tofu->date;
|
||||
tm = gmtime(&t);
|
||||
if ((l = strftime(b + i, sizeof(b) - i, "%s", tm))
|
||||
== 0) {
|
||||
c = -1;
|
||||
continue;
|
||||
}
|
||||
b[i + l++] = '\n';
|
||||
for (l = i + l, i = 0; i < l &&
|
||||
(r = write(fd, b + i, l - i)) >= 0; i += r)
|
||||
/* do nothing */;
|
||||
if (r < 0) {
|
||||
c = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (c);
|
||||
}
|
||||
|
||||
void
|
||||
free_tofu(struct gemini_tofu *tofu)
|
||||
{
|
||||
struct gemini_tofu *next;
|
||||
|
||||
while (tofu != NULL) {
|
||||
free(tofu->host);
|
||||
free(tofu->hash);
|
||||
next = tofu->next;
|
||||
free(tofu);
|
||||
tofu = next;
|
||||
}
|
||||
}
|
||||
|
||||
size_t
|
||||
escapequery(char *dst, size_t dl, const char *src, size_t sl)
|
||||
{
|
||||
size_t i;
|
||||
size_t j;
|
||||
size_t r;
|
||||
int c;
|
||||
|
||||
/* Count the truncated length */
|
||||
for (i = 0, j = 0; i < sl && j < dl; i++, j++)
|
||||
if (!ISASCIIALNUM(src[i]) &&
|
||||
strchr("!$&'()*+,-.:;=@_~", src[i]) == NULL) {
|
||||
if (dl - j < 3)
|
||||
break;
|
||||
j += 2;
|
||||
}
|
||||
/* Count the total length */
|
||||
for (r = j; i < sl; i++, r++)
|
||||
if (!ISASCIIALNUM(src[i]) &&
|
||||
strchr("!$&'()*+,-.:;=@_~", src[i]) == NULL)
|
||||
r += 2;
|
||||
if (dst == NULL)
|
||||
return (r);
|
||||
/* Write backwards to prevent clobbering (src == dst) */
|
||||
if (j < dl)
|
||||
(void)memset(dst + j, '\0', dl - j);
|
||||
for (; i > 0; i--, j--)
|
||||
if (!ISASCIIALNUM(src[i - 1]) &&
|
||||
strchr("!$&'()*+,-.:;=@_~", src[i - 1]) == NULL) {
|
||||
c = src[i - 1] & 0x0F;
|
||||
c += c < 10 ? '0' : 'A' - 10;
|
||||
dst[j - 1] = c;
|
||||
c = (src[i - 1] >> 4) & 0x0F;
|
||||
c += c < 10 ? '0' : 'A' - 10;
|
||||
dst[j - 2] = c;
|
||||
dst[j - 3] = '%';
|
||||
j -= 2;
|
||||
} else
|
||||
dst[j - 1] = src[i - 1];
|
||||
return (r);
|
||||
}
|
||||
|
||||
size_t
|
||||
pctmatch(const char *s, size_t l, const char *c)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
if (c == NULL)
|
||||
c = "";
|
||||
for (i = 0; i < l; i++) {
|
||||
if (l - i >= 3 && s[i] == '%' &&
|
||||
ISASCIIXDIGIT(s[i + 1]) &&
|
||||
ISASCIIXDIGIT(s[i + 2]))
|
||||
i += 2;
|
||||
else if (!ISASCIIALNUM(s[i]) &&
|
||||
strchr(c, s[i]) == NULL)
|
||||
break;
|
||||
}
|
||||
return (i);
|
||||
}
|
||||
|
||||
size_t
|
||||
get_hostname(char *dst, size_t dl, const char *src, size_t sl, int f)
|
||||
{
|
||||
size_t hs = 0;
|
||||
size_t he;
|
||||
size_t v;
|
||||
|
||||
if (sl == 0)
|
||||
return (0);
|
||||
/* Optionally skip the `scheme:' */
|
||||
if (ISASCIIALPHA(src[0])) {
|
||||
for (he = 1; he < sl && (ISASCIIALNUM(src[he]) ||
|
||||
src[he] == '+' || src[he] == '-' ||
|
||||
src[he] == '.'); he++)
|
||||
/* do nothing */;
|
||||
if (he < sl && src[he] == ':') {
|
||||
if (strncasecmp(src + hs, "gemini", he - hs)
|
||||
!= 0)
|
||||
return (0);
|
||||
hs = he + 1;
|
||||
}
|
||||
}
|
||||
/* Optionally skip the `//' */
|
||||
if (sl - hs >= 2 && src[hs + 0] == '/' && src[hs + 1] == '/')
|
||||
hs += 2;
|
||||
/* Optionally skip the `userinfo@' */
|
||||
he = pctmatch(src + hs, sl - hs, "~$&'()*+,-.:;=_~");
|
||||
if (hs + he < sl && src[hs + he] == '@')
|
||||
hs += he + 1;
|
||||
/*
|
||||
* Process any IP-literal.
|
||||
* It is not really worth it to actually parse IPv6 strings
|
||||
* just to extract the hostname for tls_connect(3).
|
||||
* Otherwise process the `reg-name', of which IPv4 addresses
|
||||
* are a subset.
|
||||
*/
|
||||
if (hs < sl && src[hs] == '[') {
|
||||
for (he = hs + 1; he < sl && (ISASCIIALNUM(src[he]) ||
|
||||
strchr("~$&'()*+,-.:;=_~", src[he]) != NULL);
|
||||
he++)
|
||||
/* do nothing */;
|
||||
if (he >= sl || src[he] != ']')
|
||||
return (0);
|
||||
} else if ((he = hs + pctmatch(src + hs, sl - hs,
|
||||
"~$&'()*+,-.;=_~")) == hs)
|
||||
return (0);
|
||||
/* Include the `:port', if requested. */
|
||||
if ((f & GET_HOSTNAME_PORT) && he < sl && src[he] == ':')
|
||||
for (he++; he < sl && ISASCIIDIGIT(src[he]); he++)
|
||||
/* do nothing */;
|
||||
if (f & GET_HOSTNAME_VALID) {
|
||||
v = he;
|
||||
if (v < sl && src[v] == '/')
|
||||
v += 1 + pctmatch(src + v + 1, sl - v - 1,
|
||||
"~$&'()*+,-./:;=@_~");
|
||||
if (v < sl && src[v] == '?')
|
||||
v += 1 + pctmatch(src + v + 1, sl - v - 1,
|
||||
"~$&'()*+,-./:;=?@_~");
|
||||
if (v < sl && src[v] == '#')
|
||||
v += 1 + pctmatch(src + v + 1, sl - v - 1,
|
||||
"~$&'()*+,-./:;=?@_~");
|
||||
if (v != sl)
|
||||
return (0);
|
||||
}
|
||||
/* Copy the `hostname' to the destination. */
|
||||
if (he - hs < dl) {
|
||||
(void)memmove(dst, src + hs, he - hs);
|
||||
if (he - hs + 1 < dl)
|
||||
dst[he - hs] = '\0';
|
||||
}
|
||||
return (he - hs);
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
#ifndef LIBGEMINICLIENT_H
|
||||
#define LIBGEMINICLIENT_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <tls.h>
|
||||
|
||||
#ifndef GEMINI_HOSTNAME_MAX
|
||||
#define GEMINI_HOSTNAME_MAX 1024
|
||||
#endif
|
||||
#ifndef GEMINI_URL_MAX
|
||||
#define GEMINI_URL_MAX 1024
|
||||
#endif
|
||||
#ifndef GEMINI_META_MAX
|
||||
#define GEMINI_META_MAX 1024
|
||||
#endif
|
||||
#ifndef GEMINI_REDIRECT_MAX
|
||||
#define GEMINI_REDIRECT_MAX 5
|
||||
#endif
|
||||
|
||||
enum {
|
||||
GEMINI_PROMPT = 0x01,
|
||||
GEMINI_SECPROMPT = 0x02,
|
||||
GEMINI_TOFU_WRITE = 0x08
|
||||
};
|
||||
|
||||
struct gemini_tofu {
|
||||
struct gemini_tofu *next;
|
||||
char *host;
|
||||
char *hash;
|
||||
time_t date;
|
||||
};
|
||||
|
||||
#define GEMINI_INITIALIZER \
|
||||
{ { 0 }, { 0 }, NULL, NULL, NULL, NULL, NULL, NULL, \
|
||||
NULL, NULL, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, \
|
||||
GEMINI_REDIRECT_MAX, 0, 0 }
|
||||
struct gemini {
|
||||
char meta[GEMINI_META_MAX + 1];
|
||||
char request[GEMINI_URL_MAX + 2];
|
||||
struct tls *tls;
|
||||
struct tls_config *tls_config;
|
||||
struct gemini_tofu *tofu;
|
||||
uint8_t *keymem;
|
||||
uint8_t *certmem;
|
||||
const char *keyfile;
|
||||
const char *certfile;
|
||||
const char *tofufile;
|
||||
size_t metalen;
|
||||
size_t reqlen;
|
||||
size_t keylen;
|
||||
size_t certlen;
|
||||
size_t index;
|
||||
int tofufd;
|
||||
int tofumod;
|
||||
int flags;
|
||||
int state;
|
||||
int redirects;
|
||||
int maxredirects;
|
||||
short status;
|
||||
short port;
|
||||
};
|
||||
|
||||
void gemini_init(struct gemini *);
|
||||
void gemini_reset(struct gemini *);
|
||||
struct gemini *gemini_create(void);
|
||||
int gemini_open(struct gemini *, const char *);
|
||||
void gemini_close(struct gemini *);
|
||||
void gemini_destroy(struct gemini *);
|
||||
ssize_t gemini_read(struct gemini *, void *, size_t);
|
||||
|
||||
#endif
|
Loading…
Reference in New Issue