Commit Diff


commit - fe903d30e76e89e5242c517dba13f074e645a4ad
commit + 5c7abf01515677804eeb2cf083e33e4ddd742caf
blob - 6af9942cf1a5f4b1a2829b0f6161f20636db6082
blob + fd02c845f22019234d4c77477fc88bd89b3c3fb6
--- Makefile
+++ 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}
 
blob - /dev/null
blob + 36453d8e1fdb88f96c6405f317dae66b3d39573e (mode 644)
--- /dev/null
+++ gg.1
@@ -0,0 +1,114 @@
+.\" Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+.\"
+.\" 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.
blob - /dev/null
blob + 3b86af5fac0734d5bba43ef7f12155f7af2c514d (mode 644)
--- /dev/null
+++ gg.c
@@ -0,0 +1,429 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * 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 <sys/socket.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <string.h>
+
+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;
+}