diff --git a/Cargo.lock b/Cargo.lock index a0df337..45851ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1849,7 +1849,7 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "wisp-mux" -version = "1.2.1" +version = "1.2.2" dependencies = [ "async_io_stream", "bytes", diff --git a/client/Cargo.toml b/client/Cargo.toml index b0da251..ff380cd 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "epoxy-client" -version = "1.4.0" +version = "1.4.1" edition = "2021" license = "LGPL-3.0-only" @@ -19,7 +19,7 @@ futures-util = "0.3.30" js-sys = "0.3.66" webpki-roots = "0.26.0" tokio-rustls = "0.25.0" -web-sys = { version = "0.3.66", features = ["TextEncoder", "Response", "ResponseInit", "WebSocket", "BinaryType", "MessageEvent"] } +web-sys = { version = "0.3.66", features = ["Request", "RequestInit", "Headers", "Response", "ResponseInit", "WebSocket", "BinaryType", "MessageEvent"] } wasm-streams = "0.4.0" tokio-util = { version = "0.7.10", features = ["io"] } async-compression = { version = "0.4.5", features = ["tokio", "gzip", "brotli"] } diff --git a/client/demo.js b/client/demo.js index 950305c..8a7e54f 100644 --- a/client/demo.js +++ b/client/demo.js @@ -58,16 +58,24 @@ onmessage = async (msg) => { }; if (should_feature_test) { + let formdata = new FormData(); + formdata.append("a", "b"); for (const url of [ ["https://httpbin.org/get", {}], ["https://httpbin.org/gzip", {}], ["https://httpbin.org/brotli", {}], ["https://httpbin.org/redirect/11", {}], ["https://httpbin.org/redirect/1", { redirect: "manual" }], + ["https://httpbin.org/post", { method: "POST", body: new URLSearchParams("a=b") }], + ["https://httpbin.org/post", { method: "POST", body: formdata }], + ["https://httpbin.org/post", { method: "POST", body: "a" }], + ["https://httpbin.org/post", { method: "POST", body: (new TextEncoder()).encode("abc") }], + ["https://httpbin.org/get", { headers: {"a": "b", "b": "c"} }], + ["https://httpbin.org/get", { headers: new Headers({"a": "b", "b": "c"}) }] ]) { let resp = await epoxy_client.fetch(url[0], url[1]); console.warn(url, resp, Object.fromEntries(resp.headers)); - console.warn(await resp.text()); + log(await resp.text()); } } else if (should_multiparallel_test) { const num_tests = 10; @@ -183,7 +191,7 @@ onmessage = async (msg) => { msg => { console.log(msg); log(decoder.decode(msg)) }, "google.com:443", ); - await ws.send((new TextEncoder()).encode("GET / HTTP 1.1\r\nHost: google.com\r\nConnection: close\r\n\r\n")); + await ws.send("GET / HTTP 1.1\r\nHost: google.com\r\nConnection: close\r\n\r\n"); await (new Promise((res, _) => setTimeout(res, 500))); await ws.close(); } else if (should_udp_test) { @@ -198,7 +206,7 @@ onmessage = async (msg) => { ); while (true) { log("sending `data`"); - await ws.send((new TextEncoder()).encode("data")); + await ws.send("data"); await (new Promise((res, _) => setTimeout(res, 50))); } } else if (should_reconnect_test) { diff --git a/client/package.json b/client/package.json index f734c23..703d366 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@mercuryworkshop/epoxy-tls", - "version": "1.4.0", + "version": "1.4.1", "description": "A wasm library for using raw encrypted tls/ssl/https/websocket streams on the browser", "scripts": { "build": "./build.sh" diff --git a/client/src/lib.rs b/client/src/lib.rs index 011dd7b..773578d 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -32,7 +32,6 @@ use tokio_util::{ io::{ReaderStream, StreamReader}, }; use wasm_bindgen::{intern, prelude::*}; -use web_sys::TextEncoder; use wisp_mux::{ClientMux, MuxStreamIo, StreamType}; type HttpBody = http_body_util::Full; @@ -58,6 +57,7 @@ fn init() { // utils.rs intern("value"); intern("writable"); + intern("POST"); // main.rs intern("method"); @@ -67,6 +67,7 @@ fn init() { intern("url"); intern("redirected"); intern("rawHeaders"); + intern("Content-Type"); } fn cert_to_jval(cert: &TrustAnchor) -> Result { @@ -310,31 +311,24 @@ impl EpoxyClient { Err(_) => true, }; + let mut body_content_type: Option = None; let body_jsvalue: Option = Reflect::get(&options, &jval!("body")).ok(); - let body = if let Some(val) = body_jsvalue { - if val.is_string() { - let str = val - .as_string() - .replace_err("Failed to get string from body")?; - let encoder = - TextEncoder::new().replace_err("Failed to create TextEncoder for body")?; - let encoded = encoder.encode_with_input(str.as_ref()); - Some(encoded) - } else { - Some(Uint8Array::new(&val).to_vec()) + let body_bytes: Bytes = match body_jsvalue { + Some(buf) => { + let (body, req) = utils::jval_to_u8_array_req(buf) + .await + .replace_err("Invalid body")?; + body_content_type = req.headers().get("Content-Type").ok().flatten(); + Bytes::from(body.to_vec()) } - } else { - None - }; - - let body_bytes: Bytes = match body { - Some(vec) => Bytes::from(vec), None => Bytes::new(), }; let headers = Reflect::get(&options, &jval!("headers")) .map(|val| { - if val.is_truthy() { + if web_sys::Headers::instanceof(&val) { + Some(utils::entries_of_object(&Object::from_entries(&val).ok()?)) + } else if val.is_truthy() { Some(utils::entries_of_object(&Object::from(val))) } else { None @@ -352,6 +346,9 @@ impl EpoxyClient { if body_bytes.is_empty() { headers_map.insert("Content-Length", HeaderValue::from_static("0")); } + if let Some(content_type) = body_content_type { + headers_map.insert("Content-Type", HeaderValue::from_str(&content_type)?); + } if let Some(headers) = headers { for hdr in headers { diff --git a/client/src/tls_stream.rs b/client/src/tls_stream.rs index b624753..2071b1c 100644 --- a/client/src/tls_stream.rs +++ b/client/src/tls_stream.rs @@ -49,7 +49,11 @@ impl EpxTlsStream { .call0(&Object::default()) .replace_err("Failed to call onopen")?; - Ok(Self { tx, onerror, url: url.to_string() }) + Ok(Self { + tx, + onerror, + url: url.to_string(), + }) } .await; if let Err(ret) = ret { @@ -61,9 +65,17 @@ impl EpxTlsStream { } #[wasm_bindgen] - pub async fn send(&mut self, payload: Uint8Array) -> Result<(), JsError> { + pub async fn send(&mut self, payload: JsValue) -> Result<(), JsError> { let onerr = self.onerror.clone(); - let ret = self.tx.write_all(&payload.to_vec()).await; + let ret = self + .tx + .write_all( + &utils::jval_to_u8_array(payload) + .await + .replace_err("Invalid payload")? + .to_vec(), + ) + .await; if let Err(ret) = ret { let _ = onerr.call1(&JsValue::null(), &jval!(format!("{}", ret))); Err(ret.into()) diff --git a/client/src/udp_stream.rs b/client/src/udp_stream.rs index 4fad806..c026ca7 100644 --- a/client/src/udp_stream.rs +++ b/client/src/udp_stream.rs @@ -71,9 +71,17 @@ impl EpxUdpStream { } #[wasm_bindgen] - pub async fn send(&mut self, payload: Uint8Array) -> Result<(), JsError> { + pub async fn send(&mut self, payload: JsValue) -> Result<(), JsError> { let onerr = self.onerror.clone(); - let ret = self.tx.send(payload.to_vec()).await; + let ret = self + .tx + .send( + utils::jval_to_u8_array(payload) + .await + .replace_err("Invalid payload")? + .to_vec(), + ) + .await; if let Err(ret) = ret { let _ = onerr.call1(&JsValue::null(), &jval!(format!("{}", ret))); Err(ret.into()) diff --git a/client/src/utils.rs b/client/src/utils.rs index adae84c..bd3245c 100644 --- a/client/src/utils.rs +++ b/client/src/utils.rs @@ -1,8 +1,10 @@ use crate::*; use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; use hyper::rt::Executor; +use js_sys::ArrayBuffer; use std::future::Future; use wisp_mux::{CloseReason, WispError}; @@ -188,7 +190,15 @@ pub fn get_url_port(url: &Uri) -> Result { } } -pub async fn make_mux(url: &str) -> Result<(ClientMux, impl Future>), WispError> { +pub async fn make_mux( + url: &str, +) -> Result< + ( + ClientMux, + impl Future>, + ), + WispError, +> { let (wtx, wrx) = WebSocketWrapper::connect(url, vec![]) .await .map_err(|_| WispError::WsImplSocketClosed)?; @@ -198,7 +208,11 @@ pub async fn make_mux(url: &str) -> Result<(ClientMux, impl Fu Ok(mux) } -pub fn spawn_mux_fut(mux: Arc>>, fut: impl Future> + 'static, url: String) { +pub fn spawn_mux_fut( + mux: Arc>>, + fut: impl Future> + 'static, + url: String, +) { wasm_bindgen_futures::spawn_local(async move { if let Err(e) = fut.await { error!("epoxy: error in mux future, restarting: {:?}", e); @@ -223,3 +237,30 @@ pub async fn replace_mux( spawn_mux_fut(mux, fut, url.into()); Ok(()) } + +pub async fn jval_to_u8_array(val: JsValue) -> Result { + JsFuture::from( + web_sys::Request::new_with_str_and_init( + "/", + web_sys::RequestInit::new().method("POST").body(Some(&val)), + )? + .array_buffer()?, + ) + .await? + .dyn_into::() + .map(|x| Uint8Array::new(&x)) +} + +pub async fn jval_to_u8_array_req(val: JsValue) -> Result<(Uint8Array, web_sys::Request), JsValue> { + let req = web_sys::Request::new_with_str_and_init( + "/", + web_sys::RequestInit::new().method("POST").body(Some(&val)), + )?; + Ok(( + JsFuture::from(req.array_buffer()?) + .await? + .dyn_into::() + .map(|x| Uint8Array::new(&x))?, + req, + )) +}