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);