784 lines
17 KiB
C
784 lines
17 KiB
C
#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,
|
|
(strchr(g->request, ':') == NULL ? portptr : NULL)) != 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);
|
|
}
|