Files
cell/source/qjs_http.c

273 lines
8.0 KiB
C

#include "qjs_blob.h"
#include "picohttpparser.h"
#include <mbedtls/net_sockets.h>
#include <mbedtls/ssl.h>
#include <mbedtls/entropy.h>
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/error.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
// Simple dynamic buffer for reading the response
typedef struct {
char *data;
size_t size;
size_t capacity;
} buffer_t;
static void buffer_init(buffer_t *b) {
b->data = NULL;
b->size = 0;
b->capacity = 0;
}
static int buffer_append(buffer_t *b, const void *p, size_t len) {
if (b->size + len > b->capacity) {
size_t new_cap = b->capacity ? b->capacity * 2 : 4096;
while (new_cap < b->size + len) new_cap *= 2;
char *nx = realloc(b->data, new_cap);
if (!nx) return -1;
b->data = nx;
b->capacity = new_cap;
}
memcpy(b->data + b->size, p, len);
b->size += len;
return 0;
}
static void buffer_free(buffer_t *b) {
free(b->data);
b->data = NULL;
b->size = 0;
b->capacity = 0;
}
// Parses "http://host:port/path" or "https://host:port/path"
static int parse_url(const char *url, int *use_ssl,
char **host, char **port, char **path) {
*host = *port = *path = NULL;
*use_ssl = 0;
const char *p = url;
if (strncmp(p, "https://", 8) == 0) {
*use_ssl = 1;
p += 8;
} else if (strncmp(p, "http://", 7) == 0) {
*use_ssl = 0;
p += 7;
} else {
return -1;
}
// find path
const char *path_start = strchr(p, '/');
size_t hostport_len = path_start ? (size_t)(path_start - p) : strlen(p);
*path = path_start ? strdup(path_start) : strdup("/");
if (!*path) return -1;
// check for explicit port
const char *colon = memmem(p, hostport_len, ":", 1);
if (colon) {
size_t hlen = (size_t)(colon - p);
*host = strndup(p, hlen);
size_t plen = hostport_len - hlen - 1;
*port = strndup(colon + 1, plen);
} else {
*host = strndup(p, hostport_len);
*port = strdup(*use_ssl ? "443" : "80");
}
if (!*host || !*port) {
free(*host); free(*port); free(*path);
return -1;
}
return 0;
}
// Builds a GET request into buf
static int build_get_request(buffer_t *buf,
const char *host, const char *port, const char *path) {
// HTTP/1.1 requires Host header; Connection: close for simplicity
char tmp[512];
int n = snprintf(tmp, sizeof(tmp),
"GET %s HTTP/1.1\r\n"
"Host: %s:%s\r\n"
"Connection: close\r\n"
"\r\n",
path, host, port);
if (n < 0) return -1;
return buffer_append(buf, tmp, (size_t)n);
}
// Performs a blocking HTTP GET and returns a QuickJS Blob of the body
static JSValue js_fetch_picoparser(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1 || !JS_IsString(argv[0]))
return JS_ThrowTypeError(ctx, "fetch: URL string required");
const char *url = JS_ToCString(ctx, argv[0]);
if (!url) return JS_ThrowTypeError(ctx, "fetch: invalid URL");
int use_ssl = 0;
char *host = NULL, *port = NULL, *path = NULL;
if (parse_url(url, &use_ssl, &host, &port, &path) < 0) {
JS_FreeCString(ctx, url);
return JS_ThrowTypeError(ctx, "fetch: URL parse error");
}
// Prepare mbedTLS structures
mbedtls_net_context net_ctx;
mbedtls_ssl_context ssl;
mbedtls_ssl_config conf;
mbedtls_ctr_drbg_context drbg;
mbedtls_entropy_context entropy;
mbedtls_net_init(&net_ctx);
mbedtls_ssl_init(&ssl);
mbedtls_ssl_config_init(&conf);
mbedtls_ctr_drbg_init(&drbg);
mbedtls_entropy_init(&entropy);
int ret;
const char *pers = "js_fetch";
if (use_ssl) {
if ((ret = mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy,
(const unsigned char *)pers,
strlen(pers))) != 0)
goto cleanup_tls;
if ((ret = mbedtls_ssl_config_defaults(&conf,
MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM,
MBEDTLS_SSL_PRESET_DEFAULT)) != 0)
goto cleanup_tls;
mbedtls_ssl_conf_authmode(&conf, MBEDTLS_SSL_VERIFY_OPTIONAL);
mbedtls_ssl_conf_rng(&conf, mbedtls_ctr_drbg_random, &drbg);
if ((ret = mbedtls_ssl_setup(&ssl, &conf)) != 0)
goto cleanup_tls;
if ((ret = mbedtls_ssl_set_hostname(&ssl, host)) != 0)
goto cleanup_tls;
}
// Connect to host:port
if ((ret = mbedtls_net_connect(&net_ctx, host, port,
MBEDTLS_NET_PROTO_TCP)) != 0)
goto cleanup_tls;
if (use_ssl) {
mbedtls_ssl_set_bio(&ssl, &net_ctx,
mbedtls_net_send, mbedtls_net_recv, NULL);
while ((ret = mbedtls_ssl_handshake(&ssl)) != 0) {
if (ret != MBEDTLS_ERR_SSL_WANT_READ &&
ret != MBEDTLS_ERR_SSL_WANT_WRITE)
goto cleanup_all;
}
}
// Build and send GET request
buffer_t req = {0}, resp = {0};
buffer_init(&req);
buffer_init(&resp);
if (build_get_request(&req, host, port, path) < 0)
goto cleanup_all;
// Write request
size_t off = 0;
while (off < req.size) {
if (use_ssl) {
ret = mbedtls_ssl_write(&ssl,
(const unsigned char *)req.data + off,
req.size - off);
if (ret == MBEDTLS_ERR_SSL_WANT_READ ||
ret == MBEDTLS_ERR_SSL_WANT_WRITE)
continue;
} else {
ret = mbedtls_net_send(&net_ctx,
(unsigned char *)req.data + off,
req.size - off);
}
if (ret <= 0) goto cleanup_all;
off += (size_t)ret;
}
buffer_free(&req);
// Read entire response into resp.data
unsigned char buf[4096];
do {
if (use_ssl) {
ret = mbedtls_ssl_read(&ssl, buf, sizeof(buf));
if (ret == MBEDTLS_ERR_SSL_WANT_READ ||
ret == MBEDTLS_ERR_SSL_WANT_WRITE)
continue;
} else {
ret = mbedtls_net_recv(&net_ctx, buf, sizeof(buf));
}
if (ret > 0) {
if (buffer_append(&resp, buf, (size_t)ret) < 0)
goto cleanup_all;
}
} while (ret > 0);
// Parse status-line + headers with picohttpparser
struct phr_header hdrv[32];
size_t hdr_count = sizeof(hdrv)/sizeof(hdrv[0]);
int minor, status;
const char *reason = NULL;
size_t reason_len = 0;
int pret = phr_parse_response(
resp.data, resp.size,
&minor, &status,
&reason, &reason_len, /* provide pointers, even if unused */
hdrv, &hdr_count,
0);
if (pret < 0) goto cleanup_all;
size_t header_len = (size_t)pret;
char *body_ptr = resp.data + header_len;
size_t body_len = resp.size - header_len;
// Check for chunked encoding
int is_chunked = 0;
for (size_t i = 0; i < hdr_count; i++) {
if ((hdrv[i].name_len == 17 &&
!strncasecmp(hdrv[i].name, "Transfer-Encoding", 17) &&
memmem(hdrv[i].value, hdrv[i].value_len, "chunked", 7))) {
is_chunked = 1;
break;
}
}
// If chunked, decode in-place
if (is_chunked) {
struct phr_chunked_decoder cd = {0};
size_t blen = body_len;
ssize_t rest = phr_decode_chunked(&cd, body_ptr, &blen);
if (rest < 0) goto cleanup_all;
// blen = decoded length; rest = trailer start offset (ignored)
body_len = blen;
}
// Return a Blob wrapping the body buffer (no extra copy)
JSValue blob = js_new_blob_stoned_copy(ctx,
(uint8_t *)body_ptr, body_len);
cleanup_all:
buffer_free(&resp);
mbedtls_ssl_close_notify(&ssl);
cleanup_tls:
mbedtls_net_free(&net_ctx);
mbedtls_ssl_free(&ssl);
mbedtls_ssl_config_free(&conf);
mbedtls_ctr_drbg_free(&drbg);
mbedtls_entropy_free(&entropy);
JS_FreeCString(ctx, url);
free(host); free(port); free(path);
return blob;
}
// Export the function as “fetch2” (for example)
static const JSCFunctionListEntry js_http_funcs[] = {
JS_CFUNC_DEF("fetch", 2, js_fetch_picoparser),
};
JSValue js_http_use(JSContext *js) {
JSValue obj = JS_NewObject(js);
JS_SetPropertyFunctionList(js, obj, js_http_funcs,
sizeof(js_http_funcs)/sizeof(js_http_funcs[0]));
return obj;
}