Add gemini-pipe(1)

This commit is contained in:
styan 2020-06-20 08:40:48 +00:00
parent 7e7e35e5d4
commit f1bc8a9721
4 changed files with 472 additions and 3 deletions

View File

@ -21,20 +21,27 @@ MAGICBSD=\
fi
default: magic-libgeminiclient
all: magic
magic: magic-libgeminiclient magic-gemini-cat
magic: magic-libgeminiclient magic-gemini-cat magic-gemini-pipe
magic-libgeminiclient:
@${MAGIC}; ${MAGICTLS}; ${MAKE} ${MAKEFLAGS} libgeminiclient.a
magic-gemini-cat:
@${MAGIC}; ${MAGICTLS}; ${MAGICBSD};\
${MAKE} ${MAKEFLAGS} gemini-cat
magic-gemini-pipe:
@${MAGIC}; ${MAGICTLS};\
${MAKE} ${MAKEFLAGS} gemini-pipe
libgeminiclient.a: libgeminiclient.o
${AR} ${ARFLAGS} $@ libgeminiclient.o
libgeminiclient.o: libgeminiclient.c libgeminiclient.h
${CC} -c ${CFLAGS} -o $@ libgeminiclient.c
gemini-cat: gemini-cat.o libgeminiclient.a
${CC} -o $@ gemini-cat.o libgeminiclient.a ${LDFLAGS}
gemini-pipe: gemini-pipe.o libgeminiclient.a
${CC} -o $@ gemini-pipe.o libgeminiclient.a ${LDFLAGS}
gemini-cat.o: gemini-cat.c libgeminiclient.h
${CC} -c -I. ${CFLAGS} -o $@ gemini-cat.c
gemini-pipe.o: gemini-pipe.c libgeminiclient.h
${CC} -c -I. ${CFLAGS} -o $@ gemini-pipe.c
install: libgeminiclient.a libgeminiclient.h libgemeniclient.3
install -m444 libgeminiclient.a \
"$$DESTDIR/$${PREFIX-usr/local}/lib/"
@ -54,7 +61,14 @@ install-gemini-cat: gemini-cat
install -m755 gemini-cat "$$DESTDIR/$${PREFIX-usr/local}/bin/"
install -m444 gemini-cat.1 \
"$$DESTDIR/$${PREFIX:-usr/local}/$${MANDIR:-man}/man1/"
install-gemini-pipe: gemini-cat
install -m755 gemini-pipe "$$DESTDIR/$${PREFIX-usr/local}/bin/"
install -m444 gemini-pipe.1 \
"$$DESTDIR/$${PREFIX:-usr/local}/$${MANDIR:-man}/man1/"
linstall-all: linstall-gemini-cat linstall-gemini-pipe
linstall-gemini-cat: gemini-cat
install -m755 gemini-cat "$$HOME/bin/"
linstall-gemini-pipe: gemini-pipe
install -m755 gemini-pipe "$$HOME/bin/"
clean:
rm -f *.[ao] gemini-cat
rm -f *.[ao] gemini-cat gemini-pipe

15
README
View File

@ -19,6 +19,11 @@ BUILDING
export LDFLAGS='-L/path/to/libtls -ltls'
make gemini-cat
To build gemini-pipe(1) configured manually:
export CFLAGS='-I/path/to/libtls -I/path/to/libbsd'
export LDFLAGS='-L/path/to/libtls -ltls'
make gemini-pipe
INSTALLING
To install libgeminiclient(3) on the system:
@ -27,12 +32,18 @@ INSTALLING
To install gemini-cat(1) on the system:
make install-gemini-cat
To install gemini-pipe(1) on the system:
make install-gemini-pipe
To install everything on the system:
make install-all
To install gemini-cat(1) in your home-directory:
make linstall-gemini-cat
To install gemini-pipe(1) in your home-directory:
make linstall-gemini-pipe
MAGIC
The ``magic'' make(1) targets attempt to use pkg-config(1) for
@ -40,4 +51,6 @@ MAGIC
PREPROCESSOR DEFINES
``BUFFER_SIZE'' The size of the buffer for gemini-cat(1) to use.
``BUFFER_SIZE''
The size of the buffer for gemini-cat(1) to use,
and the default buffer size for gemini-pipe(1).

113
gemini-pipe.1 Normal file
View File

@ -0,0 +1,113 @@
.Dd Jun 20, 2020
.Dt GEMINI-PIPE 1
.Os
.Sh NAME
.Nm gemini-pipe
.Nd plumb gemini pages based on the returned mime-type
.Sh SYNOPSIS
.Nm
.Op Fl w
.Op Fl c Ar cert-file
.Op Fl d Ar default-handler
.Op Fl f Ar mimedef-file
.Op Fl h Ar proxy-host
.Op Fl k Ar key-file
.Op Fl m Ar mimedef
.Op Fl p Ar port
.Op Fl r Ar max-redirects
.Op Fl t Ar tofu-file
.Ar URL ...
.Sh DESCRIPTION
Choose a pipeline to send the data received from
.Ar URL
based on the received mime-type.
.Pp
The options are as follows:
.Bl -tag -width Ds
.It Fl c Ar cert-file
The certificate file to use for TLS.
.It Fl d Ar default-handler
The pipeline to use when there is no matching mimedef.
The default is:
.Li $PAGER
With
.Ev PAGER
defaulting to
.Xr more 1 .
.It Fl f Ar default-handler
Parse the file as described below.
.It Fl h Ar proxy-host
The host to connect through.
.Ar proxy-host
can also specify a port number.
.It Fl k Ar key-file
The key file to use for TLS.
.It Fl m Ar mimedef
Parse the argument as described below.
.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 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
.Pp
.Sy mimedef
strings are parsed in a manner similar to
.Xr make 1
files.
The targeted mime-type is at the beginning of the line,
and the script to run is formed from the concatenation of all of the
lines afterwards that start with a blank (tab or space) character.
The first line of a script can be on the same line as the mime-type,
separated by a blank (tab or space)character.
Examples can be found below.
.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 .
.It Ev mime
The mime-type received.
.El
.Sh EXIT STATUS
.Ex -std
.Sh EXAMPLES
Open an HTML file received over Gemini:
.Dl gemini-pipe -m 'text/html lynx -stdin' //example.com/index.html
.Pp
An example mimedef file:
.Bd -literal -offset indent -compact
application/octet-stream od -tx1 | $PAGER
text/html w3m -T "$mime"
text/troff nroff | $PAGER
.Ed
.Sh SEE ALSO
.Xr gemini-cat 1 ,
.Xr libgeminiclient 3 ,
.Xr tls_config_set_cert_file 3 ,
.Xr tls_config_set_key_file 3

329
gemini-pipe.c Normal file
View File

@ -0,0 +1,329 @@
#include <ctype.h>
#include <errno.h>
#include <inttypes.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
struct mimedef {
struct mimedef *next;
char *target;
char *recipe;
};
static struct mimedef *mimedef_parse(const char *);
static void mimedef_append(struct mimedef **,
struct mimedef *);
static int mimedef_merge(struct mimedef *);
static int strappend(char **, const char *, size_t, int);
static void *memlcpy(void *, size_t, const void *, size_t);
int
main(int argc, char *argv[])
{
char mimestr[GEMINI_META_MAX + 1];
struct gemini gemini;
struct mimedef *mime = NULL;
struct mimedef *tmime;
const char *drecipe = "$PAGER";
FILE *fp;
char *b;
char *p;
uintmax_t bs = BUFFER_SIZE;
ssize_t r;
size_t i;
size_t j;
int c;
gemini_envinit(&gemini);
if ((p = getenv("GEMINI_TOFU_WRITE")) != NULL && *p != '\0')
gemini.flags |= GEMINI_TOFU_WRITE;
while ((c = getopt(argc, argv, "b:c:d:f:h:k:m:p:r:t:w")) != -1)
switch (c) {
case 'b':
errno = 0;
bs = strtoumax(optarg, &p, 0);
if (p == optarg || *p != '\0')
err(EX_USAGE, "Invalid number: %s",
optarg);
break;
case 'c':
gemini.certfile = optarg;
break;
case 'd':
drecipe = optarg;
break;
case 'f':
if ((fp = fopen(optarg, "r")) == NULL)
err(EX_NOINPUT, "%s", optarg);
for (j = 1;
(r = getdelim(&p, &i, '\n', fp)) > 0;
j++) {
errno = 0;
if ((tmime = mimedef_parse(p))
== NULL && errno != 0)
err(EX_DATAERR, "%s:%zu",
optarg, j);
mimedef_append(&mime, tmime);
}
break;
case 'h':
gemini.proxy = optarg;
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 'm':
if ((tmime = mimedef_parse(optarg)) == NULL)
err(EX_USAGE, "Invalid MIME-def: %s",
optarg);
mimedef_append(&mime, tmime);
break;
case 't':
gemini.tofufile = optarg;
break;
case 'w':
gemini.flags |= GEMINI_TOFU_WRITE;
break;
default:
goto usage;
}
argc -= optind;
argv += optind;
if ((b = malloc(bs)) == NULL)
err(EX_OSERR, "Could not allocate the buffer");
if (getenv("PAGER") == NULL)
(void)setenv("PAGER", "more", 1);
if (mimedef_merge(mime) != 0)
err(EX_OSERR, "Could not merge the MIME-defs");
for (i = 0; i < argc; i++) {
if (gemini_connect(&gemini, argv[i]) != 0) {
warn("Failed to connect to %s", argv[i]);
continue;
}
if ((r = gemini_read(&gemini, b, bs)) < 0) {
gemini_reset(&gemini);
warn("Gemini read error");
continue;
}
if (gemini.status / 10 != 2) {
gemini_reset(&gemini);
warnx("Gemini error %02hd: %s",
gemini.status, gemini.meta);
continue;
}
for (i = 0; i < gemini.metalen &&
isspace(gemini.meta[i]); i++)
/* do nothing */;
for (j = i; j < gemini.metalen &&
!isspace(gemini.meta[j]) && gemini.meta[j] != ';';
j++)
/* do nothing */;
if (j > i)
(void)memlcpy(mimestr, sizeof(mimestr),
gemini.meta + i, j - i);
else
(void)strlcpy(mimestr, "text/gemini",
sizeof(mimestr));
for (tmime = mime; tmime != NULL &&
strcmp(tmime->target, mimestr) != 0;
tmime = tmime->next)
/* do nothing */;
(void)setenv("mime", mimestr, 1);
fp = popen(tmime != NULL ? tmime->recipe : drecipe,
"w");
do {
(void)fwrite(b, 1, r, fp);
} while ((r = gemini_read(&gemini, b, bs)) > 0);
(void)pclose(fp);
gemini_reset(&gemini);
}
gemini_fini(&gemini);
return (0);
err:
gemini_fini(&gemini);
return (EX_SOFTWARE);
usage:
(void)fprintf(stderr, "usage: gemini-pipe [-w] [-c cert-file] "
"[-d default-handler]\n"
" [-f mimedef-file] [-h proxy-host] "
"[-k key-file]\n"
" [-m mime-def] [-p port] "
"[-r max-redirects]\n"
" [-t tofu-file] URL...\n");
return (EX_USAGE);
}
struct mimedef *
mimedef_parse(const char *s)
{
struct mimedef *mime = NULL;
struct mimedef *tmime;
char *target = NULL;
char *recipe = NULL;
size_t i = 0;
size_t j = 0;
for (;;) {
for (; isspace(s[i]) && !isblank(s[i]); i++)
/* do nothing */;
if (s[i] == '\0')
break;
if (isblank(s[i])) { /* recipe */
if (target == NULL)
goto err;
for (i++, j = i;
s[j] != '\0' && !isspace(s[j]);
j++)
/* do nothing */;
if (strappend(&recipe, s + i, j - i, '\n') != 0)
goto err;
} else { /* target */
if (target != NULL) {
if ((tmime = malloc(sizeof(*tmime)))
== NULL)
goto err;
tmime->next = NULL;
tmime->target = target;
tmime->recipe = recipe;
mimedef_append(&mime, tmime);
target = NULL;
recipe = NULL;
}
for (j = i; s[j] != '\0' && !isspace(s[j]); j++)
/* do nothing */;
if (strappend(&target, s + i, j - i, '\0') != 0)
goto err;
}
i = j;
}
if (target != NULL) {
if ((tmime = malloc(sizeof(*tmime))) == NULL)
goto err;
tmime->next = NULL;
tmime->target = target;
tmime->recipe = recipe;
mimedef_append(&mime, tmime);
}
return (mime);
err:
if (target != NULL)
free(target);
if (recipe != NULL)
free(recipe);
while (mime != NULL) {
tmime = mime;
mime = mime->next;
free(tmime->target);
free(tmime->recipe);
free(tmime);
}
return (NULL);
}
void
mimedef_append(struct mimedef **dstp, struct mimedef *src)
{
struct mimedef *dst;
if (*dstp == NULL) {
*dstp = src;
return;
}
for (dst = *dstp; dst->next != NULL; dst = dst->next)
/* do nothing */;
dst->next = src;
}
int
mimedef_merge(struct mimedef *mime)
{
struct mimedef *a;
struct mimedef *b;
struct mimedef *c;
struct mimedef *p;
for (a = mime; a != NULL; a = a->next) {
for (b = a->next, p = a; b != NULL;) {
c = b->next;
if (strcmp(a->target, b->target) == 0) {
if (strappend(&a->recipe, b->recipe,
strlen(b->recipe), '\n') != 0)
return (-1);
free(b->target);
free(b->recipe);
free(b);
p->next = c;
} else
p = b;
b = c;
}
}
return (0);
}
int
strappend(char **dstp, const char *src, size_t l, int end)
{
char *dst;
size_t dl = 0;
dst = *dstp;
if (dst == NULL)
dst = malloc(l + 2);
else
dst = realloc(dst, (dl = strlen(dst)) + l + 2);
if (dst == NULL)
return (-1);
(void)memcpy(dst + dl, src, l);
dl += l;
if (l > 0 && dst[l - 1] != end)
dst[l++] = end;
dst[l] = '\0';
*dstp = dst;
return (0);
}
void *
memlcpy(void *dst, size_t ds, const void *src, size_t ss)
{
if (ds == 0)
return (dst);
if (ss > ds - 1)
ss = ds - 1;
(void)memcpy(dst, src, ss);
((char *)dst)[ss] = '\0';
return (dst);
}