Files
cell/source/qjs_http.c
John Alanbrook 589bb365bd
Some checks failed
Build and Deploy / build-macos (push) Failing after 4s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
http now returns byte array and content type
2025-05-06 22:50:24 -05:00

270 lines
9.8 KiB
C

#include "quickjs.h"
#include <curl/curl.h>
#include <stdlib.h>
#include <string.h>
// -----------------------------------------------------------------------------
// HTTP request structure
// -----------------------------------------------------------------------------
typedef struct {
char url[512]; // URL for the request
JSContext *ctx; // JS context for callbacks
JSValue callback; // Completion callback (optional)
JSValue on_data; // Streaming data callback (optional)
char *response; // Buffer for non-streaming mode
size_t size; // Size of response buffer
CURL *curl; // CURL easy handle
int done; // Request completion flag
int curl_result; // CURL result code
} HttpRequest;
// -----------------------------------------------------------------------------
// Global data
// -----------------------------------------------------------------------------
static CURLM *g_curl_multi = NULL; // CURL multi-handle for async I/O
// -----------------------------------------------------------------------------
// Libcurl write callback for streaming or buffering
// -----------------------------------------------------------------------------
static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp)
{
size_t realsize = size * nmemb;
HttpRequest *req = (HttpRequest *)userp;
// Streaming mode: call onData if provided
if (JS_IsFunction(req->ctx, req->on_data)) {
JSValue chunk = JS_NewStringLen(req->ctx, contents, realsize);
JSValue ret = JS_Call(req->ctx, req->on_data, JS_UNDEFINED, 1, &chunk);
JS_FreeValue(req->ctx, chunk);
JS_FreeValue(req->ctx, ret); // Ignore return value
return realsize;
}
// Non-streaming mode: buffer the response
char *ptr = realloc(req->response, req->size + realsize + 1);
if (!ptr) {
return 0; // Out of memory, tell CURL to abort
}
req->response = ptr;
memcpy(&(req->response[req->size]), contents, realsize);
req->size += realsize;
req->response[req->size] = '\0'; // Null-terminate
return realsize;
}
// -----------------------------------------------------------------------------
// JS function: http.fetch(url, options)
// - Enqueues an async HTTP request with optional streaming
// -----------------------------------------------------------------------------
static JSValue js_http_fetch(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
if (argc < 2 || !JS_IsString(argv[0])) {
return JS_ThrowTypeError(ctx, "fetch expects a URL string and an options object or callback");
}
// Get URL
const char *url = JS_ToCString(ctx, argv[0]);
if (!url) {
return JS_ThrowTypeError(ctx, "Invalid URL");
}
// Allocate request object
HttpRequest *req = calloc(1, sizeof(*req));
if (!req) {
JS_FreeCString(ctx, url);
return JS_ThrowInternalError(ctx, "Failed to allocate memory");
}
strncpy(req->url, url, sizeof(req->url) - 1);
req->ctx = ctx;
req->callback = JS_NULL;
req->on_data = JS_NULL;
// Parse second argument: callback or options object
if (JS_IsFunction(ctx, argv[1])) {
req->callback = JS_DupValue(ctx, argv[1]);
} else if (JS_IsObject(argv[1])) {
JSValue callback = JS_GetPropertyStr(ctx, argv[1], "callback");
JSValue on_data = JS_GetPropertyStr(ctx, argv[1], "on_data");
if (JS_IsFunction(ctx, callback)) {
req->callback = JS_DupValue(ctx, callback);
}
if (JS_IsFunction(ctx, on_data)) {
req->on_data = JS_DupValue(ctx, on_data);
}
JS_FreeValue(ctx, callback);
JS_FreeValue(ctx, on_data);
} else {
JS_FreeCString(ctx, url);
free(req);
return JS_ThrowTypeError(ctx, "Second argument must be a callback or options object");
}
JS_FreeCString(ctx, url);
// Initialize CURL easy handle
req->curl = curl_easy_init();
if (!req->curl) {
JS_FreeValue(ctx, req->callback);
JS_FreeValue(ctx, req->on_data);
free(req);
return JS_ThrowInternalError(ctx, "Failed to create CURL handle");
}
// Set CURL options
curl_easy_setopt(req->curl, CURLOPT_URL, req->url);
curl_easy_setopt(req->curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(req->curl, CURLOPT_WRITEDATA, req);
curl_easy_setopt(req->curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(req->curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(req->curl, CURLOPT_SSL_VERIFYHOST, 2L);
curl_easy_setopt(req->curl, CURLOPT_USERAGENT, "prosperon");
curl_easy_setopt(req->curl, CURLOPT_PRIVATE, req);
// Add to multi-handle
if (!g_curl_multi) {
curl_easy_cleanup(req->curl);
JS_FreeValue(ctx, req->callback);
JS_FreeValue(ctx, req->on_data);
free(req);
return JS_ThrowInternalError(ctx, "CURL multi-handle not initialized");
}
CURLMcode mc = curl_multi_add_handle(g_curl_multi, req->curl);
if (mc != CURLM_OK) {
curl_easy_cleanup(req->curl);
JS_FreeValue(ctx, req->callback);
JS_FreeValue(ctx, req->on_data);
free(req);
return JS_ThrowInternalError(ctx, "curl_multi_add_handle failed: %s", curl_multi_strerror(mc));
}
return JS_UNDEFINED;
}
// -----------------------------------------------------------------------------
// JS function: http.poll()
// - Checks for I/O and completed requests, invoking callbacks as needed
// -----------------------------------------------------------------------------
static JSValue js_http_poll(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
if (!g_curl_multi) {
return JS_UNDEFINED;
}
// Perform pending transfers
int still_running = 0;
CURLMcode mc = curl_multi_perform(g_curl_multi, &still_running);
if (mc != CURLM_OK) {
return JS_ThrowInternalError(ctx, "curl_multi_perform failed: %s", curl_multi_strerror(mc));
}
// Check for completed requests
CURLMsg *msg;
int msgq = 0;
while ((msg = curl_multi_info_read(g_curl_multi, &msgq))) {
if (msg->msg == CURLMSG_DONE) {
CURL *easy = msg->easy_handle;
HttpRequest *req = NULL;
curl_easy_getinfo(easy, CURLINFO_PRIVATE, (char **)&req);
char *ct = NULL;
curl_easy_getinfo(easy, CURLINFO_CONTENT_TYPE, &ct);
// Remove from multi-handle
curl_multi_remove_handle(g_curl_multi, easy);
// Mark as done
req->curl_result = msg->data.result;
req->done = 1;
// Call completion callback if provided
if (JS_IsFunction(req->ctx, req->callback)) {
JSValue arg = JS_NewObject(req->ctx);
if (req->curl_result == CURLE_OK) {
JS_SetPropertyStr(req->ctx, arg, "data",
JS_NewArrayBufferCopy(req->ctx, req->response, req->size));
JS_SetPropertyStr(req->ctx, arg, "error", JS_UNDEFINED);
if (ct) JS_SetPropertyStr(req->ctx, arg, "type", JS_NewString(req->ctx, ct));
} else {
JS_DefinePropertyValueStr(req->ctx, arg, "data", JS_NULL, JS_PROP_C_W_E);
const char *err_str = curl_easy_strerror(req->curl_result);
JS_DefinePropertyValueStr(req->ctx, arg, "error",
JS_NewString(req->ctx, err_str ? err_str : "Unknown error"),
JS_PROP_C_W_E);
}
JSValue ret = JS_Call(req->ctx, req->callback, JS_UNDEFINED, 1, &arg);
JS_FreeValue(req->ctx, arg);
JS_FreeValue(req->ctx, ret);
}
// Cleanup
JS_FreeValue(req->ctx, req->callback);
JS_FreeValue(req->ctx, req->on_data);
curl_easy_cleanup(req->curl);
free(req->response);
free(req);
}
}
return JS_UNDEFINED;
}
// -----------------------------------------------------------------------------
// Module initialization
// -----------------------------------------------------------------------------
static const JSCFunctionListEntry js_http_funcs[] = {
JS_CFUNC_DEF("fetch", 2, js_http_fetch),
JS_CFUNC_DEF("poll", 0, js_http_poll),
};
JSValue js_http_use(JSContext *ctx)
{
// Initialize CURL globally (once per process)
static int s_curl_init_done = 0;
if (!s_curl_init_done) {
s_curl_init_done = 1;
if (curl_global_init(CURL_GLOBAL_ALL) != 0) {
return JS_ThrowInternalError(ctx, "Failed to initialize CURL");
}
}
// Initialize multi-handle (once per module)
if (!g_curl_multi) {
g_curl_multi = curl_multi_init();
if (!g_curl_multi) {
return JS_ThrowInternalError(ctx, "Failed to initialize CURL multi-handle");
}
}
// Export fetch and poll functions
JSValue export_obj = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, export_obj, js_http_funcs,
sizeof(js_http_funcs) / sizeof(JSCFunctionListEntry));
return export_obj;
}
static int js_http_init(JSContext *ctx, JSModuleDef *m)
{
JS_SetModuleExport(ctx, m, "default", js_http_use(ctx));
return 0;
}
#ifdef JS_SHARED_LIBRARY
#define JS_INIT_MODULE js_init_module
#else
#define JS_INIT_MODULE js_init_module_http
#endif
JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name)
{
JSModuleDef *m = JS_NewCModule(ctx, module_name, js_http_init);
if (!m) {
return NULL;
}
JS_AddModuleExport(ctx, m, "default");
return m;
}