Merge pull request #3 from ading2210/v0.4

merge v0.4
This commit is contained in:
ading2210 2024-03-07 09:34:52 -08:00 committed by GitHub
commit 327fd60161
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 528 additions and 224 deletions

View file

@ -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 ```js
libcurl.onload = () => { libcurl.onload = () => {
console.log("libcurl.js ready!"); console.log("libcurl.js ready!");
@ -73,8 +73,38 @@ Most of the standard Fetch API's features are supported, with the exception of:
- Sending credentials/cookies automatically - Sending credentials/cookies automatically
- Caching - Caching
Note that there is a hard limit of 50 active TCP connections due to emscripten limitations.
### Creating WebSocket Connections: ### 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 ```js
let ws = new libcurl.WebSocket("wss://echo.websocket.org"); let ws = new libcurl.WebSocket("wss://echo.websocket.org");
ws.addEventListener("open", () => { ws.addEventListener("open", () => {
@ -86,7 +116,38 @@ 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 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`. You can change the URL of the websocket proxy by using `libcurl.set_websocket`.
```js ```js
libcurl.set_websocket("ws://localhost:6001/"); libcurl.set_websocket("ws://localhost:6001/");
@ -94,18 +155,26 @@ 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. 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: ### 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 ```js
await libcurl.fetch("https://example.com", {_libcurl_verbose: 1}); 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. 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 ```js
libcurl.stderr = (text) => {document.body.innerHTML += text}; 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: ### 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. 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: ## 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. The proxy server consists of a standard [Wisp](https://github.com/MercuryWorkshop/wisp-protocol) server, allowing multiple TCP connections to share the same websocket.

View file

@ -80,11 +80,15 @@ VERSION=$(cat package.json | jq -r '.version')
sed -i "s/__library_version__/$VERSION/" $OUT_FILE sed -i "s/__library_version__/$VERSION/" $OUT_FILE
#add extra libraries #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/polyfill.js" $OUT_FILE
sed -i "/__extra_libraries__/r $WISP_CLIENT/wisp.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/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/websocket.js" $OUT_FILE
sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/copyright.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 #apply patches
python3 tools/patch_js.py $FRAGMENTS_DIR $OUT_FILE python3 tools/patch_js.py $FRAGMENTS_DIR $OUT_FILE

View file

@ -4,11 +4,13 @@ tick_request
active_requests active_requests
get_version get_version
get_cacert
get_error_str
recv_from_websocket recv_from_websocket
send_to_websocket send_to_websocket
close_websocket close_websocket
cleanup_websocket cleanup_handle
get_result_size get_result_size
get_result_buffer get_result_buffer
get_result_code get_result_code
@ -16,4 +18,7 @@ get_result_closed
get_result_bytes_left get_result_bytes_left
get_result_is_text get_result_is_text
recv_from_socket
send_to_socket
free free

View file

@ -1,4 +1,14 @@
/* REPLACE /* REPLACE
new WebSocketConstructor new WebSocketConstructor
*/ */
new WispWebSocket new ((() => {
if (api.transport === "wisp") {
return WispWebSocket;
}
else if (api.transport === "wsproxy") {
return WebSocket;
}
else { //custom transports
return api.transport;
}
})())

View file

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

View file

@ -42,42 +42,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 //low level interface with c code
function perform_request(url, params, js_data_callback, js_end_callback, body=null) { function perform_request(url, params, js_data_callback, js_end_callback, body=null) {
let params_str = JSON.stringify(params); let params_str = JSON.stringify(params);
@ -103,7 +67,6 @@ function perform_request(url, params, js_data_callback, js_end_callback, body=nu
_free(url_ptr); _free(url_ptr);
_free(response_json_ptr); _free(response_json_ptr);
if (error != 0) console.error("request failed with error code " + error);
active_requests --; active_requests --;
js_end_callback(error, response_info); js_end_callback(error, response_info);
} }
@ -177,51 +140,11 @@ function create_response(response_data, response_info) {
return response_obj; 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) { async function create_options(params) {
let body = null; let body = null;
if (params.body) { if (params.body) {
body = await parse_body(params.body); body = await data_to_array(params.body);
params.body = true; params.body = true;
} }
@ -248,7 +171,9 @@ function perform_request_async(url, params, body) {
let finish_callback = (error, response_info) => { let finish_callback = (error, response_info) => {
if (error != 0) { 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; return;
} }
let response_data = merge_arrays(chunks); let response_data = merge_arrays(chunks);
@ -268,12 +193,9 @@ async function libcurl_fetch(url, params={}) {
function set_websocket_url(url) { function set_websocket_url(url) {
websocket_url = url; websocket_url = url;
if (!Module.websocket && ENVIRONMENT_IS_WEB) { if (Module.websocket) {
document.addEventListener("libcurl_load", () => { Module.websocket.url = url;
set_websocket_url(url);
});
} }
else Module.websocket.url = url;
} }
function get_version() { function get_version() {
@ -288,6 +210,10 @@ function get_version() {
return version_dict; return version_dict;
} }
function get_cacert() {
return UTF8ToString(_get_cacert());
}
function main() { function main() {
wasm_ready = true; wasm_ready = true;
_init_curl(); _init_curl();
@ -311,19 +237,26 @@ api = {
fetch: libcurl_fetch, fetch: libcurl_fetch,
set_websocket: set_websocket_url, set_websocket: set_websocket_url,
load_wasm: load_wasm, load_wasm: load_wasm,
WebSocket: CurlWebSocket, WebSocket: FakeWebSocket,
CurlWebSocket: CurlWebSocket,
TLSSocket: TLSSocket,
get_cacert: get_cacert,
wisp_connections: _wisp_connections, wisp_connections: _wisp_connections,
WispConnection: WispConnection, WispConnection: WispConnection,
transport: "wisp",
get copyright() {return copyright_notice}, get copyright() {return copyright_notice},
get version() {return get_version()}, get version() {return get_version()},
get ready() {return wasm_ready}, get ready() {return wasm_ready},
get websocket_url() {return websocket_url},
get stdout() {return out}, get stdout() {return out},
set stdout(callback) {out = callback}, set stdout(callback) {out = callback},
get stderr() {return err}, get stderr() {return err},
set stderr(callback) {err = callback}, set stderr(callback) {err = callback},
get logger() {return logger},
set logger(func) {logger = func},
onload() {} onload() {}
}; };

View file

@ -0,0 +1,100 @@
//currently broken
class TLSSocket {
constructor(hostname, port, options={}) {
check_loaded(true);
this.hostname = hostname;
this.port = port;
this.url = `https://${hostname}:${port}`;
this.options = options;
this.onopen = () => {};
this.onerror = () => {};
this.onmessage = () => {};
this.onclose = () => {};
this.connected = false;
this.event_loop = null;
this.connect();
}
connect() {
let data_callback = () => {};
let finish_callback = (error, response_info) => {
if (error === 0) {
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 request_options = {
_connect_only: 1,
}
if (this.options.verbose) {
request_options._libcurl_verbose = 1;
}
this.http_handle = perform_request(this.url, request_options, data_callback, finish_callback, null);
}
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 && !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();
}
else if (result_code != 81) {
this.cleanup(result_code);
}
_free(data_ptr);
_free(result_ptr);
}
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);
_free(data_ptr);
}
cleanup(error=false) {
if (this.http_handle) _cleanup_handle(this.http_handle);
else return;
clearInterval(this.event_loop);
this.connected = false;
if (error) {
this.onerror(error);
}
else {
this.onclose();
}
}
close() {
this.cleanup();
}
}

82
client/javascript/util.js Normal file
View file

@ -0,0 +1,82 @@
//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);
}
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;
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;
}

View file

@ -1,8 +1,5 @@
//class for custom websocket class CurlWebSocket {
constructor(url, protocols=[], options={}) {
class CurlWebSocket extends EventTarget {
constructor(url, protocols=[], websocket_debug=false) {
super();
check_loaded(true); check_loaded(true);
if (!url.startsWith("wss://") && !url.startsWith("ws://")) { if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
throw new SyntaxError("invalid url"); throw new SyntaxError("invalid url");
@ -10,41 +7,45 @@ class CurlWebSocket extends EventTarget {
this.url = url; this.url = url;
this.protocols = protocols; this.protocols = protocols;
this.binaryType = "blob"; this.options = options;
this.recv_buffer = [];
this.websocket_debug = websocket_debug;
//legacy event handlers
this.onopen = () => {}; this.onopen = () => {};
this.onerror = () => {}; this.onerror = () => {};
this.onmessage = () => {}; this.onmessage = () => {};
this.onclose = () => {}; this.onclose = () => {};
this.CONNECTING = 0; this.connected = false;
this.OPEN = 1; this.event_loop = null;
this.CLOSING = 2; this.recv_buffer = [];
this.CLOSED = 3;
this.connect(); this.connect();
} }
connect() { connect() {
this.status = this.CONNECTING;
let data_callback = () => {}; let data_callback = () => {};
let finish_callback = (error, response_info) => { let finish_callback = (error, response_info) => {
this.finish_callback(error, response_info); if (error === 0) {
this.connected = true;
this.event_loop = setInterval(() => {
let data = this.recv();
if (data !== null) this.onmessage(data);
}, 0);
this.onopen();
} }
let options = {}; else {
if (this.protocols) { this.cleanup(error);
options.headers = { }
"Sec-Websocket-Protocol": this.protocols.join(", "), }
let request_options = {
headers: this.options.headers || {}
}; };
if (this.protocols) {
request_options.headers["Sec-Websocket-Protocol"] = this.protocols.join(", ");
} }
if (this.websocket_debug) { if (this.options.verbose) {
options._libcurl_verbose = 1; 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);
this.recv_loop();
} }
recv() { recv() {
@ -52,12 +53,16 @@ class CurlWebSocket extends EventTarget {
let result_ptr = _recv_from_websocket(this.http_handle, buffer_size); let result_ptr = _recv_from_websocket(this.http_handle, buffer_size);
let data_ptr = _get_result_buffer(result_ptr); let data_ptr = _get_result_buffer(result_ptr);
let result_code = _get_result_code(result_ptr); let result_code = _get_result_code(result_ptr);
let result_closed = _get_result_closed(result_ptr);
let returned_data = null;
if (result_code == 0) { //CURLE_OK - data recieved //CURLE_OK - data received
if (result_code === 0 && !result_closed) {
if (_get_result_closed(result_ptr)) { if (_get_result_closed(result_ptr)) {
//this.pass_buffer(); _free(data_ptr);
this.close_callback(); _free(result_ptr);
return; this.cleanup();
return returned_data;
} }
let data_size = _get_result_size(result_ptr); let data_size = _get_result_size(result_ptr);
@ -69,133 +74,59 @@ class CurlWebSocket extends EventTarget {
let full_data = merge_arrays(this.recv_buffer); let full_data = merge_arrays(this.recv_buffer);
let is_text = _get_result_is_text(result_ptr) let is_text = _get_result_is_text(result_ptr)
this.recv_buffer = []; this.recv_buffer = [];
this.recv_callback(full_data, 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 // websocket was cleanly closed by the server
this.close_callback(); 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(data_ptr); _free(data_ptr);
_free(result_ptr); _free(result_ptr);
return returned_data;
} }
recv_loop() { cleanup(error=0) {
this.event_loop = setInterval(() => { if (this.http_handle) _cleanup_handle(this.http_handle);
this.recv(); else return;
}, 1);
}
recv_callback(data, is_text=false) {
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);
}
close_callback(error=false) {
if (this.status == this.CLOSED) return;
this.status = this.CLOSED;
clearInterval(this.event_loop); clearInterval(this.event_loop);
_cleanup_websocket(); this.connected = false;
if (error) { if (error) {
let error_event = new Event("error"); this.onerror(error);
this.dispatchEvent(error_event);
this.onerror(error_event);
} }
else { else {
let close_event = new CloseEvent("close"); this.onclose();
this.dispatchEvent(close_event);
this.onclose(close_event);
} }
} }
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) { send(data) {
let is_text = false; let is_text = typeof data === "string";
if (this.status === this.CONNECTING) { if (!this.connected) return;
throw new DOMException("ws not ready yet");
}
if (this.status === this.CLOSED) {
return;
}
let data_array; if (is_text) {
if (typeof data === "string") { data = new TextEncoder().encode(data);
data_array = new TextEncoder().encode(data);
is_text = true;
} }
else if (data instanceof Blob) { let data_ptr = allocate_array(data);
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";
}
let data_ptr = allocate_array(data_array);
let data_len = data.length; let data_len = data.length;
_send_to_websocket(this.http_handle, data_ptr, data_len, is_text); _send_to_websocket(this.http_handle, data_ptr, data_len, is_text);
_free(data_ptr); _free(data_ptr);
} }
close() { close() {
_close_websocket(this.http_handle); this.cleanup();
}
get readyState() {
return this.status;
}
get bufferedAmount() {
return 0;
}
get protocol() {
return "";
}
get extensions() {
return "";
} }
} }

View file

@ -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;
error_msg(`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 "";
}
}

View file

@ -7,9 +7,9 @@
#include "curl/easy.h" #include "curl/easy.h"
#include "curl/header.h" #include "curl/header.h"
#include "cjson/cJSON.h" #include "cjson/cJSON.h"
#include "cacert.h"
#include "curl/multi.h" #include "curl/multi.h"
#include "cacert.h"
#include "util.h" #include "util.h"
#include "types.h" #include "types.h"
@ -83,6 +83,13 @@ CURL* start_request(const char* url, const char* json_params, DataCallback data_
curl_easy_setopt(http_handle, CURLOPT_VERBOSE, 1L); 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)) { if (strcmp(key, "method") == 0 && cJSON_IsString(item)) {
curl_easy_setopt(http_handle, CURLOPT_CUSTOMREQUEST, item->valuestring); curl_easy_setopt(http_handle, CURLOPT_CUSTOMREQUEST, item->valuestring);
} }
@ -189,6 +196,10 @@ void finish_request(CURLMsg *curl_msg) {
free(request_info); free(request_info);
} }
unsigned char* get_cacert() {
return _cacert_pem;
}
void init_curl() { void init_curl() {
curl_global_init(CURL_GLOBAL_DEFAULT); curl_global_init(CURL_GLOBAL_DEFAULT);
multi_handle = curl_multi_init(); multi_handle = curl_multi_init();

View file

@ -0,0 +1,24 @@
#include <stdlib.h>
#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;
}

View file

@ -35,3 +35,7 @@ char* get_version() {
cJSON_Delete(version_json); cJSON_Delete(version_json);
return version_json_str; return version_json_str;
} }
const char* get_error_str(CURLcode error_code) {
return curl_easy_strerror(error_code);
}

View file

@ -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 //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; struct RequestInfo *request_info;
curl_easy_getinfo(http_handle, CURLINFO_PRIVATE, &request_info); curl_easy_getinfo(http_handle, CURLINFO_PRIVATE, &request_info);

View file

@ -1,6 +1,6 @@
{ {
"name": "libcurl.js", "name": "libcurl.js",
"version": "0.3.9", "version": "0.4.0",
"description": "An experimental port of libcurl to WebAssembly for use in the browser.", "description": "An experimental port of libcurl to WebAssembly for use in the browser.",
"main": "libcurl.mjs", "main": "libcurl.mjs",
"scripts": { "scripts": {