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;
        }