diff options
author | Florian Dold <florian@dold.me> | 2024-02-23 15:54:29 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2024-02-26 21:06:41 +0100 |
commit | 47fc98540143afee8f77c6298fdeb3c0b0d7b6ec (patch) | |
tree | 868d9b582cd9745c30161f5e6753c945d8d49333 | |
parent | 0595d6b972a4c3a9f26c93ced206a82a403146de (diff) | |
download | quickjs-tart-dev/dold/native-http.tar.gz quickjs-tart-dev/dold/native-http.tar.bz2 quickjs-tart-dev/dold/native-http.zip |
native http lib interfacedev/dold/native-http
-rw-r--r-- | meson.build | 2 | ||||
-rw-r--r-- | qtart.c | 13 | ||||
-rw-r--r-- | quickjs/quickjs-http.c | 497 | ||||
-rw-r--r-- | quickjs/quickjs-http.h | 213 | ||||
-rw-r--r-- | quickjs/quickjs-libc.c | 753 | ||||
-rw-r--r-- | quickjs/quickjs-libc.h | 8 | ||||
-rw-r--r-- | quickjs/quickjs.c | 11 | ||||
-rw-r--r-- | quickjs/quickjs.h | 1 | ||||
-rw-r--r-- | taler_wallet_core_lib.c | 17 | ||||
-rw-r--r-- | taler_wallet_core_lib.h | 13 | ||||
-rw-r--r-- | taler_wallet_core_lib_preprocessed.h | 381 | ||||
-rw-r--r-- | xcode/FTalerWalletcore-Bridging-Header.h | 2 |
12 files changed, 1578 insertions, 333 deletions
diff --git a/meson.build b/meson.build index e784384..43bd182 100644 --- a/meson.build +++ b/meson.build @@ -59,7 +59,7 @@ libunicode = static_library('unicode', 'quickjs/libunicode.c') # general utilities cutils = static_library('cutils', 'quickjs/cutils.c') # standard library for quickjs (std and os modules) -quickjs_libc = static_library('quickjs-libc', 'quickjs/quickjs-libc.c', dependencies : curl_dep ) +quickjs_libc = static_library('quickjs-libc', ['quickjs/quickjs-libc.c', 'quickjs/quickjs-http.c'], dependencies : curl_dep ) # base JS interpreter quickjs = static_library('quickjs', 'quickjs/quickjs.c') sqlite3 = static_library('sqlite3', 'sqlite3/sqlite3.c') @@ -40,6 +40,7 @@ #endif #include "quickjs/cutils.h" +#include "quickjs/quickjs-http.h" #include "tart_module.h" @@ -326,6 +327,7 @@ int main(int argc, char **argv) { JSRuntime *rt; JSContext *ctx; + struct JSHttpClientImplementation *http_impl = NULL; struct trace_malloc_data trace_data = { NULL }; int optind; char *expr = NULL; @@ -459,11 +461,17 @@ int main(int argc, char **argv) js_std_set_worker_new_context_func(JS_NewCustomContext); js_std_init_handlers(rt); ctx = JS_NewCustomContext(rt); - js_os_set_host_message_handler(ctx, handle_host_message, NULL); if (!ctx) { fprintf(stderr, "qjs: cannot allocate JS context\n"); exit(2); } + js_os_set_host_message_handler(ctx, handle_host_message, NULL); + http_impl = js_curl_http_client_create(); + if (!http_impl) { + fprintf(stderr, "qjs: cannot create HTTP client implementation\n"); + exit(2); + } + js_os_set_http_impl(rt, http_impl); /* loader for ES6 modules */ JS_SetModuleLoaderFunc(rt, NULL, js_module_loader, NULL); @@ -518,6 +526,8 @@ int main(int argc, char **argv) } js_std_loop(ctx); } + + fprintf(stderr, "done with main loop\n"); if (dump_memory) { JSMemoryUsage stats; @@ -525,6 +535,7 @@ int main(int argc, char **argv) JS_DumpMemoryUsage(stdout, &stats, rt); } js_std_free_handlers(rt); + js_curl_http_client_destroy(http_impl); JS_FreeContext(ctx); JS_FreeRuntime(rt); diff --git a/quickjs/quickjs-http.c b/quickjs/quickjs-http.c new file mode 100644 index 0000000..9fb1fa2 --- /dev/null +++ b/quickjs/quickjs-http.c @@ -0,0 +1,497 @@ +/* + This file is part of GNU Taler + Copyright (C) 2024 Taler Systems SA + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +#include <stdlib.h> +#include <pthread.h> +#include <stdio.h> +#include <curl/curl.h> +#include <arpa/inet.h> +#include <strings.h> +#include <string.h> +#include <assert.h> + +#include "cutils.h" +#include "quickjs-http.h" +#include "list.h" + +struct CurlClientState { + pthread_t thread; + pthread_mutex_t mutex; + BOOL started; + BOOL stopped; + CURLSH *curlsh; + CURLM *curlm; + int last_request_id; + struct list_head request_list; /* list of CurlRequestState.link */ +}; + +struct CurlRequestState { + struct CurlClientState *ccs; + struct list_head link; + DynBuf response_data; + BOOL cancelled; + CURL *curl; + int request_id; + enum JSHttpRedirectFlag redirect; + JSHttpResponseCb response_cb; + void *response_cb_cls; + // Request headers + struct curl_slist *req_headers; + struct curl_slist *resp_headers; + char *errbuf; +}; + +// Must only be called with locked client mutex +static void destroy_curl_request_state(struct CurlRequestState *crs) +{ + struct CurlClientState *ccs; + + if (!crs) { + return; + } + ccs = crs->ccs; + crs->ccs = NULL; + + list_del(&crs->link); + curl_slist_free_all(crs->req_headers); + curl_slist_free_all(crs->resp_headers); + dbuf_free(&crs->response_data); + if (crs->curl) { + curl_easy_cleanup(crs->curl); + crs->curl = NULL; + } + free(crs->errbuf); + free(crs); +} + +static void * +handle_done(CURL *curl, CURLcode res) +{ + struct CurlRequestState *crs = NULL; + struct CurlClientState *ccs = NULL; + struct JSHttpResponseInfo hri = { 0 }; + long resp_code; + char **headers = NULL; + BOOL cancelled; + + curl_easy_getinfo(curl, CURLINFO_PRIVATE, &crs); + ccs = crs->ccs; + + hri.request_id = crs->request_id; + + if (CURLE_OK == res) { + int num_headers = 0; + int i; + char **headers; + struct curl_slist *sl = crs->resp_headers; + char *url = NULL; + + curl_easy_getinfo(curl, CURLINFO_REDIRECT_URL, &url); + + if (crs->redirect == JS_HTTP_REDIRECT_ERROR && NULL != url) { + hri.status = 0; + hri.errmsg = crs->errbuf; + strncpy(crs->errbuf, "Got redirect status, but redirects are not allowed for this request", CURL_ERROR_SIZE); + goto done; + } + + while (sl != NULL) { + if (NULL != strchr(sl->data, ':')) { + num_headers++; + } + sl = sl->next; + } + + headers = malloc((num_headers + 1) * sizeof(char *)); + if (!headers) { + hri.status = 0; + goto done; + } + memset(headers, 0, (num_headers + 1) * sizeof (char *)); + sl = crs->resp_headers; + i = 0; + while (sl != NULL) { + if (NULL != strchr(sl->data, ':')) { + headers[i] = sl->data; + i++; + } + sl = sl->next; + } + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &resp_code); + hri.status = resp_code; + hri.body = crs->response_data.buf; + hri.body_len = crs->response_data.size; + hri.response_headers = headers; + } else { + hri.status = 0; + hri.errmsg = crs->errbuf; + } + +done: + + pthread_mutex_lock(&ccs->mutex); + cancelled = crs->cancelled; + pthread_mutex_unlock(&ccs->mutex); + + if (cancelled == FALSE) { + // FIXME: What if this CB somehow destroys the client? + crs->response_cb(crs->response_cb_cls, &hri); + } + + if (NULL != headers) { + for (char **h=headers; *h; h++) { + free(*h); + } + free(headers); + } + pthread_mutex_lock(&ccs->mutex); + destroy_curl_request_state(crs); + pthread_mutex_unlock(&ccs->mutex); + return NULL; +} + +static size_t curl_header_callback(char *buffer, size_t size, + size_t nitems, void *userdata) +{ + struct CurlRequestState *crs = userdata; + size_t sz = size * nitems; + char *hval; + + hval = strndup(buffer, sz); + if (!hval) { + return 0; + } + crs->resp_headers = curl_slist_append(crs->resp_headers, hval); + free(hval); + return sz; +} + + +static size_t curl_write_cb(void *data, size_t size, size_t nmemb, void *userp) +{ + size_t realsize = size * nmemb; + struct CurlRequestState *rctx = userp; + + if (0 != dbuf_put(&rctx->response_data, data, realsize)) { + return 0; + } + + return realsize; +} + + +static int +create_impl(void *cls, struct JSHttpRequestInfo *req_info) +{ + struct CurlClientState *ccs = cls; + struct CurlRequestState *crs; + pthread_t thread; + int res; + CURL *curl; + BOOL debug = req_info->debug > 0; + const char *method = req_info->method; + + crs = malloc(sizeof *crs); + if (!crs) { + return -1; + } + memset(crs, 0, sizeof *crs); + crs->request_id = ++ccs->last_request_id; + crs->ccs = ccs; + crs->response_cb = req_info->response_cb; + crs->response_cb_cls = req_info->response_cb_cls; + crs->errbuf = malloc(CURL_ERROR_SIZE); + if (!crs->errbuf) { + goto error; + } + memset(crs->errbuf, 0, CURL_ERROR_SIZE); + dbuf_init(&crs->response_data); + + curl = curl_easy_init(); + crs->curl = curl; + curl_easy_setopt(curl, CURLOPT_PRIVATE, crs); + curl_easy_setopt(curl, CURLOPT_SHARE, ccs->curlsh); + curl_easy_setopt(curl, CURLOPT_URL, req_info->url); + curl_easy_setopt(curl, CURLOPT_DNS_SERVERS, "9.9.9.9"); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "qtart"); + 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, crs); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, crs); + + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, crs->errbuf); + + // 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); + + if (req_info->timeout_ms < 0) { + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 0L); + } else if (0 == req_info->timeout_ms) { + // Default timeout of 5 minutes. + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 5L * 60000L); + } else { + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, (long) req_info->timeout_ms); + } + + if (debug == TRUE) { + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + } + + crs->redirect = req_info->redirect; + + switch (req_info->redirect) { + case JS_HTTP_REDIRECT_TRANSPARENT: + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + break; + case JS_HTTP_REDIRECT_MANUAL: + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); + break; + case JS_HTTP_REDIRECT_ERROR: + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); + break; + default: + assert(0); + } + + if (0 == strcasecmp(req_info->method, "get")) { + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + } else if (0 == strcasecmp(method, "delete")) { + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); + } else if (0 == strcasecmp(method, "head")) { + curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + } else if ((0 == strcasecmp(method, "post")) || + (0 == strcasecmp(method, "put"))) { + curl_easy_setopt(curl, CURLOPT_POST, 1L); + if (0 == strcasecmp(method, "put")) { + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); + } + if (req_info->req_body_len > 0) { + curl_off_t len = req_info->req_body_len; + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, len); + curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, req_info->req_body); + } + } else { + goto error; + } + + if (req_info->request_headers != NULL) { + char **h = req_info->request_headers; + while (*h) { + crs->req_headers = curl_slist_append(crs->req_headers, *h); + h++; + } + } + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, crs->req_headers); + + pthread_mutex_lock(&ccs->mutex); + list_add_tail(&crs->link, &ccs->request_list); + pthread_mutex_unlock(&ccs->mutex); + + curl_multi_add_handle(ccs->curlm, curl); + curl_multi_wakeup(ccs->curlm); + + return crs->request_id; +error: + if (crs) { + dbuf_free(&crs->response_data); + if (crs->errbuf) { + free(crs->errbuf); + } + if (crs->curl) { + curl_easy_cleanup(crs->curl); + } + free(crs); + } + return -1; +} + +static int +destroy_impl(void *cls, int request_id) +{ + struct list_head *el; + struct CurlClientState *ccs = cls; + + pthread_mutex_lock(&ccs->mutex); + + list_for_each(el, &ccs->request_list) { + struct CurlRequestState *crs = list_entry(el, struct CurlRequestState, link); + if (crs->request_id == request_id) { + crs->cancelled = TRUE; + } + } + + pthread_mutex_unlock(&ccs->mutex); + + return 0; +} + +/** + * Entry point for the thread that processes HTTP requests with libcurl. + */ +static void * +curl_multi_thread_run(void *cls) +{ + struct CurlClientState *ccs = cls; + int still_running; + struct CURLMsg *m; + BOOL stopped; + + while (1) { + CURLMcode mc; + + mc = curl_multi_perform(ccs->curlm, &still_running); + + if (CURLM_OK != mc) { + fprintf(stderr, "curl_multi_perform failed\n"); + break; + } + + mc = curl_multi_poll(ccs->curlm, NULL, 0, 1000, NULL); + if (CURLM_OK != mc) { + fprintf(stderr, "curl_multi_poll failed\n"); + break; + } + + pthread_mutex_lock(&ccs->mutex); + stopped = ccs->stopped; + pthread_mutex_unlock(&ccs->mutex); + + if (stopped) { + break; + } + + do { + int msgq = 0; + m = curl_multi_info_read(ccs->curlm, &msgq); + if (m && (m->msg == CURLMSG_DONE)) { + CURL *e = m->easy_handle; + curl_multi_remove_handle(ccs->curlm, e); + handle_done(e, m->data.result); + } + } while(m); + } + if (CURLM_OK != curl_multi_cleanup(ccs->curlm)) { + fprintf(stderr, "warning: curl_multi_cleanup failed\n"); + } + if (CURLSHE_OK != curl_share_cleanup(ccs->curlsh)) { + fprintf(stderr, "warning: curl_share_cleanup failed\n"); + } +} + +struct JSHttpClientImplementation * +js_curl_http_client_create() +{ + struct JSHttpClientImplementation *impl = NULL; + struct CurlClientState *ccs = NULL; + int res; + + ccs = malloc(sizeof *ccs); + if (!ccs) { + goto error; + } + + pthread_mutex_init(&ccs->mutex, NULL); + ccs->started = FALSE; + ccs->stopped = FALSE; + ccs->last_request_id = 0; + ccs->curlsh = curl_share_init(); + if (!ccs->curlsh) { + goto error; + } + ccs->curlm = curl_multi_init(); + if (!ccs->curlm) { + goto error; + } + init_list_head(&ccs->request_list); + + curl_share_setopt(ccs->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); + curl_share_setopt(ccs->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); + curl_share_setopt(ccs->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT); + + impl = malloc(sizeof *impl); + if (!impl) { + goto error; + } + impl->req_create = &create_impl; + impl->req_cancel = &destroy_impl; + impl->cls = ccs; + + res = pthread_create(&ccs->thread, NULL, &curl_multi_thread_run, ccs); + ccs->started = TRUE; + + if (0 != res) { + goto error; + } + + return impl; +error: + if (ccs) { + curl_share_cleanup(ccs->curlsh); + curl_multi_cleanup(ccs->curlm); + free(ccs); + } + if (impl) { + free(impl); + } + return NULL; +} + +static void +destroy_client_state(struct CurlClientState *ccs) +{ + struct list_head *el, *el1; + if (!ccs) { + return; + } + if (ccs->started == TRUE) { + void *retval; + int res; + + pthread_mutex_lock(&ccs->mutex); + ccs->stopped = TRUE; + pthread_mutex_unlock(&ccs->mutex); + curl_multi_wakeup(ccs->curlm); + res = pthread_join(ccs->thread, &retval); + if (0 != res) { + fprintf(stderr, "warning: could not join with curl thread\n"); + } + ccs->started = FALSE; + } + pthread_mutex_lock(&ccs->mutex); + list_for_each_safe(el, el1, &ccs->request_list) { + struct CurlRequestState *crs = list_entry(el, struct CurlRequestState, link); + destroy_curl_request_state(crs); + } + pthread_mutex_unlock(&ccs->mutex); + pthread_mutex_destroy(&ccs->mutex); + free(ccs); +} + +void +js_curl_http_client_destroy(struct JSHttpClientImplementation *impl) +{ + if (!impl) { + return; + } + destroy_client_state(impl->cls); + impl->cls = NULL; + free(impl); +} + diff --git a/quickjs/quickjs-http.h b/quickjs/quickjs-http.h new file mode 100644 index 0000000..437731c --- /dev/null +++ b/quickjs/quickjs-http.h @@ -0,0 +1,213 @@ +/* + This file is part of GNU Taler + Copyright (C) 2024 Taler Systems SA + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + + +// ## Native HTTP client library support. + +// Considerations: +// - the API is designed for the HTTP client implementation +// to run in its own thread and *not* be integrated with the +// application's main event loop. +// - focus on small API +// - not a generic HTTP client, only supposed to serve the needs +// of a JS runtime +// - only very tiny subset of HTTP supported +// - no request/response streaming +// - should be appropriate to implement a JS HTTP fetch function +// in the style of WHATWG fetch +// - no focus on ABI compatibility whatsoever + + +#ifndef _QUICKJS_HTTP_H +#define _QUICKJS_HTTP_H + +#include <stdint.h> +#include <limits.h> +#include <stddef.h> + +// Forward declaration; +struct JSHttpResponseInfo; + +/** + * Callback called when an HTTP response has arrived. + * + * IMPORTANT: May be called from an arbitrary thread. + */ +typedef void (*JSHttpResponseCb)(void *cls, struct JSHttpResponseInfo *resp); + +enum JSHttpRedirectFlag { + /** + * Handle redirects transparently. + */ + JS_HTTP_REDIRECT_TRANSPARENT = 0, + /** + * Redirect status codes are returned to the client. + * The client can choose to follow them manually (or not). + */ + JS_HTTP_REDIRECT_MANUAL = 1, + /** + * All redirect status codes result in an error. + */ + JS_HTTP_REDIRECT_ERROR = 2, +}; + +/** + * Info needed to start a new HTTP request. + */ +struct JSHttpRequestInfo { + /** + * Callback called with the response for the request. + */ + JSHttpResponseCb response_cb; + + /** + * Closure for response_cb. + */ + void *response_cb_cls; + + /** + * Request URL. + */ + const char *url; + + /** + * Request method. + */ + const char *method; + + int num_request_headers; + + /** + * Array of `num_request_headers` request headers. + */ + char **request_headers; + + /** + * 0: Handle redirects transparently. + * 1: Handle redirects manually. + * 2: Redirects result in an error. + */ + enum JSHttpRedirectFlag redirect; + + /** + * Request timeout in milliseconds. + * + * When 0 is specified, the timeout is the default request + * timeout for the platform. + * + * When -1 is specified, there is no timeout. This might not be + * supported on all platforms. + */ + int timeout_ms; + + /** + * Enable debug output for this request. + */ + int debug; + + /** + * Request body or NULL. + */ + void *req_body; + + /** + * Length or request body or 0. + */ + size_t req_body_len; +}; + +/** + * Contents of an HTTP response. + */ +struct JSHttpResponseInfo { + + /** + * Request that this is a response to. + * + * (Think of the request ID like a file descriptor number). + */ + int request_id; + + /** + * HTTP response status code or 0 on error. + */ + int status; + + /** + * When status is 0, error message. + */ + char *errmsg; + + /** + * Array of `num_response_headers` response headers. + */ + char **response_headers; + + /** + * Number of response headers. + */ + int num_response_headers; + + /** + * Response body or NULL. + */ + void *body; + + /** + * Length of the response body or 0. + */ + size_t body_len; +}; + +/** + * Callback called when an HTTP response has arrived. + * + * IMPORTANT: May be called from an arbitrary thread. + */ +typedef void (*JSHttpResponseCb)(void *cls, struct JSHttpResponseInfo *resp); + +/** + * Function to create a new HTTP fetch request. + * The request can still be configured until it is started. + * An identifier for the request will be written to @a handle. + * + * @return negative number on error, positive request_id on success + */ +typedef int (*JSHttpReqCreateFn)(void *cls, struct JSHttpRequestInfo *req_info); + +/** + * Cancel a request. The request_id will become invalid + * and the callback won't be called with request_id. + */ +typedef int (*JSHttpReqCancelFn)(void *cls, int request_id); + +struct JSHttpClientImplementation { + /** + * Opaque closure passed to client functions. + */ + void *cls; + JSHttpReqCreateFn req_create; + JSHttpReqCancelFn req_cancel; +}; + + +struct JSHttpClientImplementation * +js_curl_http_client_create(void); + +void +js_curl_http_client_destroy(struct JSHttpClientImplementation *impl); + +#endif /* _QUICKJS_HTTP_H */ diff --git a/quickjs/quickjs-libc.c b/quickjs/quickjs-libc.c index 75114a6..acecd8b 100644 --- a/quickjs/quickjs-libc.c +++ b/quickjs/quickjs-libc.c @@ -79,10 +79,11 @@ typedef sig_t sighandler_t; */ #ifndef NO_HTTP -#include <curl/curl.h> #include <arpa/inet.h> +#include "quickjs-http.h" #endif + typedef struct { struct list_head link; int fd; @@ -135,6 +136,23 @@ typedef struct { typedef struct { struct list_head link; + int request_id; + int status; + char *errmsg; + char **response_headers; + void *body; + size_t body_len; +} JSHttpMessage; + +typedef struct { + pthread_mutex_t mutex; + struct list_head msg_queue; /* list of JSHttpMessage.link */ + int read_fd; + int write_fd; +} JSHttpMessagePipe; + +typedef struct { + struct list_head link; JSWorkerMessagePipe *recv_pipe; JSValue on_message_func; } JSWorkerMessageHandler; @@ -151,6 +169,9 @@ typedef struct JSThreadState { // send/receive message to/from the host in the main thread JSHostMessagePipe *host_pipe; + // receive messages from the HTTP client thread + JSHttpMessagePipe *http_pipe; + JSValue on_host_message_func; JSHostMessageHandlerFn host_message_handler_f; @@ -158,11 +179,12 @@ typedef struct JSThreadState { int is_worker_thread; + struct list_head http_requests; + #ifndef NO_HTTP - CURLM *curlm; - CURLSH *curlsh; - struct list_head curl_requests; + struct JSHttpClientImplementation *http_client_impl; #endif + } JSThreadState; static uint64_t os_pending_signals; @@ -2106,233 +2128,246 @@ static void js_os_timer_mark(JSRuntime *rt, JSValueConst val, typedef struct { // 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; - BOOL client_has_accept_header; - BOOL client_has_content_type_header; -} CurlRequestContext; - -static size_t curl_header_callback(char *buffer, size_t size, - size_t nitems, void *userdata) -{ - CurlRequestContext *rctx = userdata; - size_t sz = size * nitems; - - qjs_array_append_new(rctx->ctx, rctx->headers_list, JS_NewStringLen(rctx->ctx, buffer, sz)); - return sz; -} + int request_id; -static size_t curl_write_cb(void *data, size_t size, size_t nmemb, void *userp) -{ - size_t realsize = size * nmemb; - CurlRequestContext *rctx = userp; + JSValue resolve_func; + JSValue reject_func; - if (0 != dbuf_put(&rctx->response_data, data, realsize)) { - return 0; - } + JSContext* ctx; +} HttpRequestContext; - return realsize; -} -size_t read_callback(char *ptr, size_t size, size_t nmemb, void *userdata) +void js_os_set_http_impl(JSRuntime *rt, struct JSHttpClientImplementation *impl) { - CurlRequestContext *rctx = userdata; - ssize_t src_available = rctx->readlen - rctx->readpos; - size_t dst_available = size * nmemb; - size_t n; + JSThreadState *ts = JS_GetRuntimeOpaque(rt); - if (!rctx->readbuf) { - return 0; - } - if (src_available <= 0) { - return 0; - } - n = src_available; - if (dst_available < src_available) { - n = dst_available; - } - memcpy(ptr, rctx->readbuf + rctx->readpos, n); - rctx->readpos += n; - return n; + ts->http_client_impl = impl; } int expect_property_str_bool(JSContext *ctx, JSValueConst this_val, const char *prop_name) { - JSValue prop_val; - BOOL bool_val; - - prop_val = JS_GetPropertyStr(ctx, this_val, prop_name); - if (JS_IsException(prop_val)) { - return -1; - } - bool_val = JS_ToBool(ctx, prop_val); - JS_FreeValue(ctx, prop_val); - return bool_val; -} + JSValue prop_val; + BOOL bool_val; -BOOL starts_with_ignorecase(const char *str, const char *prefix) -{ - for (int i = 0; i < strlen(prefix); i++) { - if (!str[i]) { - return FALSE; - } - if (tolower(str[i]) != tolower(prefix[i])) { - return FALSE; + prop_val = JS_GetPropertyStr(ctx, this_val, prop_name); + if (JS_IsException(prop_val)) { + return -1; } - } - return TRUE; + bool_val = JS_ToBool(ctx, prop_val); + JS_FreeValue(ctx, prop_val); + return bool_val; } -static int gather_headers(JSContext *ctx, JSValueConst js_headers, CurlRequestContext *req_context) -{ - JSValue length_prop; - uint32_t length; - - length_prop = JS_GetPropertyStr(ctx, js_headers, "length"); - if (JS_IsException(length_prop)) { - return -1; - } - if (0 != JS_ToUint32(ctx, &length, length_prop)) { - return -1; - } - JS_FreeValue(ctx, length_prop); - - for (uint32_t i = 0; i < length; i++) { - JSValue item = JS_GetPropertyUint32(ctx, js_headers, i); - if (JS_IsException(item)) { - goto exception; - } - const char *cstr = JS_ToCString(ctx, item); - if (starts_with_ignorecase(cstr, "accept:")) { - req_context->client_has_accept_header = TRUE; - } else if (starts_with_ignorecase(cstr, "content-type:")) { - req_context->client_has_content_type_header = TRUE; - } - if (!cstr) { - JS_FreeValue(ctx, item); - goto exception; - } - req_context->headers = curl_slist_append (req_context->headers, cstr); - JS_FreeCString(ctx, cstr); - JS_FreeValue(ctx, item); - } - return 0; -exception: - return -1; -} - -static void free_fetch_request_context(CurlRequestContext *req_context) +static void free_http_request_context(HttpRequestContext *req_context) { JSContext *ctx; + JSThreadState *ts; if (!req_context) { return; } ctx = req_context->ctx; + ts = JS_GetRuntimeOpaque(JS_GetRuntime(ctx)); + ts->http_client_impl->req_cancel(ts->http_client_impl->cls, req_context->request_id); 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); if (NULL != req_context->link.prev) { - list_del(&req_context->link); + list_del(&req_context->link); } js_free(ctx, req_context); } -static void -finish_fetch_http(CurlRequestContext *req_context, CURLcode result) +static void js_free_http_message(JSHttpMessage *msg) { + if (msg->body) { + free(msg->body); + msg->body = NULL; + } + if (msg->errmsg) { + free(msg->errmsg); + msg->errmsg = NULL; + } + if (msg->response_headers) { + char **h; + for (h = msg->response_headers; *h; h++) { + free(*h); + } + free(msg->response_headers); + msg->response_headers = NULL; + } + free(msg); +} + +static void handle_http_resp(void *cls, struct JSHttpResponseInfo *resp_info) +{ + // Called from a different thread. + // We must enqueue something that the message loop will process + // + HttpRequestContext *req_context = cls; 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; + JSThreadState *ts = JS_GetRuntimeOpaque(JS_GetRuntime(ctx)); + JSHttpMessage *msg; + JSHttpMessagePipe *hp; + + msg = malloc(sizeof (*msg)); + if (!msg) { + goto fail; + } + memset(msg, 0, sizeof (*msg)); + + msg->status = resp_info->status; + msg->request_id = resp_info->request_id; + + if (resp_info->response_headers) { + int num_headers = 0; + char **h; + + h = resp_info->response_headers; + while (*h) { + num_headers++; + h++; + } + + msg->response_headers = malloc((num_headers + 1) * sizeof (char *)); + if (!msg->response_headers) { + goto fail; + } + memset(msg->response_headers, 0, (num_headers + 1) * sizeof (char *)); + for (int i = 0; i < num_headers; i++) { + msg->response_headers[i] = strdup(resp_info->response_headers[i]); + if (!msg->response_headers[i]) { + goto fail; + } + } + } else { + msg->response_headers = NULL; } - 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"); + if (resp_info->errmsg != NULL) { + msg->errmsg = strdup(resp_info->errmsg); + if (!msg->errmsg) { + goto fail; + } + } + + if (resp_info->body_len > 0) { + msg->body = malloc(resp_info->body_len); + if (!msg->body) { + goto fail; + } + msg->body_len = resp_info->body_len; + memcpy(msg->body, resp_info->body, resp_info->body_len); + } + + hp = ts->http_pipe; + pthread_mutex_lock(&hp->mutex); + /* indicate that data is present */ + if (list_empty(&hp->msg_queue)) { + uint8_t ch = '\0'; + int ret; + for(;;) { + ret = write(hp->write_fd, &ch, 1); + if (ret == 1) + break; + if (ret < 0 && (errno != EAGAIN || errno != EINTR)) + break; + } + } + list_add_tail(&msg->link, &hp->msg_queue); + pthread_mutex_unlock(&hp->mutex); + return; + fail: + js_free_http_message(msg); + return; +} + +static void +free_http_headers(JSContext *ctx, char **headers) +{ + if (!headers) { return; } + for (char **h = headers; *h != NULL; h++) { + js_free(ctx, *h); + } + js_free(ctx, headers); +} + +static char **gather_http_headers(JSContext *ctx, JSValueConst js_headers) +{ + JSValue length_prop; + uint32_t length; + char **headers = NULL; + + length_prop = JS_GetPropertyStr(ctx, js_headers, "length"); + if (JS_IsException(length_prop)) { + return NULL; + } + if (0 != JS_ToUint32(ctx, &length, length_prop)) { + return NULL; + } + JS_FreeValue(ctx, length_prop); - 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); + headers = js_mallocz(ctx, (length + 1) * sizeof (char *)); + if (!headers) { + goto exception; + } + + for (uint32_t i = 0; i < length; i++) { + char *hval; + JSValue item = JS_GetPropertyUint32(ctx, js_headers, i); + if (JS_IsException(item)) { + goto exception; + } + const char *cstr = JS_ToCString(ctx, item); + if (!cstr) { + JS_FreeValue(ctx, item); + goto exception; + } + hval = js_strdup(ctx, cstr); + if (!hval) { + goto exception; + } + JS_FreeCString(ctx, cstr); + JS_FreeValue(ctx, item); + headers[i] = hval; + } + return headers; +exception: + free_http_headers(ctx, headers); + return NULL; } + /** * fetchHttp(url, { method, headers, body }): Promise<Response> */ static JSValue js_os_fetchHttp(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) + int argc, JSValueConst *argv) { + JSValue ret_val = JS_UNINITIALIZED; 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; - CURLMcode mres; - CurlRequestContext *req_context = {0}; + const char *req_url = NULL; + struct JSHttpRequestInfo req = { 0 }; + HttpRequestContext *req_context = NULL; BOOL debug = FALSE; - BOOL req_started = FALSE; + int redirect = 0; + int ret; - req_context = js_mallocz(ctx, sizeof *req_context); + if (NULL == ts->http_client_impl) { + JS_ThrowInternalError(ctx, "no HTTP client implementation available"); + goto exception; + } + 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) { @@ -2342,116 +2377,106 @@ static JSValue js_os_fetchHttp(JSContext *ctx, JSValueConst this_val, 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) { + } else if (JS_VALUE_GET_TAG(options) == JS_TAG_OBJECT) { + int has_prop_redirect; + method = JS_GetPropertyStr(ctx, options, "method"); debug = expect_property_str_bool(ctx, options, "debug"); + + has_prop_redirect = JS_HasPropertyStr(ctx, options, "redirect"); + if (has_prop_redirect < 0) { + goto exception; + } + if (has_prop_redirect) { + int32_t redir_num; + JSValue redir_val = JS_GetPropertyStr(ctx, options, "redirect"); + if (JS_IsException(redir_val)) { + goto exception; + } + if (JS_ToInt32(ctx, &redir_num, redir_val)) { + goto exception; + } + if (redir_num < 0 || redir_num > JS_HTTP_REDIRECT_ERROR) { + JS_ThrowTypeError(ctx, "redirect option out of range"); + goto exception; + } + redirect = redir_num; + } } else { JS_ThrowTypeError(ctx, "invalid options"); goto exception; } - req_context->curl = curl_easy_init(); - if (!req_context->curl) { - JS_ThrowInternalError(ctx, "unable to init libcurl"); - goto exception; - } - curl_easy_setopt(req_context->curl, CURLOPT_PRIVATE, req_context); - curl_easy_setopt(req_context->curl, CURLOPT_SHARE, ts->curlsh); - curl_easy_setopt(req_context->curl, CURLOPT_URL, req_url); -// curl_easy_setopt(req_context->curl, CURLOPT_DNS_SERVERS, "8.8.8.8"); - curl_easy_setopt(req_context->curl, CURLOPT_DNS_SERVERS, "9.9.9.9"); - curl_easy_setopt(req_context->curl, CURLOPT_USERAGENT, "qtart"); - 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); - 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)) { - JS_FreeValue(ctx, header_item); - goto exception; + char **headers = gather_http_headers(ctx, header_item); + if (NULL == headers) { + JS_FreeValue(ctx, header_item); + goto exception; } + req.request_headers = headers; } JS_FreeValue(ctx, header_item); } - method_str = JS_ToCString(ctx, method); - - if (0 == strcasecmp(method_str, "get")) { - curl_easy_setopt(req_context->curl, CURLOPT_HTTPGET, 1L); - if (!req_context->client_has_accept_header) { - req_context->headers = curl_slist_append (req_context->headers, "Accept: application/json"); - } - } else if (0 == strcasecmp(method_str, "delete")) { - curl_easy_setopt(req_context->curl, CURLOPT_HTTPGET, 1L); - curl_easy_setopt(req_context->curl, CURLOPT_CUSTOMREQUEST, "DELETE"); - } else if ((0 == strcasecmp(method_str, "post")) || - (0 == strcasecmp(method_str, "put"))) { + if (JS_VALUE_GET_TAG(options) == JS_TAG_OBJECT) { JSValue data; uint8_t *data_ptr; size_t data_len; + int has_prop; - data = JS_GetPropertyStr(ctx, options, "data"); - if (JS_IsException(data)) { - goto exception; - } - data_ptr = JS_GetArrayBuffer(ctx, &data_len, data); - if (!data_ptr) { + has_prop = JS_HasPropertyStr(ctx, options, "data"); + + if (-1 == has_prop) { 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); - if (0 == strcasecmp(method_str, "put")) { - curl_easy_setopt(req_context->curl, CURLOPT_CUSTOMREQUEST, "PUT"); - } - curl_easy_setopt(req_context->curl, CURLOPT_READFUNCTION, read_callback); - curl_easy_setopt(req_context->curl, CURLOPT_READDATA, req_context); - if (!req_context->client_has_content_type_header) { - req_context->headers = curl_slist_append(req_context->headers, "Content-Type: application/json"); + + if (has_prop) { + data = JS_GetPropertyStr(ctx, options, "data"); + if (JS_IsException(data)) { + goto exception; + } + if (!JS_IsNull(data) && !JS_IsUndefined(data)) { + data_ptr = JS_GetArrayBuffer(ctx, &data_len, data); + if (!data_ptr) { + goto exception; + } + } + req.req_body = data_ptr; + req.req_body_len = data_len; } - } else { - JS_ThrowTypeError(ctx, "invalid request method"); - goto exception; } - curl_easy_setopt(req_context->curl, CURLOPT_HTTPHEADER, req_context->headers); + method_str = JS_ToCString(ctx, method); + + req.method = method_str; + req.url = req_url; + req.debug = debug; + req.redirect = redirect; + req.response_cb = &handle_http_resp; + req.response_cb_cls = req_context; + ret = ts->http_client_impl->req_create(ts->http_client_impl->cls, &req); - mres = curl_multi_add_handle(ts->curlm, req_context->curl); - if (CURLM_OK != mres) { - JS_ThrowInternalError(ctx, "fetch failed: %s", curl_multi_strerror(mres)); + if (ret < 0) { + JS_ThrowInternalError(ctx, "failed to create request"); goto exception; } - list_add_tail(&req_context->link, &ts->curl_requests); - req_started = TRUE; + + list_add_tail(&req_context->link, &ts->http_requests); ret_val = JS_NewPromiseCapability(ctx, resolving_funs); if (JS_IsException(ret_val)) { goto done; } + req_context->request_id = ret; req_context->resolve_func = resolving_funs[0]; req_context->reject_func = resolving_funs[1]; done: - if (FALSE == req_started) { - free_fetch_request_context(req_context); - } + free_http_headers(ctx, req.request_headers); JS_FreeValue(ctx, method); JS_FreeCString(ctx, req_url); JS_FreeCString(ctx, method_str); @@ -2459,6 +2484,7 @@ done: exception: ret_val = JS_EXCEPTION; goto done; + } #endif @@ -2735,38 +2761,97 @@ static int handle_host_message(JSRuntime *rt, JSContext *ctx) return ret; } -// Perform curl network requests and -static void do_curl(JSContext *ctx) +/* return 1 if a message was handled, 0 if no message */ +static int handle_http_message(JSRuntime *rt, JSContext *ctx) { #ifndef NO_HTTP - JSRuntime *rt = JS_GetRuntime(ctx); - JSThreadState *ts = JS_GetRuntimeOpaque(rt); - CURLMcode curl_ret; - struct CURLMsg *m; - int running_handles; + JSThreadState *ts = JS_GetRuntimeOpaque(JS_GetRuntime(ctx)); + JSHttpMessagePipe *hp = ts->http_pipe; + int ret; + struct list_head *el; + struct list_head *req_el; + JSHttpMessage *msg; + JSValue obj, func, retval; + HttpRequestContext *request_ctx; + + pthread_mutex_lock(&hp->mutex); + if (!list_empty(&hp->msg_queue)) { + el = hp->msg_queue.next; + msg = list_entry(el, JSHttpMessage, link); - 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; - } + /* remove the message from the queue */ + list_del(&msg->link); - 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; + if (list_empty(&hp->msg_queue)) { + uint8_t buf[16]; + int ret; + for(;;) { + ret = read(hp->read_fd, buf, sizeof(buf)); + if (ret >= 0) + break; + if (errno != EAGAIN && errno != EINTR) + break; } - curl_multi_remove_handle(ts->curlm, e); - finish_fetch_http(req_ctx, result); } - } while (m); -#endif + + pthread_mutex_unlock(&hp->mutex); + + list_for_each(req_el, &ts->http_requests) { + request_ctx = list_entry(req_el, HttpRequestContext, link); + if (request_ctx->request_id == msg->request_id) { + if (msg->status != 0) { + JSValue headers_list = JS_NewArray(ctx); + + obj = JS_NewObject(ctx); + + if (msg->response_headers) { + char **h = msg->response_headers; + while (*h) { + qjs_array_append_new(ctx, headers_list, JS_NewString(ctx, *h)); + h++; + } + } + JS_SetPropertyStr(ctx, obj, "headers", headers_list); + + //JS_SetPropertyStr(ctx, obj, "data", JS_NewTypedArray(ctx, JS_NewArrayBufferCopy(ctx, msg->body, msg->body_len), 1)); + JS_SetPropertyStr(ctx, obj, "data", JS_NewArrayBufferCopy(ctx, msg->body, msg->body_len)); + + JS_SetPropertyStr(ctx, obj, "status", JS_NewInt32(ctx, msg->status)); + func = JS_DupValue(ctx, request_ctx->resolve_func); + retval = JS_Call(ctx, func, JS_UNDEFINED, 1, (JSValueConst *)&obj); + JS_FreeValue(ctx, obj); + JS_FreeValue(ctx, func); + if (JS_IsException(retval)) { + js_std_dump_error(ctx); + } else { + JS_FreeValue(ctx, retval); + } + } else { + JSAtom atom_message; + + atom_message = JS_NewAtom(ctx, "message"); + obj = JS_NewError(ctx); + JS_DefinePropertyValue(ctx, obj, atom_message, + JS_NewString(ctx, msg->errmsg), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + retval = JS_Call(ctx, request_ctx->reject_func, JS_UNDEFINED, 1, &obj); + JS_FreeAtom(ctx, atom_message); + JS_FreeValue(ctx, retval); + } + break; + } + } + + js_free_http_message(msg); + ret = 1; + } else { + pthread_mutex_unlock(&hp->mutex); + ret = 0; + } + return ret; +#else + return 0; +#endif /* NO_HTTP */ } static int js_os_poll(JSContext *ctx) @@ -2799,7 +2884,7 @@ static int js_os_poll(JSContext *ctx) } #ifndef NO_HTTP - have_http_requests = !list_empty(&ts->curl_requests); + have_http_requests = !list_empty(&ts->http_requests); #endif if ((!have_http_requests) && list_empty(&ts->os_rw_handlers) && list_empty(&ts->os_timers) && @@ -2860,36 +2945,8 @@ 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 + fd_max = max_int(fd_max, ts->http_pipe->read_fd); + FD_SET(ts->http_pipe->read_fd, &rfds); ret = select(fd_max + 1, &rfds, &wfds, NULL, tvp); if (ret > 0) { @@ -2926,7 +2983,11 @@ static int js_os_poll(JSContext *ctx) } } - do_curl(ctx); + if (FD_ISSET(ts->http_pipe->read_fd, &rfds)) { + if (handle_http_message(rt, ctx)) { + goto done; + } + } } done: return 0; @@ -3811,6 +3872,27 @@ static JSHostMessagePipe *js_new_host_message_pipe(void) return ps; } +static JSHttpMessagePipe *js_new_http_message_pipe(void) +{ + JSHttpMessagePipe *ps; + int pipe_fds[2]; + + if (pipe(pipe_fds) < 0) + return NULL; + + ps = malloc(sizeof(*ps)); + if (!ps) { + close(pipe_fds[0]); + close(pipe_fds[1]); + return NULL; + } + init_list_head(&ps->msg_queue); + pthread_mutex_init(&ps->mutex, NULL); + ps->read_fd = pipe_fds[0]; + ps->write_fd = pipe_fds[1]; + return ps; +} + static JSWorkerMessagePipe *js_dup_message_pipe(JSWorkerMessagePipe *ps) { atomic_add_int(&ps->ref_count, 1); @@ -3880,6 +3962,32 @@ static void js_free_host_message_pipe(JSHostMessagePipe *ps) } } +#ifndef NO_HTTP + +static void js_free_http_message_pipe(JSHttpMessagePipe *ps) +{ + struct list_head *el, *el1; + JSHttpMessage *msg; + int ref_count; + + if (!ps) + return; + + assert(ref_count >= 0); + if (ref_count == 0) { + list_for_each_safe(el, el1, &ps->msg_queue) { + msg = list_entry(el, JSHttpMessage, link); + js_free_http_message(msg); + } + pthread_mutex_destroy(&ps->mutex); + close(ps->read_fd); + close(ps->write_fd); + free(ps); + } +} + +#endif + static void js_free_port(JSRuntime *rt, JSWorkerMessageHandler *port) { if (port) { @@ -4416,13 +4524,6 @@ static int js_os_init(JSContext *ctx, JSModuleDef *m) JS_NewClassID(&js_os_timer_class_id); JS_NewClass(JS_GetRuntime(ctx), js_os_timer_class_id, &js_os_timer_class); -#ifndef NO_HTTP - if (CURLE_OK != curl_global_init (CURL_GLOBAL_DEFAULT)) { - JS_ThrowInternalError(ctx, "unable to init libcurl (global)"); - return -1; - } -#endif - #ifdef USE_WORKER { JSRuntime *rt = JS_GetRuntime(ctx); @@ -4537,6 +4638,7 @@ oom_fail: init_list_head(&ts->port_list); ts->on_host_message_func = JS_NULL; ts->host_pipe = js_new_host_message_pipe(); + ts->http_pipe = js_new_http_message_pipe(); if (!ts->host_pipe) { goto oom_fail; } @@ -4544,12 +4646,7 @@ oom_fail: JS_SetRuntimeOpaque(rt, ts); #ifndef NO_HTTP - ts->curlm = curl_multi_init(); - ts->curlsh = curl_share_init(); - curl_share_setopt(ts->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); - curl_share_setopt(ts->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); - curl_share_setopt(ts->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT); - init_list_head(&ts->curl_requests); + init_list_head(&ts->http_requests); #endif #ifdef USE_WORKER @@ -4588,16 +4685,10 @@ void js_std_free_handlers(JSRuntime *rt) } #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); + list_for_each_safe(el, el1, &ts->http_requests) { + HttpRequestContext *request_ctx = list_entry(el, HttpRequestContext, link); + free_http_request_context(request_ctx); } - - curl_multi_cleanup(ts->curlm); - ts->curlm = NULL; - curl_share_cleanup(ts->curlsh); - ts->curlsh = NULL; #endif JS_FreeValueRT(rt, ts->on_host_message_func); @@ -4610,6 +4701,10 @@ void js_std_free_handlers(JSRuntime *rt) js_free_host_message_pipe(ts->host_pipe); +#ifndef NO_HTTP + js_free_http_message_pipe(ts->http_pipe); +#endif + free(ts); JS_SetRuntimeOpaque(rt, NULL); /* fail safe */ } diff --git a/quickjs/quickjs-libc.h b/quickjs/quickjs-libc.h index 4a7697e..650ff36 100644 --- a/quickjs/quickjs-libc.h +++ b/quickjs/quickjs-libc.h @@ -29,6 +29,10 @@ #include "quickjs.h" +#ifndef NO_HTTP +#include "quickjs-http.h" +#endif + #ifdef __cplusplus extern "C" { #endif @@ -55,6 +59,10 @@ void js_std_promise_rejection_tracker(JSContext *ctx, JSValueConst promise, void js_std_set_worker_new_context_func(JSContext *(*func)(JSRuntime *rt)); void js_os_set_host_message_handler(JSContext *ctx, JSHostMessageHandlerFn f, void *cls); int js_os_post_message_from_host(JSContext *ctx, const char *msg_str); + +#ifndef NO_HTTP +void js_os_set_http_impl(JSRuntime *rt, struct JSHttpClientImplementation *impl); +#endif #ifdef __cplusplus } /* extern "C" { */ diff --git a/quickjs/quickjs.c b/quickjs/quickjs.c index bae5541..561374b 100644 --- a/quickjs/quickjs.c +++ b/quickjs/quickjs.c @@ -7776,6 +7776,17 @@ int JS_PreventExtensions(JSContext *ctx, JSValueConst obj) } /* return -1 if exception otherwise TRUE or FALSE */ +int JS_HasPropertyStr(JSContext *ctx, JSValueConst obj, const char *propname) +{ + JSAtom atom; + int ret; + atom = JS_NewAtom(ctx, propname); + ret = JS_HasProperty(ctx, obj, atom); + JS_FreeAtom(ctx, atom); + return ret; +} + +/* return -1 if exception otherwise TRUE or FALSE */ int JS_HasProperty(JSContext *ctx, JSValueConst obj, JSAtom prop) { JSObject *p; diff --git a/quickjs/quickjs.h b/quickjs/quickjs.h index c1cf768..d616c16 100644 --- a/quickjs/quickjs.h +++ b/quickjs/quickjs.h @@ -751,6 +751,7 @@ int JS_SetPropertyInt64(JSContext *ctx, JSValueConst this_obj, int JS_SetPropertyStr(JSContext *ctx, JSValueConst this_obj, const char *prop, JSValue val); int JS_HasProperty(JSContext *ctx, JSValueConst this_obj, JSAtom prop); +int JS_HasPropertyStr(JSContext *ctx, JSValueConst this_obj, const char *propname); int JS_IsExtensible(JSContext *ctx, JSValueConst obj); int JS_PreventExtensions(JSContext *ctx, JSValueConst obj); int JS_DeleteProperty(JSContext *ctx, JSValueConst obj, JSAtom prop, int flags); diff --git a/taler_wallet_core_lib.c b/taler_wallet_core_lib.c index ae82358..a7c2898 100644 --- a/taler_wallet_core_lib.c +++ b/taler_wallet_core_lib.c @@ -52,6 +52,8 @@ struct TALER_WALLET_Instance TALER_WALLET_LogHandlerFn log_handler_f; void *log_handler_cls; + + struct JSHttpClientImplementation *http_impl; }; @@ -176,8 +178,14 @@ run(void *cls) wh->rt = JS_NewRuntime(); js_std_init_handlers(wh->rt); - wh->ctx = JS_NewCustomContext(wh->rt); + if (wh->http_impl) { + js_os_set_http_impl(wh->rt, wh->http_impl); + } else { + fprintf(stderr, "warning: no HTTP client implementation provided for wallet-core\n"); + } + + wh->ctx = JS_NewCustomContext(wh->rt); if (!wh->ctx) { fprintf(stderr, "qjs: cannot allocate JS context\n"); @@ -322,3 +330,10 @@ TALER_start_redirect_std(TALER_LogFn logfn, void *cls) pthread_detach(log_thr); return 0; } + +void +TALER_set_http_client_implementation(struct TALER_WALLET_Instance *instance, + struct JSHttpClientImplementation *impl) +{ + instance->http_impl = impl; +} diff --git a/taler_wallet_core_lib.h b/taler_wallet_core_lib.h index 166612d..660690d 100644 --- a/taler_wallet_core_lib.h +++ b/taler_wallet_core_lib.h @@ -25,6 +25,8 @@ #ifndef _TALER_WALLET_LIB_H #define _TALER_WALLET_LIB_H +#include "quickjs/quickjs-http.h" + /** * Opaque handle to a Taler wallet-core instance. */ @@ -153,4 +155,15 @@ typedef void (*TALER_LogFn)(void *cls, int stream, const char *msg); int TALER_start_redirect_std(TALER_LogFn logfn, void *cls); + +/** + * Set the HTTP client implementation to be used by the wallet. + * + * @param instance wallet-core instance + * @param HTTP client implementation + */ +void +TALER_set_http_client_implementation(struct TALER_WALLET_Instance *instance, + struct JSHttpClientImplementation *impl); + #endif /*_TALER_WALLET_LIB_H */ diff --git a/taler_wallet_core_lib_preprocessed.h b/taler_wallet_core_lib_preprocessed.h new file mode 100644 index 0000000..a050eb1 --- /dev/null +++ b/taler_wallet_core_lib_preprocessed.h @@ -0,0 +1,381 @@ +/* + This file is part of GNU Taler + Copyright (C) 2014-2022 Taler Systems SA + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * C interface to the functionality of wallet-core. + * + * Currently, the underlying implementation uses the JS implementation of + * wallet-core, but this may (or may not) change in the future. + * + * @author Florian Dold + */ +#ifndef _TALER_WALLET_LIB_H +#define _TALER_WALLET_LIB_H + +/* + This file is part of GNU Taler + Copyright (C) 2024 Taler Systems SA + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + + +// ## Native HTTP client library support. + +// Considerations: +// - the API is designed for the HTTP client implementation +// to run in its own thread and *not* be integrated with the +// application's main event loop. +// - focus on small API +// - not a generic HTTP client, only supposed to serve the needs +// of a JS runtime +// - only very tiny subset of HTTP supported +// - no request/response streaming +// - should be appropriate to implement a JS HTTP fetch function +// in the style of WHATWG fetch +// - no focus on ABI compatibility whatsoever + + +#ifndef _QUICKJS_HTTP_H +#define _QUICKJS_HTTP_H + +#include <stdint.h> +#include <limits.h> +#include <stddef.h> + +// Forward declaration; +struct JSHttpResponseInfo; + +/** + * Callback called when an HTTP response has arrived. + * + * IMPORTANT: May be called from an arbitrary thread. + */ +typedef void (*JSHttpResponseCb)(void *cls, struct JSHttpResponseInfo *resp); + +enum JSHttpRedirectFlag { + /** + * Handle redirects transparently. + */ + JS_HTTP_REDIRECT_TRANSPARENT = 0, + /** + * Redirect status codes are returned to the client. + * The client can choose to follow them manually (or not). + */ + JS_HTTP_REDIRECT_MANUAL = 1, + /** + * All redirect status codes result in an error. + */ + JS_HTTP_REDIRECT_ERROR = 2, +}; + +/** + * Info needed to start a new HTTP request. + */ +struct JSHttpRequestInfo { + /** + * Callback called with the response for the request. + */ + JSHttpResponseCb response_cb; + + /** + * Closure for response_cb. + */ + void *response_cb_cls; + + /** + * Request URL. + */ + const char *url; + + /** + * Request method. + */ + const char *method; + + int num_request_headers; + + /** + * Array of `num_request_headers` request headers. + */ + char **request_headers; + + /** + * 0: Handle redirects transparently. + * 1: Handle redirects manually. + * 2: Redirects result in an error. + */ + enum JSHttpRedirectFlag redirect; + + /** + * Request timeout in milliseconds. + * + * When 0 is specified, the timeout is the default request + * timeout for the platform. + * + * When -1 is specified, there is no timeout. This might not be + * supported on all platforms. + */ + int timeout_ms; + + /** + * Enable debug output for this request. + */ + int debug; + + /** + * Request body or NULL. + */ + void *req_body; + + /** + * Length or request body or 0. + */ + size_t req_body_len; +}; + +/** + * Contents of an HTTP response. + */ +struct JSHttpResponseInfo { + + /** + * Request that this is a response to. + * + * (Think of the request ID like a file descriptor number). + */ + int request_id; + + /** + * HTTP response status code or 0 on error. + */ + int status; + + /** + * When status is 0, error message. + */ + char *errmsg; + + /** + * Array of `num_response_headers` response headers. + */ + char **response_headers; + + /** + * Number of response headers. + */ + int num_response_headers; + + /** + * Response body or NULL. + */ + void *body; + + /** + * Length of the response body or 0. + */ + size_t body_len; +}; + +/** + * Callback called when an HTTP response has arrived. + * + * IMPORTANT: May be called from an arbitrary thread. + */ +typedef void (*JSHttpResponseCb)(void *cls, struct JSHttpResponseInfo *resp); + +/** + * Function to create a new HTTP fetch request. + * The request can still be configured until it is started. + * An identifier for the request will be written to @a handle. + * + * @return negative number on error, positive request_id on success + */ +typedef int (*JSHttpReqCreateFn)(void *cls, struct JSHttpRequestInfo *req_info); + +/** + * Cancel a request. The request_id will become invalid + * and the callback won't be called with request_id. + */ +typedef int (*JSHttpReqCancelFn)(void *cls, int request_id); + +struct JSHttpClientImplementation { + /** + * Opaque closure passed to client functions. + */ + void *cls; + JSHttpReqCreateFn req_create; + JSHttpReqCancelFn req_cancel; +}; + + +struct JSHttpClientImplementation * +js_curl_http_client_create(void); + +void +js_curl_http_client_destroy(struct JSHttpClientImplementation *impl); + +#endif /* _QUICKJS_HTTP_H */ + +/** + * Opaque handle to a Taler wallet-core instance. + */ +struct TALER_WALLET_Instance; + +/** + * Handler for messages from the wallet. + * + * @param handler_p opaque closure for the message handler + * @param message message from wallet-core as a JSON string + */ +typedef void (*TALER_WALLET_MessageHandlerFn)(void *handler_p, const char *message); + +enum TALER_WALLET_LogLevel { + TALER_WALLET_LOG_TRACE = 1, + TALER_WALLET_LOG_INFO = 2, + TALER_WALLET_LOG_MESSAGE = 3, + TALER_WALLET_LOG_WARN = 4, + TALER_WALLET_LOG_ERROR = 5 +}; + +/** + * Handler for log message from wallet-core. + * + * @param log_p opaque closure for the log handler + * @param level log level of the log message + * @param tag log tag (usually the file from which the message gets logged) + * @param msg the log message + */ +typedef void (*TALER_WALLET_LogHandlerFn)(void *log_p, + enum TALER_WALLET_LogLevel level, + const char *tag, + const char *msg); + +/** + * Create a new wallet-core instance.. + */ +struct TALER_WALLET_Instance * +TALER_WALLET_create(void); + +/** + * Set a handler for notification and response messages. + * Must be called before the wallet runs. + * + * Caution: The handler will be called from a different thread. + */ +void +TALER_WALLET_set_message_handler(struct TALER_WALLET_Instance *twi, + TALER_WALLET_MessageHandlerFn handler_f, + void *handler_p); + + +/** + * Set a handler for log messages from wallet-core. + * Must be called before the wallet runs. + * + * Caution: The log message handler will be called from a different thread. + */ +void +TALER_WALLET_set_log_handler(struct TALER_WALLET_Instance *twi, + TALER_WALLET_LogHandlerFn handler_f, + void *handler_p); + + +/** + * Set/override the JS file with the wallet-core implementation. + * Must be called before the wallet runs. + */ +// FIXME: Not implemented! +//void +//TALER_WALLET_set_jsfile(struct TALER_WALLET_Instance *twi, +// const char *filename); + +/** + * Send a message to wallet-core. + * + * Responses will be sent asynchronously to the message handler + * set with #TALER_WALLET_set_message_handler. + */ +int +TALER_WALLET_send_request(struct TALER_WALLET_Instance *twi, + const char *request); + +/** + * Run wallet-core in a thread. + * + * This function creates a new thread and returns immediately. + * + * Returns 0 on success or a non-zero error code otherwise. + */ +int +TALER_WALLET_run(struct TALER_WALLET_Instance *twi); + +/** + * Block until the wallet returns. + */ +void +TALER_WALLET_join(struct TALER_WALLET_Instance *twi); + +/** + * Destroy the wallet handle and free resources associated with it. + * + * Note that for a graceful shutdown of the wallet, + * an appropriate shutdown message should be sent first, + * and destroy() should only be called after the wallet has + * sent a response to the shutdown message. + */ +//void +//TALER_WALLET_destroy(struct TALER_WALLET_Instance *twi); + +/** + * Handler for messages that should be logged. + * + * @param stream NOT YET IMPLEMENTED: indicator for the stream that + * the message is coming from, + */ +typedef void (*TALER_LogFn)(void *cls, int stream, const char *msg); + +/** + * Redirect stderr and stdout to a function. + * + * Workaround for platforms where stderr is not visible in logs. + * + * @return 0 on success, error code otherwise + */ +int +TALER_start_redirect_std(TALER_LogFn logfn, void *cls); + + +/** + * Set the HTTP client implementation to be used by the wallet. + * + * @param instance wallet-core instance + * @param HTTP client implementation + */ +void +TALER_set_http_client_implementation(struct TALER_WALLET_Instance *instance, + struct JSHttpClientImplementation *impl); + +#endif /*_TALER_WALLET_LIB_H */ diff --git a/xcode/FTalerWalletcore-Bridging-Header.h b/xcode/FTalerWalletcore-Bridging-Header.h index 83a9bf6..a93d412 100644 --- a/xcode/FTalerWalletcore-Bridging-Header.h +++ b/xcode/FTalerWalletcore-Bridging-Header.h @@ -1 +1 @@ -#import "taler_wallet_core_lib.h" +#import "taler_wallet_core_lib_preprocessed.h" |