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
270 lines
9.8 KiB
C
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;
|
|
}
|