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