Large refactoring and renaming

`gemini_open' is now `gemini_connect'
`gemini_close' is now `gemini_fini'
`gemini_connect' will try to use the `socketfd' struct member, if valid.
`gemini_read' now returns 0 whenever the response header has been read.
`gemini_read' no longer handles user-input (status 10) itself.
`gemini_connect_query' has been added so callers can handle user-input.
`libgeminiclient.c' no longer needs `readpassphrase' and `libbsd'.
This commit is contained in:
styan 2020-05-01 04:22:05 +00:00
parent 3f17926bce
commit a96621e3fd
5 changed files with 205 additions and 183 deletions

View File

@ -1,21 +1,23 @@
.POSIX:
MAGIC= if pkg-config --exists libtls; then \
export CFLAGS="$$CFLAGS `pkg-config --cflags libtls`"; \
export LDFLAGS="$$LDFLAGS `pkg-config --libs libtls`"; \
else \
export LDFLAGS="$$LDFLAGS -ltls"; \
fi; \
if pkg-config --exists libbsd; then \
export CFLAGS="$$CFLAGS `pkg-config --cflags libbsd`"; \
export LDFLAGS="$$LDFLAGS `pkg-config --libs libbsd`"; \
MAGICTLS=\
if pkg-config --exists libtls; then\
export CFLAGS="$$CFLAGS `pkg-config --cflags libtls`";\
export LDFLAGS="$$LDFLAGS `pkg-config --libs libtls`";\
else\
export LDFLAGS="$$LDFLAGS -ltls";\
fi
MAGICBSD=\
if pkg-config --exists libbsd; then\
export CFLAGS="$$CFLAGS `pkg-config --cflags libbsd`";\
export LDFLAGS="$$LDFLAGS `pkg-config --libs libbsd`";\
fi
default: magic-libgeminiclient.a
all: libgeminiclient.a gemini-cat
magic: magic-libgeminiclient.a magic-gemini-cat
magic-libgeminiclient.a:
@${MAGIC}; ${MAKE} ${MAKEFLAGS} libgeminiclient.a
@${MAGICTLS}; ${MAKE} ${MAKEFLAGS} libgeminiclient.a
magic-gemini-cat:
@${MAGIC}; ${MAKE} ${MAKEFLAGS} gemini-cat
@${MAGICTLS}; ${MAGICBSD}; ${MAKE} ${MAKEFLAGS} gemini-cat
libgeminiclient.a: libgeminiclient.o
${AR} ${ARFLAGS} $@ libgeminiclient.o
libgeminiclient.o: libgeminiclient.c libgeminiclient.h

View File

@ -8,6 +8,7 @@
#include <unistd.h>
#include <err.h>
#include <readpassphrase.h>
#include <sysexits.h>
#include <tls.h>
@ -25,8 +26,10 @@ main(int argc, char *argv[])
{
char b[BUFFER_SIZE];
struct gemini gemini = GEMINI_INITIALIZER;
const char *query = NULL;
ssize_t r;
size_t i;
int prompt = 0;
int rawout;
int c;
@ -48,7 +51,7 @@ main(int argc, char *argv[])
gemini.certfile = optarg;
break;
case 'i':
gemini.flags |= GEMINI_PROMPT;
prompt = 1;
break;
case 'k':
gemini.keyfile = optarg;
@ -70,8 +73,7 @@ main(int argc, char *argv[])
warnx("Invalid number: %s", optarg);
break;
case 's':
gemini.flags |=
GEMINI_PROMPT | GEMINI_SECPROMPT;
prompt = 2;
break;
case 't':
gemini.tofufile = optarg;
@ -89,9 +91,12 @@ main(int argc, char *argv[])
pledge("stdio rpath flock inet dns tty", NULL) != 0)
err(EX_NOPERM, "Failed pledge(2)");
#endif
for (i = 0; i < argc; i++) {
if (gemini_open(&gemini, argv[i]) != 0)
for (i = 0; i < argc; i += (query == NULL)) {
if (gemini_connect_query(&gemini, argv[i], query)
!= 0) {
warn("Failed to connect to %s", argv[i]);
goto err;
}
#ifdef __OpenBSD__
if (i == 0 &&
pledge("stdio rpath flock inet dns tty", NULL) != 0)
@ -102,14 +107,38 @@ main(int argc, char *argv[])
r = stripcntrl(b, sizeof(b), b, r);
(void)fwrite(b, 1, r, stdout);
}
if (r < 0)
if (r < 0) {
warn("Gemini read error");
goto err;
}
query = NULL;
switch (gemini.status / 10) {
case 1:
if (prompt > 0 && readpassphrase(gemini.meta, b,
sizeof(b), prompt > 1 ? RPP_ECHO_OFF :
RPP_ECHO_ON) == NULL) {
warn("Could not get user-input");
goto err;
}
query = prompt > 0 ? b : NULL;
break;
case 2:
break;
case 6:
warnx("Gemini certificate error %02hd: %s",
gemini.status, gemini.meta);
break;
default:
warnx("Gemini error %02hd: %s", gemini.status,
gemini.meta);
break;
}
gemini_reset(&gemini);
}
gemini_close(&gemini);
gemini_fini(&gemini);
return (0);
err:
gemini_close(&gemini);
gemini_fini(&gemini);
return (EX_SOFTWARE);
usage:
(void)fprintf(stderr, "usage: gemini-cat [-RSisw] "

View File

@ -1,9 +1,10 @@
.Dd Apr 30, 2020
.Dd May 1, 2020
.Dt LIBGEMINICLIENT 3
.Os
.Sh NAME
.Nm gemini_create , gemini_init , gemini_open , gemini_read ,
.Nm gemini_reset , gemini_close , gemini_destroy
.Nm gemini_create , gemini_init , gemini_connect ,
.Nm gemini_connect_query , gemini_read , gemini_reset , gemini_fini ,
.Nm gemini_destroy
.Sh SYNOPSIS
.In libgeminiclient.h
.Ft struct gemini *
@ -11,13 +12,16 @@
.Ft void
.Fn gemini_init "struct gemini *ctx"
.Ft int
.Fn gemini_open "struct gemini *ctx" "const char *url"
.Fn gemini_connect "struct gemini *ctx" "const char *url"
.Ft int
.Fn gemini_connect_query "struct gemini *ctx" "const char *url" \
"const char *query"
.Ft ssize_t
.Fn gemini_read "struct gemini *ctx" "void *buffer" "size_t size"
.Ft void
.Fn gemini_reset "struct gemini *ctx"
.Ft void
.Fn gemini_close "struct gemini *ctx"
.Fn gemini_fini "struct gemini *ctx"
.Ft void
.Fn gemini_destroy "struct gemini *ctx"
.Sh DESCRIPTION
@ -25,25 +29,35 @@
allocates a
.Vt gemini
structure and calls
.Fn gemini_init on it.
.Fn gemini_init
on it.
.Pp
.Fn gemini_init
Initialized the provided
Initializes the provided
.Vt gemini
structure.
.Pp
.Fn gemini_open
.Fn gemini_connect
opens a connection to the provided
.Fa url ,
and sets up the
.Vt gemini
structure for reading.
.Pp
.Fn gemini_connect_query
calls
.Fn gemini_connect
with the escaped
.Fa query
appended to
.Fa url
separated by a question-mark.
.Pp
.Fn gemini_read
attempts to read using the
.Vt gemini
structure previously set up by
.Fn gemini_open ,
.Fn gemini_connect ,
sending the request and parsing the response as needed.
After a successful return the
.Fa meta ,
@ -82,8 +96,7 @@ META contains additional information.
prepares the provided
.Vt gemini
structure to be reused for
.Pp
.Fn gemini_open
.Fn gemini_connect
while keeping reusable data.
This prevents user-configurable information from being reset,
and prevents
@ -221,13 +234,15 @@ returns a pointer to a newly initialized
.Vt gemini
structure, or a NULL on a failure.
.Pp
.Fn gemini_open
.Fn gemini_connect
and
.Fn gemini_connect_query
returns a 0 on success and a -1 on an error.
.Pp
.Fn gemini_read
returns a -1 on an error, otherwise it returns the amount of data
read into the buffer, with 0 only being returned when there is nothing
left to read.
left to read after successfully reading the response header.
.Sh EXAMPLES
The following example shows how you initialize various values:
.Bd -literal -offset indent -compact
@ -238,7 +253,7 @@ gemini->tofufile = tofufile;
gemini->flags |= GEMINI_TOFU_WRITE;
gemini->port = port;
while ((url = get_url()) != NULL)
if (gemini_open(gemini) != 0)
if (gemini_connect(gemini) != 0)
continue;
...
gemini_reset(gemini);
@ -251,13 +266,18 @@ codes yourself:
.Bd -literal -offset indent -compact
gemini->maxredirects = 0;
while ((url = get_url()) != NULL) {
if (gemini_open(gemini, url) != 0) {
if (gemini_connect(gemini, url) != 0) {
warn("Could not open: %s", url);
continue;
}
while ((r = gemini_read(gemini, buf, bufsize)) > 0) {
...
}
if (r < 0) {
warn("Error while reading");
gemini_reset(gemini);
continue;
}
switch (gemini->status / 10) {
...
case 3:

View File

@ -8,7 +8,6 @@
#include <time.h>
#include <err.h>
#include <readpassphrase.h>
#include <fcntl.h>
#include <unistd.h>
@ -40,18 +39,9 @@ enum {
#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);
@ -78,6 +68,7 @@ gemini_init(struct gemini *g)
g->keylen = 0;
g->certlen = 0;
g->index = 0;
g->socketfd = -1;
g->tofufd = -1;
g->tofumod = 0;
g->flags = 0;
@ -108,6 +99,8 @@ gemini_reset(struct gemini *g)
g->state = GEMINI_STATE_INIT;
g->redirects = 0;
g->status = 0;
if (g->port == 0)
g->port = GEMINI_DEFAULT_PORT;
}
struct gemini *
@ -122,7 +115,34 @@ gemini_create(void)
}
int
gemini_open(struct gemini *g, const char *url)
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;
@ -133,9 +153,8 @@ gemini_open(struct gemini *g, const char *url)
int e;
short port;
if (g->port == 0)
g->port = GEMINI_DEFAULT_PORT;
if (g->state != GEMINI_STATE_INIT || g->port < 0) {
gemini_reset(g);
if (g->port < 0) {
errno = EINVAL;
return (-1);
}
@ -152,8 +171,8 @@ gemini_open(struct gemini *g, const char *url)
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) {
url, urllen, (g->socketfd < 0 ? GET_HOSTNAME_PORT : 0) |
GET_HOSTNAME_VALID)) == 0 || i > GEMINI_URL_MAX) {
errno = EINVAL;
return (-1);
}
@ -181,9 +200,10 @@ gemini_open(struct gemini *g, const char *url)
}
/* 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)) {
(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;
}
@ -217,61 +237,27 @@ gemini_open(struct gemini *g, const char *url)
* input gemini://example.com/?inputdata
* redirect gemini://example.com/some/path
*/
if ((e = set_tofu(&g->tofu, g->request, hash,
tls_peer_cert_notafter(g->tls))) < 0)
goto err;
if (e > 0)
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 (0);
return (e);
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)
{
@ -287,19 +273,21 @@ gemini_read(struct gemini *g, void *b, size_t bs)
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;
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->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)
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. */
@ -315,8 +303,8 @@ gemini_read(struct gemini *g, void *b, size_t bs)
break;
case GEMINI_STATE_RESPONSE_WHITESPACE:
/* The unspecified maximum is kind-of absurd. */
for (; i < r &&
(cb[i] == ' ' || cb[i] == '\t'); i++)
for (; i < r && (cb[i] == ' ' || cb[i] == '\t');
i++)
/* do nothing */;
if (i == r)
break;
@ -357,49 +345,17 @@ gemini_read(struct gemini *g, void *b, size_t bs)
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));
goto done;
case 2:
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_bounce(g, g->meta) != 0)
if (gemini_connect_query(g, g->meta, NULL) != 0)
goto err;
return (gemini_read(g, b, bs));
case 4:
@ -407,7 +363,7 @@ gemini_read(struct gemini *g, void *b, size_t bs)
case 5:
goto errg;
case 6:
goto errcert;
goto done;
case 7:
goto errinval;
case 8:
@ -433,20 +389,19 @@ eof:
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 err;
errr:
warnx("Too many redirects");
goto err;
errcert:
warnx("Certificate Error %02hd: %s", g->status, g->meta);
goto err;
goto done;
errinval:
warnx("Invalid response");
goto err;
@ -461,22 +416,44 @@ err:
return (-1);
}
int
gemini_bounce(struct gemini *g, const char *url)
void
gemini_fini(struct gemini *g)
{
char b[GEMINI_URL_MAX + 1];
size_t l;
gemini_reset(g);
if ((l = strlen(url)) > GEMINI_URL_MAX) {
errno = EINVAL;
return (-1);
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;
}
(void)memcpy(b, url, l);
b[l] = '\0';
return (gemini_open(g, b));
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_fini(g);
free(g);
}
int
set_tofu(struct gemini_tofu **tofu, const char *host, const char *hash,
@ -764,20 +741,23 @@ get_hostname(char *dst, size_t dl, const char *src, size_t sl, int f)
"~$&'()*+,-.;=_~")) == 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++)
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);
}

View File

@ -7,24 +7,12 @@
#include <tls.h>
#ifndef GEMINI_HOSTNAME_MAX
#define GEMINI_HOSTNAME_MAX 1024
#endif
#ifndef GEMINI_URL_MAX
#define GEMINI_URL_MAX 1024
#endif
#ifndef GEMINI_META_MAX
#define GEMINI_META_MAX 1024
#endif
#ifndef GEMINI_REDIRECT_MAX
#define GEMINI_REDIRECT_MAX 5
#endif
enum {
GEMINI_PROMPT = 0x01,
GEMINI_SECPROMPT = 0x02,
GEMINI_TOFU_WRITE = 0x08
};
#define GEMINI_TOFU_WRITE 0x01
struct gemini_tofu {
struct gemini_tofu *next;
@ -35,7 +23,7 @@ struct gemini_tofu {
#define GEMINI_INITIALIZER \
{ { 0 }, { 0 }, NULL, NULL, NULL, NULL, NULL, NULL, \
NULL, NULL, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, \
NULL, NULL, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, \
GEMINI_REDIRECT_MAX, 0, 0 }
struct gemini {
char meta[GEMINI_META_MAX + 1];
@ -53,6 +41,7 @@ struct gemini {
size_t keylen;
size_t certlen;
size_t index;
int socketfd;
int tofufd;
int tofumod;
int flags;
@ -63,12 +52,14 @@ struct gemini {
short port;
};
struct gemini *gemini_create(void);
void gemini_init(struct gemini *);
void gemini_reset(struct gemini *);
struct gemini *gemini_create(void);
int gemini_open(struct gemini *, const char *);
void gemini_close(struct gemini *);
void gemini_destroy(struct gemini *);
int gemini_connect_query(struct gemini *, const char *,
const char *);
int gemini_connect(struct gemini *, const char *);
ssize_t gemini_read(struct gemini *, void *, size_t);
void gemini_fini(struct gemini *);
void gemini_destroy(struct gemini *);
#endif