nkiss

Unnamed repository; edit this file 'description' to name the repository.
git clone git://git.nihaljere.xyz/nkiss
Log | Files | Refs

commit b23c89a9ebe5e1af347df25af6465cd26e98d933
parent 7351021083357735e5f72224e6110a89bff4d08a
Author: Nihal Jere <nihal@nihaljere.xyz>
Date:   Wed, 17 Feb 2021 20:48:22 -0600

http.*: add http(s) implementation

The http(s) implementation is ripped straight out of hurl (hiltjo)
and modified to allow for usage as a library. There is more work to be
done (for example redirection handling and timeouts) before it is
complete.

Diffstat:
Ahttp.c | 418+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahttp.h | 1+
2 files changed, 419 insertions(+), 0 deletions(-)

diff --git a/http.c b/http.c @@ -0,0 +1,418 @@ +#include <ctype.h> +#include <errno.h> +#include <libgen.h> +#include <netdb.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <tls.h> +#include <unistd.h> + +#include "util.h" + +#define READ_BUF_SIZ 16384 + +#ifndef TLS_CA_CERT_FILE +#define TLS_CA_CERT_FILE "/etc/ssl/cert.pem" +#endif + +struct uri { + char proto[48]; + char host[256]; + char path[2048]; + char port[6]; /* numeric port */ +}; + +/* time-out in seconds */ +static time_t config_timeout = 0; +/* parsed uri */ +static struct uri u; +/* raw command-line argument */ +static char *url; +/* TLS config */ +static struct tls_config *tls_config; +/* where to write to */ +FILE *dest; + +static void +sighandler(int signo) +{ + if (signo == SIGALRM) + _exit(2); +} + +static int +parseuri(const char *s, struct uri *u) +{ + const char *p = s, *b; + char *endptr = NULL; + size_t i; + unsigned long l; + + u->proto[0] = u->host[0] = u->path[0] = u->port[0] = '\0'; + if (!*p) + return 0; + + /* protocol part */ + for (p = s; *p && (isalpha((unsigned char)*p) || isdigit((unsigned char)*p) || + *p == '+' || *p == '-' || *p == '.'); p++) + ; + if (!strncmp(p, "://", 3)) { + if ((size_t)(p - s) >= sizeof(u->proto)) + return -1; /* protocol too long */ + memcpy(u->proto, s, p - s); + u->proto[p - s] = '\0'; + p += 3; /* skip "://" */ + } else { + return -1; /* no protocol specified */ + } + + /* IPv6 address */ + if (*p == '[') { + /* bracket not found or host too long */ + if (!(b = strchr(p, ']')) || (size_t)(b - p) >= (ssize_t)sizeof(u->host)) + return -1; + memcpy(u->host, p + 1, b - p - 1); + u->host[b - p - 1] = '\0'; + p = b + 1; + } else { + /* domain / host part, skip until port, path or end. */ + if ((i = strcspn(p, ":/")) >= sizeof(u->host)) + return -1; /* host too long */ + memcpy(u->host, p, i); + u->host[i] = '\0'; + p = &p[i]; + } + /* port */ + if (*p == ':') { + if ((i = strcspn(++p, "/")) >= sizeof(u->port)) + return -1; /* port too long */ + memcpy(u->port, p, i); + u->port[i] = '\0'; + /* check for valid port: range 1 - 65535 */ + errno = 0; + l = strtoul(u->port, &endptr, 10); + if (errno || u->port[0] == '\0' || *endptr || + !l || l > 65535) + return -1; + p = &p[i]; + } + if (u->host[0]) { + p = &p[strspn(p, "/")]; + memcpy(u->path, "/", 2); + } else { + return -1; + } + /* treat truncation as an error */ + if (strlcat(u->path, p, sizeof(u->path)) >= sizeof(u->path)) + return -1; + return 0; +} + +static int +edial(const char *host, const char *port) +{ + struct addrinfo hints, *res, *res0; + int error, save_errno, s; + const char *cause = NULL; + struct timeval timeout; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_NUMERICSERV; /* numeric port only */ + if ((error = getaddrinfo(host, port, &hints, &res0))) + die("%s: %s: %s:%s", __func__, gai_strerror(error), host, port); + 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; + } + break; + } + if (s == -1) + die("%s: %s: %s:%s", __func__, cause, host, port); + freeaddrinfo(res0); + + return s; +} + +static int +https_request(void) +{ + struct tls *t; + char buf[READ_BUF_SIZ], *p; + const char *errstr; + size_t n, len; + ssize_t r; + int fd = -1, httpok = 0, ret = 1, stdport; + + 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; + } + + fd = edial(u.host, u.port); + if (tls_connect_socket(t, fd, u.host) == -1) + die("tls_connect: %s", tls_error(t)); + + stdport = u.port[0] == '\0' || strcmp(u.port, "443") == 0; + + /* create and send HTTP header */ + r = snprintf(buf, sizeof(buf), + "GET %s HTTP/1.0\r\n" + "Host: %s%s%s\r\n" + "Connection: close\r\n" + "\r\n", u.path, u.host, + stdport ? "" : ":", + stdport ? "" : u.port); + if (r < 0 || (size_t)r >= sizeof(buf)) { + fprintf(stderr, "not writing header because it is truncated"); + goto err; + } + + for (len = r, p = buf; len > 0; ) { + r = tls_write(t, p, len); + if (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT) { + continue; + } else if (r == -1) { + fprintf(stderr, "tls_write: %s\n", tls_error(t)); + goto err; + } + p += r; + len -= r; + } + + /* NOTE: HTTP header must fit in the buffer */ + for (len = 0; len < sizeof(buf);) { + /* NOTE: buffer size is -1 to NUL terminate the buffer for a + string comparison. */ + r = tls_read(t, &buf[len], sizeof(buf) - len - 1); + if (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT) { + continue; + } else if (r == 0) { + break; + } else if (r == -1) { + errstr = tls_error(t); + fprintf(stderr, "tls_read: %s\n", errstr ? errstr : ""); + goto err; + } + len += r; + } + buf[len] = '\0'; + + if (!strncmp(buf, "HTTP/1.0 200 ", sizeof("HTTP/1.0 200 ") - 1) || + !strncmp(buf, "HTTP/1.1 200 ", sizeof("HTTP/1.1 200 ") - 1)) + httpok = 1; + + if (!(p = strstr(buf, "\r\n\r\n"))) { + fprintf(stderr, "no HTTP header found or header too big\n"); + goto err; + } + *p = '\0'; /* NUL terminate header part */ + p += strlen("\r\n\r\n"); + + if (httpok) { + n = len - (p - buf); + r = fwrite(p, 1, n, dest); + if (ferror(dest)) { + fprintf(stderr, "fwrite: %s\n", strerror(errno)); + goto err; + } + } else { + /* if not 200 OK print header */ + fputs(buf, stderr); + fputs("\r\n\r\n", stderr); + /* NOTE: we are nice and keep reading (not closing) until the server is done. */ + } + + while (1) { + r = tls_read(t, &buf, sizeof(buf)); + if (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT) { + continue; + } else if (r == 0) { + break; + } else if (r == -1) { + errstr = tls_error(t); + fprintf(stderr, "tls_read: %s\n", errstr ? errstr : ""); + goto err; + } + len += r; + + if (httpok) { + r = fwrite(buf, 1, r, dest); + if (ferror(dest)) { + fprintf(stderr, "fwrite: %s\n", strerror(errno)); + goto err; + } + } + + } + ret = 0; + +err: + if (t) { + tls_close(t); + tls_free(t); + } + + return httpok ? ret : 2; +} + +static int +http_request(void) +{ + char buf[READ_BUF_SIZ], *p; + size_t n, len; + ssize_t r; + int fd = -1, httpok = 0, ret = 1, stdport; + + fd = edial(u.host, u.port); + + stdport = u.port[0] == '\0' || strcmp(u.port, "80") == 0; + + /* create and send HTTP header */ + r = snprintf(buf, sizeof(buf), + "GET %s HTTP/1.0\r\n" + "Host: %s%s%s\r\n" + "Connection: close\r\n" + "\r\n", u.path, u.host, + stdport ? "" : ":", + stdport ? "" : u.port); + if (r < 0 || (size_t)r >= sizeof(buf)) { + fprintf(stderr, "not writing header because it is truncated"); + goto err; + } + if ((r = write(fd, buf, r)) == -1) { + fprintf(stderr, "write: %s\n", strerror(errno)); + goto err; + } + + /* NOTE: HTTP header must fit in the buffer */ + 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) + break; + if (r == -1) { + fprintf(stderr, "read: %s\n", strerror(errno)); + goto err; + } + } + buf[len] = '\0'; + + if (!strncmp(buf, "HTTP/1.0 200 ", sizeof("HTTP/1.0 200 ") - 1) || + !strncmp(buf, "HTTP/1.1 200 ", sizeof("HTTP/1.1 200 ") - 1)) + httpok = 1; + + if (!(p = strstr(buf, "\r\n\r\n"))) { + fprintf(stderr, "no HTTP header found or header too big\n"); + goto err; + } + *p = '\0'; /* NUL terminate header part */ + p += strlen("\r\n\r\n"); + + if (httpok) { + n = len - (p - buf); + r = fwrite(p, 1, n, dest); + if (ferror(dest)) { + fprintf(stderr, "fwrite: %s\n", strerror(errno)); + goto err; + } + } else { + /* if not 200 OK print header */ + fputs(buf, stderr); + fputs("\r\n\r\n", stderr); + /* NOTE: we are nice and keep reading (not closing) until the server is done. */ + } + + while (1) { + r = read(fd, &buf, sizeof(buf)); + if (r == 0) + break; + if (r == -1) { + fprintf(stderr, "read: %s\n", strerror(errno)); + goto err; + } + len += r; + + if (httpok) { + r = fwrite(buf, 1, r, dest); + if (ferror(dest)) { + fprintf(stderr, "fwrite: %s\n", strerror(errno)); + goto err; + } + } + + } + ret = 0; + +err: + if (fd != -1) + close(fd); + return httpok ? ret : 2; +} + +int +http_fetch(char *loc, char *path) +{ + char *end; + int statuscode; + long long l; + FILE *file; + + url = loc; + if (parseuri(url, &u) == -1) + die("invalid url: %s", url); + + if (config_timeout > 0) { + signal(SIGALRM, sighandler); + } + + mkdirs(dirname(path)); + + if ((dest = fopen(path, "w")) == NULL) + die("%s: failed to open %s:", __func__, path); + + if (!strcmp(u.proto, "https")) { + if (tls_init()) + die("tls_init failed"); + if (!(tls_config = tls_config_new())) + die("tls config failed"); + if (!u.port[0] && !strcmp(u.proto, "https")) + 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 (u.proto[0]) + die("unsupported protocol specified: %s", u.proto); + else + die("no protocol specified"); + } + + fclose(dest); + + return statuscode; +} diff --git a/http.h b/http.h @@ -0,0 +1 @@ +int http_fetch(char *loc, char *path);