git.y1.nz

hurl

Simple gopher and http(s) client.
download: http://git.y1.nz/archives/hurl.tar.gz
README | Files | Log | Refs | LICENSE

commit f486580c45c694df83a78bb772c37f79322d5ac2
parent 04cee379643425520034c1262e58aa52f428a6a1
Author: Hiltjo Posthuma <hiltjo@codemadness.org>
Date:   Mon, 12 Sep 2022 23:18:43 +0200

various improvements(tm)

- Support setting a CA file. Useful for self-signed certificates.
- Fix type warnings with -Wextra.
- Fix checking the return value of alarm(), it doesn't return -1.
- Refactor protocol handlers and reuse code for setting up a TLS and plain
  connection.
- Some code cleanups.

Diffstat:
MREADME10++++++++++
Mhurl.15++++-
Mhurl.c269++++++++++++++++++++++++++++++++++++-------------------------------------------
3 files changed, 138 insertions(+), 146 deletions(-)

diff --git a/README b/README @@ -56,3 +56,13 @@ Anti-features - No Gopher text handling (".\r\n"). - ... etc... + + +Get self-signed certificates of a host +-------------------------------------- + +To show the certificates of a host the following openssl command can be used. +This can be used to create a CA file also, useful for self-signed certificates: + + openssl s_client -showcerts -connect "codemadness.org:443" </dev/null | \ + sed -n '/^-----BEGIN CERTIFICATE-----/,/^-----END CERTIFICATE-----/p' diff --git a/hurl.1 b/hurl.1 @@ -1,4 +1,4 @@ -.Dd December 13, 2019 +.Dd September 13, 2022 .Dt HURL 1 .Os .Sh NAME @@ -6,6 +6,7 @@ .Nd HTTP/HTTPS/Gopher file grabber .Sh SYNOPSIS .Nm +.Op Fl c Ar ca_file .Op Fl H Ar headers .Op Fl l .Op Fl m Ar filesize @@ -19,6 +20,8 @@ supports the protocols: HTTP, HTTPS and Gopher. .Pp The options are as follows: .Bl -tag -width Ds +.It Fl c Ar ca_file +Set CA certificate file to use for TLS connections. .It Fl H Ar headers Add headers to HTTP or HTTPS requests. This is the raw data appended to the request header. diff --git a/hurl.c b/hurl.c @@ -49,13 +49,37 @@ static size_t config_maxresponsesiz = 0; static long long config_timeout = 0; /* legacy ciphers? */ static int config_legacy = 0; -/* parsed uri */ +/* TLS CA file */ +static char *config_ca_file; +/* parsed URI */ static struct uri u; +/* socket fd */ +static int sock = -1; /* raw command-line argument */ static char *url; +/* TLS context */ +static struct tls *t; /* TLS config */ static struct tls_config *tls_config; +/* protocol handlers */ +int gopher_request(void); +int gophers_request(void); +int http_request(void); +int https_request(void); + +static const struct { + int (*handler)(void); /* function / handler / callback */ + const char *proto; /* protocol / scheme, "gopher://" */ + const char *port; /* default port */ + int usetls; /* setup TLS (=1) or plain connection (=0) */ +} handlers[] = { + { .handler = gopher_request, .proto = "gopher://", .port = "70", .usetls = 0 }, + { .handler = gophers_request, .proto = "gophers://", .port = "70", .usetls = 1 }, + { .handler = http_request, .proto = "http://", .port = "80", .usetls = 0 }, + { .handler = https_request, .proto = "https://", .port = "443", .usetls = 1 }, +}; + void sighandler(int signo) { @@ -241,39 +265,60 @@ edial(const char *host, const char *port) return s; } -int -https_request(void) +void +setupplain(void) { - struct tls *t; - char buf[READ_BUF_SIZ], *p; - const char *errstr; - size_t bodylen, expectedlen, n, len; - ssize_t r; - int cs, fd = -1, httpok = 0, ret = 1, stdport; - - if (pledge("stdio dns inet rpath unveil", NULL) == -1) + if (pledge("stdio dns inet", NULL) == -1) err(1, "pledge"); - if (unveil(TLS_CA_CERT_FILE, "r") == -1) - err(1, "unveil: %s", TLS_CA_CERT_FILE); - if (unveil(NULL, NULL) == -1) - err(1, "unveil"); + sock = edial(u.host, u.port); +} - if (!(t = tls_client())) { - fprintf(stderr, "tls_client: %s\n", tls_error(t)); - goto err; - } - if (tls_configure(t, tls_config)) { - fprintf(stderr, "tls_configure: %s\n", tls_error(t)); - goto err; +void +setuptls(void) +{ + if (tls_init()) + errx(1, "tls_init failed"); + if (!(tls_config = tls_config_new())) + errx(1, "tls config failed"); + if (config_legacy) { + /* enable legacy cipher and negotiation. */ + if (tls_config_set_ciphers(tls_config, "legacy")) + errx(1, "tls_config_set_ciphers: %s", + tls_config_error(tls_config)); + } + if (config_ca_file) { + if (unveil(config_ca_file, "r") == -1) + err(1, "unveil: %s", config_ca_file); + if (tls_config_set_ca_file(tls_config, config_ca_file) == -1) + errx(1, "tls_config_set_ca_file: %s: %s", config_ca_file, + tls_config_error(tls_config)); + } else { + if (unveil(TLS_CA_CERT_FILE, "r") == -1) + err(1, "unveil: %s", TLS_CA_CERT_FILE); } - fd = edial(u.host, u.port); - if (tls_connect_socket(t, fd, u.host) == -1) + if (pledge("stdio dns inet rpath", NULL) == -1) + err(1, "pledge"); + + if (!(t = tls_client())) + errx(1, "tls_client: %s", tls_error(t)); + if (tls_configure(t, tls_config)) + errx(1, "tls_configure: %s", tls_error(t)); + + sock = edial(u.host, u.port); + if (tls_connect_socket(t, sock, u.host) == -1) errx(1, "tls_connect: %s", tls_error(t)); +} - if (pledge("stdio", NULL) == -1) - err(1, "pledge"); +int +https_request(void) +{ + char buf[READ_BUF_SIZ], *p; + const char *errstr; + size_t bodylen, expectedlen, n, len; + ssize_t r; + int cs, httpok = 0, ret = 1, stdport; stdport = u.port[0] == '\0' || strcmp(u.port, "443") == 0; @@ -340,7 +385,7 @@ https_request(void) if (httpok) { n = len - (p - buf); - r = fwrite(p, 1, n, stdout); + fwrite(p, 1, n, stdout); if (ferror(stdout)) { fprintf(stderr, "fwrite: stdout: %s\n", strerror(errno)); goto err; @@ -367,7 +412,7 @@ https_request(void) bodylen += r; if (httpok) { - r = fwrite(buf, 1, r, stdout); + fwrite(buf, 1, r, stdout); if (ferror(stdout)) { fprintf(stderr, "fwrite: stdout: %s\n", strerror(errno)); goto err; @@ -390,11 +435,6 @@ https_request(void) ret = 0; err: - if (t) { - tls_close(t); - tls_free(t); - } - return httpok ? ret : 2; } @@ -404,15 +444,7 @@ http_request(void) char buf[READ_BUF_SIZ], *p; size_t bodylen, expectedlen, n, len; ssize_t r; - int cs, fd = -1, httpok = 0, ret = 1, stdport; - - if (pledge("stdio dns inet", NULL) == -1) - err(1, "pledge"); - - fd = edial(u.host, u.port); - - if (pledge("stdio", NULL) == -1) - err(1, "pledge"); + int cs, httpok = 0, ret = 1, stdport; stdport = u.port[0] == '\0' || strcmp(u.port, "80") == 0; @@ -433,7 +465,7 @@ http_request(void) fprintf(stderr, "not writing header because it is truncated"); goto err; } - if ((r = write(fd, buf, r)) == -1) { + if (write(sock, buf, r) == -1) { fprintf(stderr, "write: %s\n", strerror(errno)); goto err; } @@ -442,7 +474,7 @@ http_request(void) for (len = 0; len < sizeof(buf); len += r) { /* NOTE: buffer size is -1 to NUL terminate the buffer for a string comparison. */ - if ((r = read(fd, &buf[len], sizeof(buf) - len - 1)) == 0) + if ((r = read(sock, &buf[len], sizeof(buf) - len - 1)) == 0) break; if (r == -1) { fprintf(stderr, "read: %s\n", strerror(errno)); @@ -466,7 +498,7 @@ http_request(void) if (httpok) { n = len - (p - buf); - r = fwrite(p, 1, n, stdout); + fwrite(p, 1, n, stdout); if (ferror(stdout)) { fprintf(stderr, "fwrite: stdout: %s\n", strerror(errno)); goto err; @@ -479,7 +511,7 @@ http_request(void) } while (1) { - r = read(fd, &buf, sizeof(buf)); + r = read(sock, &buf, sizeof(buf)); if (r == 0) break; if (r == -1) { @@ -490,7 +522,7 @@ http_request(void) bodylen += r; if (httpok) { - r = fwrite(buf, 1, r, stdout); + fwrite(buf, 1, r, stdout); if (ferror(stdout)) { fprintf(stderr, "fwrite: stdout: %s\n", strerror(errno)); goto err; @@ -513,8 +545,6 @@ http_request(void) ret = 0; err: - if (fd != -1) - close(fd); return httpok ? ret : 2; } @@ -525,15 +555,7 @@ gopher_request(void) const char *path; size_t len = 0; ssize_t r; - int fd = -1, ret = 1; - - if (pledge("stdio dns inet", NULL) == -1) - err(1, "pledge"); - - fd = edial(u.host, u.port); - - if (pledge("stdio", NULL) == -1) - err(1, "pledge"); + int ret = 1; /* create and send path, skip type part, empty path is allowed, see RFC 4266 The gopher URI Scheme - section 2.1 */ @@ -551,13 +573,13 @@ gopher_request(void) goto err; } - if ((r = write(fd, buf, strlen(buf))) == -1) { + if (write(sock, buf, strlen(buf)) == -1) { fprintf(stderr, "write: %s\n", strerror(errno)); goto err; } while (1) { - r = read(fd, &buf, sizeof(buf)); + r = read(sock, &buf, sizeof(buf)); if (r == 0) { break; } else if (r == -1) { @@ -566,7 +588,7 @@ gopher_request(void) } len += r; - r = fwrite(buf, 1, r, stdout); + fwrite(buf, 1, r, stdout); if (ferror(stdout)) { fprintf(stderr, "fwrite: stdout: %s\n", strerror(errno)); goto err; @@ -583,46 +605,17 @@ gopher_request(void) ret = 0; err: - if (fd != -1) - close(fd); return ret; } int gophers_request(void) { - struct tls *t; char buf[READ_BUF_SIZ], *p; const char *errstr, *path; size_t len = 0; ssize_t r; - int fd = -1, ret = 1; - - if (pledge("stdio dns inet rpath unveil", NULL) == -1) - err(1, "pledge"); - - if (unveil(TLS_CA_CERT_FILE, "r") == -1) - err(1, "unveil: %s", TLS_CA_CERT_FILE); - if (unveil(NULL, NULL) == -1) - err(1, "unveil"); - - if (!(t = tls_client())) { - errstr = tls_error(t); - fprintf(stderr, "tls_client: %s\n", errstr ? errstr : ""); - goto err; - } - if (tls_configure(t, tls_config)) { - errstr = tls_error(t); - fprintf(stderr, "tls_configure: %s\n", errstr ? errstr : ""); - goto err; - } - - fd = edial(u.host, u.port); - if (tls_connect_socket(t, fd, u.host) == -1) - errx(1, "tls_connect: %s", tls_error(t)); - - if (pledge("stdio", NULL) == -1) - err(1, "pledge"); + int ret = 1; /* create and send path, skip type part, empty path is allowed, see RFC 4266 The gopher URI Scheme - section 2.1 */ @@ -665,7 +658,7 @@ gophers_request(void) } len += r; - r = fwrite(buf, 1, r, stdout); + fwrite(buf, 1, r, stdout); if (ferror(stdout)) { fprintf(stderr, "fwrite: stdout: %s\n", strerror(errno)); goto err; @@ -682,21 +675,14 @@ gophers_request(void) ret = 0; err: - if (t) { - tls_close(t); - tls_free(t); - } - - if (fd != -1) - close(fd); return ret; } void usage(void) { - fprintf(stderr, "usage: %s [-H headers] [-l] [-m maxresponse] " - "[-t timeout] url\n", argv0); + fprintf(stderr, "usage: %s [-c ca_file] [-H headers] [-l] " + "[-m maxresponse] [-t timeout] url\n", argv0); exit(1); } @@ -704,10 +690,14 @@ int main(int argc, char **argv) { char *end; + size_t i; int statuscode; long long l; ARGBEGIN { + case 'c': + config_ca_file = EARGF(usage()); + break; case 'H': /* header(s) */ config_headers = EARGF(usage()); break; @@ -743,52 +733,41 @@ main(int argc, char **argv) if (config_timeout > 0) { signal(SIGALRM, sighandler); - if (alarm(config_timeout) == -1) - err(1, "alarm"); - } - - if (!strcmp(u.proto, "https://")) { - if (tls_init()) - errx(1, "tls_init failed"); - if (!(tls_config = tls_config_new())) - errx(1, "tls config failed"); - if (config_legacy) { - /* enable legacy cipher and negotiation. */ - if (tls_config_set_ciphers(tls_config, "legacy")) - errx(1, "tls set ciphers failed: %s", - tls_config_error(tls_config)); - } - if (!u.port[0]) - memcpy(u.port, "443", 4); - statuscode = https_request(); - } else if (!strcmp(u.proto, "http://")) { - if (!u.port[0]) - memcpy(u.port, "80", 3); - statuscode = http_request(); - } else if (!strcmp(u.proto, "gopher://")) { - if (!u.port[0]) - memcpy(u.port, "70", 3); - statuscode = gopher_request(); - } else if (!strcmp(u.proto, "gophers://")) { - if (tls_init()) - errx(1, "tls_init failed"); - if (!(tls_config = tls_config_new())) - errx(1, "tls config failed"); - if (config_legacy) { - /* enable legacy cipher and negotiation. */ - if (tls_config_set_ciphers(tls_config, "legacy")) - errx(1, "tls set ciphers failed: %s", - tls_config_error(tls_config)); - } + alarm(config_timeout); + } + + /* match a protocol handler */ + for (i = 0; i < sizeof(handlers) / sizeof(*handlers); i++) { + if (strcmp(u.proto, handlers[i].proto)) + continue; if (!u.port[0]) - memcpy(u.port, "70", 3); - statuscode = gophers_request(); - } else { - if (u.proto[0]) - errx(1, "unsupported protocol specified: %s", u.proto); + strcpy(u.port, handlers[i].port); /* default port if unset */ + + /* setup TLS or plain connection */ + if (handlers[i].usetls) + setuptls(); else - errx(1, "no protocol specified"); + setupplain(); + + if (pledge("stdio", NULL) == -1) + err(1, "pledge"); + + statuscode = handlers[i].handler(); + + /* cleanup TLS and plain connection */ + if (t) { + tls_close(t); + tls_free(t); + } + if (sock != -1) + close(sock); + + return statuscode; } + if (u.proto[0]) + errx(1, "unsupported protocol specified: %s", u.proto); + else + errx(1, "no protocol specified"); - return statuscode; + return 1; }

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.