calog/libs/calogHttp.c

1019 lines
32 KiB
C

// calogHttp.c -- calog HTTP client library (see calogHttp.h). A self-contained HTTP/1.1
// client: parse the URL, connect (getaddrinfo + socket + connect), optionally wrap the socket
// in OpenSSL TLS for https, send the request with "Connection: close", read the entire
// response to EOF, and decode it (status line, lowercased headers into a map, and a body that
// honors Transfer-Encoding: chunked / Content-Length / read-to-close). No shared state: every
// native is a self-contained connection, so the natives are INLINE and there is nothing to
// shut down. https verifies the server certificate + hostname against the system CA store by
// default; httpRequest can pass insecure=true to skip verification (self-signed / dev).
#define _GNU_SOURCE
#include "calogHttp.h"
#include <errno.h>
#include <limits.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include <openssl/ssl.h>
#include <openssl/x509v3.h>
#define HTTP_BUF_INITIAL 256
#define HTTP_READ_CHUNK 16384
#define HTTP_MAX_RESPONSE (64 * 1024 * 1024)
#define HTTP_HOST_MAX 256
#define HTTP_PORT_MAX 16
#define HTTP_PATH_MAX 4096
// A growable output byte buffer for the request and for decoded bodies.
typedef struct HttpBufT {
char *bytes;
size_t length;
size_t cap;
} HttpBufT;
// A parsed absolute URL. host/port/path are NUL-terminated so they double as C strings.
typedef struct HttpUrlT {
bool https;
char host[HTTP_HOST_MAX];
char port[HTTP_PORT_MAX];
char path[HTTP_PATH_MAX];
} HttpUrlT;
// One connection: a plain socket, or a TLS session layered over it when ssl != NULL. verify
// requests certificate + hostname authentication for an https connection (default; an https
// request can opt out with insecure=true).
typedef struct HttpConnT {
int fd;
SSL *ssl;
SSL_CTX *ctx;
bool verify;
} HttpConnT;
static int32_t httpAppendHeaders(HttpBufT *buf, const CalogAggT *headers);
static int32_t httpBufEnsure(HttpBufT *buf, size_t extra);
static void httpBufFree(HttpBufT *buf);
static int32_t httpBufPutBytes(HttpBufT *buf, const char *bytes, size_t length);
static int32_t httpBufPutStr(HttpBufT *buf, const char *s);
static int32_t httpBuildResult(const char *raw, size_t rawLen, CalogValueT *result);
static void httpConnClose(HttpConnT *conn);
static int32_t httpConnOpen(HttpConnT *conn, const HttpUrlT *url, CalogValueT *result);
static ssize_t httpConnRead(HttpConnT *conn, char *buf, size_t length);
static int32_t httpConnWriteAll(HttpConnT *conn, const char *bytes, size_t length);
static bool httpContainsChunked(const char *bytes, size_t length);
static bool httpCopyField(char *dst, size_t cap, const char *src, size_t length);
static int32_t httpDechunk(const char *bytes, size_t length, HttpBufT *out);
static bool httpDefaultPort(const HttpUrlT *url);
static int32_t httpGetNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t httpHexVal(char c);
static char httpLower(char c);
static int32_t httpMapSetAgg(CalogAggT *map, const char *key, CalogAggT *inner);
static int32_t httpMapSetInt(CalogAggT *map, const char *key, int64_t value);
static int32_t httpMapSetStr(CalogAggT *map, const char *key, const char *bytes, int64_t length);
static int64_t httpParseInt(const char *bytes, size_t length);
static int32_t httpParseStatus(const char *line, size_t length, int32_t *codeOut);
static int32_t httpParseUrl(const char *url, int64_t urlLen, HttpUrlT *out, CalogValueT *result);
static int32_t httpPerform(const char *method, const char *urlBytes, int64_t urlLen, const CalogAggT *headers, const char *body, int64_t bodyLen, bool verify, CalogValueT *result);
static int32_t httpReadResponse(HttpConnT *conn, HttpBufT *raw);
static int32_t httpRequestNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t httpTlsHandshake(HttpConnT *conn, const HttpUrlT *url, CalogValueT *result);
int32_t calogHttpRegister(CalogT *calog) {
calogRegisterInline(calog, "httpGet", httpGetNative, NULL);
calogRegisterInline(calog, "httpRequest", httpRequestNative, NULL);
return calogOkE;
}
static int32_t httpAppendHeaders(HttpBufT *buf, const CalogAggT *headers) {
int64_t index;
int32_t status;
// Only string:string pairs become request headers; anything else is skipped. The bytes are
// binary-safe (length-carried), so a header value may hold arbitrary octets.
for (index = 0; index < headers->pairCount; index++) {
const CalogValueT *key;
const CalogValueT *val;
key = &headers->pairs[index].key;
val = &headers->pairs[index].value;
if (key->type != calogStringE || val->type != calogStringE) {
continue;
}
status = httpBufPutBytes(buf, key->as.s.bytes, (size_t)key->as.s.length);
if (status == calogOkE) {
status = httpBufPutStr(buf, ": ");
}
if (status == calogOkE) {
status = httpBufPutBytes(buf, val->as.s.bytes, (size_t)val->as.s.length);
}
if (status == calogOkE) {
status = httpBufPutStr(buf, "\r\n");
}
if (status != calogOkE) {
return status;
}
}
return calogOkE;
}
static int32_t httpBufEnsure(HttpBufT *buf, size_t extra) {
size_t need;
size_t cap;
char *grown;
need = buf->length + extra;
if (need <= buf->cap) {
return calogOkE;
}
cap = (buf->cap == 0) ? HTTP_BUF_INITIAL : buf->cap;
while (cap < need) {
cap *= CALOG_GROWTH_FACTOR;
}
grown = (char *)realloc(buf->bytes, cap);
if (grown == NULL) {
return calogErrOomE;
}
buf->bytes = grown;
buf->cap = cap;
return calogOkE;
}
static void httpBufFree(HttpBufT *buf) {
free(buf->bytes);
buf->bytes = NULL;
buf->length = 0;
buf->cap = 0;
}
static int32_t httpBufPutBytes(HttpBufT *buf, const char *bytes, size_t length) {
int32_t status;
if (length == 0) {
return calogOkE;
}
status = httpBufEnsure(buf, length);
if (status != calogOkE) {
return status;
}
memcpy(buf->bytes + buf->length, bytes, length);
buf->length += length;
return calogOkE;
}
static int32_t httpBufPutStr(HttpBufT *buf, const char *s) {
return httpBufPutBytes(buf, s, strlen(s));
}
static int32_t httpBuildResult(const char *raw, size_t rawLen, CalogValueT *result) {
const char *sep;
const char *headerBlock;
const char *bodyRaw;
const char *statusEnd;
const char *cursor;
const char *headerEnd;
const char *finalBody;
HttpBufT dechunked;
CalogAggT *map;
CalogAggT *headersMap;
size_t headerLen;
size_t bodyRawLen;
size_t statusLen;
size_t finalLen;
int64_t contentLength;
int32_t code;
int32_t status;
bool haveContentLength;
bool chunked;
calogValueNil(result);
dechunked.bytes = NULL;
dechunked.length = 0;
dechunked.cap = 0;
contentLength = 0;
haveContentLength = false;
chunked = false;
// Split the head from the body at the first blank line.
sep = (const char *)memmem(raw, rawLen, "\r\n\r\n", 4);
if (sep == NULL) {
return calogFail(result, calogErrArgE, "http: malformed response (no header terminator)");
}
headerBlock = raw;
headerLen = (size_t)(sep - raw);
bodyRaw = sep + 4;
bodyRawLen = rawLen - headerLen - 4;
// The status line is the first CRLF-terminated line of the head.
statusEnd = (const char *)memmem(headerBlock, headerLen, "\r\n", 2);
if (statusEnd == NULL) {
statusLen = headerLen;
cursor = headerBlock + headerLen;
} else {
statusLen = (size_t)(statusEnd - headerBlock);
cursor = statusEnd + 2;
}
headerEnd = headerBlock + headerLen;
status = httpParseStatus(headerBlock, statusLen, &code);
if (status != calogOkE) {
return calogFail(result, calogErrArgE, "http: malformed status line");
}
// Header fields -> map keyed by lowercased name; capture the body-framing headers.
status = calogAggCreate(&headersMap, calogMapE);
if (status != calogOkE) {
return calogFail(result, status, "http: out of memory");
}
while (cursor < headerEnd) {
const char *lineEnd;
const char *colon;
size_t lineLen;
lineEnd = (const char *)memmem(cursor, (size_t)(headerEnd - cursor), "\r\n", 2);
if (lineEnd == NULL) {
lineEnd = headerEnd;
}
lineLen = (size_t)(lineEnd - cursor);
colon = (lineLen > 0) ? (const char *)memchr(cursor, ':', lineLen) : NULL;
if (colon != NULL) {
const char *nameStart;
const char *valStart;
char *lower;
size_t nameLen;
size_t valLen;
size_t i;
nameStart = cursor;
nameLen = (size_t)(colon - cursor);
valStart = colon + 1;
valLen = (size_t)(lineEnd - valStart);
while (valLen > 0 && (*valStart == ' ' || *valStart == '\t')) {
valStart++;
valLen--;
}
while (valLen > 0 && (valStart[valLen - 1] == ' ' || valStart[valLen - 1] == '\t')) {
valLen--;
}
lower = (char *)malloc(nameLen + 1);
if (lower == NULL) {
calogAggFree(headersMap);
return calogFail(result, calogErrOomE, "http: out of memory");
}
for (i = 0; i < nameLen; i++) {
lower[i] = httpLower(nameStart[i]);
}
lower[nameLen] = '\0';
status = httpMapSetStr(headersMap, lower, valStart, (int64_t)valLen);
if (status != calogOkE) {
free(lower);
calogAggFree(headersMap);
return calogFail(result, status, "http: out of memory");
}
if (strcmp(lower, "content-length") == 0) {
contentLength = httpParseInt(valStart, valLen);
if (contentLength >= 0) {
haveContentLength = true;
}
} else if (strcmp(lower, "transfer-encoding") == 0) {
if (httpContainsChunked(valStart, valLen)) {
chunked = true;
}
}
free(lower);
}
if (lineEnd >= headerEnd) {
break;
}
cursor = lineEnd + 2;
}
// Decode the body. Chunked takes precedence over Content-Length; absent both, read-to-close
// means the raw remainder is the body.
if (chunked) {
status = httpDechunk(bodyRaw, bodyRawLen, &dechunked);
if (status != calogOkE) {
httpBufFree(&dechunked);
calogAggFree(headersMap);
return calogFail(result, status, "http: malformed chunked body");
}
finalBody = dechunked.bytes;
finalLen = dechunked.length;
} else if (haveContentLength) {
finalBody = bodyRaw;
finalLen = ((size_t)contentLength < bodyRawLen) ? (size_t)contentLength : bodyRawLen;
} else {
finalBody = bodyRaw;
finalLen = bodyRawLen;
}
// Assemble the outer { status, body, headers } map.
status = calogAggCreate(&map, calogMapE);
if (status != calogOkE) {
httpBufFree(&dechunked);
calogAggFree(headersMap);
return calogFail(result, status, "http: out of memory");
}
status = httpMapSetInt(map, "status", (int64_t)code);
if (status != calogOkE) {
httpBufFree(&dechunked);
calogAggFree(map);
calogAggFree(headersMap);
return calogFail(result, status, "http: out of memory");
}
status = httpMapSetStr(map, "body", (finalBody != NULL) ? finalBody : "", (int64_t)finalLen);
if (status != calogOkE) {
httpBufFree(&dechunked);
calogAggFree(map);
calogAggFree(headersMap);
return calogFail(result, status, "http: out of memory");
}
status = httpMapSetAgg(map, "headers", headersMap);
if (status != calogOkE) {
httpBufFree(&dechunked);
calogAggFree(map);
calogAggFree(headersMap);
return calogFail(result, status, "http: out of memory");
}
// headersMap is now owned by map; only our transient decode buffer remains to free.
httpBufFree(&dechunked);
calogValueAgg(result, map);
return calogOkE;
}
static void httpConnClose(HttpConnT *conn) {
if (conn->ssl != NULL) {
SSL_shutdown(conn->ssl);
SSL_free(conn->ssl);
conn->ssl = NULL;
}
if (conn->ctx != NULL) {
SSL_CTX_free(conn->ctx);
conn->ctx = NULL;
}
// SSL_set_fd installs a BIO_NOCLOSE socket BIO, so SSL_free never closes the fd -- we own it.
if (conn->fd >= 0) {
close(conn->fd);
conn->fd = -1;
}
}
static int32_t httpConnOpen(HttpConnT *conn, const HttpUrlT *url, CalogValueT *result) {
struct addrinfo hints;
struct addrinfo *res;
struct addrinfo *rp;
int fd;
int rc;
conn->fd = -1;
conn->ssl = NULL;
conn->ctx = NULL;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
rc = getaddrinfo(url->host, url->port, &hints, &res);
if (rc != 0) {
return calogFail(result, calogErrArgE, gai_strerror(rc));
}
fd = -1;
for (rp = res; rp != NULL; rp = rp->ai_next) {
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (fd < 0) {
continue;
}
if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) {
break;
}
close(fd);
fd = -1;
}
freeaddrinfo(res);
if (fd < 0) {
return calogFail(result, calogErrArgE, "http: could not connect to host");
}
conn->fd = fd;
if (url->https) {
int32_t status;
status = httpTlsHandshake(conn, url, result);
if (status != calogOkE) {
httpConnClose(conn);
return status;
}
}
return calogOkE;
}
static ssize_t httpConnRead(HttpConnT *conn, char *buf, size_t length) {
if (conn->ssl != NULL) {
int chunk;
int r;
chunk = (length > (size_t)INT_MAX) ? INT_MAX : (int)length;
r = SSL_read(conn->ssl, buf, chunk);
// A non-positive return (clean shutdown or error) ends the read-to-EOF loop.
if (r <= 0) {
return 0;
}
return (ssize_t)r;
}
for (;;) {
ssize_t r;
r = recv(conn->fd, buf, length, 0);
if (r < 0 && errno == EINTR) {
continue;
}
return r;
}
}
static int32_t httpConnWriteAll(HttpConnT *conn, const char *bytes, size_t length) {
size_t sent;
sent = 0;
while (sent < length) {
if (conn->ssl != NULL) {
int chunk;
int w;
chunk = (length - sent > (size_t)INT_MAX) ? INT_MAX : (int)(length - sent);
w = SSL_write(conn->ssl, bytes + sent, chunk);
if (w <= 0) {
return calogErrUnsupportedE;
}
sent += (size_t)w;
} else {
ssize_t w;
w = send(conn->fd, bytes + sent, length - sent, MSG_NOSIGNAL);
if (w < 0) {
if (errno == EINTR) {
continue;
}
return calogErrArgE;
}
sent += (size_t)w;
}
}
return calogOkE;
}
static bool httpContainsChunked(const char *bytes, size_t length) {
static const char needle[] = "chunked";
size_t nlen;
size_t i;
nlen = sizeof(needle) - 1;
if (length < nlen) {
return false;
}
for (i = 0; i + nlen <= length; i++) {
size_t j;
bool match;
match = true;
for (j = 0; j < nlen; j++) {
if (httpLower(bytes[i + j]) != needle[j]) {
match = false;
break;
}
}
if (match) {
return true;
}
}
return false;
}
static bool httpCopyField(char *dst, size_t cap, const char *src, size_t length) {
if (length + 1 > cap) {
return false;
}
memcpy(dst, src, length);
dst[length] = '\0';
return true;
}
static int32_t httpDechunk(const char *bytes, size_t length, HttpBufT *out) {
size_t pos;
pos = 0;
while (pos < length) {
size_t size;
bool any;
int32_t status;
size = 0;
any = false;
while (pos < length) {
int32_t digit;
digit = httpHexVal(bytes[pos]);
if (digit < 0) {
break;
}
// Reject a chunk size that would overflow size_t (a hostile server can send a huge
// hex length to wrap the bounds check below).
if (size > (SIZE_MAX - (size_t)digit) / 16) {
return calogErrArgE;
}
size = size * 16 + (size_t)digit;
any = true;
pos++;
}
if (!any) {
return calogErrArgE;
}
// Skip any chunk extension (";name=value") through the CRLF that ends the size line.
while (pos < length && bytes[pos] != '\n') {
pos++;
}
if (pos >= length) {
return calogErrArgE;
}
pos++;
if (size == 0) {
break;
}
if (size > length - pos) { // pos <= length here, so no overflow
return calogErrArgE;
}
status = httpBufPutBytes(out, bytes + pos, size);
if (status != calogOkE) {
return status;
}
pos += size;
if (pos < length && bytes[pos] == '\r') {
pos++;
}
if (pos < length && bytes[pos] == '\n') {
pos++;
}
}
return calogOkE;
}
static bool httpDefaultPort(const HttpUrlT *url) {
if (url->https) {
return strcmp(url->port, "443") == 0;
}
return strcmp(url->port, "80") == 0;
}
static int32_t httpGetNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "httpGet expects (url)");
}
return httpPerform("GET", args[0].as.s.bytes, args[0].as.s.length, NULL, NULL, 0, true, result);
}
static int32_t httpHexVal(char c) {
if (c >= '0' && c <= '9') {
return (int32_t)(c - '0');
}
if (c >= 'a' && c <= 'f') {
return (int32_t)(c - 'a' + 10);
}
if (c >= 'A' && c <= 'F') {
return (int32_t)(c - 'A' + 10);
}
return -1;
}
static char httpLower(char c) {
if (c >= 'A' && c <= 'Z') {
return (char)(c - 'A' + 'a');
}
return c;
}
static int32_t httpMapSetAgg(CalogAggT *map, const char *key, CalogAggT *inner) {
CalogValueT keyValue;
CalogValueT aggValue;
int32_t status;
status = calogValueString(&keyValue, key, (int64_t)strlen(key));
if (status != calogOkE) {
return status;
}
calogValueAgg(&aggValue, inner);
status = calogAggSet(map, &keyValue, &aggValue);
if (status != calogOkE) {
// calogAggSet did not take ownership; free the key but leave `inner` (which aggValue
// only wraps) for the caller to free.
calogValueFree(&keyValue);
}
return status;
}
static int32_t httpMapSetInt(CalogAggT *map, const char *key, int64_t value) {
CalogValueT keyValue;
CalogValueT intValue;
int32_t status;
status = calogValueString(&keyValue, key, (int64_t)strlen(key));
if (status != calogOkE) {
return status;
}
calogValueInt(&intValue, value);
status = calogAggSet(map, &keyValue, &intValue);
if (status != calogOkE) {
calogValueFree(&keyValue);
}
return status;
}
static int32_t httpMapSetStr(CalogAggT *map, const char *key, const char *bytes, int64_t length) {
CalogValueT keyValue;
CalogValueT stringValue;
int32_t status;
status = calogValueString(&keyValue, key, (int64_t)strlen(key));
if (status != calogOkE) {
return status;
}
status = calogValueString(&stringValue, bytes, length);
if (status != calogOkE) {
calogValueFree(&keyValue);
return status;
}
status = calogAggSet(map, &keyValue, &stringValue);
if (status != calogOkE) {
calogValueFree(&keyValue);
calogValueFree(&stringValue);
}
return status;
}
static int64_t httpParseInt(const char *bytes, size_t length) {
int64_t value;
size_t i;
bool any;
value = 0;
any = false;
for (i = 0; i < length; i++) {
if (bytes[i] < '0' || bytes[i] > '9') {
break;
}
value = value * 10 + (int64_t)(bytes[i] - '0');
any = true;
if (value > (int64_t)HTTP_MAX_RESPONSE) {
break;
}
}
if (!any) {
return -1;
}
return value;
}
static int32_t httpParseStatus(const char *line, size_t length, int32_t *codeOut) {
int32_t code;
size_t i;
bool any;
i = 0;
// Skip the "HTTP/x.y" token and the space(s) before the status code.
while (i < length && line[i] != ' ') {
i++;
}
while (i < length && line[i] == ' ') {
i++;
}
code = 0;
any = false;
while (i < length && line[i] >= '0' && line[i] <= '9') {
code = code * 10 + (int32_t)(line[i] - '0');
any = true;
i++;
}
if (!any) {
return calogErrArgE;
}
*codeOut = code;
return calogOkE;
}
static int32_t httpParseUrl(const char *url, int64_t urlLen, HttpUrlT *out, CalogValueT *result) {
size_t len;
size_t pos;
size_t hostStart;
size_t hostEnd;
len = (size_t)urlLen;
if (len >= 7 && memcmp(url, "http://", 7) == 0) {
out->https = false;
pos = 7;
} else if (len >= 8 && memcmp(url, "https://", 8) == 0) {
out->https = true;
pos = 8;
} else {
return calogFail(result, calogErrArgE, "http: URL must start with http:// or https://");
}
hostStart = pos;
while (pos < len && url[pos] != ':' && url[pos] != '/') {
pos++;
}
hostEnd = pos;
if (hostEnd == hostStart) {
return calogFail(result, calogErrArgE, "http: URL has an empty host");
}
if (!httpCopyField(out->host, sizeof(out->host), url + hostStart, hostEnd - hostStart)) {
return calogFail(result, calogErrArgE, "http: URL host too long");
}
if (pos < len && url[pos] == ':') {
size_t portStart;
pos++;
portStart = pos;
while (pos < len && url[pos] != '/') {
pos++;
}
if (!httpCopyField(out->port, sizeof(out->port), url + portStart, pos - portStart)) {
return calogFail(result, calogErrArgE, "http: URL port too long");
}
if (out->port[0] == '\0') {
return calogFail(result, calogErrArgE, "http: URL has an empty port");
}
} else {
strcpy(out->port, out->https ? "443" : "80");
}
if (pos < len) {
if (!httpCopyField(out->path, sizeof(out->path), url + pos, len - pos)) {
return calogFail(result, calogErrArgE, "http: URL path too long");
}
} else {
strcpy(out->path, "/");
}
return calogOkE;
}
static int32_t httpPerform(const char *method, const char *urlBytes, int64_t urlLen, const CalogAggT *headers, const char *body, int64_t bodyLen, bool verify, CalogValueT *result) {
HttpUrlT url;
HttpConnT conn;
HttpBufT request;
HttpBufT raw;
int32_t status;
calogValueNil(result);
request.bytes = NULL;
request.length = 0;
request.cap = 0;
raw.bytes = NULL;
raw.length = 0;
raw.cap = 0;
status = httpParseUrl(urlBytes, urlLen, &url, result);
if (status != calogOkE) {
return status;
}
// Request line + Host + Connection: close.
status = httpBufPutStr(&request, method);
if (status == calogOkE) {
status = httpBufPutStr(&request, " ");
}
if (status == calogOkE) {
status = httpBufPutStr(&request, url.path);
}
if (status == calogOkE) {
status = httpBufPutStr(&request, " HTTP/1.1\r\nHost: ");
}
if (status == calogOkE) {
status = httpBufPutStr(&request, url.host);
}
if (status == calogOkE && !httpDefaultPort(&url)) {
status = httpBufPutStr(&request, ":");
if (status == calogOkE) {
status = httpBufPutStr(&request, url.port);
}
}
if (status == calogOkE) {
status = httpBufPutStr(&request, "\r\nConnection: close\r\n");
}
if (status != calogOkE) {
httpBufFree(&request);
return calogFail(result, status, "http: out of memory");
}
// Caller-supplied headers.
if (headers != NULL) {
status = httpAppendHeaders(&request, headers);
if (status != calogOkE) {
httpBufFree(&request);
return calogFail(result, status, "http: out of memory");
}
}
// Content-Length + the blank line + body.
if (bodyLen > 0) {
char lengthLine[64];
int n;
n = snprintf(lengthLine, sizeof(lengthLine), "Content-Length: %lld\r\n", (long long)bodyLen);
status = httpBufPutBytes(&request, lengthLine, (size_t)n);
if (status != calogOkE) {
httpBufFree(&request);
return calogFail(result, status, "http: out of memory");
}
}
status = httpBufPutStr(&request, "\r\n");
if (status == calogOkE && bodyLen > 0) {
status = httpBufPutBytes(&request, body, (size_t)bodyLen);
}
if (status != calogOkE) {
httpBufFree(&request);
return calogFail(result, status, "http: out of memory");
}
conn.verify = verify;
status = httpConnOpen(&conn, &url, result);
if (status != calogOkE) {
httpBufFree(&request);
return status;
}
status = httpConnWriteAll(&conn, request.bytes, request.length);
httpBufFree(&request);
if (status != calogOkE) {
httpConnClose(&conn);
return calogFail(result, status, "http: failed to send request");
}
status = httpReadResponse(&conn, &raw);
httpConnClose(&conn);
if (status != calogOkE) {
httpBufFree(&raw);
return calogFail(result, status, "http: failed to read response");
}
status = httpBuildResult((raw.bytes != NULL) ? raw.bytes : "", raw.length, result);
httpBufFree(&raw);
return status;
}
static int32_t httpReadResponse(HttpConnT *conn, HttpBufT *raw) {
char chunk[HTTP_READ_CHUNK];
// "Connection: close" means the server closes at end of message, so reading to EOF yields
// the entire response (head + body).
for (;;) {
ssize_t n;
int32_t status;
n = httpConnRead(conn, chunk, sizeof(chunk));
if (n <= 0) {
break;
}
if (raw->length + (size_t)n > (size_t)HTTP_MAX_RESPONSE) {
return calogErrRangeE;
}
status = httpBufPutBytes(raw, chunk, (size_t)n);
if (status != calogOkE) {
return status;
}
}
return calogOkE;
}
static int32_t httpRequestNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogAggT *opts;
CalogValueT *urlValue;
CalogValueT *methodValue;
CalogValueT *headersValue;
CalogValueT *bodyValue;
CalogValueT *insecureValue;
CalogValueT keyValue;
const CalogAggT *headers;
const char *method;
const char *body;
int64_t bodyLen;
int32_t status;
bool verify;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogAggE) {
return calogFail(result, calogErrArgE, "httpRequest expects (options)");
}
opts = args[0].as.agg;
// url (required).
status = calogValueString(&keyValue, "url", 3);
if (status != calogOkE) {
return calogFail(result, status, "httpRequest: out of memory");
}
urlValue = calogAggGet(opts, &keyValue);
calogValueFree(&keyValue);
if (urlValue == NULL || urlValue->type != calogStringE) {
return calogFail(result, calogErrArgE, "httpRequest: options.url must be a string");
}
// method (optional; default GET).
method = "GET";
status = calogValueString(&keyValue, "method", 6);
if (status != calogOkE) {
return calogFail(result, status, "httpRequest: out of memory");
}
methodValue = calogAggGet(opts, &keyValue);
calogValueFree(&keyValue);
if (methodValue != NULL && methodValue->type == calogStringE) {
method = methodValue->as.s.bytes;
}
// headers (optional).
headers = NULL;
status = calogValueString(&keyValue, "headers", 7);
if (status != calogOkE) {
return calogFail(result, status, "httpRequest: out of memory");
}
headersValue = calogAggGet(opts, &keyValue);
calogValueFree(&keyValue);
if (headersValue != NULL && headersValue->type == calogAggE) {
headers = headersValue->as.agg;
}
// body (optional).
body = NULL;
bodyLen = 0;
status = calogValueString(&keyValue, "body", 4);
if (status != calogOkE) {
return calogFail(result, status, "httpRequest: out of memory");
}
bodyValue = calogAggGet(opts, &keyValue);
calogValueFree(&keyValue);
if (bodyValue != NULL && bodyValue->type == calogStringE) {
body = bodyValue->as.s.bytes;
bodyLen = bodyValue->as.s.length;
}
// insecure (optional): true skips certificate + hostname verification for an https request.
verify = true;
status = calogValueString(&keyValue, "insecure", 8);
if (status != calogOkE) {
return calogFail(result, status, "httpRequest: out of memory");
}
insecureValue = calogAggGet(opts, &keyValue);
calogValueFree(&keyValue);
if (insecureValue != NULL && insecureValue->type == calogBoolE && insecureValue->as.b) {
verify = false;
}
return httpPerform(method, urlValue->as.s.bytes, urlValue->as.s.length, headers, body, bodyLen, verify, result);
}
static int32_t httpTlsHandshake(HttpConnT *conn, const HttpUrlT *url, CalogValueT *result) {
conn->ctx = SSL_CTX_new(TLS_client_method());
if (conn->ctx == NULL) {
return calogFail(result, calogErrUnsupportedE, "http: TLS context creation failed");
}
if (conn->verify) {
// Authenticate the server: require a certificate chain to a trusted CA. Load the system
// CA store plus OpenSSL's compiled-in defaults; with no trust anchors the handshake
// fails closed rather than open. (An https request can pass insecure=true to skip this.)
SSL_CTX_set_verify(conn->ctx, SSL_VERIFY_PEER, NULL);
(void)SSL_CTX_set_default_verify_paths(conn->ctx);
(void)SSL_CTX_load_verify_locations(conn->ctx, "/etc/ssl/certs/ca-certificates.crt", "/etc/ssl/certs");
}
conn->ssl = SSL_new(conn->ctx);
if (conn->ssl == NULL) {
return calogFail(result, calogErrUnsupportedE, "http: TLS session creation failed");
}
if (SSL_set_fd(conn->ssl, conn->fd) != 1) {
return calogFail(result, calogErrUnsupportedE, "http: TLS SSL_set_fd failed");
}
// SNI so name-based virtual hosts serve the right certificate.
(void)SSL_set_tlsext_host_name(conn->ssl, url->host);
if (conn->verify) {
// Bind verification to the requested hostname, so a valid certificate issued for a
// DIFFERENT host is still rejected.
SSL_set_hostflags(conn->ssl, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
if (SSL_set1_host(conn->ssl, url->host) != 1) {
return calogFail(result, calogErrUnsupportedE, "http: could not set TLS verification hostname");
}
}
if (SSL_connect(conn->ssl) != 1) {
return calogFail(result, calogErrUnsupportedE, conn->verify ? "http: TLS handshake or certificate verification failed" : "http: TLS handshake failed");
}
return calogOkE;
}