984 lines
30 KiB
C
984 lines
30 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. TLS does NO certificate verification in v1 (documented in the header).
|
|
|
|
#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>
|
|
|
|
#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.
|
|
typedef struct HttpConnT {
|
|
int fd;
|
|
SSL *ssl;
|
|
SSL_CTX *ctx;
|
|
} 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, 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, 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, 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");
|
|
}
|
|
|
|
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 keyValue;
|
|
const CalogAggT *headers;
|
|
const char *method;
|
|
const char *body;
|
|
int64_t bodyLen;
|
|
int32_t status;
|
|
|
|
(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;
|
|
}
|
|
|
|
return httpPerform(method, urlValue->as.s.bytes, urlValue->as.s.length, headers, body, bodyLen, 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");
|
|
}
|
|
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. NOTE: v1 does NO certificate
|
|
// verification -- https here is transport encryption only, not server authentication.
|
|
(void)SSL_set_tlsext_host_name(conn->ssl, url->host);
|
|
if (SSL_connect(conn->ssl) != 1) {
|
|
return calogFail(result, calogErrUnsupportedE, "http: TLS handshake failed");
|
|
}
|
|
return calogOkE;
|
|
}
|