From 5c7abf01515677804eeb2cf083e33e4ddd742caf Mon Sep 17 00:00:00 2001 From: Omar Polo Date: Wed, 29 Dec 2021 18:01:08 +0000 Subject: reimplement gg This is a better version of gg. Initially it grew with flags directly needed to the specific test cases I wanted to write, so it's ugly to use but handy for tests. This is a new and re-thought implementation that it is (hopefully) easier to use both and "curl-like for gemini" but also for scripts and tests cases. One completely new feature is the proxying support with -P to send the request to the given host. --- Makefile | 5 +- gg.1 | 114 +++++++++++++++++ gg.c | 429 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 547 insertions(+), 1 deletion(-) create mode 100644 gg.1 create mode 100644 gg.c diff --git a/Makefile b/Makefile index 6af9942..fd02c84 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ TESTS= .PHONY: all static clean regress install -all: Makefile.local gmid TAGS compile_flags.txt +all: Makefile.local gmid gg TAGS compile_flags.txt Makefile.local: configure ./configure @@ -21,6 +21,9 @@ OBJS = ${SRCS:.c=.o} y.tab.o ${COMPAT} gmid: ${OBJS} ${CC} ${OBJS} -o gmid ${LDFLAGS} +gg: gg.o iri.o utf8.o ${COMPAT} + ${CC} gg.o iri.o utf8.o ${COMPAT} -o $@ ${LDFLAGS} + static: ${OBJS} ${CC} ${OBJS} -o gmid ${LDFLAGS} ${STATIC} diff --git a/gg.1 b/gg.1 new file mode 100644 index 0000000..36453d8 --- /dev/null +++ b/gg.1 @@ -0,0 +1,114 @@ +.\" Copyright (c) 2021 Omar Polo +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.Dd $Mdocdate: December 29 2021$ +.Dt GG 1 +.Os +.Sh NAME +.Nm gg +.Nd gemini client +.Sh SYNOPSIS +.Nm +.Bk -words +.Op Fl 23Nnv +.Op Fl C Ar cert +.Op Fl d Ar mode +.Op Fl H Ar sni +.Op Fl K Ar key +.Op Fl P Ar host : Ns Oo Ar port Oc +.Op Fl T Ar seconds +.Ar gemini://... +.Ek +.Sh DESCRIPTION +.Nm +.Pq gemini get +fetches the given gemini page and prints it to standard output. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl 2 +Accept only TLSv1.2. +.It Fl 3 +Accept only TLSv1.3. +.It Fl C Ar certificate +Use the given client +.Ar certificate . +.It Fl d Ar mode +Specify what +.Nm +should print. +.Ar mode +can be one of: +.Bl -tag -width header -compact +.It none +print only the body of the reply +.It code +print only the response code +.It header +print only the response header +.It meta +print only the response meta +.It whole +print the whole response as-is. +.El +.It Fl H Ar sni +Use the given +.Ar sni +host name instead of the one deducted by the IRI or proxy. +.It Fl K Ar key +Specify the key for the certificate. +It's mandatory if +.Fl C +is used. +.It Fl N +Disables the server name verification. +.It Fl n +Check that the given IRI is valid, but don't make any requests. +.It Fl P Ar host : Ns Op Ar port +Connect to the given +.Ar host +and +.Ar port +to do the request instead of the ones extracted by the IRI. +.Ar port +is by default 1965. +.It Fl T Ar seconds +Kill +.Nm +after +.Ar seconds . +.El +.Sh EXIT STATUS +The +.Nm +utility exits with zero if the response code was in the 2x range. +.Sh ACKNOWLEDGEMENTS +.Nm +uses the +.Dq Flexible and Economical +UTF-8 decoder written by +.An Bjoern Hoehrmann . +.Sh AUTHORS +.An -nosplit +The +.Nm +utility was written by +.An Omar Polo Aq Mt op@omarpolo.com . +.Sh CAVEATS +.Nm +doesn't do any TOFU +.Pq Trust On First Use +or any X.509 certificate validation beyond the name verification. +.Pp +.Nm +doesn't follow redirects. diff --git a/gg.c b/gg.c new file mode 100644 index 0000000..3b86af5 --- /dev/null +++ b/gg.c @@ -0,0 +1,429 @@ +/* + * Copyright (c) 2021 Omar Polo + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "gmid.h" + +#include + +#include +#include +#include +#include + +enum debug { + DEBUG_NONE, + DEBUG_CODE, + DEBUG_HEADER, + DEBUG_META, + DEBUG_WHOLE, +}; + +/* flags */ +int debug; +int dont_verify_name; +int flag2; +int flag3; +int nop; +int redirects = 5; +int timer; +int verbose; +const char *cert; +const char *key; +const char *proxy_host; +const char *proxy_port; +const char *sni; + +/* state */ +struct tls_config *tls_conf; + +static void +timeout(int signo) +{ + dprintf(2, "%s: timer expired\n", getprogname()); + exit(1); +} + +static void +load_tls_conf(void) +{ + if ((tls_conf = tls_config_new()) == NULL) + err(1, "tls_config_new"); + + tls_config_insecure_noverifycert(tls_conf); + if (dont_verify_name) + tls_config_insecure_noverifyname(tls_conf); + + if (flag2 && + tls_config_set_protocols(tls_conf, TLS_PROTOCOL_TLSv1_2) == -1) + errx(1, "can't set TLSv1.2"); + if (flag3 && + tls_config_set_protocols(tls_conf, TLS_PROTOCOL_TLSv1_3) == -1) + errx(1, "can't set TLSv1.3"); + + if (cert != NULL && + tls_config_set_keypair_file(tls_conf, cert, key) == -1) + errx(1, "can't load client certificate %s", cert); +} + +static void +connectto(struct tls *ctx, const char *host, const char *port) +{ + struct addrinfo hints, *res, *res0; + int error; + int saved_errno; + int s; + const char *cause = NULL; + const char *sname; + + if (proxy_host != NULL) { + host = proxy_host; + port = proxy_port; + } + + if ((sname = sni) == NULL) + sname = host; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + error = getaddrinfo(host, port, &hints, &res0); + if (error) + errx(1, "%s", gai_strerror(error)); + + s = -1; + for (res = res0; res != NULL; res = res->ai_next) { + s = socket(res->ai_family, res->ai_socktype, + res->ai_protocol); + if (s == -1) { + cause = "socket"; + continue; + } + + if (connect(s, res->ai_addr, res->ai_addrlen) == -1) { + cause = "connect"; + saved_errno = errno; + close(s); + errno = saved_errno; + s = -1; + continue; + } + + break; + } + + if (s == -1) + err(1, "%s: can't connect to %s:%s", cause, + host, port); + + freeaddrinfo(res0); + + if (tls_connect_socket(ctx, s, sname) == -1) + errx(1, "tls_connect_socket: %s", tls_error(ctx)); +} + +static void +doreq(struct tls *ctx, const char *buf) +{ + size_t s; + ssize_t w; + + s = strlen(buf); + while (s != 0) { + switch (w = tls_write(ctx, buf, s)) { + case 0: + case -1: + errx(1, "tls_write: %s", tls_error(ctx)); + case TLS_WANT_POLLIN: + case TLS_WANT_POLLOUT: + continue; + } + + s -= w; + buf += w; + } +} + +static size_t +dorep(struct tls *ctx, void *buf, size_t len) +{ + ssize_t w; + size_t tot = 0; + + while (len != 0) { + switch (w = tls_read(ctx, buf, len)) { + case 0: + return tot; + case -1: + errx(1, "tls_write: %s", tls_error(ctx)); + case TLS_WANT_POLLIN: + case TLS_WANT_POLLOUT: + continue; + } + + len -= w; + buf += w; + tot += w; + } + + return tot; +} + +static int +get(const char *r) +{ + struct tls *ctx; + struct iri iri; + int foundhdr = 0, code = -1, od; + char iribuf[GEMINI_URL_LEN]; + char req[GEMINI_URL_LEN]; + char buf[2048]; + const char *parse_err, *host, *port; + + if (strlcpy(iribuf, r, sizeof(iribuf)) >= sizeof(iribuf)) + errx(1, "iri too long: %s", r); + + if (strlcpy(req, r, sizeof(req)) >= sizeof(req)) + errx(1, "iri too long: %s", r); + + if (strlcat(req, "\r\n", sizeof(req)) >= sizeof(req)) + errx(1, "iri too long: %s", r); + + if (!parse_iri(iribuf, &iri, &parse_err)) + errx(1, "invalid IRI: %s", parse_err); + + if (nop) + errx(0, "IRI OK"); + + if ((ctx = tls_client()) == NULL) + errx(1, "can't create tls context"); + + if (tls_configure(ctx, tls_conf) == -1) + errx(1, "tls_configure: %s", tls_error(ctx)); + + host = iri.host; + port = "1965"; + if (*iri.port != '\0') + port = iri.port; + + connectto(ctx, host, port); + + od = 0; + while (!od) { + switch (tls_handshake(ctx)) { + case 0: + od = 1; + break; + case -1: + errx(1, "handshake: %s", tls_error(ctx)); + } + } + + if (verbose) + printf("%s", req); + + doreq(ctx, req); + + for (;;) { + char *t; + size_t len; + + len = dorep(ctx, buf, sizeof(buf)); + if (len == 0) + goto close; + + if (foundhdr) { + write(1, buf, len); + continue; + } + foundhdr = 1; + + if (memmem(buf, len, "\r\n", 2) == NULL) + errx(1, "invalid reply: no \\r\\n"); + if (!isdigit(buf[0]) || !isdigit(buf[1]) || buf[2] != ' ') + errx(1, "invalid reply: invalid response format"); + + code = (buf[0] - '0') * 10 + buf[1] - '0'; + + if (debug == DEBUG_CODE) { + printf("%d\n", code); + goto close; + } + + if (debug == DEBUG_HEADER) { + t = memmem(buf, len, "\r\n", 2); + assert(t != NULL); + *t = '\0'; + printf("%s\n", buf); + goto close; + } + + if (debug == DEBUG_META) { + t = memmem(buf, len, "\r\n", 2); + assert(t != NULL); + *t = '\0'; + printf("%s\n", buf+3); + goto close; + } + + if (debug == DEBUG_WHOLE) { + write(1, buf, len); + continue; + } + + /* skip the header */ + t = memmem(buf, len, "\r\n", 2); + assert(t != NULL); + t += 2; /* skip \r\n */ + len -= t - buf; + write(1, t, len); + } + +close: + od = tls_close(ctx); + if (od == TLS_WANT_POLLIN || od == TLS_WANT_POLLOUT) + goto close; + + tls_close(ctx); + tls_free(ctx); + return code; +} + +static void __attribute__((noreturn)) +usage(void) +{ + fprintf(stderr, "usage: %s [-23Nnv] [-C cert] [-d mode] [-H sni] " + "[-K key] [-P proxy]\n", + getprogname()); + fprintf(stderr, " [-T seconds] gemini://...\n"); + exit(1); +} + +static int +parse_debug(const char *arg) +{ + if (!strcmp(arg, "none")) + return DEBUG_NONE; + if (!strcmp(arg, "code")) + return DEBUG_CODE; + if (!strcmp(arg, "header")) + return DEBUG_HEADER; + if (!strcmp(arg, "meta")) + return DEBUG_META; + if (!strcmp(arg, "whole")) + return DEBUG_WHOLE; + usage(); +} + +static void +parse_proxy(const char *arg) +{ + char *at; + + if ((proxy_host = strdup(arg)) == NULL) + err(1, "strdup"); + + proxy_port = "1965"; + + if ((at = strchr(proxy_host, ':')) == NULL) + return; + *at = '\0'; + proxy_port = ++at; + + if (strchr(proxy_port, ':') != NULL) + errx(1, "invalid port %s", proxy_port); +} + +int +main(int argc, char **argv) +{ + int ch, code; + const char *errstr; + + while ((ch = getopt(argc, argv, "23C:d:H:K:NP:T:v")) != -1) { + switch (ch) { + case '2': + flag2 = 1; + break; + case '3': + flag3 = 1; + break; + case 'C': + cert = optarg; + break; + case 'd': + debug = parse_debug(optarg); + break; + case 'H': + sni = optarg; + break; + case 'K': + key = optarg; + break; + case 'N': + dont_verify_name = 1; + break; + case 'n': + nop = 1; + break; + case 'P': + parse_proxy(optarg); + dont_verify_name = 1; + break; + case 'T': + timer = strtonum(optarg, 1, 1000, &errstr); + if (errstr != NULL) + errx(1, "timeout is %s: %s", + errstr, optarg); + signal(SIGALRM, timeout); + alarm(timer); + break; + case 'v': + verbose++; + break; + default: + usage(); + } + } + argc -= optind; + argv += optind; + + if (flag2 + flag3 > 1) { + warnx("only -2 or -3 can be specified at the same time"); + usage(); + } + + if ((cert != NULL && key == NULL) || + (cert == NULL && key != NULL)) { + warnx("cert or key is missing"); + usage(); + } + + signal(SIGPIPE, SIG_IGN); + + load_tls_conf(); + +#ifdef __OpenBSD__ + if (pledge("stdio inet dns", NULL) == -1) + err(1, "pledge"); +#endif + + if (argc != 1) + usage(); + + code = get(*argv); + + return code < 20 || code >= 30; +} -- cgit v1.2.3