// 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 #include #include #include #include #include #include #include #include #include #include #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; }