diff options
author | Omar Polo <op@omarpolo.com> | 2023-07-22 15:47:02 +0000 |
---|---|---|
committer | Omar Polo <op@omarpolo.com> | 2023-07-22 15:47:02 +0000 |
commit | 2ff1e2a9237514d5d473b2b3562ec767542b55bc (patch) | |
tree | 50409b81b7e123b9885de0dd54d8b32387dc95d1 /titan.c | |
parent | 81bab00246906b389f6e7d162196eb4a36b848c9 (diff) |
add titan(1) -- a draft titan client
Diffstat (limited to 'titan.c')
-rw-r--r-- | titan.c | 303 |
1 files changed, 303 insertions, 0 deletions
@@ -0,0 +1,303 @@ +/* + * Copyright (c) 2023 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 "config.h" + +#include <sys/stat.h> +#include <sys/socket.h> + +#include <errno.h> +#include <fcntl.h> +#include <netdb.h> +#include <poll.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <tls.h> +#include <unistd.h> + +#include "iri.h" + +static int +dial(const char *hostname, const char *port) +{ + struct addrinfo hints, *res, *res0; + int error, save_errno, s; + const char *cause = NULL; + + if (port == NULL || *port == '\0') + port = "1965"; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + error = getaddrinfo(hostname, port, &hints, &res0); + if (error) + errx(1, "can't resolve %s: %s", hostname, gai_strerror(error)); + + s = -1; + for (res = res0; res; 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"; + save_errno = errno; + close(s); + errno = save_errno; + s = -1; + continue; + } + + if (fcntl(s, F_SETFL, O_NONBLOCK) == -1) + err(1, "fcntl"); + break; /* got one */ + } + if (s == -1) + err(1, "%s", cause); + freeaddrinfo(res0); + return (s); +} + +/* returns read bytes, or -1 on error */ +static ssize_t +iomux(struct tls *ctx, int fd, const char *in, size_t inlen, char *out, + size_t outlen) +{ + struct pollfd pfd; + size_t outwrote = 0; + ssize_t ret; + + memset(&pfd, 0, sizeof(pfd)); + pfd.fd = fd; + pfd.events = POLLIN|POLLOUT; + + for (;;) { + if (poll(&pfd, 1, INFTIM) == -1) + err(1, "poll"); + if (pfd.revents & (POLLERR|POLLNVAL)) + errx(1, "bad fd %d", pfd.fd); + + /* attempt to read */ + if (out != NULL) { + switch (ret = tls_read(ctx, out, outlen)) { + case TLS_WANT_POLLIN: + case TLS_WANT_POLLOUT: + break; + case -1: + return -1; + case 0: + return outwrote; + default: + outwrote += ret; + out += ret; + outlen -= ret; + } + + /* + * don't write if we're reading; titan works + * like this. + */ + if (outwrote != 0) + continue; + } + + if (inlen == 0 && out == NULL) + break; + if (inlen == 0) + continue; + + switch (ret = tls_write(ctx, in, inlen)) { + case TLS_WANT_POLLIN: + case TLS_WANT_POLLOUT: + continue; + case 0: + case -1: + return -1; + default: + in += ret; + inlen -= ret; + } + } + + return 0; +} + +static void __dead +usage(void) +{ + fprintf(stderr, + "usage: %s [-C cert] [-K key] [-m mime] [-t token] url file\n", + getprogname()); + exit(1); +} + +int +main(int argc, char **argv) +{ + struct tls_config *config; + struct tls *ctx; + struct stat sb; + struct iri iri; + FILE *in; + const char *cert = NULL, *key = NULL, *mime = NULL, *token = NULL; + const char *parse_err; + char iribuf[1025]; + char resbuf[1025]; + char *req; + int sock, ch; + + if (pledge("stdio rpath inet dns", NULL) == -1) + err(1, "pledge"); + + while ((ch = getopt(argc, argv, "C:K:m:t:")) != -1) { + switch (ch) { + case 'C': + cert = optarg; + break; + case 'K': + key = optarg; + break; + case 'm': + mime = optarg; + break; + case 't': + token = optarg; + break; + } + } + argc -= optind; + argv += optind; + + if (cert == NULL && key != NULL) + usage(); + if (cert != NULL && key == NULL) + key = cert; + + if (argc != 2) + usage(); + + if ((in = fopen(argv[1], "r")) == NULL) + err(1, "can't open %s", argv[1]); + + /* drop rpath */ + if (pledge("stdio inet dns", NULL) == -1) + err(1, "pledge"); + + if (fstat(fileno(in), &sb) == -1) + err(1, "fstat"); + + /* prepare the URL */ + if (token && mime) { + if (asprintf(&req, "%s;size=%lld;token=%s;mime=%s\r\n", argv[0], + (long long)sb.st_size, token, mime) == -1) + err(1, "asprintf"); + } else if (token) { + if (asprintf(&req, "%s;size=%lld;token=%s\r\n", argv[0], + (long long)sb.st_size, token) == -1) + err(1, "asprintf"); + } else if (mime) { + if (asprintf(&req, "%s;size=%lld;mime=%s\r\n", argv[0], + (long long)sb.st_size, mime) == -1) + err(1, "asprintf"); + } else { + if (asprintf(&req, "%s;size=%lld\r\n", argv[0], + (long long)sb.st_size) == -1) + err(1, "asprintf"); + } + + if (strlcpy(iribuf, argv[0], sizeof(iribuf)) >= sizeof(iribuf)) + errx(1, "URL too long"); + + if (!parse_iri(iribuf, &iri, &parse_err)) + errx(1, "invalid IRI: %s", parse_err); + + if ((config = tls_config_new()) == NULL) + err(1, "tls_config_new"); + tls_config_insecure_noverifycert(config); + tls_config_insecure_noverifyname(config); + + if (cert && tls_config_set_keypair_file(config, cert, key) == -1) + errx(1, "cant load certificate client %s", cert); + + if ((ctx = tls_client()) == NULL) + errx(1, "can't create tls context"); + + if (tls_configure(ctx, config) == -1) + errx(1, "tls_configure: %s", tls_error(ctx)); + + sock = dial(iri.host, iri.port); + if (tls_connect_socket(ctx, sock, iri.host) == -1) + errx(1, "failed to connect to %s:%s: %s", iri.host, + *iri.port == '\0' ? "1965" : iri.port, tls_error(ctx)); + + /* drop inet tls */ + if (pledge("stdio", NULL) == -1) + err(1, "pledge"); + + /* send request */ + if (iomux(ctx, sock, req, strlen(req), NULL, 0) == -1) + errx(1, "I/O error: %s", tls_error(ctx)); + + for (;;) { + static char buf[BUFSIZ]; + size_t buflen; + ssize_t w; + char *m; + + /* will be zero on EOF */ + buflen = fread(buf, 1, sizeof(buf), in); + + w = iomux(ctx, sock, buf, buflen, resbuf, sizeof(resbuf)); + if (w == -1) + errx(1, "I/O error: %s", tls_error(ctx)); + if (w != 0) { + if ((m = memmem(resbuf, w, "\r\n", 2)) == NULL) + errx(1, "invalid reply"); + *m = '\0'; + /* XXX parse */ + puts(resbuf); + break; + } + } + + /* close connection */ + for (;;) { + struct pollfd pfd; + + memset(&pfd, 0, sizeof(pfd)); + pfd.fd = sock; + pfd.events = POLLIN|POLLOUT; + + switch (tls_close(ctx)) { + case TLS_WANT_POLLIN: + case TLS_WANT_POLLOUT: + if (poll(&pfd, 1, INFTIM) == -1) + err(1, "poll"); + break; + case -1: + warnx("tls_close: %s", tls_error(ctx)); + /* fallthrough */ + default: + tls_free(ctx); + return (0); + } + } +} |