#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <curl/curl.h>
#include <string.h>
#include <quickjs.h>
#include "eventloop.h"
#include "console.h"
#define OUT_OF_MEMORY "XMLHttpRequest: Out of Memory\n"
typedef struct thread_data {
int id;
char *data;
char *status;
int statusCode;
} thread_data;
typedef struct url_data {
int id;
char *body;
char *url;
char *method;
int headers_length;
} url_data;
thread_data **results;
int curl_length = 0;
pthread_mutex_t lock;
int running = 0;
JSValue *xmlhttprequest_requests;
static int prop_is_callable(JSContext *ctx, JSValue obj, JSAtom prop) {
int rc = 0;
JSValue val;
if (JS_HasProperty(ctx, obj, prop)) {
val = JS_GetProperty(ctx, obj, prop);
if (JS_IsFunction(ctx, val)) {
rc = 1;
}
JS_FreeValue(ctx, val);
}
JS_FreeAtom(ctx, prop);
return rc;
}
static JSValue build_event(JSContext *ctx, JSValue obj) {
JSValue event = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, event, "target", JS_DupValue(ctx, obj));
return event;
}
static void call_method(JSContext *ctx, JSValue obj, JSValue prop) {
JSValue val;
JSValueConst args[1];
JSValue event = build_event(ctx, obj);
args[0] = event;
val = JS_Call(ctx, prop, obj, 1, args);
if (JS_IsException(val)) {
js_std_dump_error(ctx);
request_loop_exit();
} else {
JS_FreeValue(ctx, val);
}
JS_FreeValue(ctx, event);
JS_FreeValue(ctx, prop);
}
static void call_event_listener(JSContext *ctx, int runOnload, JSValue obj) {
if (prop_is_callable(ctx, obj, JS_NewAtom(ctx, "onreadystatechange"))) {
call_method(ctx, obj, JS_GetPropertyStr(ctx, obj,
"onreadystatechange"));
}
JSValue listeners = JS_GetPropertyStr(ctx, obj, "eventListeners");
JSValue onreadystatechange = JS_GetPropertyStr(ctx, listeners,
"onreadystatechange");
JSValue onload = JS_GetPropertyStr(ctx, listeners, "onload");
JSValue onerror = JS_GetPropertyStr(ctx, listeners, "onerror");
int64_t length;
if (!JS_IsNull(onreadystatechange)) {
JSValue lenJS = JS_GetPropertyStr(ctx, onreadystatechange, "length");
JS_ToInt64(ctx, &length, lenJS);
JS_FreeValue(ctx, lenJS);
for (int i = 0; i < length; i++) {
if (prop_is_callable(ctx, onreadystatechange, JS_NewAtomUInt32(ctx,
i))) {
call_method(ctx, obj, JS_GetPropertyUint32(ctx,
onreadystatechange, i));
}
}
}
if (runOnload == 1) {
if (prop_is_callable(ctx, obj, JS_NewAtom(ctx, "onload"))) {
call_method(ctx, obj, JS_GetPropertyStr(ctx, obj, "onload"));
}
if (!JS_IsNull(onload)) {
JSValue lenJS = JS_GetPropertyStr(ctx, onload, "length");
JS_ToInt64(ctx, &length, lenJS);
JS_FreeValue(ctx, lenJS);
for (int i = 0; i < length; i++) {
if (prop_is_callable(ctx, onload, JS_NewAtomUInt32(ctx, i))) {
call_method(ctx, obj, JS_GetPropertyUint32(ctx, onload, i));
}
}
}
} else if (runOnload == -1) {
if (prop_is_callable(ctx, obj, JS_NewAtom(ctx, "onerror"))) {
call_method(ctx, obj, JS_GetPropertyStr(ctx, obj, "onerror"));
}
if (!JS_IsNull(onload)) {
JSValue lenJS = JS_GetPropertyStr(ctx, onerror, "length");
JS_ToInt64(ctx, &length, lenJS);
JS_FreeValue(ctx, lenJS);
for (int i = 0; i < length; i++) {
if (prop_is_callable(ctx, onerror, JS_NewAtomUInt32(ctx, i))) {
call_method(ctx, obj, JS_GetPropertyUint32(ctx, onerror,
i));
}
}
}
}
JS_FreeValue(ctx, onreadystatechange);
JS_FreeValue(ctx, onload);
JS_FreeValue(ctx, onerror);
JS_FreeValue(ctx, listeners);
}
static void on_done(JSContext *ctx, thread_data *result) {
running--;
JSValue obj = xmlhttprequest_requests[result->id];
JS_DefinePropertyValueStr(ctx, obj, "readyState", JS_NewInt64(ctx, 4),
JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE | JS_PROP_HAS_WRITABLE |
JS_PROP_HAS_VALUE);
JS_DefinePropertyValueStr(ctx, obj, "response", JS_NewString(ctx,
result->data), JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE |
JS_PROP_HAS_WRITABLE | JS_PROP_HAS_VALUE);
JS_DefinePropertyValueStr(ctx, obj, "responseText", JS_NewString(ctx,
result->data), JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE |
JS_PROP_HAS_WRITABLE | JS_PROP_HAS_VALUE);
JS_DefinePropertyValueStr(ctx, obj, "status", JS_NewInt64(ctx,
result->statusCode), JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE |
JS_PROP_HAS_WRITABLE | JS_PROP_HAS_VALUE);
JS_DefinePropertyValueStr(ctx, obj, "statusText", JS_NewString(ctx,
result->status), JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE |
JS_PROP_HAS_WRITABLE | JS_PROP_HAS_VALUE);
JS_DefinePropertyValueStr(ctx, obj, "responseType", JS_NewString(ctx,
"text"), JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE | JS_PROP_HAS_WRITABLE
| JS_PROP_HAS_VALUE);
if (result->statusCode == 200) {
call_event_listener(ctx, 1, obj);
} else {
call_event_listener(ctx, -1, obj);
}
JS_FreeValue(ctx, obj);
xmlhttprequest_requests[result->id] = JS_NULL;
free(result);
}
typedef struct curl_data {
char *memory;
size_t size;
} curl_data;
static size_t curl_callback(void *contents, size_t size, size_t nmemb, void
*userp) {
size_t realsize = size * nmemb;
struct curl_data *mem = (struct curl_data *) userp;
char *ptr = realloc(mem->memory, mem->size + realsize + 1);
if (ptr == NULL) {
fprintf(stderr, OUT_OF_MEMORY);
fflush(stderr);
exit(1);
}
mem->memory = ptr;
memcpy(&(mem->memory[mem->size]), contents, realsize);
mem->size += realsize;
mem->memory[mem->size] = 0;
return realsize;
}
typedef struct header_data {
char *status;
int done;
} header_data;
static size_t header_callback(char *buffer, size_t size, size_t nitems, void
*userdata) {
size_t numbytes = size * nitems;
header_data *data = (header_data *) userdata;
if (!data->done) {
int length = 0;
char last_char = buffer[length];
while (buffer[length] != '\0' && buffer[length] != '\n' && length <
nitems) {
length++;
last_char = buffer[length];
}
int add_eof = 0;
if (last_char == '\n') {
buffer[length - 1] = '\0';
} else if (last_char != '\0') {
add_eof = 1;
length++;
}
char *data_str = malloc(length * sizeof (char));
for (int i = 0; i < length; i++) {
if (add_eof && i == length - 1) {
data_str[i] = '\0';
} else {
data_str[i] = buffer[i];
}
}
data->status = data_str;
data->done = 1;
}
return numbytes;
}
static void *get_data(void *void_data) {
url_data *data = (url_data *) void_data;
thread_data *new_data = (thread_data *) malloc(sizeof (thread_data));
new_data->id = data->id;
CURL *curl_handle;
CURLcode res;
curl_data *chunk = malloc(sizeof (curl_data));
chunk->memory = malloc(1);
chunk->size = 0;
curl_handle = curl_easy_init();
curl_easy_setopt(curl_handle, CURLOPT_URL, data->url);
curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, curl_callback);
curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *) chunk);
curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "libcurl-agent/1.0");
curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYHOST, 1L);
curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
header_data *statusText = malloc(sizeof (header_data));
statusText->done = 0;
curl_easy_setopt(curl_handle, CURLOPT_HEADERDATA, (void *) statusText);
curl_easy_setopt(curl_handle, CURLOPT_HEADERFUNCTION, header_callback);
curl_easy_setopt(curl_handle, CURLOPT_ACCEPT_ENCODING, "");
if (strcmp(data->method, "GET") == 0) {
curl_easy_setopt(curl_handle, CURLOPT_HTTPGET, 1);
} else if (strcmp(data->method, "POST") == 0) {
curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDS, data->body);
}
res = curl_easy_perform(curl_handle);
if (res != CURLE_OK) {
new_data->status = (char *) curl_easy_strerror(res);
long response_code;
curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &response_code);
new_data->statusCode = response_code;
new_data->data = "";
goto realloc;
} else {
long response_code;
curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &response_code);
new_data->statusCode = response_code;
new_data->status = statusText->status;
}
curl_easy_cleanup(curl_handle);
char *out = realloc(chunk->memory, chunk->size + sizeof (char));
if (!out) {
fprintf(stderr, OUT_OF_MEMORY);
fflush(stderr);
exit(1);
}
out[chunk->size / sizeof (char)] = '\0';
new_data->data = out;
realloc:
free(data);
free(chunk);
free(statusText);
pthread_mutex_lock(&lock);
curl_length++;
thread_data **tmp = realloc(results, curl_length * sizeof *results);
if (tmp) {
results = tmp;
} else {
fprintf(stderr, OUT_OF_MEMORY);
fflush(stderr);
exit(1);
}
results[curl_length - 1] = new_data;
pthread_mutex_unlock(&lock);
eventloop_interrupt_sleep();
return NULL;
}
int global_id = 0;
static JSValue xmlhttprequest_open(JSContext *ctx, JSValueConst this_val, int
argc, JSValueConst *argv) {
if (argc != 2) {
return JS_EXCEPTION;
}
JS_SetPropertyStr(ctx, this_val, "_method", JS_DupValue(ctx, argv[0]));
JS_SetPropertyStr(ctx, this_val, "_url", JS_DupValue(ctx, argv[1]));
JS_DefinePropertyValueStr(ctx, this_val, "readyState", JS_NewInt64(ctx, 1),
JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE | JS_PROP_HAS_WRITABLE |
JS_PROP_HAS_VALUE);
call_event_listener(ctx, 0, this_val);
return JS_UNDEFINED;
}
static JSValue xmlhttprequest_send(JSContext *ctx, JSValueConst this_val, int
argc, JSValueConst *argv) {
url_data *data = (url_data *) malloc(sizeof (url_data));
data->id = global_id;
JSValue val;
int64_t ready_state;
if (argc < 0 || argc > 1) {
return JS_EXCEPTION;
}
val = JS_GetPropertyStr(ctx, this_val, "readyState");
if (JS_ToInt64(ctx, &ready_state, val)) {
return JS_EXCEPTION;
}
JS_FreeValue(ctx, val);
if (ready_state < 1) {
return JS_ThrowInternalError(ctx, "open() has not been called");
}
if (ready_state > 1) {
return JS_ThrowInternalError(ctx, "send() has already been called");
}
JS_DefinePropertyValueStr(ctx, this_val, "readyState", JS_NewInt64(ctx, 3),
JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE | JS_PROP_HAS_WRITABLE |
JS_PROP_HAS_VALUE);
call_event_listener(ctx, 0, this_val);
JSValue urlJS = JS_GetPropertyStr(ctx, this_val, "_url");
JSValue methodJS = JS_GetPropertyStr(ctx, this_val, "_method");
const char *url = JS_ToCString(ctx, urlJS);
data->url = strdup(url);
JS_FreeCString(ctx, url);
const char *method = JS_ToCString(ctx, methodJS);
data->method = strdup(method);
JS_FreeCString(ctx, method);
if (argc == 1) {
const char *body = JS_ToCString(ctx, argv[0]);
data->body = strdup(body);
JS_FreeCString(ctx, body);
} else {
data->body = "";
}
JS_FreeValue(ctx, urlJS);
JS_FreeValue(ctx, methodJS);
JSValue *ptr = realloc(xmlhttprequest_requests, (data->id + 1) * sizeof
(JSValue));
if (ptr == NULL) {
fprintf(stderr, OUT_OF_MEMORY);
fflush(stderr);
exit(1);
}
xmlhttprequest_requests = ptr;
xmlhttprequest_requests[data->id] = JS_DupValue(ctx, this_val);
global_id++;
pthread_t thread;
pthread_create(&thread, NULL, get_data, (void *) data);
running++;
return JS_UNDEFINED;
}
static JSValue xmlhttprequest_addeventlistener(JSContext *ctx, JSValueConst
this_val, int argc, JSValueConst *argv) {
if (argc != 2) {
return JS_EXCEPTION;
}
const char *event;
JSValueConst func;
event = JS_ToCString(ctx, argv[0]);
if (JS_IsFunction(ctx, argv[1])) {
func = argv[1];
} else {
return JS_ThrowTypeError(ctx, "not a function");
}
JSValue listeners = JS_GetPropertyStr(ctx, this_val, "eventListeners");
JSValue onreadystatechange = JS_GetPropertyStr(ctx, listeners,
"onreadystatechange");
JSValue onload = JS_GetPropertyStr(ctx, listeners, "onload");
JSValue onerror = JS_GetPropertyStr(ctx, listeners, "onerror");
int64_t length;
if (strcmp(event, "readystatechange") == 0) {
JS_ToInt64(ctx, &length, JS_GetPropertyStr(ctx, onreadystatechange,
"length"));
JS_SetPropertyUint32(ctx, onreadystatechange, length, func);
} else if (strcmp(event, "load") == 0) {
JS_ToInt64(ctx, &length, JS_GetPropertyStr(ctx, onload, "length"));
JS_SetPropertyUint32(ctx, onload, length, func);
} else if (strcmp(event, "error") == 0) {
JS_ToInt64(ctx, &length, JS_GetPropertyStr(ctx, onerror, "length"));
JS_SetPropertyUint32(ctx, onerror, length, func);
} else {
return JS_ThrowTypeError(ctx, "not a supported event");
}
JS_FreeValue(ctx, onreadystatechange);
JS_FreeValue(ctx, onload);
JS_FreeValue(ctx, onerror);
JS_FreeValue(ctx, listeners);
return JS_UNDEFINED;
}
static JSValue xmlhttprequest_constructor(JSContext *ctx, JSValueConst
this_val, int argc, JSValueConst *argv) {
if (argc != 0) {
return JS_EXCEPTION;
}
JSValue proto = JS_GetPropertyStr(ctx, this_val, "prototype");
JSValue obj = JS_NewObjectProto(ctx, proto);
JS_FreeValue(ctx, proto);
JSValue listeners = JS_NewObject(ctx);
JSValue onreadystatechange = JS_NewArray(ctx);
JS_DefinePropertyValueStr(ctx, listeners, "onreadysatechange",
onreadystatechange, JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE |
JS_PROP_HAS_WRITABLE | JS_PROP_HAS_VALUE);
JSValue onload = JS_NewArray(ctx);
JS_DefinePropertyValueStr(ctx, listeners, "onload", onload,
JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE | JS_PROP_HAS_WRITABLE |
JS_PROP_HAS_VALUE);
JSValue onerror = JS_NewArray(ctx);
JS_DefinePropertyValueStr(ctx, listeners, "onerror", onerror,
JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE | JS_PROP_HAS_WRITABLE |
JS_PROP_HAS_VALUE);
JS_DefinePropertyValueStr(ctx, obj, "eventListeners", listeners,
JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE | JS_PROP_HAS_WRITABLE |
JS_PROP_HAS_VALUE);
JS_FreeValue(ctx, listeners);
JS_DefinePropertyValueStr(ctx, obj, "readyState", JS_NewInt64(ctx, 0),
JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE | JS_PROP_HAS_WRITABLE |
JS_PROP_HAS_VALUE);
JS_DefinePropertyValueStr(ctx, obj, "response", JS_NULL,
JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE | JS_PROP_HAS_WRITABLE |
JS_PROP_HAS_VALUE);
JS_DefinePropertyValueStr(ctx, obj, "responseText", JS_NULL,
JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE | JS_PROP_HAS_WRITABLE |
JS_PROP_HAS_VALUE);
JS_DefinePropertyValueStr(ctx, obj, "status", JS_NULL,
JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE | JS_PROP_HAS_WRITABLE |
JS_PROP_HAS_VALUE);
JS_DefinePropertyValueStr(ctx, obj, "statusText", JS_NULL,
JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE | JS_PROP_HAS_WRITABLE |
JS_PROP_HAS_VALUE);
JS_DefinePropertyValueStr(ctx, obj, "responseType", JS_NULL,
JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE | JS_PROP_HAS_WRITABLE |
JS_PROP_HAS_VALUE);
return obj;
}
void xmlhttprequest_init(JSContext *ctx) {
JSValue func = JS_NewCFunction2(ctx, xmlhttprequest_constructor,
"XMLHttpRequest", 0, JS_CFUNC_constructor, 0);
JSValue proto = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, proto, "open", JS_NewCFunction(ctx,
xmlhttprequest_open, "open", 2));
JS_SetPropertyStr(ctx, proto, "send", JS_NewCFunction(ctx,
xmlhttprequest_send, "send", 1));
JS_SetPropertyStr(ctx, proto, "addEventListener", JS_NewCFunction(ctx,
xmlhttprequest_addeventlistener, "addEventListener", 2));
JS_SetPropertyStr(ctx, func, "prototype", proto);
JS_SetPropertyStr(ctx, JS_GetGlobalObject(ctx), "XMLHttpRequest", func);
xmlhttprequest_requests = malloc(sizeof (JSValue));
}
void xmlhttprequest_loop(JSContext *ctx) {
pthread_mutex_lock(&lock);
if (curl_length > 0) {
for (int i = 0; i < curl_length; i++) {
on_done(ctx, results[i]);
}
curl_length = 0;
results = NULL;
}
pthread_mutex_unlock(&lock);
}
void xmlhttprequest_cleanup(JSContext *ctx) {
pthread_mutex_destroy(&lock);
curl_global_cleanup();
}
int xmlhttprequest_isdone() {
return running < 1;
}
The loop function is invoked in a loop, init initializes the object, and
cleanup cleans up the object. This uses libcurl. The error only happens
sometimes.
0x5636929eb4d0: invalid refcount (-1835109552)
0x5636929eb4d0 -1835109552 - - <null>
js: /home/connor/Downloads/test/quickjs/quickjs.c:5064: gc_decref_child:
Assertion `p->header.ref_count > 0' failed.
Aborted (core dumped)
The JS code is:
let xhr = new XMLHttpRequest();
console.log(xhr);
xhr.onload = function (e) {
console.log(this.statusText);
};
xhr.open("GET", "https://thebrokenrail.com";);
console.log(xhr.readyState);
xhr.send();
A core dump is attached.