libgeminiclient/libgeminiclient.c

818 lines
18 KiB
C

#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <err.h>
#include <fcntl.h>
#include <strings.h>
#include <unistd.h>
#include <tls.h>
#include "libgeminiclient.h"
enum {
GEMINI_STATE_INIT = 0,
GEMINI_STATE_REQUEST,
GEMINI_STATE_RESPONSE,
GEMINI_STATE_READ,
GEMINI_STATE_DONE,
GEMINI_STATE_ERROR
};
#ifndef GEMINI_DEFAULT_PORT
#define GEMINI_DEFAULT_PORT 1965
#endif
#define ISASCIIBLANK(c) ((c) == '\t' || (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 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);
static int setflock(int, 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->tofufile = NULL;
g->proxy = NULL;
g->meta = NULL;
g->extra = NULL;
g->reqlen = 0;
g->reslen = 0;
g->metalen = 0;
g->extralen = 0;
g->keylen = 0;
g->certlen = 0;
g->index = 0;
g->socketfd = -1;
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_envinit(struct gemini *g)
{
const char *s;
char *p;
unsigned long ul;
gemini_init(g);
g->keyfile = getenv("GEMINI_KEYFILE");
g->certfile = getenv("GEMINI_CERTFILE");
g->certfile = getenv("GEMINI_TOFUFILE");
g->proxy = getenv("GEMINI_PROXY");
if ((s = getenv("GEMINI_MAX_REDIRECTS")) != NULL) {
errno = 0;
ul = strtoul(s, &p, 0);
if (errno == 0 && (p == s || *p != '\0'))
errno = EINVAL;
if (errno == 0 && ul > INT_MAX)
errno = ERANGE;
if (errno == 0)
g->maxredirects = ul;
else
warn("GEMINI_MAX_REDIRECTS");
}
if ((s = getenv("GEMINI_PORT")) != NULL) {
errno = 0;
ul = strtoul(s, &p, 0);
if (errno == 0 && (p == s || *p != '\0'))
errno = EINVAL;
if (errno == 0 && ul > SHRT_MAX)
errno = ERANGE;
if (errno == 0)
g->port = ul;
else
warn("GEMINI_PORT");
}
}
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->meta = NULL;
g->extra = NULL;
g->reqlen = 0;
g->reslen = 0;
g->metalen = 0;
g->extralen = 0;
g->index = 0;
g->state = GEMINI_STATE_INIT;
g->status = 0;
if (g->port == 0)
g->port = GEMINI_DEFAULT_PORT;
}
struct gemini *
gemini_create(void)
{
struct gemini *g;
if ((g = malloc(sizeof(*g))) == NULL)
return (NULL);
gemini_init(g);
return (g);
}
int
gemini_connect_query(struct gemini *g, const char *url, const char *q)
{
char b[GEMINI_URL_MAX + 1];
size_t urllen;
size_t querylen;
if ((urllen = strlen(url)) > GEMINI_URL_MAX) {
errno = EINVAL;
return (-1);
}
(void)memcpy(b, url, urllen);
b[urllen] = '\0';
if (q != NULL) {
b[urllen] = '?';
querylen = strlen(q);
if (urllen == GEMINI_URL_MAX ||
escapequery(b + urllen + 1,
GEMINI_URL_MAX - urllen - 1 + 1, q, querylen) >
GEMINI_URL_MAX - urllen - 1) {
errno = EINVAL;
return (-1);
}
}
return (gemini_connect(g, b));
}
int
gemini_connect(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;
gemini_reset(g);
if (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, (g->socketfd < 0 ? GET_HOSTNAME_PORT : 0) |
GET_HOSTNAME_VALID)) == 0 || i > GEMINI_URL_MAX) {
errno = EINVAL;
return (-1);
}
/* Proxy */
if (g->proxy != NULL && strncpy(g->request, g->proxy,
sizeof(g->request)) >= g->request + sizeof(g->request)) {
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));
goto err;
}
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 ||
(g->socketfd >= 0 && tls_connect_socket(g->tls, g->socketfd,
g->request) != 0) || (g->socketfd < 0 && tls_connect(g->tls,
g->request, (strchr(g->request, ':') == NULL ? portptr :
NULL))) || (e = tls_handshake(g->tls)) == -1) {
warnx("TLS Error: %s", tls_error(g->tls));
goto err;
}
if ((hash = tls_peer_cert_hash(g->tls)) == NULL)
goto err;
if (g->tofufile != NULL) {
if (g->tofufd < 0 && (g->tofufd = open(g->tofufile,
(g->flags & GEMINI_TOFU_WRITE) ?
(O_RDWR | O_CREAT) : O_RDONLY, 0600)) < 0 &&
errno != ENOENT) {
warn("Could not open: %s", g->tofufile);
goto err;
}
if (g->tofufd >= 0 && (g->flags & GEMINI_TOFU_WRITE) &&
setflock(g->tofufd, F_WRLCK) == -1) {
(void)close(g->tofufd);
g->tofufd = -1;
warn("Could not lock the TOFU file");
goto err;
}
if (g->tofu == NULL && g->tofufd >= 0 &&
load_tofu(&g->tofu, g->tofufd) < 0) {
(void)setflock(g->tofufd, F_UNLCK);
(void)close(g->tofufd);
g->tofufd = -1;
warn("Could not load: %s", g->tofufile);
goto err;
}
}
/* The certificate against the current list */
if (g->tofu != NULL && !check_tofu(g->tofu, g->request, hash))
goto err;
/*
* Add the host's information to the list.
* Even without a file-backing this is still useful for
* long-running clients.
* open gemini://example.com
* redirect gemini://example.com/
* input gemini://example.com/?inputdata
* redirect gemini://example.com/some/path
*/
switch (set_tofu(&g->tofu, g->request, hash,
tls_peer_cert_notafter(g->tls))) {
case 0:
break;
case 1:
g->tofumod = 1;
break;
default:
goto err;
}
/* 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 (e);
err:
gemini_reset(g);
return (-1);
}
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;
if (g->state == GEMINI_STATE_ERROR)
return (-1);
if (g->state == GEMINI_STATE_DONE)
return (0);
if (g->state == GEMINI_STATE_REQUEST) {
while (g->index < g->reqlen) {
if ((r = tls_write(g->tls,
g->request + g->index,
g->reqlen - g->index)) <= 0)
goto errt;
g->index += r;
}
g->reslen = 0;
g->index = 0;
g->status = 0;
g->state = GEMINI_STATE_RESPONSE;
}
if (g->state == GEMINI_STATE_RESPONSE) {
do {
if (g->reslen >= sizeof(g->response))
goto errinval;
if ((r = tls_read(g->tls,
g->response + g->reslen,
sizeof(g->response) - g->reslen)) <= 0)
goto errt;
for (i = g->reslen, g->reslen += r;
i < g->reslen && g->response[i] != '\n';
i++)
/* do nothing */;
} while (i == g->reslen);
g->extra = g->response + i + 1;
g->extralen = g->reslen - i - 1;
g->reslen = i - (g->response[i - 1] == '\r');
g->response[g->reslen] = '\0';
if (g->reslen < 2)
goto errinval;
if (!ISASCIIDIGIT(g->response[0]) ||
!ISASCIIDIGIT(g->response[1]))
goto errinval;
if (g->reslen > 2 && !ISASCIIBLANK(g->response[2]))
goto errinval;
g->meta = g->response + (g->reslen > 2 ? 3 : 2);
g->metalen = g->reslen > 2 ? g->reslen - 3 : 0;
g->status = (g->response[0] - '0') * 10 +
(g->response[1] - '0');
if (g->response[0] != '3')
g->redirects = 0;
if (g->response[0] != '2' && g->extralen > 0)
goto errinval;
switch (g->response[0]) {
case '0':
goto errinval;
case '1':
goto done;
case '2':
g->index = 0;
g->state = GEMINI_STATE_READ;
break;
case '3':
if (g->maxredirects == 0)
goto done;
if (g->redirects >= g->maxredirects)
goto err;
g->redirects++;
if (gemini_connect_query(g, g->meta, NULL) != 0)
goto err;
return (gemini_read(g, b, bs));
case '4':
goto done;
case '5':
goto done;
case '6':
goto done;
case '7':
goto errinval;
case '8':
goto errinval;
case '9':
goto errinval;
default:
abort(); /* unreachable */
}
}
if (g->state != GEMINI_STATE_READ)
abort(); /* unreachable */
if (g->index < g->extralen) {
i = g->extralen - g->index;
i = i < bs ? i : bs;
(void)memcpy(b, g->extra + g->index, i);
g->index += i;
return (i);
}
return (tls_read(g->tls, b, bs));
eof:
if (g->state != GEMINI_STATE_READ)
goto err;
done:
g->state = GEMINI_STATE_DONE;
return (0);
errt:
if (r == 0)
goto eof;
if (r != -1)
return (r);
warnx("TLS Error: %s", tls_error(g->tls));
goto err;
errinval:
g->status = -1;
g->meta = "Invalid response";
goto err;
err:
g->state = GEMINI_STATE_ERROR;
return (-1);
}
void
gemini_fini(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)setflock(g->tofufd, F_UNLCK);
(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_fini(g);
free(g);
}
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. */
v = he;
if (v < sl && src[v] == ':')
for (v++; v < sl && ISASCIIDIGIT(src[v]); v++)
/* do nothing */;
if (f & GET_HOSTNAME_PORT)
he = v;
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);
}
int
setflock(int fd, int lock)
{
struct flock l;
(void)memset(&l, 0, sizeof(l));
l.l_type = lock;
l.l_whence = SEEK_SET;
l.l_start = 0;
l.l_len = 0;
return (fcntl(fd, F_SETLK, &l));
}