825 lines
18 KiB
C
825 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->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 && strlcpy(g->request, g->proxy,
|
|
sizeof(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));
|
|
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 errredir;
|
|
g->redirects++;
|
|
if (gemini_connect_query(g, g->meta, NULL) != 0)
|
|
goto err;
|
|
return (gemini_read(g, b, bs));
|
|
case '4':
|
|
goto errg;
|
|
case '5':
|
|
goto errg;
|
|
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) {
|
|
warnx("Unexpected EOF");
|
|
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;
|
|
errg:
|
|
warnx("Gemini Error %02hd: %s", g->status, g->meta);
|
|
goto done;
|
|
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);
|
|
}
|
|
|
|
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));
|
|
}
|