[quickjs-devel] Inconsistent Error with custom XMLHttpRequest

  • From: Connor Nolan <connor24nolan@xxxxxxxx>
  • To: "quickjs-devel@xxxxxxxxxxxxx" <quickjs-devel@xxxxxxxxxxxxx>
  • Date: Tue, 6 Aug 2019 18:21:32 +0000


#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.

Other related posts:

  • » [quickjs-devel] Inconsistent Error with custom XMLHttpRequest - Connor Nolan