Add `gemini_envinit', add proxy support, simplfy header handling

Add `gemini_envinit' which initializes some parts of the structure
from the environment.
Add proxy support in the form of a `proxy' field in the structure
that can hold a host string.
Simplfy header handling in `gemini_read', removing the
`GEMINI_STRICT' flag.
Add the `-h' option to gemini-cat(1) to set the proxy host.
Remove the `-e' option from gemini-cat(1).
Add environment variable support to gemini-cat(1), including
`GEMINI_TOFU_WRITE', which will set the corresponding flag.
This commit is contained in:
styan 2020-06-20 04:56:34 +00:00
parent e0d7ff540e
commit 5dcddc7077
5 changed files with 238 additions and 132 deletions

View File

@ -1,4 +1,4 @@
.Dd Jun 10, 2020 .Dd Jun 20, 2020
.Dt GEMINI-CAT 1 .Dt GEMINI-CAT 1
.Os .Os
.Sh NAME .Sh NAME
@ -6,8 +6,9 @@
.Nd concatnate responses from a Gemini URLs .Nd concatnate responses from a Gemini URLs
.Sh SYNOPSIS .Sh SYNOPSIS
.Nm .Nm
.Op Fl RSeisw .Op Fl RSisw
.Op Fl c Ar cert-file .Op Fl c Ar cert-file
.Op Fl h Ar proxy-host
.Op Fl k Ar key-file .Op Fl k Ar key-file
.Op Fl p Ar port .Op Fl p Ar port
.Op Fl r Ar max-redirects .Op Fl r Ar max-redirects
@ -26,8 +27,10 @@ Strip out non-space control-characters before writing, the default for
TTY outputs. TTY outputs.
.It Fl c Ar cert-file .It Fl c Ar cert-file
The certificate file to use for TLS. The certificate file to use for TLS.
.It Fl e .It Fl h Ar proxy-host
Enforce a strict interpretation of the Gemini specification. The host to connect through.
.Ar proxy-host
can also specify a port number.
.It Fl i .It Fl i
Interactive mode (allow prompts). Interactive mode (allow prompts).
.It Fl k Ar key-file .It Fl k Ar key-file
@ -47,6 +50,30 @@ The file to read host certificate hashes from.
Write any new hashes to the Write any new hashes to the
.Ar tofu-file . .Ar tofu-file .
.El .El
.Sh ENVIRONMENT
.Bl -tag -width Ds
.It Ev GEMINI_KEYFILE
Sets the TLS key file before
.Fl k .
.It Ev GEMINI_CERTFILE
Sets the TLS certificate file before
.Fl c .
.It Ev GEMINI_TOFUFILE
Sets the TOFU tile before
.Fl t .
.It Ev GEMINI_PROXY
Sets the proxy host before
.Fl h .
.It Ev GEMINI_MAX_REDIRECTS
Sets the maximum number of redirects before
.Fl r .
.It Ev GEMINI_PORT
Sets the default port number before
.Fl p .
.It Ev GEMINI_TOFU_WRITE
If set to a non-empty string, it is equivalent to
.Fl w .
.El
.Sh EXIT STATUS .Sh EXIT STATUS
.Ex -std .Ex -std
.Sh EXAMPLES .Sh EXAMPLES
@ -54,6 +81,15 @@ Fetch the
.Lk tilde.black .Lk tilde.black
root index: root index:
.Dl gemini-cat gemini://tilde.black/ | more .Dl gemini-cat gemini://tilde.black/ | more
Fetch
.Lk tilde.black
from its onion address:
.Bd -literal -offset indent -compact
torsocks gemini-cat -h \\
black6kfjetfuzaeozz7fs53whh7xtd4e27telrf5fg5kgdt5ah5plad.onion \\
//tilde.black/
.Ed
.Sh SEE ALSO .Sh SEE ALSO
.Xr tls_config_set_cert_file 3 .Xr libgeminiclient 3 ,
.Xr tls_config_set_cert_file 3 ,
.Xr tls_config_set_key_file 3 .Xr tls_config_set_key_file 3

View File

@ -32,19 +32,23 @@ main(int argc, char *argv[])
char b[BUFFER_SIZE]; char b[BUFFER_SIZE];
struct gemini gemini = GEMINI_INITIALIZER; struct gemini gemini = GEMINI_INITIALIZER;
const char *query = NULL; const char *query = NULL;
char *p;
ssize_t r; ssize_t r;
size_t i; size_t i;
int prompt = 0; int prompt = 0;
int rawout; int rawout;
int c; int c;
gemini_envinit(&gemini);
if ((p = getenv("GEMINI_TOFU_WRITE")) != NULL && *p != '\0')
gemini.flags |= GEMINI_TOFU_WRITE;
rawout = !isatty(STDOUT_FILENO); rawout = !isatty(STDOUT_FILENO);
#ifdef __OpenBSD__ #ifdef __OpenBSD__
if (pledge("stdio cpath rpath wpath flock inet dns tty", NULL) if (pledge("stdio cpath rpath wpath flock inet dns tty", NULL)
!= 0) != 0)
err(EX_NOPERM, "Failed pledge(2)"); err(EX_NOPERM, "Failed pledge(2)");
#endif #endif
while ((c = getopt(argc, argv, "RSc:eik:p:r:st:w")) != -1) while ((c = getopt(argc, argv, "RSc:h:ik:p:r:st:w")) != -1)
switch (c) { switch (c) {
case 'R': case 'R':
rawout = 1; rawout = 1;
@ -55,8 +59,8 @@ main(int argc, char *argv[])
case 'c': case 'c':
gemini.certfile = optarg; gemini.certfile = optarg;
break; break;
case 'e': case 'h':
gemini.flags |= GEMINI_STRICT; gemini.proxy = optarg;
break; break;
case 'i': case 'i':
prompt = 1; prompt = 1;
@ -157,10 +161,10 @@ err:
gemini_fini(&gemini); gemini_fini(&gemini);
return (EX_SOFTWARE); return (EX_SOFTWARE);
usage: usage:
(void)fprintf(stderr, "usage: gemini-cat [-RSeisw] " (void)fprintf(stderr, "usage: gemini-cat [-RSisw] "
"[-c cert-file] [-k key-file] [-p port]\n" "[-c cert-file] [-h proxy-host] [-k key-file]\n"
" [-r max-redirects] [-t tofu-file] " " [-p port] [-r max-redirects] "
"URL...\n"); "[-t tofu-file] URL...\n");
return (EX_USAGE); return (EX_USAGE);
} }

View File

@ -1,4 +1,4 @@
.Dd May 1, 2020 .Dd Jun 20, 2020
.Dt LIBGEMINICLIENT 3 .Dt LIBGEMINICLIENT 3
.Os .Os
.Sh NAME .Sh NAME
@ -11,6 +11,8 @@
.Fn gemini_create void .Fn gemini_create void
.Ft void .Ft void
.Fn gemini_init "struct gemini *ctx" .Fn gemini_init "struct gemini *ctx"
.Ft void
.Fn gemini_envinit "struct gemini *ctx"
.Ft int .Ft int
.Fn gemini_connect "struct gemini *ctx" "const char *url" .Fn gemini_connect "struct gemini *ctx" "const char *url"
.Ft int .Ft int
@ -37,6 +39,39 @@ Initializes the provided
.Vt gemini .Vt gemini
structure. structure.
.Pp .Pp
.Fn gemini_envinit
Initializes the provided
.Vt gemini
structure, using the following environment variables,
if they are provided:
.Pp
.Bl -tag -width Ds -compact
.It Ev GEMINI_KEYFILE
Initializes the
.Fa keyfile
field.
.It Ev GEMINI_CERTFILE
Initializes the
.Fa certfile
field.
.It Ev GEMINI_TOFUFILE
Initializes the
.Fa tofufile
field.
.It Ev GEMINI_PROXY
Initializes the
.Fa proxy
field.
.It Ev GEMINI_MAX_REDIRECTS
Initializes the
.Fa maxredirects
field.
.It Ev GEMINI_PORT
Initializes the
.Fa port
field.
.El
.Pp
.Fn gemini_connect .Fn gemini_connect
opens a connection to the provided opens a connection to the provided
.Fa url , .Fa url ,
@ -123,16 +158,17 @@ The useful members of the
structure are as follows: structure are as follows:
.Bd -literal -offset indent -compact .Bd -literal -offset indent -compact
struct gemini { struct gemini {
char meta[GEMINI_META_MAX + 1]; const char *certfile; /* Certificate file */
const char *certfile; /* Certificate file */ const char *keyfile; /* Key file */
const char *keyfile; /* Key file */ const char *tofufile; /* Known-hosts file */
const char *tofufile; /* Known-hosts file */ const char *proxy; /* Proxy host */
size_t metalen; /* Length of `meta' */ char *meta; /* Response META */
int flags; /* Configuration flags */ size_t metalen; /* Length of `meta' */
int maxredirects; /* Default 5 */ int flags; /* Configuration flags */
int socketfd; /* The socket to use */ int maxredirects; /* Default 5 */
short status; /* Response STATUS */ int socketfd; /* The socket to use */
short port; /* Default port */ short status; /* Response STATUS */
short port; /* Default port */
}; };
.Ed .Ed
.Pp .Pp
@ -145,8 +181,6 @@ Write any new certificate information to
.Fa tofufile .Fa tofufile
on on
.Dn gemini_close . .Dn gemini_close .
.It Dv GEMINI_STRICT
Enforce a strict interpretation of the Gemini protocol.
.El .El
.Pp .Pp
All knows All knows

View File

@ -1,6 +1,8 @@
#include <assert.h> #include <assert.h>
#include <ctype.h> #include <ctype.h>
#include <errno.h> #include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <stddef.h> #include <stddef.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@ -21,8 +23,6 @@ enum {
GEMINI_STATE_INIT = 0, GEMINI_STATE_INIT = 0,
GEMINI_STATE_REQUEST, GEMINI_STATE_REQUEST,
GEMINI_STATE_RESPONSE, GEMINI_STATE_RESPONSE,
GEMINI_STATE_RESPONSE_WHITESPACE,
GEMINI_STATE_RESPONSE_META,
GEMINI_STATE_READ, GEMINI_STATE_READ,
GEMINI_STATE_DONE, GEMINI_STATE_DONE,
GEMINI_STATE_ERROR GEMINI_STATE_ERROR
@ -32,6 +32,7 @@ enum {
#define GEMINI_DEFAULT_PORT 1965 #define GEMINI_DEFAULT_PORT 1965
#endif #endif
#define ISASCIIBLANK(c) ((c) == '\t' || (c) == ' ')
#define ISASCIILOWER(c) ((c) >= 'a' && (c) <= 'z') #define ISASCIILOWER(c) ((c) >= 'a' && (c) <= 'z')
#define ISASCIIUPPER(c) ((c) >= 'A' && (c) <= 'Z') #define ISASCIIUPPER(c) ((c) >= 'A' && (c) <= 'Z')
#define ISASCIIDIGIT(c) ((c) >= '0' && (c) <= '9') #define ISASCIIDIGIT(c) ((c) >= '0' && (c) <= '9')
@ -65,8 +66,13 @@ gemini_init(struct gemini *g)
g->certmem = NULL; g->certmem = NULL;
g->keyfile = NULL; g->keyfile = NULL;
g->certfile = NULL; g->certfile = NULL;
g->metalen = 0; g->proxy = NULL;
g->meta = NULL;
g->extra = NULL;
g->reqlen = 0; g->reqlen = 0;
g->reslen = 0;
g->metalen = 0;
g->extralen = 0;
g->keylen = 0; g->keylen = 0;
g->certlen = 0; g->certlen = 0;
g->index = 0; g->index = 0;
@ -81,6 +87,44 @@ gemini_init(struct gemini *g)
g->port = 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 void
gemini_reset(struct gemini *g) gemini_reset(struct gemini *g)
{ {
@ -95,8 +139,12 @@ gemini_reset(struct gemini *g)
} }
g->tls = NULL; g->tls = NULL;
g->tls_config = NULL; g->tls_config = NULL;
g->metalen = 0; g->meta = NULL;
g->extra = NULL;
g->reqlen = 0; g->reqlen = 0;
g->reslen = 0;
g->metalen = 0;
g->extralen = 0;
g->index = 0; g->index = 0;
g->state = GEMINI_STATE_INIT; g->state = GEMINI_STATE_INIT;
g->status = 0; g->status = 0;
@ -177,6 +225,12 @@ gemini_connect(struct gemini *g, const char *url)
errno = EINVAL; errno = EINVAL;
return (-1); return (-1);
} }
/* Proxy */
if (g->proxy != NULL && strlcpy(g->request, g->proxy,
sizeof(g->request)) >= sizeof(g->request)) {
errno = EINVAL;
return (-1);
}
/* Initialize TLS */ /* Initialize TLS */
if ((g->tls_config = tls_config_new()) == NULL) if ((g->tls_config = tls_config_new()) == NULL)
return (-1); return (-1);
@ -276,7 +330,6 @@ gemini_read(struct gemini *g, void *b, size_t bs)
size_t j; size_t j;
int c; int c;
cb = b;
if (g->state == GEMINI_STATE_ERROR) if (g->state == GEMINI_STATE_ERROR)
return (-1); return (-1);
if (g->state == GEMINI_STATE_DONE) if (g->state == GEMINI_STATE_DONE)
@ -289,113 +342,87 @@ gemini_read(struct gemini *g, void *b, size_t bs)
goto errt; goto errt;
g->index += r; g->index += r;
} }
g->status = 0; g->reslen = 0;
g->index = 0; g->index = 0;
g->status = 0;
g->state = GEMINI_STATE_RESPONSE; g->state = GEMINI_STATE_RESPONSE;
} }
for (;;) { if (g->state == GEMINI_STATE_RESPONSE) {
i = i != r ? i : 0; do {
if (i == 0 && (r = tls_read(g->tls, b, bs)) <= 0) if (g->reslen >= sizeof(g->response))
goto errt; goto errinval;
switch (g->state) { if ((r = tls_read(g->tls,
case GEMINI_STATE_RESPONSE: g->response + g->reslen,
/* This is (probably) over-engineered. */ sizeof(g->response) - g->reslen)) <= 0)
for (; i < r && g->index < 2; i++, g->index++) { goto errt;
if (!ISASCIIDIGIT(cb[i])) for (i = g->reslen, g->reslen += r;
goto errinval; i < g->reslen && g->response[i] != '\n';
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++) i++)
/* do nothing */; /* do nothing */;
if (i == r) } while (i == g->reslen);
break; g->extra = g->response + i + 1;
g->metalen = 0; g->extralen = g->reslen - i - 1;
g->state = GEMINI_STATE_RESPONSE_META; 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; break;
case GEMINI_STATE_RESPONSE_META: case '3':
/* if (g->maxredirects == 0)
* If the white-space had a reasonable maximum
* then this would be easier.
*/
for (j = i; j < r && cb[j] != '\n'; j++)
/* do nothing */;
if (g->metalen + j - i > sizeof(g->meta))
goto errinval;
(void)memcpy(g->meta + g->metalen, cb + i,
j - i);
g->metalen += j - i;
if (cb[j] != '\n') {
i = j;
break;
}
if (g->metalen > 0 &&
g->meta[g->metalen - 1] == '\r') {
g->metalen--;
} else if (g->flags & GEMINI_STRICT) {
if (g->metalen == GEMINI_META_MAX)
goto errinval;
g->meta[g->metalen++] = '\n';
i = j + 1;
break;
}
if (g->metalen > GEMINI_META_MAX)
goto errinval;
g->meta[g->metalen] = '\0';
i = j + 1;
if (g->status / 10 != 3)
g->redirects = 0;
switch (g->status / 10) {
case 0:
goto errinval;
case 1:
goto done; goto done;
case 2: if (g->redirects >= g->maxredirects)
g->state = GEMINI_STATE_READ; goto errredir;
break; g->redirects++;
case 3: if (gemini_connect_query(g, g->meta, NULL) != 0)
if (g->maxredirects == 0) goto err;
goto done; return (gemini_read(g, b, bs));
if (g->redirects >= g->maxredirects) case '4':
goto errredir; goto errg;
g->redirects++; case '5':
if (gemini_connect_query(g, g->meta, goto errg;
NULL) != 0) case '6':
goto err; goto done;
return (gemini_read(g, b, bs)); case '7':
case 4: goto errinval;
goto errg; case '8':
case 5: goto errinval;
goto errg; case '9':
case 6: goto errinval;
goto done;
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: default:
abort(); /* unreachable */; 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: eof:
if (g->state != GEMINI_STATE_READ) { if (g->state != GEMINI_STATE_READ) {
warnx("Unexpected EOF"); warnx("Unexpected EOF");

View File

@ -13,7 +13,6 @@
#define GEMINI_REDIRECT_MAX 5 #define GEMINI_REDIRECT_MAX 5
#define GEMINI_TOFU_WRITE 0x01 #define GEMINI_TOFU_WRITE 0x01
#define GEMINI_STRICT 0x02
struct gemini_tofu { struct gemini_tofu {
struct gemini_tofu *next; struct gemini_tofu *next;
@ -24,10 +23,10 @@ struct gemini_tofu {
#define GEMINI_INITIALIZER \ #define GEMINI_INITIALIZER \
{ { 0 }, { 0 }, NULL, NULL, NULL, NULL, NULL, NULL, \ { { 0 }, { 0 }, NULL, NULL, NULL, NULL, NULL, NULL, \
NULL, NULL, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, \ NULL, NULL, NULL, NULL, NULL, 0, 0, 0, 0, 0, 0, 0, \
GEMINI_REDIRECT_MAX, 0, 0 } -1, -1, 0, 0, 0, 0, GEMINI_REDIRECT_MAX, 0, 0 }
struct gemini { struct gemini {
char meta[GEMINI_META_MAX + 1]; char response[3 + GEMINI_META_MAX + 2];
char request[GEMINI_URL_MAX + 2]; char request[GEMINI_URL_MAX + 2];
struct tls *tls; struct tls *tls;
struct tls_config *tls_config; struct tls_config *tls_config;
@ -37,8 +36,13 @@ struct gemini {
const char *keyfile; const char *keyfile;
const char *certfile; const char *certfile;
const char *tofufile; const char *tofufile;
size_t metalen; const char *proxy;
const char *meta;
const char *extra;
size_t reqlen; size_t reqlen;
size_t reslen;
size_t metalen;
size_t extralen;
size_t keylen; size_t keylen;
size_t certlen; size_t certlen;
size_t index; size_t index;
@ -55,6 +59,7 @@ struct gemini {
struct gemini *gemini_create(void); struct gemini *gemini_create(void);
void gemini_init(struct gemini *); void gemini_init(struct gemini *);
void gemini_envinit(struct gemini *);
void gemini_reset(struct gemini *); void gemini_reset(struct gemini *);
int gemini_connect_query(struct gemini *, const char *, int gemini_connect_query(struct gemini *, const char *,
const char *); const char *);