284 lines
8.4 KiB
C
284 lines
8.4 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) {
|
|
// If port is “80” for HTTP or “443” for HTTPS, omit the “:port” in Host:
|
|
char host_header[512];
|
|
if ((strcmp(port, "80") == 0) || (strcmp(port, "443") == 0)) {
|
|
// default port: just “Host: hostname”
|
|
snprintf(host_header, sizeof(host_header), "Host: %s\r\n", host);
|
|
} else {
|
|
// non-default port: “Host: hostname:port”
|
|
snprintf(host_header, sizeof(host_header), "Host: %s:%s\r\n", host, port);
|
|
}
|
|
|
|
// Now build the rest of the request, including a User-Agent:
|
|
char tmp[1024];
|
|
int n = snprintf(tmp, sizeof(tmp),
|
|
"GET %s HTTP/1.1\r\n"
|
|
"%s" // the host_header line
|
|
"User-Agent: ProsperonFetch/1.0\r\n"
|
|
"Accept: */*\r\n"
|
|
"Connection: close\r\n"
|
|
"\r\n",
|
|
path, host_header);
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|