aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOmar Polo <op@omarpolo.com>2021-12-29 18:01:08 +0000
committerOmar Polo <op@omarpolo.com>2021-12-29 18:01:08 +0000
commit5c7abf01515677804eeb2cf083e33e4ddd742caf (patch)
treeb9ba79695a9b3cf735515e9b5a171a2b9c3f808d
parentfe903d30e76e89e5242c517dba13f074e645a4ad (diff)
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.
-rw-r--r--Makefile5
-rw-r--r--gg.1114
-rw-r--r--gg.c429
3 files changed, 547 insertions, 1 deletions
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 <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.
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 <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;
+}