837 lines
27 KiB
C
837 lines
27 KiB
C
#include "qjs_http.h"
|
|
#include "qjs_blob.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <mbedtls/net_sockets.h>
|
|
#include <mbedtls/ssl.h>
|
|
#include <mbedtls/entropy.h>
|
|
#include <mbedtls/ctr_drbg.h>
|
|
#include <mbedtls/error.h>
|
|
#include <ctype.h>
|
|
|
|
typedef struct {
|
|
char *data;
|
|
size_t size;
|
|
size_t capacity;
|
|
} buffer_t;
|
|
|
|
typedef struct {
|
|
mbedtls_net_context server_fd;
|
|
mbedtls_ssl_context ssl;
|
|
mbedtls_ssl_config conf;
|
|
mbedtls_entropy_context entropy;
|
|
mbedtls_ctr_drbg_context ctr_drbg;
|
|
int use_ssl;
|
|
int headers_complete;
|
|
buffer_t header_buf;
|
|
size_t content_length;
|
|
size_t bytes_read;
|
|
int chunked_encoding;
|
|
size_t chunk_size;
|
|
size_t chunk_remaining;
|
|
int chunk_state; // 0: size, 1: data, 2: trailer
|
|
} http_connection_t;
|
|
|
|
static void buffer_init(buffer_t *buf) {
|
|
buf->data = NULL;
|
|
buf->size = 0;
|
|
buf->capacity = 0;
|
|
}
|
|
|
|
static int buffer_append(buffer_t *buf, const void *data, size_t len) {
|
|
if (buf->size + len > buf->capacity) {
|
|
size_t new_capacity = buf->capacity ? buf->capacity * 2 : 4096;
|
|
while (new_capacity < buf->size + len) {
|
|
new_capacity *= 2;
|
|
}
|
|
char *new_data = realloc(buf->data, new_capacity);
|
|
if (!new_data) return -1;
|
|
buf->data = new_data;
|
|
buf->capacity = new_capacity;
|
|
}
|
|
memcpy(buf->data + buf->size, data, len);
|
|
buf->size += len;
|
|
return 0;
|
|
}
|
|
|
|
static void buffer_free(buffer_t *buf) {
|
|
free(buf->data);
|
|
buf->data = NULL;
|
|
buf->size = 0;
|
|
buf->capacity = 0;
|
|
}
|
|
|
|
// Parse URL into components
|
|
static int parse_url(const char *url, char **host, char **port, char **path, int *use_ssl) {
|
|
*host = NULL;
|
|
*port = NULL;
|
|
*path = NULL;
|
|
*use_ssl = 0;
|
|
|
|
const char *p = url;
|
|
|
|
// Parse scheme
|
|
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; // Invalid scheme
|
|
}
|
|
|
|
// Find host end
|
|
const char *host_start = p;
|
|
const char *host_end = strchr(p, '/');
|
|
const char *port_start = strchr(p, ':');
|
|
|
|
if (port_start && (!host_end || port_start < host_end)) {
|
|
// Has explicit port
|
|
*host = strndup(host_start, port_start - host_start);
|
|
port_start++;
|
|
if (host_end) {
|
|
*port = strndup(port_start, host_end - port_start);
|
|
} else {
|
|
*port = strdup(port_start);
|
|
}
|
|
} else {
|
|
// No explicit port
|
|
if (host_end) {
|
|
*host = strndup(host_start, host_end - host_start);
|
|
} else {
|
|
*host = strdup(host_start);
|
|
}
|
|
*port = strdup(*use_ssl ? "443" : "80");
|
|
}
|
|
|
|
// Path
|
|
if (host_end) {
|
|
*path = strdup(host_end);
|
|
} else {
|
|
*path = strdup("/");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Perform HTTP request over plain socket
|
|
static int http_request(const char *host, const char *port, const char *request, size_t request_len, buffer_t *response) {
|
|
mbedtls_net_context server_fd;
|
|
int ret;
|
|
|
|
mbedtls_net_init(&server_fd);
|
|
|
|
// Connect to server
|
|
if ((ret = mbedtls_net_connect(&server_fd, host, port, MBEDTLS_NET_PROTO_TCP)) != 0) {
|
|
mbedtls_net_free(&server_fd);
|
|
return ret;
|
|
}
|
|
|
|
// Send request
|
|
size_t written = 0;
|
|
while (written < request_len) {
|
|
ret = mbedtls_net_send(&server_fd, (unsigned char *)request + written, request_len - written);
|
|
if (ret < 0) {
|
|
mbedtls_net_free(&server_fd);
|
|
return ret;
|
|
}
|
|
written += ret;
|
|
}
|
|
|
|
// Read response
|
|
unsigned char buf[4096];
|
|
do {
|
|
ret = mbedtls_net_recv(&server_fd, buf, sizeof(buf));
|
|
if (ret > 0) {
|
|
if (buffer_append(response, buf, ret) < 0) {
|
|
mbedtls_net_free(&server_fd);
|
|
return -1;
|
|
}
|
|
}
|
|
} while (ret > 0);
|
|
|
|
mbedtls_net_free(&server_fd);
|
|
return (ret == 0 || ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) ? 0 : ret;
|
|
}
|
|
|
|
// Perform HTTPS request over SSL
|
|
static int https_request(const char *host, const char *port, const char *request, size_t request_len, buffer_t *response) {
|
|
mbedtls_net_context server_fd;
|
|
mbedtls_ssl_context ssl;
|
|
mbedtls_ssl_config conf;
|
|
mbedtls_entropy_context entropy;
|
|
mbedtls_ctr_drbg_context ctr_drbg;
|
|
const char *pers = "qjs_https_client";
|
|
int ret;
|
|
|
|
// Initialize structures
|
|
mbedtls_net_init(&server_fd);
|
|
mbedtls_ssl_init(&ssl);
|
|
mbedtls_ssl_config_init(&conf);
|
|
mbedtls_entropy_init(&entropy);
|
|
mbedtls_ctr_drbg_init(&ctr_drbg);
|
|
|
|
// Seed RNG
|
|
if ((ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy,
|
|
(const unsigned char *)pers, strlen(pers))) != 0) {
|
|
goto exit;
|
|
}
|
|
|
|
// Connect to server
|
|
if ((ret = mbedtls_net_connect(&server_fd, host, port, MBEDTLS_NET_PROTO_TCP)) != 0) {
|
|
goto exit;
|
|
}
|
|
|
|
// Configure SSL
|
|
if ((ret = mbedtls_ssl_config_defaults(&conf, MBEDTLS_SSL_IS_CLIENT,
|
|
MBEDTLS_SSL_TRANSPORT_STREAM,
|
|
MBEDTLS_SSL_PRESET_DEFAULT)) != 0) {
|
|
goto exit;
|
|
}
|
|
|
|
mbedtls_ssl_conf_rng(&conf, mbedtls_ctr_drbg_random, &ctr_drbg);
|
|
mbedtls_ssl_conf_authmode(&conf, MBEDTLS_SSL_VERIFY_OPTIONAL);
|
|
|
|
if ((ret = mbedtls_ssl_setup(&ssl, &conf)) != 0) {
|
|
goto exit;
|
|
}
|
|
|
|
if ((ret = mbedtls_ssl_set_hostname(&ssl, host)) != 0) {
|
|
goto exit;
|
|
}
|
|
|
|
mbedtls_ssl_set_bio(&ssl, &server_fd, mbedtls_net_send, mbedtls_net_recv, NULL);
|
|
|
|
// Handshake
|
|
while ((ret = mbedtls_ssl_handshake(&ssl)) != 0) {
|
|
if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
|
|
goto exit;
|
|
}
|
|
}
|
|
|
|
// Send request
|
|
size_t written = 0;
|
|
while (written < request_len) {
|
|
ret = mbedtls_ssl_write(&ssl, (const unsigned char *)request + written, request_len - written);
|
|
if (ret < 0) {
|
|
if (ret != MBEDTLS_ERR_SSL_WANT_WRITE && ret != MBEDTLS_ERR_SSL_WANT_READ) {
|
|
goto exit;
|
|
}
|
|
} else {
|
|
written += ret;
|
|
}
|
|
}
|
|
|
|
// Read response
|
|
unsigned char buf[4096];
|
|
do {
|
|
ret = mbedtls_ssl_read(&ssl, buf, sizeof(buf));
|
|
if (ret > 0) {
|
|
if (buffer_append(response, buf, ret) < 0) {
|
|
ret = -1;
|
|
goto exit;
|
|
}
|
|
}
|
|
} while (ret > 0);
|
|
|
|
if (ret < 0 && ret != MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY &&
|
|
ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
|
|
goto exit;
|
|
}
|
|
|
|
ret = 0;
|
|
|
|
exit:
|
|
mbedtls_ssl_close_notify(&ssl);
|
|
mbedtls_net_free(&server_fd);
|
|
mbedtls_ssl_free(&ssl);
|
|
mbedtls_ssl_config_free(&conf);
|
|
mbedtls_ctr_drbg_free(&ctr_drbg);
|
|
mbedtls_entropy_free(&entropy);
|
|
|
|
return ret;
|
|
}
|
|
|
|
// Extract body from HTTP response
|
|
static char *extract_body(const char *response, size_t response_len, size_t *body_len) {
|
|
const char *body_start = strstr(response, "\r\n\r\n");
|
|
if (!body_start) {
|
|
return NULL;
|
|
}
|
|
body_start += 4;
|
|
|
|
*body_len = response_len - (body_start - response);
|
|
return (char *)body_start;
|
|
}
|
|
|
|
// Parse HTTP headers
|
|
static int parse_headers(http_connection_t *conn) {
|
|
char *headers_end = strstr(conn->header_buf.data, "\r\n\r\n");
|
|
if (!headers_end) {
|
|
return 0; // Headers not complete
|
|
}
|
|
|
|
*headers_end = '\0';
|
|
|
|
// Parse Content-Length
|
|
char *content_length = strstr(conn->header_buf.data, "Content-Length:");
|
|
if (!content_length) {
|
|
content_length = strstr(conn->header_buf.data, "content-length:");
|
|
}
|
|
if (content_length) {
|
|
content_length += 15;
|
|
while (*content_length == ' ') content_length++;
|
|
conn->content_length = strtoul(content_length, NULL, 10);
|
|
}
|
|
|
|
// Check for chunked encoding
|
|
char *transfer_encoding = strstr(conn->header_buf.data, "Transfer-Encoding:");
|
|
if (!transfer_encoding) {
|
|
transfer_encoding = strstr(conn->header_buf.data, "transfer-encoding:");
|
|
}
|
|
if (transfer_encoding) {
|
|
transfer_encoding += 18;
|
|
while (*transfer_encoding == ' ') transfer_encoding++;
|
|
if (strncmp(transfer_encoding, "chunked", 7) == 0) {
|
|
conn->chunked_encoding = 1;
|
|
}
|
|
}
|
|
|
|
conn->headers_complete = 1;
|
|
|
|
// Move any body data to the beginning of the buffer
|
|
headers_end += 4;
|
|
size_t body_len = conn->header_buf.size - (headers_end - conn->header_buf.data);
|
|
if (body_len > 0) {
|
|
memmove(conn->header_buf.data, headers_end, body_len);
|
|
conn->header_buf.size = body_len;
|
|
} else {
|
|
conn->header_buf.size = 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Initialize HTTP connection
|
|
static void http_connection_init(http_connection_t *conn) {
|
|
memset(conn, 0, sizeof(http_connection_t));
|
|
mbedtls_net_init(&conn->server_fd);
|
|
mbedtls_ssl_init(&conn->ssl);
|
|
mbedtls_ssl_config_init(&conn->conf);
|
|
mbedtls_entropy_init(&conn->entropy);
|
|
mbedtls_ctr_drbg_init(&conn->ctr_drbg);
|
|
buffer_init(&conn->header_buf);
|
|
}
|
|
|
|
// Free HTTP connection
|
|
static void http_connection_free(http_connection_t *conn) {
|
|
if (conn->use_ssl) {
|
|
mbedtls_ssl_close_notify(&conn->ssl);
|
|
}
|
|
mbedtls_net_free(&conn->server_fd);
|
|
mbedtls_ssl_free(&conn->ssl);
|
|
mbedtls_ssl_config_free(&conn->conf);
|
|
mbedtls_ctr_drbg_free(&conn->ctr_drbg);
|
|
mbedtls_entropy_free(&conn->entropy);
|
|
buffer_free(&conn->header_buf);
|
|
}
|
|
|
|
// Start HTTP connection
|
|
static int http_connection_start(http_connection_t *conn, const char *host, const char *port,
|
|
const char *request, size_t request_len, int use_ssl) {
|
|
conn->use_ssl = use_ssl;
|
|
int ret;
|
|
|
|
// Connect to server
|
|
if ((ret = mbedtls_net_connect(&conn->server_fd, host, port, MBEDTLS_NET_PROTO_TCP)) != 0) {
|
|
return ret;
|
|
}
|
|
|
|
if (use_ssl) {
|
|
const char *pers = "qjs_https_client";
|
|
|
|
// Seed RNG
|
|
if ((ret = mbedtls_ctr_drbg_seed(&conn->ctr_drbg, mbedtls_entropy_func, &conn->entropy,
|
|
(const unsigned char *)pers, strlen(pers))) != 0) {
|
|
return ret;
|
|
}
|
|
|
|
// Configure SSL
|
|
if ((ret = mbedtls_ssl_config_defaults(&conn->conf, MBEDTLS_SSL_IS_CLIENT,
|
|
MBEDTLS_SSL_TRANSPORT_STREAM,
|
|
MBEDTLS_SSL_PRESET_DEFAULT)) != 0) {
|
|
return ret;
|
|
}
|
|
|
|
mbedtls_ssl_conf_rng(&conn->conf, mbedtls_ctr_drbg_random, &conn->ctr_drbg);
|
|
mbedtls_ssl_conf_authmode(&conn->conf, MBEDTLS_SSL_VERIFY_OPTIONAL);
|
|
|
|
if ((ret = mbedtls_ssl_setup(&conn->ssl, &conn->conf)) != 0) {
|
|
return ret;
|
|
}
|
|
|
|
if ((ret = mbedtls_ssl_set_hostname(&conn->ssl, host)) != 0) {
|
|
return ret;
|
|
}
|
|
|
|
mbedtls_ssl_set_bio(&conn->ssl, &conn->server_fd, mbedtls_net_send, mbedtls_net_recv, NULL);
|
|
|
|
// Handshake
|
|
while ((ret = mbedtls_ssl_handshake(&conn->ssl)) != 0) {
|
|
if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
// Send request
|
|
size_t written = 0;
|
|
while (written < request_len) {
|
|
ret = mbedtls_ssl_write(&conn->ssl, (const unsigned char *)request + written, request_len - written);
|
|
if (ret < 0) {
|
|
if (ret != MBEDTLS_ERR_SSL_WANT_WRITE && ret != MBEDTLS_ERR_SSL_WANT_READ) {
|
|
return ret;
|
|
}
|
|
} else {
|
|
written += ret;
|
|
}
|
|
}
|
|
} else {
|
|
// Send request for plain HTTP
|
|
size_t written = 0;
|
|
while (written < request_len) {
|
|
ret = mbedtls_net_send(&conn->server_fd, (unsigned char *)request + written, request_len - written);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
written += ret;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Read chunk from connection
|
|
static int http_connection_read_chunk(http_connection_t *conn, void *buf, size_t max_len, size_t *actual_len) {
|
|
int ret;
|
|
*actual_len = 0;
|
|
|
|
// First, read headers if not complete
|
|
if (!conn->headers_complete) {
|
|
unsigned char temp[1024];
|
|
|
|
if (conn->use_ssl) {
|
|
ret = mbedtls_ssl_read(&conn->ssl, temp, sizeof(temp));
|
|
} else {
|
|
ret = mbedtls_net_recv(&conn->server_fd, temp, sizeof(temp));
|
|
}
|
|
|
|
if (ret > 0) {
|
|
buffer_append(&conn->header_buf, temp, ret);
|
|
if (parse_headers(conn)) {
|
|
// Headers complete, check if there's body data in the buffer
|
|
if (conn->header_buf.size > 0 && max_len > 0) {
|
|
size_t to_copy = conn->header_buf.size > max_len ? max_len : conn->header_buf.size;
|
|
memcpy(buf, conn->header_buf.data, to_copy);
|
|
*actual_len = to_copy;
|
|
conn->bytes_read += to_copy;
|
|
|
|
// Remove copied data from buffer
|
|
if (to_copy < conn->header_buf.size) {
|
|
memmove(conn->header_buf.data, conn->header_buf.data + to_copy,
|
|
conn->header_buf.size - to_copy);
|
|
conn->header_buf.size -= to_copy;
|
|
} else {
|
|
conn->header_buf.size = 0;
|
|
}
|
|
}
|
|
}
|
|
} else if (ret < 0 && ret != MBEDTLS_ERR_SSL_WANT_READ) {
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Headers are complete, read body data
|
|
if (conn->chunked_encoding) {
|
|
// Handle chunked encoding - simplified for now
|
|
// In a full implementation, we'd need to parse chunk sizes
|
|
if (conn->use_ssl) {
|
|
ret = mbedtls_ssl_read(&conn->ssl, buf, max_len);
|
|
} else {
|
|
ret = mbedtls_net_recv(&conn->server_fd, buf, max_len);
|
|
}
|
|
} else {
|
|
// Regular content-length based reading
|
|
size_t remaining = conn->content_length - conn->bytes_read;
|
|
if (remaining == 0) {
|
|
return 0; // EOF
|
|
}
|
|
|
|
size_t to_read = remaining > max_len ? max_len : remaining;
|
|
|
|
if (conn->use_ssl) {
|
|
ret = mbedtls_ssl_read(&conn->ssl, buf, to_read);
|
|
} else {
|
|
ret = mbedtls_net_recv(&conn->server_fd, buf, to_read);
|
|
}
|
|
}
|
|
|
|
if (ret > 0) {
|
|
*actual_len = ret;
|
|
conn->bytes_read += ret;
|
|
} else if (ret == 0 || ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) {
|
|
return 0; // EOF
|
|
} else if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
|
|
return 0; // Try again
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
// JS function: fetch_start(url, options) - starts a chunked download
|
|
static JSValue js_fetch_start(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
|
if (argc < 1 || !JS_IsString(argv[0])) {
|
|
return JS_ThrowTypeError(ctx, "fetch_start expects a URL string");
|
|
}
|
|
|
|
const char *url = JS_ToCString(ctx, argv[0]);
|
|
if (!url) {
|
|
return JS_ThrowTypeError(ctx, "Invalid URL");
|
|
}
|
|
|
|
char *host = NULL;
|
|
char *port = NULL;
|
|
char *path = NULL;
|
|
int use_ssl = 0;
|
|
buffer_t request_buf;
|
|
JSValue result = JS_EXCEPTION;
|
|
|
|
buffer_init(&request_buf);
|
|
|
|
// Parse URL
|
|
if (parse_url(url, &host, &port, &path, &use_ssl) < 0) {
|
|
JS_FreeCString(ctx, url);
|
|
return JS_ThrowTypeError(ctx, "Invalid URL format");
|
|
}
|
|
|
|
// Build request
|
|
buffer_append(&request_buf, "GET ", 4);
|
|
buffer_append(&request_buf, path, strlen(path));
|
|
buffer_append(&request_buf, " HTTP/1.1\r\n", 11);
|
|
buffer_append(&request_buf, "Host: ", 6);
|
|
buffer_append(&request_buf, host, strlen(host));
|
|
buffer_append(&request_buf, "\r\n", 2);
|
|
|
|
// Add headers from options if provided
|
|
if (argc >= 2 && JS_IsObject(argv[1])) {
|
|
JSValue headers = JS_GetPropertyStr(ctx, argv[1], "headers");
|
|
if (JS_IsObject(headers)) {
|
|
JSPropertyEnum *tab;
|
|
uint32_t len;
|
|
if (JS_GetOwnPropertyNames(ctx, &tab, &len, headers, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) {
|
|
for (uint32_t i = 0; i < len; i++) {
|
|
JSValue key = JS_AtomToString(ctx, tab[i].atom);
|
|
JSValue val = JS_GetProperty(ctx, headers, tab[i].atom);
|
|
|
|
const char *key_str = JS_ToCString(ctx, key);
|
|
const char *val_str = JS_ToCString(ctx, val);
|
|
|
|
if (key_str && val_str) {
|
|
buffer_append(&request_buf, key_str, strlen(key_str));
|
|
buffer_append(&request_buf, ": ", 2);
|
|
buffer_append(&request_buf, val_str, strlen(val_str));
|
|
buffer_append(&request_buf, "\r\n", 2);
|
|
}
|
|
|
|
JS_FreeCString(ctx, key_str);
|
|
JS_FreeCString(ctx, val_str);
|
|
JS_FreeValue(ctx, key);
|
|
JS_FreeValue(ctx, val);
|
|
}
|
|
js_free(ctx, tab);
|
|
}
|
|
}
|
|
JS_FreeValue(ctx, headers);
|
|
}
|
|
|
|
buffer_append(&request_buf, "Connection: close\r\n\r\n", 21);
|
|
|
|
// Create connection object
|
|
http_connection_t *conn = js_mallocz(ctx, sizeof(http_connection_t));
|
|
if (!conn) {
|
|
result = JS_ThrowOutOfMemory(ctx);
|
|
goto cleanup;
|
|
}
|
|
|
|
http_connection_init(conn);
|
|
|
|
// Start connection
|
|
int ret = http_connection_start(conn, host, port, request_buf.data, request_buf.size, use_ssl);
|
|
if (ret != 0) {
|
|
char error_buf[256];
|
|
mbedtls_strerror(ret, error_buf, sizeof(error_buf));
|
|
http_connection_free(conn);
|
|
js_free(ctx, conn);
|
|
result = JS_ThrowInternalError(ctx, "Connection failed: %s", error_buf);
|
|
goto cleanup;
|
|
}
|
|
|
|
// Create JS object to represent the connection
|
|
result = JS_NewObjectClass(ctx, JS_CLASS_OBJECT);
|
|
JS_SetOpaque(result, conn);
|
|
|
|
// Add properties
|
|
JS_DefinePropertyValueStr(ctx, result, "url", JS_NewString(ctx, url), JS_PROP_C_W_E);
|
|
JS_DefinePropertyValueStr(ctx, result, "_connection", JS_NewInt64(ctx, (intptr_t)conn), JS_PROP_C_W_E);
|
|
|
|
cleanup:
|
|
JS_FreeCString(ctx, url);
|
|
free(host);
|
|
free(port);
|
|
free(path);
|
|
buffer_free(&request_buf);
|
|
|
|
return result;
|
|
}
|
|
|
|
// JS function: fetch_read_chunk(connection, maxBytes) - reads a chunk
|
|
static JSValue js_fetch_read_chunk(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
|
if (argc < 1) {
|
|
return JS_ThrowTypeError(ctx, "fetch_read_chunk expects a connection object");
|
|
}
|
|
|
|
JSValue conn_val = JS_GetPropertyStr(ctx, argv[0], "_connection");
|
|
if (JS_IsUndefined(conn_val)) {
|
|
return JS_ThrowTypeError(ctx, "Invalid connection object");
|
|
}
|
|
|
|
int64_t conn_ptr;
|
|
if (JS_ToInt64(ctx, &conn_ptr, conn_val) < 0) {
|
|
JS_FreeValue(ctx, conn_val);
|
|
return JS_ThrowTypeError(ctx, "Invalid connection pointer");
|
|
}
|
|
JS_FreeValue(ctx, conn_val);
|
|
|
|
http_connection_t *conn = (http_connection_t *)(intptr_t)conn_ptr;
|
|
|
|
size_t max_bytes = 8192;
|
|
if (argc >= 2) {
|
|
int64_t mb;
|
|
if (JS_ToInt64(ctx, &mb, argv[1]) == 0 && mb > 0) {
|
|
max_bytes = mb;
|
|
}
|
|
}
|
|
|
|
uint8_t *buf = js_malloc(ctx, max_bytes);
|
|
if (!buf) {
|
|
return JS_ThrowOutOfMemory(ctx);
|
|
}
|
|
|
|
size_t actual_len;
|
|
int ret = http_connection_read_chunk(conn, buf, max_bytes, &actual_len);
|
|
|
|
JSValue result;
|
|
if (ret < 0 && ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
|
|
char error_buf[256];
|
|
mbedtls_strerror(ret, error_buf, sizeof(error_buf));
|
|
js_free(ctx, buf);
|
|
return JS_ThrowInternalError(ctx, "Read failed: %s", error_buf);
|
|
}
|
|
|
|
if (actual_len == 0 && ret == 0) {
|
|
// EOF or would block
|
|
js_free(ctx, buf);
|
|
return JS_NULL;
|
|
}
|
|
|
|
// Return chunk as ArrayBuffer
|
|
result = js_new_blob_stoned_copy(ctx, buf, actual_len);
|
|
js_free(ctx, buf);
|
|
|
|
return result;
|
|
}
|
|
|
|
// JS function: fetch_info(connection) - gets connection info
|
|
static JSValue js_fetch_info(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
|
if (argc < 1) {
|
|
return JS_ThrowTypeError(ctx, "fetch_info expects a connection object");
|
|
}
|
|
|
|
JSValue conn_val = JS_GetPropertyStr(ctx, argv[0], "_connection");
|
|
if (JS_IsUndefined(conn_val)) {
|
|
return JS_ThrowTypeError(ctx, "Invalid connection object");
|
|
}
|
|
|
|
int64_t conn_ptr;
|
|
if (JS_ToInt64(ctx, &conn_ptr, conn_val) < 0) {
|
|
JS_FreeValue(ctx, conn_val);
|
|
return JS_ThrowTypeError(ctx, "Invalid connection pointer");
|
|
}
|
|
JS_FreeValue(ctx, conn_val);
|
|
|
|
http_connection_t *conn = (http_connection_t *)(intptr_t)conn_ptr;
|
|
|
|
JSValue result = JS_NewObject(ctx);
|
|
JS_DefinePropertyValueStr(ctx, result, "headers_complete", JS_NewBool(ctx, conn->headers_complete), JS_PROP_C_W_E);
|
|
JS_DefinePropertyValueStr(ctx, result, "content_length", JS_NewInt64(ctx, conn->content_length), JS_PROP_C_W_E);
|
|
JS_DefinePropertyValueStr(ctx, result, "bytes_read", JS_NewInt64(ctx, conn->bytes_read), JS_PROP_C_W_E);
|
|
JS_DefinePropertyValueStr(ctx, result, "chunked_encoding", JS_NewBool(ctx, conn->chunked_encoding), JS_PROP_C_W_E);
|
|
|
|
return result;
|
|
}
|
|
|
|
// JS function: fetch_close(connection) - closes the connection
|
|
static JSValue js_fetch_close(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
|
if (argc < 1) {
|
|
return JS_ThrowTypeError(ctx, "fetch_close expects a connection object");
|
|
}
|
|
|
|
JSValue conn_val = JS_GetPropertyStr(ctx, argv[0], "_connection");
|
|
if (JS_IsUndefined(conn_val)) {
|
|
return JS_ThrowTypeError(ctx, "Invalid connection object");
|
|
}
|
|
|
|
int64_t conn_ptr;
|
|
if (JS_ToInt64(ctx, &conn_ptr, conn_val) < 0) {
|
|
JS_FreeValue(ctx, conn_val);
|
|
return JS_ThrowTypeError(ctx, "Invalid connection pointer");
|
|
}
|
|
JS_FreeValue(ctx, conn_val);
|
|
|
|
http_connection_t *conn = (http_connection_t *)(intptr_t)conn_ptr;
|
|
http_connection_free(conn);
|
|
js_free(ctx, conn);
|
|
|
|
// Clear the connection pointer
|
|
JS_DefinePropertyValueStr(ctx, argv[0], "_connection", JS_UNDEFINED, JS_PROP_C_W_E);
|
|
|
|
return JS_UNDEFINED;
|
|
}
|
|
|
|
// JS function: fetch(url, options)
|
|
static JSValue js_fetch(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
|
if (argc < 1 || !JS_IsString(argv[0])) {
|
|
return JS_ThrowTypeError(ctx, "fetch expects a URL string");
|
|
}
|
|
|
|
const char *url = JS_ToCString(ctx, argv[0]);
|
|
if (!url) {
|
|
return JS_ThrowTypeError(ctx, "Invalid URL");
|
|
}
|
|
|
|
char *host = NULL;
|
|
char *port = NULL;
|
|
char *path = NULL;
|
|
int use_ssl = 0;
|
|
buffer_t request_buf;
|
|
buffer_t response_buf;
|
|
JSValue result = JS_EXCEPTION;
|
|
|
|
buffer_init(&request_buf);
|
|
buffer_init(&response_buf);
|
|
|
|
// Parse URL
|
|
if (parse_url(url, &host, &port, &path, &use_ssl) < 0) {
|
|
JS_FreeCString(ctx, url);
|
|
return JS_ThrowTypeError(ctx, "Invalid URL format");
|
|
}
|
|
|
|
// Build request
|
|
buffer_append(&request_buf, "GET ", 4);
|
|
buffer_append(&request_buf, path, strlen(path));
|
|
buffer_append(&request_buf, " HTTP/1.1\r\n", 11);
|
|
buffer_append(&request_buf, "Host: ", 6);
|
|
buffer_append(&request_buf, host, strlen(host));
|
|
buffer_append(&request_buf, "\r\n", 2);
|
|
|
|
// Add headers from options if provided
|
|
if (argc >= 2 && JS_IsObject(argv[1])) {
|
|
JSValue headers = JS_GetPropertyStr(ctx, argv[1], "headers");
|
|
if (JS_IsObject(headers)) {
|
|
JSPropertyEnum *tab;
|
|
uint32_t len;
|
|
if (JS_GetOwnPropertyNames(ctx, &tab, &len, headers, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) {
|
|
for (uint32_t i = 0; i < len; i++) {
|
|
JSValue key = JS_AtomToString(ctx, tab[i].atom);
|
|
JSValue val = JS_GetProperty(ctx, headers, tab[i].atom);
|
|
|
|
const char *key_str = JS_ToCString(ctx, key);
|
|
const char *val_str = JS_ToCString(ctx, val);
|
|
|
|
if (key_str && val_str) {
|
|
buffer_append(&request_buf, key_str, strlen(key_str));
|
|
buffer_append(&request_buf, ": ", 2);
|
|
buffer_append(&request_buf, val_str, strlen(val_str));
|
|
buffer_append(&request_buf, "\r\n", 2);
|
|
}
|
|
|
|
JS_FreeCString(ctx, key_str);
|
|
JS_FreeCString(ctx, val_str);
|
|
JS_FreeValue(ctx, key);
|
|
JS_FreeValue(ctx, val);
|
|
}
|
|
js_free(ctx, tab);
|
|
}
|
|
}
|
|
JS_FreeValue(ctx, headers);
|
|
}
|
|
|
|
buffer_append(&request_buf, "Connection: close\r\n\r\n", 21);
|
|
|
|
// Perform request
|
|
int ret;
|
|
if (use_ssl) {
|
|
ret = https_request(host, port, request_buf.data, request_buf.size, &response_buf);
|
|
} else {
|
|
ret = http_request(host, port, request_buf.data, request_buf.size, &response_buf);
|
|
}
|
|
|
|
if (ret == 0 && response_buf.data) {
|
|
// Extract body
|
|
size_t body_len;
|
|
char *body = extract_body(response_buf.data, response_buf.size, &body_len);
|
|
|
|
if (body) {
|
|
// Return body as ArrayBuffer
|
|
result = js_new_blob_stoned_copy(ctx, (uint8_t *)body, body_len);
|
|
} else {
|
|
result = JS_ThrowInternalError(ctx, "Failed to parse HTTP response");
|
|
}
|
|
} else {
|
|
char error_buf[256];
|
|
mbedtls_strerror(ret, error_buf, sizeof(error_buf));
|
|
result = JS_ThrowInternalError(ctx, "Request failed: %s", error_buf);
|
|
}
|
|
|
|
// Cleanup
|
|
JS_FreeCString(ctx, url);
|
|
free(host);
|
|
free(port);
|
|
free(path);
|
|
buffer_free(&request_buf);
|
|
buffer_free(&response_buf);
|
|
|
|
return result;
|
|
}
|
|
|
|
#define countof(a) (sizeof(a)/sizeof(*(a)))
|
|
|
|
// Module exports
|
|
static const JSCFunctionListEntry js_http_funcs[] = {
|
|
JS_CFUNC_DEF("fetch", 2, js_fetch),
|
|
JS_CFUNC_DEF("fetch_start", 2, js_fetch_start),
|
|
JS_CFUNC_DEF("fetch_read_chunk", 2, js_fetch_read_chunk),
|
|
JS_CFUNC_DEF("fetch_info", 1, js_fetch_info),
|
|
JS_CFUNC_DEF("fetch_close", 1, js_fetch_close),
|
|
};
|
|
|
|
JSValue js_http_use(JSContext *js)
|
|
{
|
|
JSValue exp = JS_NewObject(js);
|
|
JS_SetPropertyFunctionList(js, exp, js_http_funcs, countof(js_http_funcs));
|
|
return exp;
|
|
}
|