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:
A | http.c | | | 418 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | http.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);