1787 lines
63 KiB
C
1787 lines
63 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 "calogInternal.h"
|
|
#include "calogPlatform.h"
|
|
|
|
#include <limits.h>
|
|
#include <signal.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <sys/time.h>
|
|
|
|
#include <openssl/err.h>
|
|
#include <openssl/ssl.h>
|
|
#include <openssl/x509v3.h>
|
|
|
|
// Native trust-store access is platform specific. Windows reads the system certificate stores via
|
|
// wincrypt.h, whose macros collide with OpenSSL's X.509 API, so they are undefined right after
|
|
// inclusion. macOS (only when built against the Apple SDK) reads the keychain via the Security
|
|
// framework. Every POSIX target also probes the well-known CA-bundle files, which needs unistd.h.
|
|
#if defined(_WIN32)
|
|
#include <windows.h>
|
|
#include <wincrypt.h>
|
|
#undef X509_NAME
|
|
#undef X509_EXTENSIONS
|
|
#undef PKCS7_ISSUER_AND_SERIAL
|
|
#undef PKCS7_SIGNER_INFO
|
|
#undef OCSP_REQUEST
|
|
#undef OCSP_RESPONSE
|
|
#else
|
|
#include <unistd.h>
|
|
#if defined(__APPLE__) && defined(CALOG_MAC_KEYCHAIN_TRUST)
|
|
#include <CoreFoundation/CoreFoundation.h>
|
|
#include <Security/Security.h>
|
|
#endif
|
|
#endif
|
|
|
|
#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
|
|
// Connect, read, and write are all bounded by this single deadline (seconds): a stalled or
|
|
// blackholed peer must not hang the calling script thread (or a shutdown joining it) forever.
|
|
#define HTTP_TIMEOUT_SEC 30
|
|
#define HTTP_PORT_HTTP "80"
|
|
#define HTTP_PORT_HTTPS "443"
|
|
// Follow 3xx redirects, but stop after this many hops so a redirect loop cannot spin forever.
|
|
#define HTTP_MAX_REDIRECTS 16
|
|
#define HTTP_METHOD_MAX 16
|
|
#define HTTP_URL_MAX (HTTP_HOST_MAX + HTTP_PORT_MAX + HTTP_PATH_MAX + 32)
|
|
|
|
// 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. ipv6 is
|
|
// true when the URL wrote the host as a bracketed literal ("[::1]"); host itself is stored
|
|
// WITHOUT the brackets (getaddrinfo/SNI/certificate-hostname all want the bare address), and
|
|
// the Host header re-adds them.
|
|
typedef struct HttpUrlT {
|
|
bool https;
|
|
bool ipv6;
|
|
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 {
|
|
CalogSocketT fd;
|
|
SSL *ssl;
|
|
SSL_CTX *ctx;
|
|
bool verify;
|
|
} HttpConnT;
|
|
|
|
static CalogValueT *httpAggField(CalogAggT *agg, const char *name);
|
|
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 bool httpConnectTimeout(CalogSocketT fd, const struct sockaddr *addr, socklen_t addrLen, int timeoutSec);
|
|
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 CalogAggT *httpFilterHeaders(const CalogAggT *headers, bool dropCreds, bool dropContent);
|
|
static int32_t httpFollow(const char *method, const char *urlBytes, int64_t urlLen, const CalogAggT *headers, const char *body, int64_t bodyLen, bool verify, int32_t maxRedirects, CalogValueT *result);
|
|
static int32_t httpGetNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static bool httpHasBadByte(const char *bytes, size_t length, bool rejectSpace);
|
|
static bool httpHeaderIn(const char *name, int64_t len, const char *const *list);
|
|
static int32_t httpHexVal(char c);
|
|
static bool httpIsRedirect(int32_t code);
|
|
#if defined(__APPLE__) && defined(CALOG_MAC_KEYCHAIN_TRUST)
|
|
static bool httpLoadMacRoots(SSL_CTX *ctx);
|
|
#endif
|
|
#ifndef _WIN32
|
|
static bool httpLoadProbedPaths(SSL_CTX *ctx);
|
|
#endif
|
|
static bool httpLoadTrustStore(SSL_CTX *ctx);
|
|
#if defined(_WIN32)
|
|
static bool httpLoadWindowsRoots(SSL_CTX *ctx);
|
|
#endif
|
|
static char httpLower(char c);
|
|
static int32_t httpMapSetAgg(CalogAggT *map, const char *key, CalogAggT *inner);
|
|
static bool httpMethodHasBody(const char *method);
|
|
static CalogValueT *httpOptGet(CalogAggT *opts, const char *name, int32_t *status);
|
|
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 bool httpResolveLocation(const HttpUrlT *base, const char *loc, char *out, size_t cap);
|
|
static const CalogValueT *httpResultHeader(const CalogValueT *result, const char *name);
|
|
static int32_t httpResultStatus(const CalogValueT *result);
|
|
static bool httpSameOrigin(const HttpUrlT *a, const HttpUrlT *b);
|
|
static void httpSetTimeouts(CalogSocketT fd, int timeoutSec);
|
|
static int32_t httpTlsHandshake(HttpConnT *conn, const HttpUrlT *url, CalogValueT *result);
|
|
static int httpWriteAuthority(char *out, size_t cap, const char *scheme, const HttpUrlT *base);
|
|
|
|
|
|
int32_t calogHttpRegister(CalogT *calog) {
|
|
// Winsock must be started before socket()/getaddrinfo() on Windows. calogNet/calogSsh also do
|
|
// this, but an embedder that registers ONLY http would otherwise have no working sockets there
|
|
// (WSAStartup reference-counts, so calling it from every ...Register is safe).
|
|
calogPlatformNetInit();
|
|
// SSL_write/SSL_shutdown (and plain send()) can hit a broken pipe if the peer resets or
|
|
// closes mid-write; with the default disposition that raises SIGPIPE and kills the whole
|
|
// broker process. The plain path already passes MSG_NOSIGNAL, but OpenSSL's socket BIO does
|
|
// not, so ignore SIGPIPE process-wide (idempotent; safe to call from every ...Register).
|
|
#ifndef _WIN32
|
|
// Windows has no SIGPIPE (a reset/closed peer surfaces as a normal send/recv error instead).
|
|
signal(SIGPIPE, SIG_IGN);
|
|
#endif
|
|
calogRegisterInline(calog, "httpGet", httpGetNative, NULL);
|
|
calogRegisterInline(calog, "httpRequest", httpRequestNative, NULL);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
// Look up a string-keyed field in an aggregate with a stack key (no allocation). The returned
|
|
// pointer aliases the aggregate and is valid only until it is freed or mutated.
|
|
static CalogValueT *httpAggField(CalogAggT *agg, const char *name) {
|
|
CalogValueT key;
|
|
|
|
calogValueNil(&key);
|
|
key.type = calogStringE;
|
|
key.as.s.bytes = (char *)name;
|
|
key.as.s.length = (int64_t)strlen(name);
|
|
return calogAggGet(agg, &key);
|
|
}
|
|
|
|
|
|
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 = calogMapSetStr(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) {
|
|
// A body shorter than the promised Content-Length means a read error (connection reset,
|
|
// unclean TLS close) was silently mapped to EOF upstream -- surface it as a failure
|
|
// rather than delivering a truncated body as a status-200 success.
|
|
if (bodyRawLen < (size_t)contentLength) {
|
|
httpBufFree(&dechunked);
|
|
calogAggFree(headersMap);
|
|
return calogFail(result, calogErrArgE, "http: response body shorter than Content-Length");
|
|
}
|
|
finalBody = bodyRaw;
|
|
finalLen = (size_t)contentLength;
|
|
} 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 = calogMapSetInt(map, "status", (int64_t)code);
|
|
if (status != calogOkE) {
|
|
httpBufFree(&dechunked);
|
|
calogAggFree(map);
|
|
calogAggFree(headersMap);
|
|
return calogFail(result, status, "http: out of memory");
|
|
}
|
|
status = calogMapSetStr(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 != CALOG_INVALID_SOCKET) {
|
|
calogSockClose(conn->fd);
|
|
conn->fd = CALOG_INVALID_SOCKET;
|
|
}
|
|
}
|
|
|
|
|
|
static bool httpConnectTimeout(CalogSocketT fd, const struct sockaddr *addr, socklen_t addrLen, int timeoutSec) {
|
|
int rc;
|
|
int err;
|
|
socklen_t errLen;
|
|
#ifndef _WIN32
|
|
struct pollfd pfd;
|
|
#endif
|
|
|
|
// A fresh socket starts blocking; drive a bounded connect by flipping it non-blocking, polling
|
|
// for writability, then restoring blocking mode (portable across BSD sockets and Winsock).
|
|
if (calogSockSetNonblock(fd, 1) != 0) {
|
|
return connect(fd, addr, addrLen) == 0; // best effort: no bounded-connect fallback
|
|
}
|
|
rc = connect(fd, addr, addrLen);
|
|
if (rc == 0) {
|
|
calogSockSetNonblock(fd, 0);
|
|
return true;
|
|
}
|
|
if (!calogSockInProgress(calogSockErrno())) {
|
|
calogSockSetNonblock(fd, 0);
|
|
return false;
|
|
}
|
|
#ifdef _WIN32
|
|
// WSAPoll does not reliably report a FAILED connect on older Windows (it can block for the full
|
|
// timeout), so use select(), whose exceptfds set flags a refused/unreachable peer immediately.
|
|
{
|
|
fd_set writeSet;
|
|
fd_set exceptSet;
|
|
struct timeval tv;
|
|
|
|
FD_ZERO(&writeSet);
|
|
FD_ZERO(&exceptSet);
|
|
FD_SET(fd, &writeSet);
|
|
FD_SET(fd, &exceptSet);
|
|
tv.tv_sec = timeoutSec;
|
|
tv.tv_usec = 0;
|
|
rc = select(0, NULL, &writeSet, &exceptSet, &tv);
|
|
if (rc <= 0 || FD_ISSET(fd, &exceptSet)) {
|
|
calogSockSetNonblock(fd, 0);
|
|
return false; // timeout, error, or connect refused/unreachable
|
|
}
|
|
}
|
|
#else
|
|
pfd.fd = fd;
|
|
pfd.events = POLLOUT;
|
|
rc = calogPoll(&pfd, 1, timeoutSec * 1000);
|
|
if (rc <= 0) {
|
|
calogSockSetNonblock(fd, 0);
|
|
return false; // timeout or poll error
|
|
}
|
|
#endif
|
|
errLen = sizeof(err);
|
|
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *)&err, &errLen) < 0 || err != 0) {
|
|
calogSockSetNonblock(fd, 0);
|
|
return false;
|
|
}
|
|
calogSockSetNonblock(fd, 0);
|
|
return true;
|
|
}
|
|
|
|
|
|
static int32_t httpConnOpen(HttpConnT *conn, const HttpUrlT *url, CalogValueT *result) {
|
|
struct addrinfo hints;
|
|
struct addrinfo *res;
|
|
struct addrinfo *rp;
|
|
CalogSocketT fd;
|
|
int rc;
|
|
|
|
conn->fd = CALOG_INVALID_SOCKET;
|
|
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 = CALOG_INVALID_SOCKET;
|
|
for (rp = res; rp != NULL; rp = rp->ai_next) {
|
|
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
|
if (fd == CALOG_INVALID_SOCKET) {
|
|
continue;
|
|
}
|
|
httpSetTimeouts(fd, HTTP_TIMEOUT_SEC);
|
|
if (httpConnectTimeout(fd, rp->ai_addr, (socklen_t)rp->ai_addrlen, HTTP_TIMEOUT_SEC)) {
|
|
break;
|
|
}
|
|
calogSockClose(fd);
|
|
fd = CALOG_INVALID_SOCKET;
|
|
}
|
|
freeaddrinfo(res);
|
|
if (fd == CALOG_INVALID_SOCKET) {
|
|
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;
|
|
int err;
|
|
chunk = (length > (size_t)INT_MAX) ? INT_MAX : (int)length;
|
|
r = SSL_read(conn->ssl, buf, chunk);
|
|
if (r > 0) {
|
|
return (ssize_t)r;
|
|
}
|
|
// Distinguish a clean TLS close_notify (end of response, like a plain-socket EOF) from
|
|
// any other error (reset, protocol violation, truncated shutdown) -- the latter must NOT
|
|
// be treated as a successful end of response.
|
|
err = SSL_get_error(conn->ssl, r);
|
|
if (err == SSL_ERROR_ZERO_RETURN) {
|
|
return 0;
|
|
}
|
|
return -1;
|
|
}
|
|
for (;;) {
|
|
ssize_t r;
|
|
r = recv(conn->fd, buf, length, 0);
|
|
if (r < 0 && calogSockErrno() == 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, CALOG_MSG_NOSIGNAL);
|
|
if (w < 0) {
|
|
if (calogSockErrno() == 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, HTTP_PORT_HTTPS) == 0;
|
|
}
|
|
return strcmp(url->port, HTTP_PORT_HTTP) == 0;
|
|
}
|
|
|
|
|
|
// Copy headers, dropping credential headers when dropCreds (a redirect crossed to another origin)
|
|
// and/or body-describing headers when dropContent (a redirect dropped the request body). Returns
|
|
// an owned aggregate, or NULL on out of memory.
|
|
static CalogAggT *httpFilterHeaders(const CalogAggT *headers, bool dropCreds, bool dropContent) {
|
|
static const char *const creds[] = { "authorization", "cookie", "proxy-authorization", NULL };
|
|
static const char *const content[] = { "content-type", "content-length", "content-encoding", "transfer-encoding", NULL };
|
|
CalogAggT *out;
|
|
int64_t index;
|
|
|
|
if (calogAggCreate(&out, calogMapE) != calogOkE) {
|
|
return NULL;
|
|
}
|
|
for (index = 0; index < headers->pairCount; index++) {
|
|
const CalogValueT *key;
|
|
CalogValueT keyCopy;
|
|
CalogValueT valCopy;
|
|
|
|
key = &headers->pairs[index].key;
|
|
if (key->type == calogStringE && ((dropCreds && httpHeaderIn(key->as.s.bytes, key->as.s.length, creds)) || (dropContent && httpHeaderIn(key->as.s.bytes, key->as.s.length, content)))) {
|
|
continue;
|
|
}
|
|
if (calogValueCopy(&keyCopy, &headers->pairs[index].key) != calogOkE) {
|
|
calogAggFree(out);
|
|
return NULL;
|
|
}
|
|
if (calogValueCopy(&valCopy, &headers->pairs[index].value) != calogOkE) {
|
|
calogValueFree(&keyCopy);
|
|
calogAggFree(out);
|
|
return NULL;
|
|
}
|
|
if (calogAggSet(out, &keyCopy, &valCopy) != calogOkE) {
|
|
calogValueFree(&keyCopy);
|
|
calogValueFree(&valCopy);
|
|
calogAggFree(out);
|
|
return NULL;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
|
|
// Perform a request and follow up to maxRedirects 3xx redirects (0 disables following, handing
|
|
// back the raw 3xx). Each hop reissues httpPerform against the resolved Location; the response
|
|
// map of every non-final hop is freed before the next. Method/body follow the usual rules:
|
|
// 303 (and 301/302 for a body-bearing method) drop to GET with no body, 307/308 are preserved.
|
|
static int32_t httpFollow(const char *method, const char *urlBytes, int64_t urlLen, const CalogAggT *headers, const char *body, int64_t bodyLen, bool verify, int32_t maxRedirects, CalogValueT *result) {
|
|
char curUrl[HTTP_URL_MAX];
|
|
char curMethod[HTTP_METHOD_MAX];
|
|
const char *curBody;
|
|
const CalogAggT *curHeaders;
|
|
CalogAggT *filtered; // owned; headers with credential/content headers stripped, or NULL
|
|
HttpUrlT origin; // scheme+host+port of the ORIGINAL request, for cross-origin checks
|
|
int64_t curBodyLen;
|
|
int32_t redirects;
|
|
int32_t status;
|
|
bool bodyDropped;
|
|
|
|
calogValueNil(result);
|
|
filtered = NULL;
|
|
curHeaders = headers;
|
|
bodyDropped = false;
|
|
if (urlLen < 0 || (size_t)urlLen >= sizeof(curUrl)) {
|
|
return calogFail(result, calogErrArgE, "http: URL too long");
|
|
}
|
|
if (strlen(method) >= sizeof(curMethod)) {
|
|
return calogFail(result, calogErrArgE, "http: method too long");
|
|
}
|
|
memcpy(curUrl, urlBytes, (size_t)urlLen);
|
|
curUrl[urlLen] = '\0';
|
|
strcpy(curMethod, method);
|
|
curBody = body;
|
|
curBodyLen = bodyLen;
|
|
status = httpParseUrl(curUrl, (int64_t)strlen(curUrl), &origin, result);
|
|
if (status != calogOkE) {
|
|
return status;
|
|
}
|
|
|
|
for (redirects = 0; ; redirects++) {
|
|
int32_t code;
|
|
const CalogValueT *location;
|
|
HttpUrlT base;
|
|
HttpUrlT target;
|
|
char locBuf[HTTP_URL_MAX];
|
|
char next[HTTP_URL_MAX];
|
|
size_t locLen;
|
|
|
|
status = httpPerform(curMethod, curUrl, (int64_t)strlen(curUrl), curHeaders, curBody, curBodyLen, verify, result);
|
|
if (status != calogOkE) {
|
|
goto done;
|
|
}
|
|
code = httpResultStatus(result);
|
|
if (maxRedirects <= 0 || !httpIsRedirect(code)) {
|
|
goto done; // redirects disabled, or a final (non-3xx) response
|
|
}
|
|
location = httpResultHeader(result, "location");
|
|
if (location == NULL || location->type != calogStringE || location->as.s.length == 0 || memchr(location->as.s.bytes, '\0', (size_t)location->as.s.length) != NULL) {
|
|
goto done; // a 3xx with no usable Location (missing, empty, or containing a NUL)
|
|
}
|
|
if (redirects >= maxRedirects) {
|
|
calogValueFree(result);
|
|
status = calogFail(result, calogErrArgE, "http: too many redirects");
|
|
goto done;
|
|
}
|
|
// Copy the Location out of the response map, then free the map (which the pointer aliases).
|
|
locLen = (size_t)location->as.s.length;
|
|
if (locLen >= sizeof(locBuf)) {
|
|
calogValueFree(result);
|
|
status = calogFail(result, calogErrArgE, "http: redirect Location too long");
|
|
goto done;
|
|
}
|
|
memcpy(locBuf, location->as.s.bytes, locLen);
|
|
locBuf[locLen] = '\0';
|
|
calogValueFree(result);
|
|
// Resolve the (possibly relative) Location against the URL just fetched.
|
|
status = httpParseUrl(curUrl, (int64_t)strlen(curUrl), &base, result);
|
|
if (status != calogOkE) {
|
|
goto done;
|
|
}
|
|
if (!httpResolveLocation(&base, locBuf, next, sizeof(next))) {
|
|
status = calogFail(result, calogErrArgE, "http: could not resolve redirect Location");
|
|
goto done;
|
|
}
|
|
status = httpParseUrl(next, (int64_t)strlen(next), &target, result);
|
|
if (status != calogOkE) {
|
|
goto done;
|
|
}
|
|
// 303 (and 301/302 for a body-bearing method) -> GET with no body; 307/308 keep both.
|
|
if (code == 303 || ((code == 301 || code == 302) && httpMethodHasBody(curMethod))) {
|
|
strcpy(curMethod, "GET");
|
|
curBody = NULL;
|
|
curBodyLen = 0;
|
|
bodyDropped = true;
|
|
}
|
|
// Choose the headers for the next hop: drop credential headers (Authorization/Cookie/...)
|
|
// when the target is a different origin -- so a secret is never resent to another host,
|
|
// including over an https->http downgrade -- and drop Content-* once the body is gone.
|
|
if (headers != NULL) {
|
|
bool dropCreds;
|
|
|
|
dropCreds = !httpSameOrigin(&origin, &target);
|
|
if (dropCreds || bodyDropped) {
|
|
if (filtered != NULL) {
|
|
calogAggFree(filtered);
|
|
}
|
|
filtered = httpFilterHeaders(headers, dropCreds, bodyDropped);
|
|
if (filtered == NULL) {
|
|
status = calogFail(result, calogErrOomE, "http: out of memory");
|
|
goto done;
|
|
}
|
|
curHeaders = filtered;
|
|
} else {
|
|
curHeaders = headers;
|
|
}
|
|
}
|
|
strcpy(curUrl, next);
|
|
}
|
|
done:
|
|
if (filtered != NULL) {
|
|
calogAggFree(filtered);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
|
|
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 httpFollow("GET", args[0].as.s.bytes, args[0].as.s.length, NULL, NULL, 0, true, HTTP_MAX_REDIRECTS, result);
|
|
}
|
|
|
|
|
|
static bool httpHasBadByte(const char *bytes, size_t length, bool rejectSpace) {
|
|
size_t i;
|
|
|
|
// CR/LF would let a raw byte string smuggle extra header lines (or a second request) into
|
|
// the request line; a method additionally cannot carry a bare space.
|
|
for (i = 0; i < length; i++) {
|
|
if (bytes[i] == '\r' || bytes[i] == '\n') {
|
|
return true;
|
|
}
|
|
if (rejectSpace && bytes[i] == ' ') {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// Case-insensitive membership test of a (length-carried) header name in a NULL-terminated list
|
|
// of lowercase names.
|
|
static bool httpHeaderIn(const char *name, int64_t len, const char *const *list) {
|
|
size_t which;
|
|
|
|
for (which = 0; list[which] != NULL; which++) {
|
|
size_t nameLen;
|
|
size_t index;
|
|
bool match;
|
|
|
|
nameLen = strlen(list[which]);
|
|
if ((size_t)len != nameLen) {
|
|
continue;
|
|
}
|
|
match = true;
|
|
for (index = 0; index < nameLen; index++) {
|
|
if (httpLower(name[index]) != list[which][index]) {
|
|
match = false;
|
|
break;
|
|
}
|
|
}
|
|
if (match) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
// The 3xx codes that carry a Location this client follows (301/302/303 and 307/308).
|
|
static bool httpIsRedirect(int32_t code) {
|
|
return code == 301 || code == 302 || code == 303 || code == 307 || code == 308;
|
|
}
|
|
|
|
|
|
#if defined(__APPLE__) && defined(CALOG_MAC_KEYCHAIN_TRUST)
|
|
// Load trust anchors from the macOS system keychain via the Security framework. This needs the
|
|
// Apple SDK, so calog's SDK-less cross build leaves it disabled (falling back to
|
|
// httpLoadProbedPaths); it is compiled only for a native macOS build made with
|
|
// -DCALOG_MAC_KEYCHAIN_TRUST that links -framework Security -framework CoreFoundation. Each anchor
|
|
// is DER, so it round-trips through OpenSSL's d2i_X509 into ctx's store. Returns true once any
|
|
// anchor was added.
|
|
static bool httpLoadMacRoots(SSL_CTX *ctx) {
|
|
X509_STORE *store;
|
|
CFArrayRef anchors;
|
|
CFIndex count;
|
|
CFIndex i;
|
|
bool loaded;
|
|
|
|
store = SSL_CTX_get_cert_store(ctx);
|
|
anchors = NULL;
|
|
loaded = false;
|
|
if (SecTrustCopyAnchorCertificates(&anchors) != errSecSuccess || anchors == NULL) {
|
|
return false;
|
|
}
|
|
count = CFArrayGetCount(anchors);
|
|
for (i = 0; i < count; i++) {
|
|
SecCertificateRef cert;
|
|
CFDataRef der;
|
|
const unsigned char *bytes;
|
|
X509 *x;
|
|
|
|
cert = (SecCertificateRef)CFArrayGetValueAtIndex(anchors, i);
|
|
if (cert == NULL) {
|
|
continue;
|
|
}
|
|
der = SecCertificateCopyData(cert);
|
|
if (der == NULL) {
|
|
continue;
|
|
}
|
|
bytes = CFDataGetBytePtr(der);
|
|
x = d2i_X509(NULL, &bytes, (long)CFDataGetLength(der));
|
|
CFRelease(der);
|
|
if (x == NULL) {
|
|
continue;
|
|
}
|
|
if (X509_STORE_add_cert(store, x) == 1) {
|
|
loaded = true;
|
|
}
|
|
X509_free(x);
|
|
}
|
|
CFRelease(anchors);
|
|
ERR_clear_error();
|
|
return loaded;
|
|
}
|
|
#endif
|
|
|
|
|
|
#ifndef _WIN32
|
|
// Load trust anchors from the CA-bundle files (and hash directories) that Linux distributions and
|
|
// the BSDs/macOS ship. The well-known locations are tried in order -- one bundle is enough (a
|
|
// system generally has exactly one), but a hash directory is also tried for distros that ship only
|
|
// that. access() gates each try so a missing path does not push errors onto OpenSSL's error queue.
|
|
// Returns true once any anchors loaded. (CALOG_CA_BUNDLE / SSL_CERT_* overrides are handled, ahead
|
|
// of this probe, by httpLoadTrustStore.)
|
|
static bool httpLoadProbedPaths(SSL_CTX *ctx) {
|
|
static const char *const files[] = {
|
|
"/etc/ssl/certs/ca-certificates.crt", // Debian, Ubuntu, Arch, Gentoo, Alpine
|
|
"/etc/pki/tls/certs/ca-bundle.crt", // Fedora, RHEL, CentOS
|
|
"/etc/ssl/ca-bundle.pem", // openSUSE
|
|
"/etc/pki/tls/cacert.pem", // OpenELEC
|
|
"/etc/ssl/cert.pem", // Alpine, macOS, OpenBSD, FreeBSD
|
|
"/usr/local/share/certs/ca-root-nss.crt", // FreeBSD (ports)
|
|
"/etc/openssl/certs/ca-certificates.crt" // NetBSD
|
|
};
|
|
static const char *const dirs[] = {
|
|
"/etc/ssl/certs", // Debian, openSUSE (hashed)
|
|
"/etc/pki/tls/certs", // Fedora, RHEL
|
|
"/system/etc/security/cacerts" // Android
|
|
};
|
|
size_t i;
|
|
bool loaded;
|
|
|
|
loaded = false;
|
|
for (i = 0; i < sizeof(files) / sizeof(files[0]); i++) {
|
|
if (access(files[i], R_OK) != 0) {
|
|
continue;
|
|
}
|
|
if (SSL_CTX_load_verify_locations(ctx, files[i], NULL) == 1) {
|
|
loaded = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!loaded) {
|
|
for (i = 0; i < sizeof(dirs) / sizeof(dirs[0]); i++) {
|
|
if (access(dirs[i], R_OK) != 0) {
|
|
continue;
|
|
}
|
|
if (SSL_CTX_load_verify_locations(ctx, NULL, dirs[i]) == 1) {
|
|
loaded = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return loaded;
|
|
}
|
|
#endif
|
|
|
|
|
|
// Populate ctx's certificate trust store from the host's native trust configuration. OpenSSL's
|
|
// compiled-in default paths point at the vendored build prefix, which does not exist at runtime, so
|
|
// they resolve nothing on their own; instead we consult the operating system directly (the Windows
|
|
// system stores, the macOS keychain, or the distro CA-bundle files). Overrides are honored ahead of
|
|
// the native store, in priority order: CALOG_CA_BUNDLE (authoritative and EXCLUSIVE -- trust only
|
|
// that bundle, so it can pin to a private CA, and fail closed if it cannot be loaded), then the
|
|
// OpenSSL-standard SSL_CERT_FILE / SSL_CERT_DIR. Returns true only if a trust source actually
|
|
// loaded (not merely because an override variable is set), so a verified request can fail with a
|
|
// clear "no trust store" error instead of a misleading per-certificate verification failure.
|
|
static bool httpLoadTrustStore(SSL_CTX *ctx) {
|
|
const char *pin;
|
|
const char *envFile;
|
|
const char *envDir;
|
|
bool loaded;
|
|
|
|
// A pinned bundle is authoritative and exclusive: trust EXACTLY it and nothing else, and fail
|
|
// closed if it will not load. Load return value (not mere presence of the variable) decides.
|
|
pin = getenv("CALOG_CA_BUNDLE");
|
|
if (pin != NULL) {
|
|
return SSL_CTX_load_verify_locations(ctx, pin, NULL) == 1;
|
|
}
|
|
loaded = false;
|
|
// The OpenSSL-standard operator override. Load each explicitly and confirm it, so a broken
|
|
// SSL_CERT_FILE still surfaces the clear no-trust-store error rather than counting as loaded.
|
|
// (An SSL_CERT_DIR is a lazy hash-dir lookup, so its load cannot be pre-confirmed; a set-but-
|
|
// empty directory is the one case that still counts as loaded, which is fail-closed-safe.)
|
|
envFile = getenv("SSL_CERT_FILE");
|
|
if (envFile != NULL && SSL_CTX_load_verify_locations(ctx, envFile, NULL) == 1) {
|
|
loaded = true;
|
|
}
|
|
envDir = getenv("SSL_CERT_DIR");
|
|
if (envDir != NULL && SSL_CTX_load_verify_locations(ctx, NULL, envDir) == 1) {
|
|
loaded = true;
|
|
}
|
|
#if defined(_WIN32)
|
|
if (httpLoadWindowsRoots(ctx)) {
|
|
loaded = true;
|
|
}
|
|
#else
|
|
#if defined(__APPLE__) && defined(CALOG_MAC_KEYCHAIN_TRUST)
|
|
if (httpLoadMacRoots(ctx)) {
|
|
loaded = true;
|
|
}
|
|
#endif
|
|
if (httpLoadProbedPaths(ctx)) {
|
|
loaded = true;
|
|
}
|
|
#endif
|
|
return loaded;
|
|
}
|
|
|
|
|
|
#if defined(_WIN32)
|
|
// Load trust anchors from the Windows system certificate stores (ROOT = trusted roots, CA =
|
|
// intermediates). Each store certificate is DER, so it round-trips through OpenSSL's d2i_X509 into
|
|
// ctx's store. Returns true once any certificate was added.
|
|
static bool httpLoadWindowsRoots(SSL_CTX *ctx) {
|
|
static const char *const stores[] = { "ROOT", "CA" };
|
|
X509_STORE *store;
|
|
size_t i;
|
|
bool loaded;
|
|
|
|
store = SSL_CTX_get_cert_store(ctx);
|
|
loaded = false;
|
|
for (i = 0; i < sizeof(stores) / sizeof(stores[0]); i++) {
|
|
HCERTSTORE sys;
|
|
PCCERT_CONTEXT cert;
|
|
|
|
sys = CertOpenSystemStoreA(0, stores[i]);
|
|
if (sys == NULL) {
|
|
continue;
|
|
}
|
|
cert = NULL;
|
|
while ((cert = CertEnumCertificatesInStore(sys, cert)) != NULL) {
|
|
const unsigned char *der;
|
|
X509 *x;
|
|
|
|
der = cert->pbCertEncoded;
|
|
x = d2i_X509(NULL, &der, (long)cert->cbCertEncoded);
|
|
if (x == NULL) {
|
|
continue;
|
|
}
|
|
// X509_STORE_add_cert up-refs on success, so our reference is always released here; a
|
|
// duplicate (already present) returns 0 and is simply skipped.
|
|
if (X509_STORE_add_cert(store, x) == 1) {
|
|
loaded = true;
|
|
}
|
|
X509_free(x);
|
|
}
|
|
CertCloseStore(sys, 0);
|
|
}
|
|
// Duplicate-add attempts push errors onto OpenSSL's thread-local queue; clear them so a later
|
|
// SSL_get_error is not misled by this bookkeeping.
|
|
ERR_clear_error();
|
|
return loaded;
|
|
}
|
|
#endif
|
|
|
|
|
|
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 bool httpMethodHasBody(const char *method) {
|
|
// RFC 9110 8.6: a request with a method that commonly carries content SHOULD send
|
|
// Content-Length even when the body is empty; strict servers/proxies otherwise answer
|
|
// 411 Length Required. GET/HEAD never carry content, so they alone are excluded.
|
|
return strcasecmp(method, "GET") != 0 && strcasecmp(method, "HEAD") != 0;
|
|
}
|
|
|
|
|
|
static CalogValueT *httpOptGet(CalogAggT *opts, const char *name, int32_t *status) {
|
|
CalogValueT keyValue;
|
|
CalogValueT *value;
|
|
|
|
*status = calogValueString(&keyValue, name, (int64_t)strlen(name));
|
|
if (*status != calogOkE) {
|
|
return NULL;
|
|
}
|
|
value = calogAggGet(opts, &keyValue);
|
|
calogValueFree(&keyValue);
|
|
return value;
|
|
}
|
|
|
|
|
|
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;
|
|
size_t digits;
|
|
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;
|
|
digits = 0;
|
|
// RFC 7230 status codes are exactly 3 digits; cap accumulation there so a hostile/garbage
|
|
// status line (e.g. 11+ digits) cannot signed-overflow code (matches httpParseInt's guard).
|
|
while (i < length && line[i] >= '0' && line[i] <= '9') {
|
|
if (digits < 3) {
|
|
code = code * 10 + (int32_t)(line[i] - '0');
|
|
digits++;
|
|
}
|
|
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;
|
|
if (pos < len && url[pos] == '[') {
|
|
// IPv6 literal host, e.g. "[::1]:8080" -- scan to the matching ']' instead of stopping
|
|
// at the first ':' (an IPv6 address is full of colons).
|
|
pos++;
|
|
hostStart = pos;
|
|
while (pos < len && url[pos] != ']') {
|
|
pos++;
|
|
}
|
|
if (pos >= len) {
|
|
return calogFail(result, calogErrArgE, "http: URL has an unterminated IPv6 host literal");
|
|
}
|
|
hostEnd = pos;
|
|
pos++; // consume ']'
|
|
out->ipv6 = true;
|
|
} else {
|
|
while (pos < len && url[pos] != ':' && url[pos] != '/') {
|
|
pos++;
|
|
}
|
|
hostEnd = pos;
|
|
out->ipv6 = false;
|
|
}
|
|
if (hostEnd == hostStart) {
|
|
return calogFail(result, calogErrArgE, "http: URL has an empty host");
|
|
}
|
|
if (httpHasBadByte(url + hostStart, hostEnd - hostStart, false)) {
|
|
return calogFail(result, calogErrArgE, "http: URL host contains invalid characters");
|
|
}
|
|
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 ? HTTP_PORT_HTTPS : HTTP_PORT_HTTP);
|
|
}
|
|
if (pos < len) {
|
|
if (httpHasBadByte(url + pos, len - pos, false)) {
|
|
return calogFail(result, calogErrArgE, "http: URL path contains invalid characters");
|
|
}
|
|
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 && url.ipv6) {
|
|
status = httpBufPutStr(&request, "[");
|
|
}
|
|
if (status == calogOkE) {
|
|
status = httpBufPutStr(&request, url.host);
|
|
}
|
|
if (status == calogOkE && url.ipv6) {
|
|
status = httpBufPutStr(&request, "]");
|
|
}
|
|
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. A method that commonly carries content still gets
|
|
// an explicit Content-Length: 0 even with an empty/absent body (see httpMethodHasBody);
|
|
// otherwise strict servers/proxies may answer 411 Length Required.
|
|
if (bodyLen > 0 || httpMethodHasBody(method)) {
|
|
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) {
|
|
// A genuine read error (reset, unclean TLS shutdown) is not the same as a clean EOF
|
|
// -- do not hand a truncated response to the script as a success.
|
|
return calogErrArgE;
|
|
}
|
|
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 *redirectValue;
|
|
const CalogAggT *headers;
|
|
const char *method;
|
|
const char *body;
|
|
int64_t bodyLen;
|
|
int32_t status;
|
|
int32_t maxRedirects;
|
|
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 = calogOkE;
|
|
urlValue = httpOptGet(opts, "url", &status);
|
|
if (status != calogOkE) {
|
|
return calogFail(result, status, "httpRequest: out of memory");
|
|
}
|
|
if (urlValue == NULL || urlValue->type != calogStringE) {
|
|
return calogFail(result, calogErrArgE, "httpRequest: options.url must be a string");
|
|
}
|
|
|
|
// method (optional; default GET).
|
|
method = "GET";
|
|
status = calogOkE;
|
|
methodValue = httpOptGet(opts, "method", &status);
|
|
if (status != calogOkE) {
|
|
return calogFail(result, status, "httpRequest: out of memory");
|
|
}
|
|
if (methodValue != NULL && methodValue->type == calogStringE) {
|
|
method = methodValue->as.s.bytes;
|
|
}
|
|
if (httpHasBadByte(method, strlen(method), true)) {
|
|
return calogFail(result, calogErrArgE, "httpRequest: options.method contains invalid characters");
|
|
}
|
|
|
|
// headers (optional).
|
|
headers = NULL;
|
|
status = calogOkE;
|
|
headersValue = httpOptGet(opts, "headers", &status);
|
|
if (status != calogOkE) {
|
|
return calogFail(result, status, "httpRequest: out of memory");
|
|
}
|
|
if (headersValue != NULL && headersValue->type == calogAggE) {
|
|
headers = headersValue->as.agg;
|
|
}
|
|
|
|
// body (optional).
|
|
body = NULL;
|
|
bodyLen = 0;
|
|
status = calogOkE;
|
|
bodyValue = httpOptGet(opts, "body", &status);
|
|
if (status != calogOkE) {
|
|
return calogFail(result, status, "httpRequest: out of memory");
|
|
}
|
|
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 = calogOkE;
|
|
insecureValue = httpOptGet(opts, "insecure", &status);
|
|
if (status != calogOkE) {
|
|
return calogFail(result, status, "httpRequest: out of memory");
|
|
}
|
|
if (insecureValue != NULL && insecureValue->type == calogBoolE && insecureValue->as.b) {
|
|
verify = false;
|
|
}
|
|
|
|
// maxRedirects (optional): how many 3xx redirects to follow. Default 16; 0 (or negative)
|
|
// disables following and returns the raw 3xx. Values above 16 are capped, so a caller can
|
|
// never widen the loop guard.
|
|
maxRedirects = HTTP_MAX_REDIRECTS;
|
|
status = calogOkE;
|
|
redirectValue = httpOptGet(opts, "maxRedirects", &status);
|
|
if (status != calogOkE) {
|
|
return calogFail(result, status, "httpRequest: out of memory");
|
|
}
|
|
if (redirectValue != NULL && redirectValue->type == calogIntE) {
|
|
if (redirectValue->as.i < 0) {
|
|
maxRedirects = 0;
|
|
} else if (redirectValue->as.i < HTTP_MAX_REDIRECTS) {
|
|
maxRedirects = (int32_t)redirectValue->as.i;
|
|
}
|
|
}
|
|
|
|
return httpFollow(method, urlValue->as.s.bytes, urlValue->as.s.length, headers, body, bodyLen, verify, maxRedirects, result);
|
|
}
|
|
|
|
|
|
// Resolve a redirect Location against the URL just fetched, writing an absolute URL into out.
|
|
// Handles an absolute URL, a protocol-relative "//host/path", an absolute path "/path", and a
|
|
// relative path (merged against the base path's directory). Leading whitespace is skipped;
|
|
// dot-segments (../) are NOT normalized. Returns false on overflow.
|
|
static bool httpResolveLocation(const HttpUrlT *base, const char *loc, char *out, size_t cap) {
|
|
const char *scheme;
|
|
int authority;
|
|
|
|
while (*loc == ' ' || *loc == '\t') {
|
|
loc++;
|
|
}
|
|
if (strncmp(loc, "http://", 7) == 0 || strncmp(loc, "https://", 8) == 0) {
|
|
if (strlen(loc) >= cap) {
|
|
return false;
|
|
}
|
|
strcpy(out, loc);
|
|
return true;
|
|
}
|
|
scheme = base->https ? "https" : "http";
|
|
if (loc[0] == '/' && loc[1] == '/') {
|
|
int n;
|
|
n = snprintf(out, cap, "%s:%s", scheme, loc);
|
|
return n > 0 && (size_t)n < cap;
|
|
}
|
|
authority = httpWriteAuthority(out, cap, scheme, base);
|
|
if (authority < 0) {
|
|
return false;
|
|
}
|
|
if (loc[0] == '/') {
|
|
if ((size_t)authority + strlen(loc) >= cap) {
|
|
return false;
|
|
}
|
|
strcpy(out + authority, loc);
|
|
return true;
|
|
}
|
|
if (loc[0] == '?' || loc[0] == '#') {
|
|
// Query- or fragment-only reference (RFC 3986 5.3): keep the whole base path, replace the
|
|
// rest -- NOT the directory-merge below, which would drop the base path's last segment.
|
|
size_t basePathLen;
|
|
|
|
basePathLen = strcspn(base->path, "?#");
|
|
if ((size_t)authority + basePathLen + strlen(loc) >= cap) {
|
|
return false;
|
|
}
|
|
memcpy(out + authority, base->path, basePathLen);
|
|
strcpy(out + (size_t)authority + basePathLen, loc);
|
|
return true;
|
|
}
|
|
// A relative path: append the base path's directory (up to and including its last '/', within
|
|
// the path part before any query/fragment), then loc.
|
|
{
|
|
size_t pathLen;
|
|
size_t dirLen;
|
|
size_t index;
|
|
|
|
pathLen = strcspn(base->path, "?#");
|
|
dirLen = 1; // base->path always begins with '/'
|
|
for (index = 0; index < pathLen; index++) {
|
|
if (base->path[index] == '/') {
|
|
dirLen = index + 1;
|
|
}
|
|
}
|
|
if ((size_t)authority + dirLen + strlen(loc) >= cap) {
|
|
return false;
|
|
}
|
|
memcpy(out + authority, base->path, dirLen);
|
|
strcpy(out + (size_t)authority + dirLen, loc);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
// A response header value (name lowercase, as httpBuildResult keys them) from a built result
|
|
// map, or NULL. Aliases the map; valid until it is freed.
|
|
static const CalogValueT *httpResultHeader(const CalogValueT *result, const char *name) {
|
|
CalogValueT *headers;
|
|
|
|
if (result->type != calogAggE) {
|
|
return NULL;
|
|
}
|
|
headers = httpAggField(result->as.agg, "headers");
|
|
if (headers == NULL || headers->type != calogAggE) {
|
|
return NULL;
|
|
}
|
|
return httpAggField(headers->as.agg, name);
|
|
}
|
|
|
|
|
|
// The integer status code from a built result map, or -1 if absent/malformed.
|
|
static int32_t httpResultStatus(const CalogValueT *result) {
|
|
CalogValueT *value;
|
|
|
|
if (result->type != calogAggE) {
|
|
return -1;
|
|
}
|
|
value = httpAggField(result->as.agg, "status");
|
|
if (value == NULL || value->type != calogIntE) {
|
|
return -1;
|
|
}
|
|
return (int32_t)value->as.i;
|
|
}
|
|
|
|
|
|
// Same origin = same scheme, host, and port -- the boundary at which credential headers are
|
|
// dropped from a followed redirect.
|
|
static bool httpSameOrigin(const HttpUrlT *a, const HttpUrlT *b) {
|
|
return a->https == b->https && strcmp(a->host, b->host) == 0 && strcmp(a->port, b->port) == 0;
|
|
}
|
|
|
|
|
|
static void httpSetTimeouts(CalogSocketT fd, int timeoutSec) {
|
|
// Best effort: if the kernel rejects these (unusual), the connection just falls back to no
|
|
// read/write deadline rather than failing the request outright. Winsock's SO_RCVTIMEO takes a
|
|
// DWORD of milliseconds; BSD sockets take a struct timeval.
|
|
#if defined(_WIN32)
|
|
DWORD ms;
|
|
ms = (DWORD)timeoutSec * 1000u;
|
|
(void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&ms, sizeof(ms));
|
|
(void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (const char *)&ms, sizeof(ms));
|
|
#else
|
|
struct timeval tv;
|
|
tv.tv_sec = timeoutSec;
|
|
tv.tv_usec = 0;
|
|
(void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof(tv));
|
|
(void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (const char *)&tv, sizeof(tv));
|
|
#endif
|
|
}
|
|
|
|
|
|
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 CA trusted by the host, loaded
|
|
// from its native trust configuration. With no anchors available the handshake would fail
|
|
// with a misleading per-certificate error, so surface a clear one here instead. (An https
|
|
// request can pass insecure=true to skip verification.)
|
|
SSL_CTX_set_verify(conn->ctx, SSL_VERIFY_PEER, NULL);
|
|
if (!httpLoadTrustStore(conn->ctx)) {
|
|
return calogFail(result, calogErrUnsupportedE, "http: no system CA trust store found (set SSL_CERT_FILE or CALOG_CA_BUNDLE, or pass insecure=true)");
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
|
|
// Write "scheme://host" (bracketing an IPv6 host, and adding ":port" when non-default) into out.
|
|
// Returns the number of bytes written (excluding the NUL), or -1 if it does not fit.
|
|
static int httpWriteAuthority(char *out, size_t cap, const char *scheme, const HttpUrlT *base) {
|
|
int n;
|
|
|
|
if (base->ipv6 && !httpDefaultPort(base)) {
|
|
n = snprintf(out, cap, "%s://[%s]:%s", scheme, base->host, base->port);
|
|
} else if (base->ipv6) {
|
|
n = snprintf(out, cap, "%s://[%s]", scheme, base->host);
|
|
} else if (!httpDefaultPort(base)) {
|
|
n = snprintf(out, cap, "%s://%s:%s", scheme, base->host, base->port);
|
|
} else {
|
|
n = snprintf(out, cap, "%s://%s", scheme, base->host);
|
|
}
|
|
return (n > 0 && (size_t)n < cap) ? n : -1;
|
|
}
|