Initial commit

This commit is contained in:
styan 2020-04-29 05:03:38 +00:00
commit f8f55b167c
5 changed files with 1062 additions and 0 deletions

19
Makefile Normal file
View File

@ -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

55
gemini-cat.1 Normal file
View File

@ -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

132
gemini-cat.c Normal file
View File

@ -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);
}

782
libgeminiclient.c Normal file
View File

@ -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);
}

74
libgeminiclient.h Normal file
View File

@ -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