/* $Id: http.c,v 1.29 2020/01/20 22:10:27 sthen Exp $ */ /* * Copyright (c) 2016 Kristaps Dzonsons * * 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 AUTHORS DISCLAIM ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include "http.h" #include "extern.h" /* * A buffer for transferring HTTP/S data. */ struct httpxfer { char *hbuf; /* header transfer buffer */ size_t hbufsz; /* header buffer size */ int headok; /* header has been parsed */ char *bbuf; /* body transfer buffer */ size_t bbufsz; /* body buffer size */ int bodyok; /* body has been parsed */ char *headbuf; /* lookaside buffer for headers */ struct httphead *head; /* parsed headers */ size_t headsz; /* number of headers */ }; /* * An HTTP/S connection object. */ struct http { int fd; /* connected socket */ short port; /* port number */ struct source src; /* endpoint (raw) host */ char *path; /* path to request */ char *host; /* name of endpoint host */ struct tls *ctx; /* if TLS */ writefp writer; /* write function */ readfp reader; /* read function */ }; struct tls_config *tlscfg; static ssize_t dosysread(char *buf, size_t sz, const struct http *http) { ssize_t rc; rc = read(http->fd, buf, sz); if (rc == -1) warn("%s: read", http->src.ip); return rc; } static ssize_t dosyswrite(const void *buf, size_t sz, const struct http *http) { ssize_t rc; rc = write(http->fd, buf, sz); if (rc == -1) warn("%s: write", http->src.ip); return rc; } static ssize_t dotlsread(char *buf, size_t sz, const struct http *http) { ssize_t rc; do { rc = tls_read(http->ctx, buf, sz); } while (rc == TLS_WANT_POLLIN || rc == TLS_WANT_POLLOUT); if (rc == -1) warnx("%s: tls_read: %s", http->src.ip, tls_error(http->ctx)); return rc; } static ssize_t dotlswrite(const void *buf, size_t sz, const struct http *http) { ssize_t rc; do { rc = tls_write(http->ctx, buf, sz); } while (rc == TLS_WANT_POLLIN || rc == TLS_WANT_POLLOUT); if (rc == -1) warnx("%s: tls_write: %s", http->src.ip, tls_error(http->ctx)); return rc; } int http_init() { if (tlscfg != NULL) return 0; if (tls_init() == -1) { warn("tls_init"); goto err; } tlscfg = tls_config_new(); if (tlscfg == NULL) { warn("tls_config_new"); goto err; } if (tls_config_set_ca_file(tlscfg, tls_default_ca_cert_file()) == -1) { warn("tls_config_set_ca_file: %s", tls_config_error(tlscfg)); goto err; } return 0; err: tls_config_free(tlscfg); tlscfg = NULL; return -1; } static ssize_t http_read(char *buf, size_t sz, const struct http *http) { ssize_t ssz, xfer; xfer = 0; do { if ((ssz = http->reader(buf, sz, http)) < 0) return -1; if (ssz == 0) break; xfer += ssz; sz -= ssz; buf += ssz; } while (ssz > 0 && sz > 0); return xfer; } static int http_write(const char *buf, size_t sz, const struct http *http) { ssize_t ssz, xfer; xfer = sz; while (sz > 0) { if ((ssz = http->writer(buf, sz, http)) < 0) return -1; sz -= ssz; buf += (size_t)ssz; } return xfer; } void http_disconnect(struct http *http) { int rc; if (http->ctx != NULL) { /* TLS connection. */ do { rc = tls_close(http->ctx); } while (rc == TLS_WANT_POLLIN || rc == TLS_WANT_POLLOUT); tls_free(http->ctx); } if (http->fd != -1) { if (close(http->fd) == -1) warn("%s: close", http->src.ip); } http->fd = -1; http->ctx = NULL; } void http_free(struct http *http) { if (http == NULL) return; http_disconnect(http); free(http->host); free(http->path); free(http->src.ip); free(http); } struct http * http_alloc(const struct source *addrs, size_t addrsz, const char *host, short port, const char *path) { struct sockaddr_storage ss; int family, fd, c; socklen_t len; size_t cur, i = 0; struct http *http; /* Do this while we still have addresses to connect. */ again: if (i == addrsz) return NULL; cur = i++; /* Convert to PF_INET or PF_INET6 address from string. */ memset(&ss, 0, sizeof(struct sockaddr_storage)); if (addrs[cur].family == 4) { family = PF_INET; ((struct sockaddr_in *)&ss)->sin_family = AF_INET; ((struct sockaddr_in *)&ss)->sin_port = htons(port); c = inet_pton(AF_INET, addrs[cur].ip, &((struct sockaddr_in *)&ss)->sin_addr); len = sizeof(struct sockaddr_in); } else if (addrs[cur].family == 6) { family = PF_INET6; ((struct sockaddr_in6 *)&ss)->sin6_family = AF_INET6; ((struct sockaddr_in6 *)&ss)->sin6_port = htons(port); c = inet_pton(AF_INET6, addrs[cur].ip, &((struct sockaddr_in6 *)&ss)->sin6_addr); len = sizeof(struct sockaddr_in6); } else { warnx("%s: unknown family", addrs[cur].ip); goto again; } if (c < 0) { warn("%s: inet_ntop", addrs[cur].ip); goto again; } else if (c == 0) { warnx("%s: inet_ntop", addrs[cur].ip); goto again; } /* Create socket and connect. */ fd = socket(family, SOCK_STREAM, 0); if (fd == -1) { warn("%s: socket", addrs[cur].ip); goto again; } else if (connect(fd, (struct sockaddr *)&ss, len) == -1) { warn("%s: connect", addrs[cur].ip); close(fd); goto again; } /* Allocate the communicator. */ http = calloc(1, sizeof(struct http)); if (http == NULL) { warn("calloc"); close(fd); return NULL; } http->fd = fd; http->port = port; http->src.family = addrs[cur].family; http->src.ip = strdup(addrs[cur].ip); http->host = strdup(host); http->path = strdup(path); if (http->src.ip == NULL || http->host == NULL || http->path == NULL) { warn("strdup"); goto err; } /* If necessary, do our TLS setup. */ if (port != 443) { http->writer = dosyswrite; http->reader = dosysread; return http; } http->writer = dotlswrite; http->reader = dotlsread; if ((http->ctx = tls_client()) == NULL) { warn("tls_client"); goto err; } else if (tls_configure(http->ctx, tlscfg) == -1) { warnx("%s: tls_configure: %s", http->src.ip, tls_error(http->ctx)); goto err; } if (tls_connect_socket(http->ctx, http->fd, http->host) != 0) { warnx("%s: tls_connect_socket: %s, %s", http->src.ip, http->host, tls_error(http->ctx)); goto err; } return http; err: http_free(http); return NULL; } struct httpxfer * http_open(const struct http *http, int headreq, const void *p, size_t psz) { char *req; int c; struct httpxfer *trans; if (p == NULL) { if (headreq) c = asprintf(&req, "HEAD %s HTTP/1.0\r\n" "Host: %s\r\n" "User-Agent: OpenBSD-acme-client\r\n" "\r\n", http->path, http->host); else c = asprintf(&req, "GET %s HTTP/1.0\r\n" "Host: %s\r\n" "User-Agent: OpenBSD-acme-client\r\n" "\r\n", http->path, http->host); } else { c = asprintf(&req, "POST %s HTTP/1.0\r\n" "Host: %s\r\n" "Content-Length: %zu\r\n" "Content-Type: application/jose+json\r\n" "User-Agent: OpenBSD-acme-client\r\n" "\r\n", http->path, http->host, psz); } if (c == -1) { warn("asprintf"); return NULL; } else if (!http_write(req, c, http)) { free(req); return NULL; } else if (p != NULL && !http_write(p, psz, http)) { free(req); return NULL; } free(req); trans = calloc(1, sizeof(struct httpxfer)); if (trans == NULL) warn("calloc"); return trans; } void http_close(struct httpxfer *x) { if (x == NULL) return; free(x->hbuf); free(x->bbuf); free(x->headbuf); free(x->head); free(x); } /* * Read the HTTP body from the wire. * If invoked multiple times, this will return the same pointer with the * same data (or NULL, if the original invocation returned NULL). * Returns NULL if read or allocation errors occur. * You must not free the returned pointer. */ char * http_body_read(const struct http *http, struct httpxfer *trans, size_t *sz) { char buf[BUFSIZ]; ssize_t ssz; void *pp; size_t szp; if (sz == NULL) sz = &szp; /* Have we already parsed this? */ if (trans->bodyok > 0) { *sz = trans->bbufsz; return trans->bbuf; } else if (trans->bodyok < 0) return NULL; *sz = 0; trans->bodyok = -1; do { /* If less than sizeof(buf), at EOF. */ if ((ssz = http_read(buf, sizeof(buf), http)) < 0) return NULL; else if (ssz == 0) break; pp = recallocarray(trans->bbuf, trans->bbufsz, trans->bbufsz + ssz, 1); if (pp == NULL) { warn("recallocarray"); return NULL; } trans->bbuf = pp; memcpy(trans->bbuf + trans->bbufsz, buf, ssz); trans->bbufsz += ssz; } while (ssz == sizeof(buf)); trans->bodyok = 1; *sz = trans->bbufsz; return trans->bbuf; } struct httphead * http_head_get(const char *v, struct httphead *h, size_t hsz) { size_t i; for (i = 0; i < hsz; i++) { if (strcasecmp(h[i].key, v) == 0) return &h[i]; } return NULL; } /* * Look through the headers and determine our HTTP code. * This will return -1 on failure, otherwise the code. */ int http_head_status(const struct http *http, struct httphead *h, size_t sz) { int rc; unsigned int code; struct httphead *st; if ((st = http_head_get("Status", h, sz)) == NULL) { warnx("%s: no status header", http->src.ip); return -1; } rc = sscanf(st->val, "%*s %u %*s", &code); if (rc < 0) { warn("sscanf"); return -1; } else if (rc != 1) { warnx("%s: cannot convert status header", http->src.ip); return -1; } return code; } /* * Parse headers from the transfer. * Malformed headers are skipped. * A special "Status" header is added for the HTTP status line. * This can only happen once http_head_read has been called with * success. * This can be invoked multiple times: it will only parse the headers * once and after that it will just return the cache. * You must not free the returned pointer. * If the original header parse failed, or if memory allocation fails * internally, this returns NULL. */ struct httphead * http_head_parse(const struct http *http, struct httpxfer *trans, size_t *sz) { size_t hsz, szp; struct httphead *h; char *cp, *ep, *ccp, *buf; if (sz == NULL) sz = &szp; /* * If we've already parsed the headers, return the * previously-parsed buffer now. * If we have errors on the stream, return NULL now. */ if (trans->head != NULL) { *sz = trans->headsz; return trans->head; } else if (trans->headok <= 0) return NULL; if ((buf = strdup(trans->hbuf)) == NULL) { warn("strdup"); return NULL; } hsz = 0; cp = buf; do { if ((cp = strstr(cp, "\r\n")) != NULL) cp += 2; hsz++; } while (cp != NULL); /* * Allocate headers, then step through the data buffer, parsing * out headers as we have them. * We know at this point that the buffer is NUL-terminated in * the usual way. */ h = calloc(hsz, sizeof(struct httphead)); if (h == NULL) { warn("calloc"); free(buf); return NULL; } *sz = hsz; hsz = 0; cp = buf; do { if ((ep = strstr(cp, "\r\n")) != NULL) { *ep = '\0'; ep += 2; } if (hsz == 0) { h[hsz].key = "Status"; h[hsz++].val = cp; continue; } /* Skip bad headers. */ if ((ccp = strchr(cp, ':')) == NULL) { warnx("%s: header without separator", http->src.ip); continue; } *ccp++ = '\0'; while (isspace((int)*ccp)) ccp++; h[hsz].key = cp; h[hsz++].val = ccp; } while ((cp = ep) != NULL); trans->headbuf = buf; trans->head = h; trans->headsz = hsz; return h; } /* * Read the HTTP headers from the wire. * If invoked multiple times, this will return the same pointer with the * same data (or NULL, if the original invocation returned NULL). * Returns NULL if read or allocation errors occur. * You must not free the returned pointer. */ char * http_head_read(const struct http *http, struct httpxfer *trans, size_t *sz) { char buf[BUFSIZ]; ssize_t ssz; char *ep; void *pp; size_t szp; if (sz == NULL) sz = &szp; /* Have we already parsed this? */ if (trans->headok > 0) { *sz = trans->hbufsz; return trans->hbuf; } else if (trans->headok < 0) return NULL; *sz = 0; ep = NULL; trans->headok = -1; /* * Begin by reading by BUFSIZ blocks until we reach the header * termination marker (two CRLFs). * We might read into our body, but that's ok: we'll copy out * the body parts into our body buffer afterward. */ do { /* If less than sizeof(buf), at EOF. */ if ((ssz = http_read(buf, sizeof(buf), http)) < 0) return NULL; else if (ssz == 0) break; pp = recallocarray(trans->hbuf, trans->hbufsz, trans->hbufsz + ssz, 1); if (pp == NULL) { warn("recallocarray"); return NULL; } trans->hbuf = pp; memcpy(trans->hbuf + trans->hbufsz, buf, ssz); trans->hbufsz += ssz; /* Search for end of headers marker. */ ep = memmem(trans->hbuf, trans->hbufsz, "\r\n\r\n", 4); } while (ep == NULL && ssz == sizeof(buf)); if (ep == NULL) { warnx("%s: partial transfer", http->src.ip); return NULL; } *ep = '\0'; /* * The header data is invalid if it has any binary characters in * it: check that now. * This is important because we want to guarantee that all * header keys and pairs are properly NUL-terminated. */ if (strlen(trans->hbuf) != (uintptr_t)(ep - trans->hbuf)) { warnx("%s: binary data in header", http->src.ip); return NULL; } /* * Copy remaining buffer into body buffer. */ ep += 4; trans->bbufsz = (trans->hbuf + trans->hbufsz) - ep; trans->bbuf = malloc(trans->bbufsz); if (trans->bbuf == NULL) { warn("malloc"); return NULL; } memcpy(trans->bbuf, ep, trans->bbufsz); trans->headok = 1; *sz = trans->hbufsz; return trans->hbuf; } void http_get_free(struct httpget *g) { if (g == NULL) return; http_close(g->xfer); http_free(g->http); free(g); } struct httpget * http_get(const struct source *addrs, size_t addrsz, const char *domain, short port, const char *path, int headreq, const void *post, size_t postsz) { struct http *h; struct httpxfer *x; struct httpget *g; struct httphead *head; size_t headsz, bodsz, headrsz; int code; char *bod, *headr; h = http_alloc(addrs, addrsz, domain, port, path); if (h == NULL) return NULL; if ((x = http_open(h, headreq, post, postsz)) == NULL) { http_free(h); return NULL; } else if ((headr = http_head_read(h, x, &headrsz)) == NULL) { http_close(x); http_free(h); return NULL; } else if ((bod = http_body_read(h, x, &bodsz)) == NULL) { http_close(x); http_free(h); return NULL; } http_disconnect(h); if ((head = http_head_parse(h, x, &headsz)) == NULL) { http_close(x); http_free(h); return NULL; } else if ((code = http_head_status(h, head, headsz)) < 0) { http_close(x); http_free(h); return NULL; } if ((g = calloc(1, sizeof(struct httpget))) == NULL) { warn("calloc"); http_close(x); http_free(h); return NULL; } g->headpart = headr; g->headpartsz = headrsz; g->bodypart = bod; g->bodypartsz = bodsz; g->head = head; g->headsz = headsz; g->code = code; g->xfer = x; g->http = h; return g; }