various improvements(tm) - hurl - Gopher/HTTP/HTTPS file grabber
(HTM) git clone git://git.codemadness.org/hurl
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
(DIR) LICENSE
---
(DIR) commit f486580c45c694df83a78bb772c37f79322d5ac2
(DIR) parent 04cee379643425520034c1262e58aa52f428a6a1
(HTM) 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:
M README | 10 ++++++++++
M hurl.1 | 5 ++++-
M hurl.c | 269 ++++++++++++++-----------------
3 files changed, 138 insertions(+), 146 deletions(-)
---
(DIR) 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'
(DIR) 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.
(DIR) 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;
}