From 9205ae150709c66847c26e24204b60358c0a8ffd Mon Sep 17 00:00:00 2001 From: ading2210 Date: Wed, 28 Feb 2024 10:29:47 -0800 Subject: [PATCH 1/6] add old v0.4 code --- client/build.sh | 5 +- client/exported_funcs.txt | 5 +- client/index.html | 2 +- client/javascript/custom_websocket.js | 112 +++++++++++++++++ client/javascript/main.js | 40 +----- client/javascript/tls_socket.js | 73 +++++++++++ client/javascript/util.js | 70 +++++++++++ client/javascript/websocket.js | 174 ++++++-------------------- client/libcurl/main.c | 7 ++ client/libcurl/tls_socket.c | 24 ++++ client/libcurl/websocket.c | 2 +- client/package.json | 2 +- client/wisp_client | 2 +- server/wisp_server | 2 +- 14 files changed, 340 insertions(+), 180 deletions(-) create mode 100644 client/javascript/custom_websocket.js create mode 100644 client/javascript/tls_socket.js create mode 100644 client/javascript/util.js create mode 100644 client/libcurl/tls_socket.c diff --git a/client/build.sh b/client/build.sh index be013ab..c77d2e7 100755 --- a/client/build.sh +++ b/client/build.sh @@ -80,11 +80,14 @@ VERSION=$(cat package.json | jq -r '.version') sed -i "s/__library_version__/$VERSION/" $OUT_FILE #add extra libraries +sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/copyright.js" $OUT_FILE sed -i "/__extra_libraries__/r $WISP_CLIENT/polyfill.js" $OUT_FILE sed -i "/__extra_libraries__/r $WISP_CLIENT/wisp.js" $OUT_FILE sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/messages.js" $OUT_FILE +sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/tls_socket.js" $OUT_FILE sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/websocket.js" $OUT_FILE -sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/copyright.js" $OUT_FILE +sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/custom_websocket.js" $OUT_FILE +sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/util.js" $OUT_FILE #apply patches python3 tools/patch_js.py $FRAGMENTS_DIR $OUT_FILE diff --git a/client/exported_funcs.txt b/client/exported_funcs.txt index f437e28..ccd56dc 100644 --- a/client/exported_funcs.txt +++ b/client/exported_funcs.txt @@ -8,7 +8,7 @@ get_version recv_from_websocket send_to_websocket close_websocket -cleanup_websocket +cleanup_handle get_result_size get_result_buffer get_result_code @@ -16,4 +16,7 @@ get_result_closed get_result_bytes_left get_result_is_text +recv_from_socket +send_to_socket + free \ No newline at end of file diff --git a/client/index.html b/client/index.html index 9760e4c..8555276 100644 --- a/client/index.html +++ b/client/index.html @@ -6,7 +6,7 @@ diff --git a/client/javascript/custom_websocket.js b/client/javascript/custom_websocket.js new file mode 100644 index 0000000..eb91f46 --- /dev/null +++ b/client/javascript/custom_websocket.js @@ -0,0 +1,112 @@ +//class for custom websocket +//multiple classes attempt to replicate the websocket API +//so this prevents code duplication + +class CustomWebSocket extends EventTarget { + constructor(url, protocols=[]) { + super(); + + this.url = url; + this.protocols = protocols; + this.binaryType = "blob"; + + //legacy event handlers + this.onopen = () => {}; + this.onerror = () => {}; + this.onmessage = () => {}; + this.onclose = () => {}; + + this.CONNECTING = 0; + this.OPEN = 1; + this.CLOSING = 2; + this.CLOSED = 3; + + this.status = this.CONNECTING; + } + + custom_recv() {} + recv() { + let {success, data, is_text} = this.custom_recv(); + if (!success) return; + + let converted; + if (is_text) { + converted = new TextDecoder().decode(data); + } + else { + if (this.binaryType == "blob") { + converted = new Blob(data); + } + else if (this.binaryType == "arraybuffer") { + converted = data.buffer; + } + else { + throw "invalid binaryType string"; + } + } + + let msg_event = new MessageEvent("message", {data: converted}); + this.onmessage(msg_event); + this.dispatchEvent(msg_event); + } + + recv_loop() { + this.event_loop = setInterval(() => { + this.recv(); + }, 0); + } + + close_callback(error) { + if (this.status == this.CLOSED) return; + this.status = this.CLOSED; + + if (error) { + let error_event = new Event("error"); + this.dispatchEvent(error_event); + this.onerror(error_event); + } + else { + let close_event = new CloseEvent("close"); + this.dispatchEvent(close_event); + this.onclose(close_event); + } + } + + open_callback() { + this.status = this.OPEN; + let open_event = new Event("open"); + this.onopen(open_event); + this.dispatchEvent(open_event); + } + + send(data) { + let is_text = typeof data === "string"; + if (this.status === this.CONNECTING) { + throw new DOMException("ws not ready yet"); + } + if (this.status === this.CLOSED) { + return; + } + + let data_array = any_to_array(data); + this.custom_send(data_array, is_text); + } + + close() { + this.status = this.CLOSING; + this.custom_close(); + } + + get readyState() { + return this.status; + } + get bufferedAmount() { + return 0; + } + get protocol() { + return this.protocols; + } + get extensions() { + return ""; + } +} \ No newline at end of file diff --git a/client/javascript/main.js b/client/javascript/main.js index f42596b..3110e53 100644 --- a/client/javascript/main.js +++ b/client/javascript/main.js @@ -45,42 +45,6 @@ function check_loaded(check_websocket) { } } -//a case insensitive dictionary for request headers -class HeadersDict { - constructor(obj) { - for (let key in obj) { - this[key] = obj[key]; - } - return new Proxy(this, this); - } - get(target, prop) { - let keys = Object.keys(this); - for (let key of keys) { - if (key.toLowerCase() === prop.toLowerCase()) { - return this[key]; - } - } - } - set(target, prop, value) { - let keys = Object.keys(this); - for (let key of keys) { - if (key.toLowerCase() === prop.toLowerCase()) { - this[key] = value; - } - } - this[prop] = value; - return true; - } -} - -function allocate_str(str) { - return allocate(intArrayFromString(str), ALLOC_NORMAL); -} - -function allocate_array(array) { - return allocate(array, ALLOC_NORMAL); -} - //low level interface with c code function perform_request(url, params, js_data_callback, js_end_callback, body=null) { let params_str = JSON.stringify(params); @@ -313,15 +277,17 @@ return { load_wasm: load_wasm, wisp: _wisp_connections, WebSocket: CurlWebSocket, + TLSSocket: TLSSocket, get copyright() {return copyright_notice}, get version() {return get_version()}, get ready() {return wasm_ready}, + get websocket_url() {return websocket_url}, get stdout() {return out}, set stdout(callback) {out = callback}, get stderr() {return err}, - set stderr(callback) {err = callback} + set stderr(callback) {err = callback}, } })() \ No newline at end of file diff --git a/client/javascript/tls_socket.js b/client/javascript/tls_socket.js new file mode 100644 index 0000000..36eca50 --- /dev/null +++ b/client/javascript/tls_socket.js @@ -0,0 +1,73 @@ +class TLSSocket extends CustomWebSocket { + constructor(hostname, port, debug) { + super(); + this.hostname = hostname; + this.port = port; + this.url = `https://${hostname}:${port}`; + this.debug = debug; + + this.connect(); + } + + connect() { + let data_callback = () => {}; + let finish_callback = (error, response_info) => { + this.status = this.OPEN; + if (error === 0) { + this.open_callback(); + this.recv_loop(); + } + else { + this.cleanup(error); + } + } + let options = { + _connect_only: 1, + } + if (this.debug) options._libcurl_verbose = 1; + + this.http_handle = perform_request(this.url, options, data_callback, finish_callback, null); + } + + custom_recv() { + let buffer_size = 64*1024; + let result_ptr = _recv_from_socket(this.http_handle, buffer_size); + let data_ptr = _get_result_buffer(result_ptr); + let result_code = _get_result_code(result_ptr); + + if (result_code == 0) { //CURLE_OK - data received + if (_get_result_closed(result_ptr)) { + this.close_callback(); + return; + } + + let data_size = _get_result_size(result_ptr); + let data_heap = Module.HEAPU8.subarray(data_ptr, data_ptr + data_size); + let data = new Uint8Array(data_heap); + + let message_event = new MessageEvent("message", {data: data}); + this.dispatchEvent(message_event); + } + + _free(data_ptr); + _free(result_ptr); + } + + custom_send(data_array) { + let data_ptr = allocate_array(data_array); + let data_len = data_array.length; + _send_to_socket(this.http_handle, data_ptr, data_len); + _free(data_ptr); + } + + cleanup(error=false) { + if (this.http_handle) _cleanup_handle(this.http_handle); + clearInterval(this.event_loop); + this.close_callback(error); + } + + custom_close() { + this.cleanup(); + this.status = this.CLOSED; + } +} \ No newline at end of file diff --git a/client/javascript/util.js b/client/javascript/util.js new file mode 100644 index 0000000..989d1b8 --- /dev/null +++ b/client/javascript/util.js @@ -0,0 +1,70 @@ +//a case insensitive dictionary for request headers +class HeadersDict { + constructor(obj) { + for (let key in obj) { + this[key] = obj[key]; + } + return new Proxy(this, this); + } + get(target, prop) { + let keys = Object.keys(this); + for (let key of keys) { + if (key.toLowerCase() === prop.toLowerCase()) { + return this[key]; + } + } + } + set(target, prop, value) { + let keys = Object.keys(this); + for (let key of keys) { + if (key.toLowerCase() === prop.toLowerCase()) { + this[key] = value; + } + } + this[prop] = value; + return true; + } +} + +function allocate_str(str) { + return allocate(intArrayFromString(str), ALLOC_NORMAL); +} + +function allocate_array(array) { + return allocate(array, ALLOC_NORMAL); +} + +//convert any data to a uint8array +function any_to_array(data) { + let data_array; + if (typeof data === "string") { + data_array = new TextEncoder().encode(data); + } + else if (data instanceof Blob) { + data.arrayBuffer().then(array_buffer => { + data_array = new Uint8Array(array_buffer); + this.send(data_array); + }); + return; + } + //any typedarray + else if (data instanceof ArrayBuffer) { + //dataview objects + if (ArrayBuffer.isView(data) && data instanceof DataView) { + data_array = new Uint8Array(data.buffer); + } + //regular arraybuffers + else { + data_array = new Uint8Array(data); + } + } + //regular typed arrays + else if (ArrayBuffer.isView(data)) { + data_array = Uint8Array.from(data); + } + else { + throw "invalid data type"; + } + + return data_array; +} \ No newline at end of file diff --git a/client/javascript/websocket.js b/client/javascript/websocket.js index 23af368..3330e1f 100644 --- a/client/javascript/websocket.js +++ b/client/javascript/websocket.js @@ -1,38 +1,30 @@ -//class for custom websocket - -class CurlWebSocket extends EventTarget { - constructor(url, protocols=[], websocket_debug=false) { - super(); +class CurlWebSocket extends CustomWebSocket { + constructor(url, protocols=[], debug=false) { + super(url, protocols); check_loaded(true); if (!url.startsWith("wss://") && !url.startsWith("ws://")) { throw new SyntaxError("invalid url"); } - - this.url = url; + this.protocols = protocols; - this.binaryType = "blob"; + this.debug = debug; this.recv_buffer = []; - this.websocket_debug = websocket_debug; - - //legacy event handlers - this.onopen = () => {}; - this.onerror = () => {}; - this.onmessage = () => {}; - this.onclose = () => {}; - - this.CONNECTING = 0; - this.OPEN = 1; - this.CLOSING = 2; - this.CLOSED = 3; this.connect(); } connect() { - this.status = this.CONNECTING; let data_callback = () => {}; let finish_callback = (error, response_info) => { - this.finish_callback(error, response_info); + if (error === 0) { + this.status = this.OPEN; + this.open_callback(); + this.recv_loop(); + } + else { + this.status = this.CLOSED; + this.cleanup(error); + } } let options = {}; if (this.protocols) { @@ -40,23 +32,23 @@ class CurlWebSocket extends EventTarget { "Sec-Websocket-Protocol": this.protocols.join(", "), }; } - if (this.websocket_debug) { + if (this.debug) { options._libcurl_verbose = 1; } this.http_handle = perform_request(this.url, options, data_callback, finish_callback, null); - this.recv_loop(); } - recv() { + custom_recv() { let buffer_size = 64*1024; let result_ptr = _recv_from_websocket(this.http_handle, buffer_size); let data_ptr = _get_result_buffer(result_ptr); let result_code = _get_result_code(result_ptr); - if (result_code == 0) { //CURLE_OK - data recieved + if (result_code == 0) { //CURLE_OK - data received if (_get_result_closed(result_ptr)) { - //this.pass_buffer(); - this.close_callback(); + _free(data_ptr); + _free(result_ptr); + this.cleanup(); return; } @@ -69,133 +61,43 @@ class CurlWebSocket extends EventTarget { let full_data = merge_arrays(this.recv_buffer); let is_text = _get_result_is_text(result_ptr) this.recv_buffer = []; - this.recv_callback(full_data, is_text); + return { + success: true, + data: full_data, + is_text: is_text + } } } if (result_code == 52) { //CURLE_GOT_NOTHING - socket closed - this.close_callback(); + this.cleanup(); } _free(data_ptr); _free(result_ptr); - } - recv_loop() { - this.event_loop = setInterval(() => { - this.recv(); - }, 1); - } - - recv_callback(data, is_text=false) { - let converted; - if (is_text) { - converted = new TextDecoder().decode(data); + return { + success: false, + data: null, + is_text: false } - else { - if (this.binaryType == "blob") { - converted = new Blob(data); - } - else if (this.binaryType == "arraybuffer") { - converted = data.buffer; - } - else { - throw "invalid binaryType string"; - } - } - - let msg_event = new MessageEvent("message", {data: converted}); - this.onmessage(msg_event); - this.dispatchEvent(msg_event); } - close_callback(error=false) { - if (this.status == this.CLOSED) return; - this.status = this.CLOSED; - + cleanup(error=false) { + if (this.http_handle) _cleanup_handle(this.http_handle); clearInterval(this.event_loop); - _cleanup_websocket(); - - if (error) { - let error_event = new Event("error"); - this.dispatchEvent(error_event); - this.onerror(error_event); - } - else { - let close_event = new CloseEvent("close"); - this.dispatchEvent(close_event); - this.onclose(close_event); - } + this.close_callback(error); } - finish_callback(error, response_info) { - this.status = this.OPEN; - if (error != 0) this.close_callback(true); - let open_event = new Event("open"); - this.onopen(open_event); - this.dispatchEvent(open_event); - } - - send(data) { - let is_text = false; - if (this.status === this.CONNECTING) { - throw new DOMException("ws not ready yet"); - } - if (this.status === this.CLOSED) { - return; - } - - let data_array; - if (typeof data === "string") { - data_array = new TextEncoder().encode(data); - is_text = true; - } - else if (data instanceof Blob) { - data.arrayBuffer().then(array_buffer => { - data_array = new Uint8Array(array_buffer); - this.send(data_array); - }); - return; - } - //any typedarray - else if (data instanceof ArrayBuffer) { - //dataview objects - if (ArrayBuffer.isView(data) && data instanceof DataView) { - data_array = new Uint8Array(data.buffer); - } - //regular arraybuffers - else { - data_array = new Uint8Array(data); - } - } - //regular typed arrays - else if (ArrayBuffer.isView(data)) { - data_array = Uint8Array.from(data); - } - else { - throw "invalid data type to be sent"; - } - + custom_send(data_array, is_text) { let data_ptr = allocate_array(data_array); - let data_len = data.length; + let data_len = data_array.length; _send_to_websocket(this.http_handle, data_ptr, data_len, is_text); _free(data_ptr); } - close() { - _close_websocket(this.http_handle); - } - - get readyState() { - return this.status; - } - get bufferedAmount() { - return 0; - } - get protocol() { - return ""; - } - get extensions() { - return ""; + custom_close() { + this.cleanup(); + this.status = this.CLOSED; } } \ No newline at end of file diff --git a/client/libcurl/main.c b/client/libcurl/main.c index f085d7a..ebb04f4 100644 --- a/client/libcurl/main.c +++ b/client/libcurl/main.c @@ -83,6 +83,13 @@ CURL* start_request(const char* url, const char* json_params, DataCallback data_ curl_easy_setopt(http_handle, CURLOPT_VERBOSE, 1L); } + if (strcmp(key, "_connect_only") == 0) { + curl_easy_setopt(http_handle, CURLOPT_CONNECT_ONLY, 1L); + curl_easy_setopt(http_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_setopt(http_handle, CURLOPT_SSL_ENABLE_ALPN, 0L); + prevent_cleanup = 1; + } + if (strcmp(key, "method") == 0 && cJSON_IsString(item)) { curl_easy_setopt(http_handle, CURLOPT_CUSTOMREQUEST, item->valuestring); } diff --git a/client/libcurl/tls_socket.c b/client/libcurl/tls_socket.c new file mode 100644 index 0000000..c890769 --- /dev/null +++ b/client/libcurl/tls_socket.c @@ -0,0 +1,24 @@ +#include + +#include "curl/curl.h" +#include "curl/easy.h" +#include "types.h" + +struct WSResult* recv_from_socket(CURL* http_handle, int buffer_size) { + size_t nread; + char* buffer = malloc(buffer_size); + CURLcode res = curl_easy_recv(http_handle, buffer, buffer_size, &nread); + + struct WSResult* result = malloc(sizeof(struct WSResult)); + result->buffer_size = nread; + result->buffer = buffer; + result->res = (int) res; + result->closed = (nread == 0); + return result; +} + +int send_to_socket(CURL* http_handle, const char* data, int data_len) { + size_t sent; + CURLcode res = curl_easy_send(http_handle, data, data_len, &sent); + return (int) res; +} \ No newline at end of file diff --git a/client/libcurl/websocket.c b/client/libcurl/websocket.c index 4c7e5fd..49d2ddf 100644 --- a/client/libcurl/websocket.c +++ b/client/libcurl/websocket.c @@ -37,7 +37,7 @@ void close_websocket(CURL* http_handle) { } //clean up the http handle associated with the websocket, since the main loop can't do this automatically -void cleanup_websocket(CURL* http_handle) { +void cleanup_handle(CURL* http_handle) { struct RequestInfo *request_info; curl_easy_getinfo(http_handle, CURLINFO_PRIVATE, &request_info); diff --git a/client/package.json b/client/package.json index 47caa58..0981d4e 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "libcurl.js", - "version": "0.3.3", + "version": "0.4.0", "description": "An experimental port of libcurl to WebAssembly for use in the browser.", "main": "libcurl.mjs", "scripts": { diff --git a/client/wisp_client b/client/wisp_client index 51ad95a..84b71a2 160000 --- a/client/wisp_client +++ b/client/wisp_client @@ -1 +1 @@ -Subproject commit 51ad95a6d912ec404c20284f0cded40c0b5c4e62 +Subproject commit 84b71a2882378dcb48d6d421fdb19290ebe0df23 diff --git a/server/wisp_server b/server/wisp_server index 65d3124..3b0d432 160000 --- a/server/wisp_server +++ b/server/wisp_server @@ -1 +1 @@ -Subproject commit 65d312414aa896d8f9e18f48e8fe37b72d75ee5c +Subproject commit 3b0d432e89cff7eaa850ba8605b180189e237f1b From c681e21777e546b48d2e6a7197756321cdb637d7 Mon Sep 17 00:00:00 2001 From: ading2210 Date: Tue, 5 Mar 2024 21:48:35 -0500 Subject: [PATCH 2/6] add function to get cert bundle --- client/exported_funcs.txt | 1 + client/javascript/main.js | 5 +++++ client/libcurl/main.c | 6 +++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/client/exported_funcs.txt b/client/exported_funcs.txt index ccd56dc..d1b4b83 100644 --- a/client/exported_funcs.txt +++ b/client/exported_funcs.txt @@ -4,6 +4,7 @@ tick_request active_requests get_version +get_cacert recv_from_websocket send_to_websocket diff --git a/client/javascript/main.js b/client/javascript/main.js index 211f7d3..0b8d6ab 100644 --- a/client/javascript/main.js +++ b/client/javascript/main.js @@ -252,6 +252,10 @@ function get_version() { return version_dict; } +function get_cacert() { + return UTF8ToString(_get_cacert()); +} + function main() { wasm_ready = true; _init_curl(); @@ -277,6 +281,7 @@ api = { load_wasm: load_wasm, WebSocket: CurlWebSocket, TLSSocket: TLSSocket, + get_cacert: get_cacert, wisp_connections: _wisp_connections, WispConnection: WispConnection, diff --git a/client/libcurl/main.c b/client/libcurl/main.c index 7725ad3..47a5dd4 100644 --- a/client/libcurl/main.c +++ b/client/libcurl/main.c @@ -7,9 +7,9 @@ #include "curl/easy.h" #include "curl/header.h" #include "cjson/cJSON.h" -#include "cacert.h" #include "curl/multi.h" +#include "cacert.h" #include "util.h" #include "types.h" @@ -196,6 +196,10 @@ void finish_request(CURLMsg *curl_msg) { free(request_info); } +unsigned char* get_cacert() { + return _cacert_pem; +} + void init_curl() { curl_global_init(CURL_GLOBAL_DEFAULT); multi_handle = curl_multi_init(); From c9236f90d558900e05d88caaa772b82fa8a0b750 Mon Sep 17 00:00:00 2001 From: ading2210 Date: Wed, 6 Mar 2024 15:53:11 -0500 Subject: [PATCH 3/6] refractor websocket again --- README.md | 5 ++ client/build.sh | 2 +- client/javascript/custom_websocket.js | 112 ------------------------- client/javascript/logger.js | 18 ++++ client/javascript/main.js | 47 ++--------- client/javascript/tls_socket.js | 2 + client/javascript/util.js | 35 ++++---- client/javascript/websocket.js | 107 +++++++++++++++--------- client/javascript/ws_polyfill.js | 113 ++++++++++++++++++++++++++ 9 files changed, 233 insertions(+), 208 deletions(-) delete mode 100644 client/javascript/custom_websocket.js create mode 100644 client/javascript/logger.js create mode 100644 client/javascript/ws_polyfill.js diff --git a/README.md b/README.md index 2b6d87c..04bd825 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,8 @@ Most of the standard Fetch API's features are supported, with the exception of: - Sending credentials/cookies automatically - Caching +Note that there is a hard limit of 50 active TCP connections due to emscripten limitations. + ### Creating WebSocket Connections: To use WebSockets, create a `libcurl.WebSocket` object, which works identically to the regular [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object. ```js @@ -106,6 +108,9 @@ libcurl.stderr = (text) => {document.body.innerHTML += text}; ### Getting Version Info: You can get version information from the `libcurl.version` object. This object will also contain the versions of all the C libraries that libcurl.js uses. `libcurl.version.lib` returns the version of libcurl.js itself. +### Getting the CA Certificates Bundle: +You can get the CA cert bundle that libcurl uses by calling `libcurl.get_cacert()`. The function will return a string with the certificates in PEM format. The cert bundle comes from the [official curl website](https://curl.se/docs/caextract.html), which is extracted from the Mozilla Firefox source code. + ## Proxy Server: The proxy server consists of a standard [Wisp](https://github.com/MercuryWorkshop/wisp-protocol) server, allowing multiple TCP connections to share the same websocket. diff --git a/client/build.sh b/client/build.sh index 4b5566c..43a59bd 100755 --- a/client/build.sh +++ b/client/build.sh @@ -86,7 +86,7 @@ sed -i "/__extra_libraries__/r $WISP_CLIENT/wisp.js" $OUT_FILE sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/messages.js" $OUT_FILE sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/tls_socket.js" $OUT_FILE sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/websocket.js" $OUT_FILE -sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/custom_websocket.js" $OUT_FILE +sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/ws_polyfill.js" $OUT_FILE sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/util.js" $OUT_FILE #apply patches diff --git a/client/javascript/custom_websocket.js b/client/javascript/custom_websocket.js deleted file mode 100644 index eb91f46..0000000 --- a/client/javascript/custom_websocket.js +++ /dev/null @@ -1,112 +0,0 @@ -//class for custom websocket -//multiple classes attempt to replicate the websocket API -//so this prevents code duplication - -class CustomWebSocket extends EventTarget { - constructor(url, protocols=[]) { - super(); - - this.url = url; - this.protocols = protocols; - this.binaryType = "blob"; - - //legacy event handlers - this.onopen = () => {}; - this.onerror = () => {}; - this.onmessage = () => {}; - this.onclose = () => {}; - - this.CONNECTING = 0; - this.OPEN = 1; - this.CLOSING = 2; - this.CLOSED = 3; - - this.status = this.CONNECTING; - } - - custom_recv() {} - recv() { - let {success, data, is_text} = this.custom_recv(); - if (!success) return; - - let converted; - if (is_text) { - converted = new TextDecoder().decode(data); - } - else { - if (this.binaryType == "blob") { - converted = new Blob(data); - } - else if (this.binaryType == "arraybuffer") { - converted = data.buffer; - } - else { - throw "invalid binaryType string"; - } - } - - let msg_event = new MessageEvent("message", {data: converted}); - this.onmessage(msg_event); - this.dispatchEvent(msg_event); - } - - recv_loop() { - this.event_loop = setInterval(() => { - this.recv(); - }, 0); - } - - close_callback(error) { - if (this.status == this.CLOSED) return; - this.status = this.CLOSED; - - if (error) { - let error_event = new Event("error"); - this.dispatchEvent(error_event); - this.onerror(error_event); - } - else { - let close_event = new CloseEvent("close"); - this.dispatchEvent(close_event); - this.onclose(close_event); - } - } - - open_callback() { - this.status = this.OPEN; - let open_event = new Event("open"); - this.onopen(open_event); - this.dispatchEvent(open_event); - } - - send(data) { - let is_text = typeof data === "string"; - if (this.status === this.CONNECTING) { - throw new DOMException("ws not ready yet"); - } - if (this.status === this.CLOSED) { - return; - } - - let data_array = any_to_array(data); - this.custom_send(data_array, is_text); - } - - close() { - this.status = this.CLOSING; - this.custom_close(); - } - - get readyState() { - return this.status; - } - get bufferedAmount() { - return 0; - } - get protocol() { - return this.protocols; - } - get extensions() { - return ""; - } -} \ No newline at end of file diff --git a/client/javascript/logger.js b/client/javascript/logger.js new file mode 100644 index 0000000..02d329c --- /dev/null +++ b/client/javascript/logger.js @@ -0,0 +1,18 @@ +function logger(type, text) { + if (type === "log") + console.log(text); + else if (type === "warn") + console.warn(text); + else if (type === "error") + console.error(text); +} + +function log_msg(text) { + logger("log", text); +} +function warn_msg(text) { + logger("warn", text); +} +function error_msg(text) { + logger("error", text); +} diff --git a/client/javascript/main.js b/client/javascript/main.js index 0b8d6ab..8af171c 100644 --- a/client/javascript/main.js +++ b/client/javascript/main.js @@ -141,51 +141,11 @@ function create_response(response_data, response_info) { return response_obj; } -async function parse_body(data) { - let data_array = null; - if (typeof data === "string") { - data_array = new TextEncoder().encode(data); - } - - else if (data instanceof Blob) { - let array_buffer = await data.arrayBuffer(); - data_array = new Uint8Array(array_buffer); - } - - //any typedarray - else if (data instanceof ArrayBuffer) { - //dataview objects - if (ArrayBuffer.isView(data) && data instanceof DataView) { - data_array = new Uint8Array(data.buffer); - } - //regular typed arrays - else if (ArrayBuffer.isView(data)) { - data_array = Uint8Array.from(data); - } - //regular arraybuffers - else { - data_array = new Uint8Array(data); - } - } - - else if (data instanceof ReadableStream) { - let chunks = []; - for await (let chunk of data) { - chunks.push(chunk); - } - data_array = merge_arrays(chunks); - } - - else { - throw "invalid data type to be sent"; - } - return data_array; -} async function create_options(params) { let body = null; if (params.body) { - body = await parse_body(params.body); + body = await data_to_array(params.body); params.body = true; } @@ -279,7 +239,8 @@ api = { fetch: libcurl_fetch, set_websocket: set_websocket_url, load_wasm: load_wasm, - WebSocket: CurlWebSocket, + WebSocket: FakeWebSocket, + CurlWebSocket: CurlWebSocket, TLSSocket: TLSSocket, get_cacert: get_cacert, @@ -295,6 +256,8 @@ api = { set stdout(callback) {out = callback}, get stderr() {return err}, set stderr(callback) {err = callback}, + get logger() {return logger}, + set logger(func) {logger = func}, onload() {} }; diff --git a/client/javascript/tls_socket.js b/client/javascript/tls_socket.js index 36eca50..62b378c 100644 --- a/client/javascript/tls_socket.js +++ b/client/javascript/tls_socket.js @@ -1,3 +1,5 @@ +//currently broken + class TLSSocket extends CustomWebSocket { constructor(hostname, port, debug) { super(); diff --git a/client/javascript/util.js b/client/javascript/util.js index 989d1b8..bff95e7 100644 --- a/client/javascript/util.js +++ b/client/javascript/util.js @@ -35,36 +35,43 @@ function allocate_array(array) { } //convert any data to a uint8array -function any_to_array(data) { - let data_array; +async function data_to_array(data) { + let data_array = null; if (typeof data === "string") { data_array = new TextEncoder().encode(data); } + else if (data instanceof Blob) { - data.arrayBuffer().then(array_buffer => { - data_array = new Uint8Array(array_buffer); - this.send(data_array); - }); - return; + let array_buffer = await data.arrayBuffer(); + data_array = new Uint8Array(array_buffer); } + //any typedarray else if (data instanceof ArrayBuffer) { //dataview objects if (ArrayBuffer.isView(data) && data instanceof DataView) { data_array = new Uint8Array(data.buffer); } + //regular typed arrays + else if (ArrayBuffer.isView(data)) { + data_array = Uint8Array.from(data); + } //regular arraybuffers else { data_array = new Uint8Array(data); } } - //regular typed arrays - else if (ArrayBuffer.isView(data)) { - data_array = Uint8Array.from(data); - } - else { - throw "invalid data type"; + + else if (data instanceof ReadableStream) { + let chunks = []; + for await (let chunk of data) { + chunks.push(chunk); + } + data_array = merge_arrays(chunks); } + else { + throw "invalid data type to be sent"; + } return data_array; -} \ No newline at end of file +} diff --git a/client/javascript/websocket.js b/client/javascript/websocket.js index 3330e1f..08746c7 100644 --- a/client/javascript/websocket.js +++ b/client/javascript/websocket.js @@ -1,13 +1,21 @@ -class CurlWebSocket extends CustomWebSocket { - constructor(url, protocols=[], debug=false) { - super(url, protocols); +class CurlWebSocket { + constructor(url, protocols=[], options={}) { check_loaded(true); if (!url.startsWith("wss://") && !url.startsWith("ws://")) { throw new SyntaxError("invalid url"); } + this.url = url; this.protocols = protocols; - this.debug = debug; + this.options = options; + + this.onopen = () => {}; + this.onerror = () => {}; + this.onmessage = () => {}; + this.onclose = () => {}; + + this.connected = false; + this.event_loop = null; this.recv_buffer = []; this.connect(); @@ -17,87 +25,108 @@ class CurlWebSocket extends CustomWebSocket { let data_callback = () => {}; let finish_callback = (error, response_info) => { if (error === 0) { - this.status = this.OPEN; - this.open_callback(); - this.recv_loop(); + this.connected = true; + this.event_loop = setInterval(() => { + let data = this.recv(); + if (data !== null) this.onmessage(data); + }, 0); + this.onopen(); } else { this.status = this.CLOSED; this.cleanup(error); } } - let options = {}; + let request_options = { + headers: this.options.headers || {} + }; if (this.protocols) { - options.headers = { - "Sec-Websocket-Protocol": this.protocols.join(", "), - }; + request_options.headers["Sec-Websocket-Protocol"] = this.protocols.join(", "); } - if (this.debug) { - options._libcurl_verbose = 1; + if (this.options.verbose) { + request_options._libcurl_verbose = 1; } - this.http_handle = perform_request(this.url, options, data_callback, finish_callback, null); + this.http_handle = perform_request(this.url, request_options, data_callback, finish_callback, null); } - custom_recv() { + recv() { let buffer_size = 64*1024; let result_ptr = _recv_from_websocket(this.http_handle, buffer_size); let data_ptr = _get_result_buffer(result_ptr); let result_code = _get_result_code(result_ptr); + let returned_data = null; - if (result_code == 0) { //CURLE_OK - data received + function free_result() { + _free(data_ptr); + _free(result_ptr); + } + console.log(result_code); + + if (result_code === 0) { //CURLE_OK - data received if (_get_result_closed(result_ptr)) { - _free(data_ptr); - _free(result_ptr); + free_result(); this.cleanup(); - return; + return returned_data; } let data_size = _get_result_size(result_ptr); let data_heap = Module.HEAPU8.subarray(data_ptr, data_ptr + data_size); let data = new Uint8Array(data_heap); + + console.log(data, data_size, buffer_size, _get_result_bytes_left(result_ptr)); this.recv_buffer.push(data); if (data_size !== buffer_size && !_get_result_bytes_left(result_ptr)) { //message finished let full_data = merge_arrays(this.recv_buffer); let is_text = _get_result_is_text(result_ptr) this.recv_buffer = []; - return { - success: true, - data: full_data, - is_text: is_text + if (is_text) { + returned_data = new TextDecoder().decode(full_data); + } + else { + returned_data = full_data; } } } - - if (result_code == 52) { //CURLE_GOT_NOTHING - socket closed + + //CURLE_GOT_NOTHING, CURLE_RECV_ERROR, CURLE_SEND_ERROR - socket closed + else if (result_code === 52 || result_code === 55 || result_code === 56) { this.cleanup(); } - _free(data_ptr); - _free(result_ptr); - - return { - success: false, - data: null, - is_text: false - } + free_result(); + return returned_data; } cleanup(error=false) { if (this.http_handle) _cleanup_handle(this.http_handle); clearInterval(this.event_loop); - this.close_callback(error); + this.connected = false; + + if (error) { + this.onerror(error); + } + else { + this.onclose(); + } } - custom_send(data_array, is_text) { - let data_ptr = allocate_array(data_array); - let data_len = data_array.length; + send(data) { + let is_text = typeof data === "string"; + if (!this.connected) { + throw new DOMException("websocket not connected"); + } + + if (is_text) { + data = new TextEncoder().encode(data); + } + let data_ptr = allocate_array(data); + let data_len = data.length; _send_to_websocket(this.http_handle, data_ptr, data_len, is_text); _free(data_ptr); } - custom_close() { + close() { this.cleanup(); - this.status = this.CLOSED; } } \ No newline at end of file diff --git a/client/javascript/ws_polyfill.js b/client/javascript/ws_polyfill.js new file mode 100644 index 0000000..c3c64b4 --- /dev/null +++ b/client/javascript/ws_polyfill.js @@ -0,0 +1,113 @@ +//class for websocket polyfill + +class FakeWebSocket extends EventTarget { + constructor(url, protocols=[], options={}) { + super(); + + this.url = url; + this.protocols = protocols; + this.options = options; + this.binaryType = "blob"; + + //legacy event handlers + this.onopen = () => {}; + this.onerror = () => {}; + this.onmessage = () => {}; + this.onclose = () => {}; + + this.CONNECTING = 0; + this.OPEN = 1; + this.CLOSING = 2; + this.CLOSED = 3; + this.status = this.CONNECTING; + + this.socket = null; + this.connect(); + } + + connect() { + this.socket = new CurlWebSocket(this.url, this.protocols, this.options); + + this.socket.onopen = () => { + this.status = this.OPEN; + let open_event = new Event("open"); + this.onopen(open_event); + this.dispatchEvent(open_event); + } + + this.socket.onclose = () => { + this.status = this.CLOSED; + let close_event = new CloseEvent("close"); + this.dispatchEvent(close_event); + this.onclose(close_event); + }; + + this.socket.onerror = (error) => { + this.status = this.CLOSED; + console.error(`websocket ${this.url} encountered an error (${error})`); + let error_event = new Event("error"); + this.dispatchEvent(error_event); + this.onerror(error_event); + } + + this.socket.onmessage = (data) => { + let converted; + if (typeof data === "string") { + converted = data; + } + else { //binary frame received as uint8array + if (this.binaryType == "blob") { + converted = new Blob(data); + } + else if (this.binaryType == "arraybuffer") { + converted = data.buffer; + } + else { + throw "invalid binaryType string"; + } + } + + let msg_event = new MessageEvent("message", {data: converted}); + this.onmessage(msg_event); + this.dispatchEvent(msg_event); + } + } + + send(data) { + let is_text = typeof data === "string"; + if (this.status === this.CONNECTING) { + throw new DOMException("websocket not ready yet"); + } + if (this.status === this.CLOSED) { + return; + } + + (async () => { + if (is_text) { + this.socket.send(data); + } + else { + let data_array = await data_to_array(data); + this.send(data_array); + } + })(); + } + + close() { + this.status = this.CLOSING; + this.socket.close(); + } + + get readyState() { + return this.status; + } + get bufferedAmount() { + return 0; + } + get protocol() { + return this.protocols[0] || ""; + } + get extensions() { + return ""; + } +} \ No newline at end of file From 155d5ea5b68ea7e9379d58d78f20554fa4698b45 Mon Sep 17 00:00:00 2001 From: ading2210 Date: Wed, 6 Mar 2024 17:02:55 -0500 Subject: [PATCH 4/6] add libcurl error strings to fetch error messages --- client/build.sh | 1 + client/exported_funcs.txt | 1 + client/javascript/main.js | 5 +++-- client/javascript/tls_socket.js | 3 +-- client/javascript/util.js | 5 +++++ client/javascript/websocket.js | 5 +---- client/javascript/ws_polyfill.js | 4 ++-- client/libcurl/util.c | 4 ++++ 8 files changed, 18 insertions(+), 10 deletions(-) diff --git a/client/build.sh b/client/build.sh index 43a59bd..394d976 100755 --- a/client/build.sh +++ b/client/build.sh @@ -88,6 +88,7 @@ sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/tls_socket.js" $OUT_FILE sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/websocket.js" $OUT_FILE sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/ws_polyfill.js" $OUT_FILE sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/util.js" $OUT_FILE +sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/logger.js" $OUT_FILE #apply patches python3 tools/patch_js.py $FRAGMENTS_DIR $OUT_FILE diff --git a/client/exported_funcs.txt b/client/exported_funcs.txt index d1b4b83..f925f95 100644 --- a/client/exported_funcs.txt +++ b/client/exported_funcs.txt @@ -5,6 +5,7 @@ active_requests get_version get_cacert +get_error_str recv_from_websocket send_to_websocket diff --git a/client/javascript/main.js b/client/javascript/main.js index 8af171c..a2da526 100644 --- a/client/javascript/main.js +++ b/client/javascript/main.js @@ -67,7 +67,6 @@ function perform_request(url, params, js_data_callback, js_end_callback, body=nu _free(url_ptr); _free(response_json_ptr); - if (error != 0) console.error("request failed with error code " + error); active_requests --; js_end_callback(error, response_info); } @@ -172,7 +171,9 @@ function perform_request_async(url, params, body) { let finish_callback = (error, response_info) => { if (error != 0) { - reject("libcurl.js encountered an error: " + error); + let error_str = `Request failed with error code ${error}: ${get_error_str(error)}`; + if (error != 0) error_msg(error_str); + reject(error_str); return; } let response_data = merge_arrays(chunks); diff --git a/client/javascript/tls_socket.js b/client/javascript/tls_socket.js index 62b378c..7db9d3f 100644 --- a/client/javascript/tls_socket.js +++ b/client/javascript/tls_socket.js @@ -1,8 +1,7 @@ //currently broken -class TLSSocket extends CustomWebSocket { +class TLSSocket { constructor(hostname, port, debug) { - super(); this.hostname = hostname; this.port = port; this.url = `https://${hostname}:${port}`; diff --git a/client/javascript/util.js b/client/javascript/util.js index bff95e7..f5db34a 100644 --- a/client/javascript/util.js +++ b/client/javascript/util.js @@ -34,6 +34,11 @@ function allocate_array(array) { return allocate(array, ALLOC_NORMAL); } +function get_error_str(error_code) { + let error_ptr = _get_error_str(error_code); + return UTF8ToString(error_ptr); +} + //convert any data to a uint8array async function data_to_array(data) { let data_array = null; diff --git a/client/javascript/websocket.js b/client/javascript/websocket.js index 08746c7..d6791aa 100644 --- a/client/javascript/websocket.js +++ b/client/javascript/websocket.js @@ -60,7 +60,6 @@ class CurlWebSocket { _free(data_ptr); _free(result_ptr); } - console.log(result_code); if (result_code === 0) { //CURLE_OK - data received if (_get_result_closed(result_ptr)) { @@ -72,8 +71,6 @@ class CurlWebSocket { let data_size = _get_result_size(result_ptr); let data_heap = Module.HEAPU8.subarray(data_ptr, data_ptr + data_size); let data = new Uint8Array(data_heap); - - console.log(data, data_size, buffer_size, _get_result_bytes_left(result_ptr)); this.recv_buffer.push(data); if (data_size !== buffer_size && !_get_result_bytes_left(result_ptr)) { //message finished @@ -88,7 +85,7 @@ class CurlWebSocket { } } } - + //CURLE_GOT_NOTHING, CURLE_RECV_ERROR, CURLE_SEND_ERROR - socket closed else if (result_code === 52 || result_code === 55 || result_code === 56) { this.cleanup(); diff --git a/client/javascript/ws_polyfill.js b/client/javascript/ws_polyfill.js index c3c64b4..54074bb 100644 --- a/client/javascript/ws_polyfill.js +++ b/client/javascript/ws_polyfill.js @@ -44,7 +44,7 @@ class FakeWebSocket extends EventTarget { this.socket.onerror = (error) => { this.status = this.CLOSED; - console.error(`websocket ${this.url} encountered an error (${error})`); + error_msg(`websocket ${this.url} encountered an error (${error})`); let error_event = new Event("error"); this.dispatchEvent(error_event); this.onerror(error_event); @@ -66,7 +66,7 @@ class FakeWebSocket extends EventTarget { throw "invalid binaryType string"; } } - + let msg_event = new MessageEvent("message", {data: converted}); this.onmessage(msg_event); this.dispatchEvent(msg_event); diff --git a/client/libcurl/util.c b/client/libcurl/util.c index 170d3ce..3aadde4 100644 --- a/client/libcurl/util.c +++ b/client/libcurl/util.c @@ -34,4 +34,8 @@ char* get_version() { char* version_json_str = cJSON_Print(version_json); cJSON_Delete(version_json); return version_json_str; +} + +const char* get_error_str(CURLcode error_code) { + return curl_easy_strerror(error_code); } \ No newline at end of file From 53a474430aa0a5fc3499701a70def23f1f633d0e Mon Sep 17 00:00:00 2001 From: ading2210 Date: Thu, 7 Mar 2024 12:16:28 -0500 Subject: [PATCH 5/6] fix tls socket, add documentation --- README.md | 61 +++++++++++++++++++++++++++-- client/javascript/main.js | 7 +--- client/javascript/tls_socket.js | 68 +++++++++++++++++++++++---------- client/javascript/websocket.js | 35 +++++++++-------- 4 files changed, 125 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 04bd825..6c30bf5 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ document.addEventListener("libcurl_load", ()=>{ }); ``` -Alternatively, the `libcurl.onload` callback can be used. +You may also use the, the `libcurl.onload` callback, which can be useful for running libcurl.js inside a web worker. ```js libcurl.onload = () => { console.log("libcurl.js ready!"); @@ -76,7 +76,35 @@ Most of the standard Fetch API's features are supported, with the exception of: Note that there is a hard limit of 50 active TCP connections due to emscripten limitations. ### Creating WebSocket Connections: -To use WebSockets, create a `libcurl.WebSocket` object, which works identically to the regular [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object. +To use WebSockets, create a `libcurl.CurlWebSocket` object, which takes the following arguments: +- `url` - The Websocket URL. +- `protocols` - A optional list of websocket subprotocols, as an array of strings. +- `options` - An optional object with extra settings to pass to curl. + +The valid WebSocket options are: +- `headers` - HTTP request headers for the websocket handshake. +- `verbose` - A boolean flag that toggles the verbose libcurl output. This verbose output will be passed to the function defined in `libcurl.stderr`, which is `console.warn` by default. + +The following callbacks are available: +- `CurlWebSocket.onopen` - Called when the websocket is successfully connected. +- `CurlWebSocket.message` - Called when a websocket message is received from the server. The data is passed to the first argument of the function, and it will be either a `Uint8Array` or a string, depending on the type of message. +- `CurlWebSocket.onclose` - Called when the websocket is cleanly closed with no error. +- `CurlWebSocket.onerror` - Called when the websocket encounters an unexpected error. The [error code](https://curl.se/libcurl/c/libcurl-errors.html) is passed to the first argument of the function. + +The `CurlWebSocket.send` function can be used to send data to the websocket. The only argument is the data that is to be sent, which must be either a string or a `Uint8Array`. + +```js +let ws = new libcurl.CurlWebSocket("wss://echo.websocket.org", [], {verbose: 1}); +ws.onopen = () => { + console.log("ws connected!"); + ws.send("hello".repeat(100)); +}; +ws.onmessage = (data) => { + console.log(data); +}; +``` + +You can also use the `libcurl.WebSocket` object, which works identically to the regular [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object. It uses the same arguments as the simpler `CurlWebSocket` API. ```js let ws = new libcurl.WebSocket("wss://echo.websocket.org"); ws.addEventListener("open", () => { @@ -88,7 +116,32 @@ ws.addEventListener("message", (event) => { }); ``` -### Changing the Websocket URL: +### Using TLS Sockets: +Raw TLS sockets can be created with the `libcurl.TLSSocket` class, which takes the following arguments: +- `host` - The hostname to connect to. +- `port` - The TCP port to connect to. +- `options` - An optional object with extra settings to pass to curl. + +The valid TLS socket options are: +- `verbose` - A boolean flag that toggles the verbose libcurl output. + +The callbacks work similarly to the `libcurl.CurlWebSocket` object, with the main difference being that the `onmessage` callback always returns a `Uint8Array`. + +The `TLSSocket.send` function can be used to send data to the socket. The only argument is the data that is to be sent, which must be a `Uint8Array`. + +```js +let socket = new libcurl.TLSSocket("ading.dev", 443, {verbose: 1}); +socket.onopen = () => { + console.log("socket connected!"); + let str = "GET /all HTTP/1.1\r\nHost: ading.dev\r\nConnection: close\r\n\r\n"; + socket.send(new TextEncoder().encode(str)); +}; +socket.onmessage = (data) => { + console.log(new TextDecoder().decode(data)); +}; +``` + +### Changing the Websocket Proxy URL: You can change the URL of the websocket proxy by using `libcurl.set_websocket`. ```js libcurl.set_websocket("ws://localhost:6001/"); @@ -96,7 +149,7 @@ libcurl.set_websocket("ws://localhost:6001/"); If the websocket proxy URL is not set and one of the other API functions is called, an error will be thrown. Note that this URL must end with a trailing slash. ### Getting Libcurl's Output: -If you want more information about a connection, you can pass the `_libcurl_verbose` argument to the `libcurl.fetch` function. +If you want more information about a connection, you can pass the `_libcurl_verbose` argument to the `libcurl.fetch` function. These are the same messages that you would see if you ran `curl -v` on the command line. ```js await libcurl.fetch("https://example.com", {_libcurl_verbose: 1}); ``` diff --git a/client/javascript/main.js b/client/javascript/main.js index a2da526..1db8f67 100644 --- a/client/javascript/main.js +++ b/client/javascript/main.js @@ -193,12 +193,9 @@ async function libcurl_fetch(url, params={}) { function set_websocket_url(url) { websocket_url = url; - if (!Module.websocket && ENVIRONMENT_IS_WEB) { - document.addEventListener("libcurl_load", () => { - set_websocket_url(url); - }); + if (Module.websocket) { + Module.websocket.url = url; } - else Module.websocket.url = url; } function get_version() { diff --git a/client/javascript/tls_socket.js b/client/javascript/tls_socket.js index 7db9d3f..3f4592d 100644 --- a/client/javascript/tls_socket.js +++ b/client/javascript/tls_socket.js @@ -1,11 +1,21 @@ //currently broken class TLSSocket { - constructor(hostname, port, debug) { + constructor(hostname, port, options={}) { + check_loaded(true); + this.hostname = hostname; this.port = port; this.url = `https://${hostname}:${port}`; - this.debug = debug; + this.options = options; + + this.onopen = () => {}; + this.onerror = () => {}; + this.onmessage = () => {}; + this.onclose = () => {}; + + this.connected = false; + this.event_loop = null; this.connect(); } @@ -13,48 +23,56 @@ class TLSSocket { connect() { let data_callback = () => {}; let finish_callback = (error, response_info) => { - this.status = this.OPEN; if (error === 0) { - this.open_callback(); - this.recv_loop(); + this.connected = true; + this.event_loop = setInterval(() => { + let data = this.recv(); + if (data != null) this.onmessage(data); + }, 0); + this.onopen(); } else { this.cleanup(error); } } - let options = { + let request_options = { _connect_only: 1, } - if (this.debug) options._libcurl_verbose = 1; + if (this.options.verbose) { + request_options._libcurl_verbose = 1; + } - this.http_handle = perform_request(this.url, options, data_callback, finish_callback, null); + this.http_handle = perform_request(this.url, request_options, data_callback, finish_callback, null); } - custom_recv() { + recv() { let buffer_size = 64*1024; let result_ptr = _recv_from_socket(this.http_handle, buffer_size); let data_ptr = _get_result_buffer(result_ptr); let result_code = _get_result_code(result_ptr); + let result_closed = _get_result_closed(result_ptr); - if (result_code == 0) { //CURLE_OK - data received - if (_get_result_closed(result_ptr)) { - this.close_callback(); - return; - } - + if (result_code === 0 && !result_closed) { //CURLE_OK - data received let data_size = _get_result_size(result_ptr); let data_heap = Module.HEAPU8.subarray(data_ptr, data_ptr + data_size); let data = new Uint8Array(data_heap); + this.onmessage(data) + } + + else if (result_code === 0 && result_closed) { + this.cleanup(); + } - let message_event = new MessageEvent("message", {data: data}); - this.dispatchEvent(message_event); + else if (result_code != 81) { + this.cleanup(result_code); } _free(data_ptr); _free(result_ptr); } - custom_send(data_array) { + send(data_array) { + if (!this.connected) return; let data_ptr = allocate_array(data_array); let data_len = data_array.length; _send_to_socket(this.http_handle, data_ptr, data_len); @@ -63,12 +81,20 @@ class TLSSocket { cleanup(error=false) { if (this.http_handle) _cleanup_handle(this.http_handle); + else return; + clearInterval(this.event_loop); - this.close_callback(error); + this.connected = false; + + if (error) { + this.onerror(error); + } + else { + this.onclose(); + } } - custom_close() { + close() { this.cleanup(); - this.status = this.CLOSED; } } \ No newline at end of file diff --git a/client/javascript/websocket.js b/client/javascript/websocket.js index d6791aa..1117cfc 100644 --- a/client/javascript/websocket.js +++ b/client/javascript/websocket.js @@ -33,7 +33,6 @@ class CurlWebSocket { this.onopen(); } else { - this.status = this.CLOSED; this.cleanup(error); } } @@ -54,16 +53,14 @@ class CurlWebSocket { let result_ptr = _recv_from_websocket(this.http_handle, buffer_size); let data_ptr = _get_result_buffer(result_ptr); let result_code = _get_result_code(result_ptr); + let result_closed = _get_result_closed(result_ptr); let returned_data = null; - function free_result() { - _free(data_ptr); - _free(result_ptr); - } - - if (result_code === 0) { //CURLE_OK - data received + //CURLE_OK - data received + if (result_code === 0 && !result_closed) { if (_get_result_closed(result_ptr)) { - free_result(); + _free(data_ptr); + _free(result_ptr); this.cleanup(); return returned_data; } @@ -85,18 +82,26 @@ class CurlWebSocket { } } } - - //CURLE_GOT_NOTHING, CURLE_RECV_ERROR, CURLE_SEND_ERROR - socket closed - else if (result_code === 52 || result_code === 55 || result_code === 56) { + + // websocket was cleanly closed by the server + else if (result_code === 0 && result_closed) { this.cleanup(); } + + //code is not CURLE_AGAIN - an error must have occurred + else if (result_code !== 81) { + this.cleanup(result_code); + } - free_result(); + _free(data_ptr); + _free(result_ptr); return returned_data; } - cleanup(error=false) { + cleanup(error=0) { if (this.http_handle) _cleanup_handle(this.http_handle); + else return; + clearInterval(this.event_loop); this.connected = false; @@ -110,9 +115,7 @@ class CurlWebSocket { send(data) { let is_text = typeof data === "string"; - if (!this.connected) { - throw new DOMException("websocket not connected"); - } + if (!this.connected) return; if (is_text) { data = new TextEncoder().encode(data); From be3fc7286ca47bcfeb5039cfe1d1396ccb77997a Mon Sep 17 00:00:00 2001 From: ading2210 Date: Thu, 7 Mar 2024 12:34:20 -0500 Subject: [PATCH 6/6] add custom network transport support --- README.md | 11 +++++++++++ client/fragments/wisp_support.js | 12 +++++++++++- client/javascript/main.js | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c30bf5..a1130ee 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,12 @@ socket.onmessage = (data) => { }; ``` +### Changing the Network Transport: +You can change the underlying network transport by setting `libcurl.transport`. The following values are accepted: +- `"wisp"` - Use the [Wisp protocol](https://github.com/MercuryWorkshop/wisp-protocol). +- `"wsproxy"` - Use the wsproxy protocol, where a new websocket is created for each TCP connection. +- Any custom class - Use a custom network protocol. If you pass in custom code here, it must be roughly conformant with the standard `WebSocket` API. The URL that is passed into this fake websocket always looks like `"wss://example.com/ws/ading.dev:443"`, where `wss://example.com/ws/` is the proxy server URL, and `ading.dev:443` is the destination server. + ### Changing the Websocket Proxy URL: You can change the URL of the websocket proxy by using `libcurl.set_websocket`. ```js @@ -153,11 +159,16 @@ If you want more information about a connection, you can pass the `_libcurl_verb ```js await libcurl.fetch("https://example.com", {_libcurl_verbose: 1}); ``` + By default this will print the output to the browser console, but you can set `libcurl.stdout` and `libcurl.stderr` to intercept these messages. This callback will be executed on every line of text that libcurl outputs. ```js libcurl.stderr = (text) => {document.body.innerHTML += text}; ``` +Libcurl.js will also output some error messages to the browser console. You can intercept these messages by setting the `libcurl.logger` callback, which takes two arguments: +- `type` - The type of message. This will be one of the following: `"log"`, `"warn"`, `"error"` +- `text` - The text that is to be logged. + ### Getting Version Info: You can get version information from the `libcurl.version` object. This object will also contain the versions of all the C libraries that libcurl.js uses. `libcurl.version.lib` returns the version of libcurl.js itself. diff --git a/client/fragments/wisp_support.js b/client/fragments/wisp_support.js index 8ed84cd..f383585 100644 --- a/client/fragments/wisp_support.js +++ b/client/fragments/wisp_support.js @@ -1,4 +1,14 @@ /* REPLACE new WebSocketConstructor */ -new WispWebSocket \ No newline at end of file +new ((() => { + if (api.transport === "wisp") { + return WispWebSocket; + } + else if (api.transport === "wsproxy") { + return WebSocket; + } + else { //custom transports + return api.transport; + } +})()) diff --git a/client/javascript/main.js b/client/javascript/main.js index 1db8f67..d6b1884 100644 --- a/client/javascript/main.js +++ b/client/javascript/main.js @@ -244,6 +244,7 @@ api = { wisp_connections: _wisp_connections, WispConnection: WispConnection, + transport: "wisp", get copyright() {return copyright_notice}, get version() {return get_version()},