commit 0918af4e6851caf406075b5c8c0ec6f1387bc941
parent e5b20a5cce755a16019ed2b74151b9b8172d60e3
Author: Florian Dold <florian@dold.me>
Date: Mon, 9 Jan 2023 23:03:07 +0100
implement asynchronous http requests
Diffstat:
2 files changed, 310 insertions(+), 145 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -4,3 +4,6 @@ tags
# compiled wallet core file
taler-wallet-core-qjs.mjs
cross/android-*.cross.txt
+
+build-*
+builddir
diff --git a/quickjs/quickjs-libc.c b/quickjs/quickjs-libc.c
@@ -154,9 +154,14 @@ typedef struct JSThreadState {
JSValue on_host_message_func;
JSHostMessageHandlerFn host_message_handler_f;
- void *host_message_handler_cls;
+ void *host_message_handler_cls;
int is_worker_thread;
+
+#ifndef NO_HTTP
+ CURLM *curlm;
+ struct list_head curl_requests;
+#endif
} JSThreadState;
static uint64_t os_pending_signals;
@@ -512,8 +517,6 @@ static JSValue js_std_writeFile(JSContext *ctx, JSValueConst this_val,
if (0 == sret) {
break;
}
- fprintf(stderr, "wrote %llu/%llu bytes\n", (unsigned long long) sret, (unsigned long long) data_len);
- fprintf(stderr, "requested %llu bytes\n", (unsigned long long) (data_len - bytes_written));
bytes_written += sret;
}
@@ -2100,12 +2103,20 @@ static void js_os_timer_mark(JSRuntime *rt, JSValueConst val,
#ifndef NO_HTTP
typedef struct {
- DynBuf response_data;
- JSValue headers_list;
- JSContext *ctx;
- uint8_t *readbuf;
- size_t readpos;
- size_t readlen;
+ // linked list of all requests
+ struct list_head link;
+ DynBuf response_data;
+ // curl request headers, must be kept around during the request
+ struct curl_slist *headers;
+ // Response headers
+ JSValue headers_list;
+ JSValue resolve_func;
+ JSValue reject_func;
+ JSContext* ctx;
+ uint8_t *readbuf;
+ size_t readpos;
+ size_t readlen;
+ CURL *curl;
} CurlRequestContext;
static size_t curl_header_callback(char *buffer, size_t size,
@@ -2199,152 +2210,217 @@ exception:
return -1;
}
-/**
- * fetchHttp(url, { method, headers, body })
- */
-static JSValue js_os_fetchHttp(JSContext *ctx, JSValueConst this_val,
- int argc, JSValueConst *argv)
+static void free_fetch_request_context(CurlRequestContext *req_context)
{
- const char *req_url = NULL;
- JSValue ret_val = JS_UNDEFINED;
- JSValue options = JS_UNINITIALIZED;
- JSValue method = JS_UNINITIALIZED;
- const char *method_str = NULL;
- CURLcode res;
- CURL *curl = NULL;
- CurlRequestContext req_context = { 0 };
- struct curl_slist *headers = NULL;
- int debug = FALSE;
-
- req_context.ctx = ctx;
- req_context.headers_list = JS_NewArray(ctx);
- dbuf_init(&req_context.response_data);
-
- req_url = JS_ToCString(ctx, argv[0]);
- if (!req_url) {
- goto exception;
- }
+ JSContext *ctx;
- options = argv[1];
- if (JS_VALUE_GET_TAG (options) == JS_TAG_UNDEFINED) {
- method = JS_NewString(ctx, "get");
- } else if (JS_VALUE_GET_TAG(options) == JS_TAG_OBJECT) {
- method = JS_GetPropertyStr(ctx, options, "method");
- debug = expect_property_str_bool (ctx, options, "debug");
- } else {
- JS_ThrowTypeError(ctx, "invalid options");
- goto exception;
- }
+ if (!req_context) {
+ return;
+ }
+ ctx = req_context->ctx;
+ req_context->ctx = NULL;
+ if (req_context->curl) {
+ curl_easy_cleanup(req_context->curl);
+ req_context->curl = NULL;
+ }
+ if (NULL != req_context->readbuf) {
+ free(req_context->readbuf);
+ }
+ dbuf_free(&req_context->response_data);
+ JS_FreeValue(ctx, req_context->headers_list);
+ JS_FreeValue(ctx, req_context->resolve_func);
+ JS_FreeValue(ctx, req_context->reject_func);
+ curl_slist_free_all(req_context->headers);
+ list_del(&req_context->link);
+ js_free(ctx, req_context);
+}
- curl = curl_easy_init();
- if (!curl) {
- JS_ThrowInternalError(ctx, "unable to init libcurl");
- goto exception;
- }
- curl_easy_setopt(curl, CURLOPT_URL, req_url);
- curl_easy_setopt(curl, CURLOPT_CAINFO, "/etc/ssl/certs/ca-certificates.crt");
- curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_header_callback);
- curl_easy_setopt(curl, CURLOPT_HEADERDATA, &req_context);
- curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb);
- curl_easy_setopt(curl, CURLOPT_WRITEDATA, &req_context);
- if (debug == TRUE) {
- curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
- }
+static void
+finish_fetch_http(CurlRequestContext *req_context, CURLcode result)
+{
+ JSContext *ctx = req_context->ctx;
+ long resp_code;
+ JSValue ret_val;
+ JSValue cb_ret;
+
+ if (CURLE_OK != result) {
+ const char *errmsg;
+ JSAtom atom_message;
+
+ atom_message = JS_NewAtom(ctx, "message");
+ errmsg = curl_easy_strerror(result);
+ ret_val = JS_NewError(ctx);
+ JS_DefinePropertyValue(ctx, ret_val, atom_message,
+ JS_NewString(ctx, errmsg),
+ JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE);
+ cb_ret = JS_Call(ctx, req_context->reject_func, JS_UNDEFINED, 1, &ret_val);
+ JS_FreeAtom(ctx, atom_message);
+ JS_FreeValue(ctx, cb_ret);
+ JS_FreeValue(ctx, ret_val);
+ free_fetch_request_context(req_context);
+ return;
+ }
- // FIXME: This is only a temporary hack until we have proper TLS CA support
- // on all platforms
- curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
- curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
+ ret_val = JS_NewObject(ctx);
+ if (JS_IsException(ret_val)) {
+ // Huh, we're out of memory in the request handler?
+ fprintf(stderr, "fatal: can't allocate object in finish_fetch_http\n");
+ return;
+ }
- method_str = JS_ToCString(ctx, method);
+ curl_easy_getinfo(req_context->curl, CURLINFO_RESPONSE_CODE, &resp_code);
+ JS_SetPropertyStr(ctx, ret_val, "status", JS_NewInt32(ctx, resp_code));
+ JS_SetPropertyStr(ctx, ret_val, "headers", req_context->headers_list);
+ req_context->headers_list = JS_UNINITIALIZED;
+ // FIXME: Don't copy, own buffer
+ JS_SetPropertyStr(ctx,
+ ret_val,
+ "data",
+ JS_NewArrayBufferCopy(ctx,
+ req_context->response_data.buf,
+ req_context->response_data.size));
+ cb_ret = JS_Call(ctx, req_context->resolve_func, JS_UNDEFINED, 1, &ret_val);
+ JS_FreeValue(ctx, cb_ret);
+ JS_FreeValue(ctx, ret_val);
+ free_fetch_request_context(req_context);
+}
- if (0 == strcasecmp(method_str, "get")) {
- curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
- headers = curl_slist_append (headers, "Accept: application/json");
- } else if (0 == strcasecmp(method_str, "post")) {
- JSValue data;
- uint8_t *data_ptr;
- size_t data_len;
+/**
+ * fetchHttp(url, { method, headers, body }): Promise<Response>
+ */
+static JSValue js_os_fetchHttp(JSContext *ctx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ JSRuntime *rt = JS_GetRuntime(ctx);
+ JSThreadState *ts = JS_GetRuntimeOpaque(rt);
+ const char *req_url = NULL;
+ JSValue ret_val = JS_UNDEFINED;
+ JSValue resolving_funs[2];
+ JSValue options = JS_UNINITIALIZED;
+ JSValue method = JS_UNINITIALIZED;
+ const char *method_str = NULL;
+ CURLcode res;
+ CURLMcode mres;
+ CurlRequestContext *req_context = {0};
+ BOOL debug = FALSE;
+ BOOL req_started = FALSE;
+
+ req_context = js_mallocz(ctx, sizeof *req_context);
+
+ req_context->ctx = ctx;
+ req_context->headers_list = JS_NewArray(ctx);
+ dbuf_init(&req_context->response_data);
+
+ req_url = JS_ToCString(ctx, argv[0]);
+ if (!req_url) {
+ goto exception;
+ }
- data = JS_GetPropertyStr(ctx, options, "data");
- if (JS_IsException(data)) {
- goto exception;
+ options = argv[1];
+ if (JS_VALUE_GET_TAG(options) == JS_TAG_UNDEFINED) {
+ method = JS_NewString(ctx, "get");
}
- data_ptr = JS_GetArrayBuffer(ctx, &data_len, data);
- if (!data_ptr) {
- goto exception;
+ else if (JS_VALUE_GET_TAG(options) == JS_TAG_OBJECT) {
+ method = JS_GetPropertyStr(ctx, options, "method");
+ debug = expect_property_str_bool(ctx, options, "debug");
+ } else {
+ JS_ThrowTypeError(ctx, "invalid options");
+ goto exception;
}
- req_context.readbuf = malloc(data_len);
- memcpy(req_context.readbuf, data_ptr, data_len);
- req_context.readlen = data_len;
- JS_FreeValue(ctx, data);
- curl_easy_setopt(curl, CURLOPT_POST, 1L);
- curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback);
- curl_easy_setopt(curl, CURLOPT_READDATA, &req_context);
- headers = curl_slist_append (headers, "Content-Type: application/json");
- } else {
- JS_ThrowTypeError(ctx, "invalid request method");
- goto exception;
- }
- if (JS_VALUE_GET_TAG(options) == JS_TAG_OBJECT) {
- JSValue header_item = JS_GetPropertyStr(ctx, options, "headers");
- if (JS_IsException(header_item)) {
- goto exception;
+ req_context->curl = curl_easy_init();
+ if (!req_context->curl) {
+ JS_ThrowInternalError(ctx, "unable to init libcurl");
+ goto exception;
}
- if (JS_VALUE_GET_TAG(header_item) == JS_TAG_OBJECT) {
- if (0 != gather_headers(ctx, header_item , &headers)) {
- JS_FreeValue(ctx, header_item);
+ curl_easy_setopt(req_context->curl, CURLOPT_PRIVATE, req_context);
+ curl_easy_setopt(req_context->curl, CURLOPT_URL, req_url);
+ curl_easy_setopt(req_context->curl, CURLOPT_CAINFO, "/etc/ssl/certs/ca-certificates.crt");
+ curl_easy_setopt(req_context->curl, CURLOPT_HEADERFUNCTION, curl_header_callback);
+ curl_easy_setopt(req_context->curl, CURLOPT_HEADERDATA, req_context);
+ curl_easy_setopt(req_context->curl, CURLOPT_WRITEFUNCTION, curl_write_cb);
+ curl_easy_setopt(req_context->curl, CURLOPT_WRITEDATA, req_context);
+ if (debug == TRUE) {
+ curl_easy_setopt(req_context->curl, CURLOPT_VERBOSE, 1L);
+ }
+
+ // FIXME: This is only a temporary hack until we have proper TLS CA support
+ // on all platforms
+ curl_easy_setopt(req_context->curl, CURLOPT_SSL_VERIFYPEER, 0);
+ curl_easy_setopt(req_context->curl, CURLOPT_SSL_VERIFYHOST, 0);
+
+ method_str = JS_ToCString(ctx, method);
+
+ if (0 == strcasecmp(method_str, "get")) {
+ curl_easy_setopt(req_context->curl, CURLOPT_HTTPGET, 1L);
+ req_context->headers = curl_slist_append (req_context->headers, "Accept: application/json");
+ } else if (0 == strcasecmp(method_str, "post")) {
+ JSValue data;
+ uint8_t *data_ptr;
+ size_t data_len;
+
+ data = JS_GetPropertyStr(ctx, options, "data");
+ if (JS_IsException(data)) {
+ goto exception;
+ }
+ data_ptr = JS_GetArrayBuffer(ctx, &data_len, data);
+ if (!data_ptr) {
+ goto exception;
+ }
+ req_context->readbuf = malloc(data_len);
+ memcpy(req_context->readbuf, data_ptr, data_len);
+ req_context->readlen = data_len;
+ JS_FreeValue(ctx, data);
+ curl_easy_setopt(req_context->curl, CURLOPT_POST, 1L);
+ curl_easy_setopt(req_context->curl, CURLOPT_READFUNCTION, read_callback);
+ curl_easy_setopt(req_context->curl, CURLOPT_READDATA, req_context);
+ req_context->headers = curl_slist_append(req_context->headers, "Content-Type: application/json");
+ } else {
+ JS_ThrowTypeError(ctx, "invalid request method");
goto exception;
- }
}
- JS_FreeValue(ctx, header_item);
- }
- curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+ if (JS_VALUE_GET_TAG(options) == JS_TAG_OBJECT) {
+ JSValue header_item = JS_GetPropertyStr(ctx, options, "headers");
+ if (JS_IsException(header_item)) {
+ goto exception;
+ }
+ if (JS_VALUE_GET_TAG(header_item) == JS_TAG_OBJECT) {
+ if (0 != gather_headers(ctx, header_item, &req_context->headers)) {
+ JS_FreeValue(ctx, header_item);
+ goto exception;
+ }
+ }
+ JS_FreeValue(ctx, header_item);
+ }
+
+ curl_easy_setopt(req_context->curl, CURLOPT_HTTPHEADER, req_context->headers);
- res = curl_easy_perform(curl);
- if (CURLE_OK != res) {
- JS_ThrowInternalError(ctx, "fetch failed: %s", curl_easy_strerror (res));
- goto exception;
- }
- ret_val = JS_NewObject(ctx);
- if (JS_IsException(ret_val)) {
- goto exception;
- }
- {
- long resp_code;
+ mres = curl_multi_add_handle(ts->curlm, req_context->curl);
+ if (CURLM_OK != mres) {
+ JS_ThrowInternalError(ctx, "fetch failed: %s", curl_multi_strerror(mres));
+ goto exception;
+ }
+ list_add_tail(&req_context->link, &ts->curl_requests);
+ req_started = TRUE;
- curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &resp_code);
- JS_SetPropertyStr(ctx, ret_val, "status", JS_NewInt32(ctx, resp_code));
- JS_SetPropertyStr(ctx, ret_val, "headers", req_context.headers_list);
- req_context.headers_list = JS_UNINITIALIZED;
- // FIXME: Don't copy, own buffer
- JS_SetPropertyStr(ctx,
- ret_val,
- "data",
- JS_NewArrayBufferCopy(ctx,
- req_context.response_data.buf,
- req_context.response_data.size));
- }
+ ret_val = JS_NewPromiseCapability(ctx, resolving_funs);
+ if (JS_IsException(ret_val)) {
+ goto done;
+ }
+ req_context->resolve_func = resolving_funs[0];
+ req_context->reject_func = resolving_funs[1];
done:
- if (curl) {
- curl_easy_cleanup(curl);
- curl = NULL;
- }
- if (NULL != req_context.readbuf) {
- free(req_context.readbuf);
- }
- JS_FreeCString(ctx, method_str);
- dbuf_free(&req_context.response_data);
- JS_FreeValue(ctx, method);
- JS_FreeValue(ctx, req_context.headers_list);
- JS_FreeCString(ctx, req_url);
- curl_slist_free_all(headers);
- return ret_val;
+ if (FALSE == req_started) {
+ free_fetch_request_context(req_context);
+ }
+ JS_FreeValue(ctx, method);
+ JS_FreeCString(ctx, req_url);
+ JS_FreeCString(ctx, method_str);
+ return ret_val;
exception:
- ret_val = JS_EXCEPTION;
- goto done;
+ ret_val = JS_EXCEPTION;
+ goto done;
}
#endif
@@ -2622,16 +2698,51 @@ static int handle_host_message(JSRuntime *rt, JSContext *ctx)
return ret;
}
+// Perform curl network requests and
+static void do_curl(JSContext *ctx)
+{
+#ifndef NO_HTTP
+ JSRuntime *rt = JS_GetRuntime(ctx);
+ JSThreadState *ts = JS_GetRuntimeOpaque(rt);
+ CURLMcode curl_ret;
+ struct CURLMsg *m;
+ int running_handles;
+
+ curl_ret = curl_multi_perform(ts->curlm, &running_handles);
+ if (0 != curl_ret) {
+ fprintf(stderr, "curlm error: %s\n", curl_multi_strerror(curl_ret));
+ return;
+ }
+
+ do {
+ int msgq = 0;
+ m = curl_multi_info_read(ts->curlm, &msgq);
+ if (m && (m->msg == CURLMSG_DONE)) {
+ CurlRequestContext *req_ctx;
+ CURL *e = m->easy_handle;
+ CURLcode result = m->data.result;
+ if (CURLE_OK != curl_easy_getinfo(e, CURLINFO_PRIVATE, &req_ctx)) {
+ fprintf(stderr, "fatal: curl handle has no private data");
+ continue;
+ }
+ curl_multi_remove_handle(ts->curlm, e);
+ finish_fetch_http(req_ctx, result);
+ }
+ } while (m);
+#endif
+}
+
static int js_os_poll(JSContext *ctx)
{
JSRuntime *rt = JS_GetRuntime(ctx);
JSThreadState *ts = JS_GetRuntimeOpaque(rt);
int ret, fd_max, min_delay;
int64_t cur_time, delay;
- fd_set rfds, wfds;
+ fd_set rfds, wfds, ecxfds;
JSOSRWHandler *rh;
struct list_head *el;
- struct timeval tv, *tvp;
+ struct timeval tv = { 0 }, *tvp;
+ BOOL have_http_requests = FALSE;
/* only check signals in the main thread */
if (!ts->is_worker_thread &&
@@ -2650,9 +2761,14 @@ static int js_os_poll(JSContext *ctx)
}
}
- if (list_empty(&ts->os_rw_handlers) && list_empty(&ts->os_timers) &&
- list_empty(&ts->port_list) && JS_IsNull(ts->on_host_message_func))
+#ifndef NO_HTTP
+ have_http_requests = !list_empty(&ts->curl_requests);
+#endif
+
+ if ((!have_http_requests) && list_empty(&ts->os_rw_handlers) && list_empty(&ts->os_timers) &&
+ list_empty(&ts->port_list) && JS_IsNull(ts->on_host_message_func)) {
return -1; /* no more events */
+ }
if (!list_empty(&ts->os_timers)) {
cur_time = get_time_ms();
@@ -2684,6 +2800,7 @@ static int js_os_poll(JSContext *ctx)
FD_ZERO(&rfds);
FD_ZERO(&wfds);
+ FD_ZERO(&ecxfds);
fd_max = -1;
list_for_each(el, &ts->os_rw_handlers) {
rh = list_entry(el, JSOSRWHandler, link);
@@ -2706,6 +2823,37 @@ static int js_os_poll(JSContext *ctx)
fd_max = max_int(fd_max, ts->host_pipe->read_fd);
FD_SET(ts->host_pipe->read_fd, &rfds);
+#ifndef NO_HTTP
+ {
+ int curl_fd_max = -1;
+ CURLMcode mret;
+ long timeo = -1;
+
+ mret = curl_multi_fdset(ts->curlm, &rfds, &wfds, &ecxfds, &curl_fd_max);
+ if (CURLM_OK != mret) {
+ fprintf(stderr, "curlm error: %s\n", curl_multi_strerror(mret));
+ } else if (curl_fd_max != -1) {
+ fd_max = max_int(fd_max, curl_fd_max);
+ }
+
+ curl_multi_timeout(ts->curlm, &timeo);
+ if (timeo > 0) {
+ long timeo_sec = timeo / 1000;
+ long timeo_usec = (timeo % 1000) * 1000;
+ tvp = &tv;
+ if (tv.tv_sec < timeo_sec) {
+ tv.tv_sec = timeo_sec;
+ tv.tv_usec = timeo_usec;
+ } else if ((tv.tv_sec == timeo_sec) && tv.tv_usec < timeo_usec) {
+ tv.tv_usec = timeo_usec;
+ }
+ } else if (timeo == 0) {
+ do_curl(ctx);
+ goto done;
+ }
+ }
+#endif
+
ret = select(fd_max + 1, &rfds, &wfds, NULL, tvp);
if (ret > 0) {
list_for_each(el, &ts->os_rw_handlers) {
@@ -2740,6 +2888,8 @@ static int js_os_poll(JSContext *ctx)
goto done;
}
}
+
+ do_curl(ctx);
}
done:
return 0;
@@ -4357,6 +4507,11 @@ oom_fail:
JS_SetRuntimeOpaque(rt, ts);
+#ifndef NO_HTTP
+ ts->curlm = curl_multi_init();
+ init_list_head(&ts->curl_requests);
+#endif
+
#ifdef USE_WORKER
/* set the SharedArrayBuffer memory handlers */
{
@@ -4392,6 +4547,17 @@ void js_std_free_handlers(JSRuntime *rt)
free_timer(rt, th);
}
+#ifndef NO_HTTP
+
+ list_for_each_safe(el, el1, &ts->curl_requests) {
+ CurlRequestContext *request_ctx = list_entry(el, CurlRequestContext, link);
+ free_fetch_request_context(request_ctx);
+ }
+
+ curl_multi_cleanup(ts->curlm);
+ ts->curlm = NULL;
+#endif
+
JS_FreeValueRT(rt, ts->on_host_message_func);
#ifdef USE_WORKER
@@ -4416,7 +4582,6 @@ static void js_dump_obj(JSContext *ctx, FILE *f, JSValueConst val)
JS_FreeCString(ctx, str);
} else {
fprintf(f, "[exception]\n");
- abort();
}
}
@@ -4426,9 +4591,7 @@ static void js_std_dump_error1(JSContext *ctx, JSValueConst exception_val)
BOOL is_error;
is_error = JS_IsError(ctx, exception_val);
- fprintf(stderr, "dumping error, is_error: %u\n", is_error);
js_dump_obj(ctx, stderr, exception_val);
- fprintf(stderr, "dumped value\n");
if (is_error) {
val = JS_GetPropertyStr(ctx, exception_val, "stack");
if (!JS_IsUndefined(val)) {
@@ -4476,7 +4639,6 @@ void js_std_loop(JSContext *ctx)
}
if (!os_poll_func || os_poll_func(ctx)) {
- fprintf(stderr, "nothing more to wait on\n");
break;
}
}