various small improvements

This commit is contained in:
Toshit Chawda 2024-02-28 23:08:56 -08:00
parent 8b2a8a3eb3
commit 5be02151e6
No known key found for this signature in database
GPG key ID: 91480ED99E2B3D9D
11 changed files with 146 additions and 105 deletions

View file

@ -9,8 +9,12 @@ importScripts("epoxy-bundled.js");
const { EpoxyClient } = await epoxy();
let client = await new EpoxyClient("wss://localhost:4000", navigator.userAgent, 10);
// You can view and change the user agent and redirect limit
console.log(client.userAgent);
client.redirect_limit = 5;
let response = await client.fetch("https://httpbin.org/get");
await response.text();
console.log(await response.text());
```
See `client/demo.js` for more examples.

View file

@ -12,7 +12,7 @@ http = "1.0.0"
http-body-util = "0.1.0"
hyper = { version = "1.1.0", features = ["client", "http1", "http2"] }
pin-project-lite = "0.2.13"
wasm-bindgen = "0.2"
wasm-bindgen = { version = "0.2.91", features = ["enable-interning"] }
wasm-bindgen-futures = "0.4.39"
ws_stream_wasm = { version = "0.7.4", features = ["tokio_io"] }
futures-util = "0.3.30"

View file

@ -27,7 +27,7 @@ echo "module.exports = epoxy" >> epoxy-module-bundled.js
AUTOGENERATED_TYPEDEFS=$(<"out/epoxy_client.d.ts")
AUTOGENERATED_TYPEDEFS=${AUTOGENERATED_TYPEDEFS%%export class IntoUnderlyingByteSource*}
echo "$AUTOGENERATED_TYPEDEFS" >"epoxy-module-bundled.d.ts"
echo "} export default function epoxy(): Promise<typeof wasm_bindgen>;" >> "epoxy-module-bundled.d.ts"
echo "} export default function epoxy(maybe_memory?: WebAssembly.Memory): Promise<typeof wasm_bindgen>;" >> "epoxy-module-bundled.d.ts"
cp out/epoxy_client.js epoxy.js
cp out/epoxy_client.d.ts epoxy.d.ts

View file

@ -1,6 +1,6 @@
importScripts("epoxy-bundled.js");
onmessage = async (msg) => {
console.debug("recieved:", msg);
console.debug("recieved demo:", msg);
let [should_feature_test, should_multiparallel_test, should_parallel_test, should_multiperf_test, should_perf_test, should_ws_test, should_tls_test] = msg.data;
console.log(
"%cWASM is significantly slower with DevTools open!",
@ -8,10 +8,15 @@ onmessage = async (msg) => {
);
const log = (str) => {
console.warn(str);
console.log(str);
postMessage(str);
}
const plog = (str) => {
console.log(str);
postMessage(JSON.stringify(str, null, 4));
}
const { EpoxyClient } = await epoxy();
const tconn0 = performance.now();
@ -20,6 +25,11 @@ onmessage = async (msg) => {
const tconn1 = performance.now();
log(`conn establish took ${tconn1 - tconn0} ms or ${(tconn1 - tconn0) / 1000} s`);
// epoxy classes are inspectable
console.log(epoxy_client);
// you can change the user agent and redirect limit in JS
epoxy_client.redirectLimit = 15;
const test_mux = async (url) => {
const t0 = performance.now();
await epoxy_client.fetch(url);
@ -138,23 +148,24 @@ onmessage = async (msg) => {
log(`avg mux - avg native (${num_tests}): ${total_mux - total_native} ms or ${(total_mux - total_native) / 1000} s`);
} else if (should_ws_test) {
let ws = await epoxy_client.connect_ws(
() => console.log("opened"),
() => console.log("closed"),
() => log("opened"),
() => log("closed"),
err => console.error(err),
msg => console.log(msg),
msg => log(msg),
"wss://echo.websocket.events",
[],
"localhost"
);
while (true) {
log("sending `data`");
await ws.send("data");
await (new Promise((res, _) => setTimeout(res, 100)));
await (new Promise((res, _) => setTimeout(res, 50)));
}
} else if (should_tls_test) {
let decoder = new TextDecoder();
let ws = await epoxy_client.connect_tls(
() => console.log("opened"),
() => console.log("closed"),
() => log("opened"),
() => log("closed"),
err => console.error(err),
msg => { console.log(msg); console.log(decoder.decode(msg)) },
"alicesworld.tech:443",
@ -163,8 +174,8 @@ onmessage = async (msg) => {
await ws.close();
} else {
let resp = await epoxy_client.fetch("https://httpbin.org/get");
console.warn(resp, Object.fromEntries(resp.headers));
console.warn(await resp.text());
console.log(resp, Object.fromEntries(resp.headers));
plog(await resp.json());
}
log("done");
};

View file

@ -3,8 +3,9 @@
<head>
<title>epoxy</title>
<style>
body { font-family: sans-serif }
#logs > * { font-family: monospace }
body { font-family: monospace; font-weight: bold; width: 100%; height: 100%; padding: 0; margin: 0 }
body > div { padding: 1em; }
#logs > * { margin: 0; font-weight: normal; }
</style>
<script>
const params = (new URL(window.location.href)).searchParams;
@ -17,8 +18,8 @@
const should_tls_test = params.has("rawtls_test");
const worker = new Worker("demo.js");
worker.onmessage = (msg) => {
let el = document.createElement("div");
el.textContent = msg.data;
let el = document.createElement("pre");
el.innerHTML = msg.data;
document.getElementById("logs").appendChild(el);
window.scrollTo(0, document.body.scrollHeight);
};
@ -28,9 +29,10 @@
<body>
<div>
running... (wait for the browser alert if not running ws test)
<div>running... (note: WASM is significantly slower when DevTools is open)</div>
<div>logs:</div>
<div id="logs"></div>
</div>
<div id="logs"></div>
</body>
</html>

View file

@ -6,7 +6,7 @@ mod websocket;
mod wrappers;
use tls_stream::EpxTlsStream;
use utils::{ReplaceErr, UriExt};
use utils::{Boolinator, ReplaceErr, UriExt};
use websocket::EpxWebSocket;
use wrappers::{IncomingBody, TlsWispService};
@ -25,7 +25,7 @@ use tokio_util::{
either::Either,
io::{ReaderStream, StreamReader},
};
use wasm_bindgen::prelude::*;
use wasm_bindgen::{intern, prelude::*};
use web_sys::TextEncoder;
use wisp_mux::{tokioio::TokioIo, tower::ServiceWrapper, ClientMux, MuxStreamIo, StreamType};
use ws_stream_wasm::{WsMessage, WsMeta, WsStream};
@ -52,13 +52,16 @@ fn init() {
console_error_panic_hook::set_once();
}
#[wasm_bindgen]
#[wasm_bindgen(inspectable)]
pub struct EpoxyClient {
rustls_config: Arc<rustls::ClientConfig>,
mux: Arc<ClientMux<SplitSink<WsStream, WsMessage>>>,
hyper_client: Client<TlsWispService<SplitSink<WsStream, WsMessage>>, HttpBody>,
useragent: String,
redirect_limit: usize,
#[wasm_bindgen(getter_with_clone)]
pub useragent: String,
#[wasm_bindgen(js_name = "redirectLimit")]
pub redirect_limit: usize,
}
#[wasm_bindgen]
@ -81,10 +84,11 @@ impl EpoxyClient {
}
debug!("connecting to ws {:?}", ws_url);
let (_, ws) = WsMeta::connect(ws_url, vec!["wisp-v1"])
let (_, ws) = WsMeta::connect(ws_url, vec![])
.await
.replace_err("Failed to connect to websocket")?;
debug!("connected!");
let (wtx, wrx) = ws.split();
let (mux, fut) = ClientMux::new(wrx, wtx).await?;
let mux = Arc::new(mux);
@ -128,19 +132,17 @@ impl EpoxyClient {
.replace_err("Failed to create multiplexor channel")?
.into_io()
.into_asyncrw();
let cloned_uri = url_host.to_string().clone();
let connector = TlsConnector::from(self.rustls_config.clone());
debug!("connecting channel");
let io = connector
.connect(
cloned_uri
url_host
.to_string()
.try_into()
.replace_err("Failed to parse URL (rustls)")?,
channel,
)
.await
.replace_err("Failed to perform TLS handshake")?;
debug!("connected channel");
Ok(io)
}
@ -155,24 +157,18 @@ impl EpoxyClient {
None
};
debug!("sending req");
let res = self
.hyper_client
.request(req)
.await
.replace_err("Failed to send request");
debug!("recieved res");
match res {
Ok(res) => {
if utils::is_redirect(res.status().as_u16())
&& let Some(mut new_req) = new_req
&& let Some(location) = res.headers().get("Location")
&& let Ok(redirect_url) = new_req.uri().get_redirect(location)
&& let Some(redirect_url_authority) = redirect_url
.clone()
.authority()
.replace_err("Redirect URL must have an authority")
.ok()
&& let Some(redirect_url_authority) = redirect_url.clone().authority()
{
*new_req.uri_mut() = redirect_url;
new_req.headers_mut().insert(
@ -246,19 +242,18 @@ impl EpoxyClient {
let uri = url.parse::<uri::Uri>().replace_err("Failed to parse URL")?;
let uri_scheme = uri.scheme().replace_err("URL must have a scheme")?;
if *uri_scheme != uri::Scheme::HTTP && *uri_scheme != uri::Scheme::HTTPS {
return Err(jerr!("Scheme must be either `http` or `https`"));
return Err(jerri!("Scheme must be either `http` or `https`"));
}
let uri_host = uri.host().replace_err("URL must have a host")?;
let req_method_string: String = match Reflect::get(&options, &jval!("method")) {
let req_method_string: String = match Reflect::get(&options, &jvali!("method")) {
Ok(val) => val.as_string().unwrap_or("GET".to_string()),
Err(_) => "GET".to_string(),
};
let req_method: http::Method =
http::Method::try_from(<String as AsRef<str>>::as_ref(&req_method_string))
.replace_err("Invalid http method")?;
let req_method: http::Method = http::Method::try_from(req_method_string.as_str())
.replace_err("Invalid http method")?;
let req_should_redirect = match Reflect::get(&options, &jval!("redirect")) {
let req_should_redirect = match Reflect::get(&options, &jvali!("redirect")) {
Ok(val) => !matches!(
val.as_string().unwrap_or_default().as_str(),
"error" | "manual"
@ -266,7 +261,7 @@ impl EpoxyClient {
Err(_) => true,
};
let body_jsvalue: Option<JsValue> = Reflect::get(&options, &jval!("body")).ok();
let body_jsvalue: Option<JsValue> = Reflect::get(&options, &jvali!("body")).ok();
let body = if let Some(val) = body_jsvalue {
if val.is_string() {
let str = val
@ -288,7 +283,7 @@ impl EpoxyClient {
None => Bytes::new(),
};
let headers: Option<Vec<Vec<String>>> = Reflect::get(&options, &jval!("headers"))
let headers = Reflect::get(&options, &jvali!("headers"))
.map(|val| {
if val.is_truthy() {
Some(utils::entries_of_object(&Object::from(val)))
@ -301,12 +296,12 @@ impl EpoxyClient {
let mut builder = Request::builder().uri(uri.clone()).method(req_method);
let headers_map = builder.headers_mut().replace_err("Failed to get headers")?;
headers_map.insert("Accept-Encoding", HeaderValue::from_str("gzip, br")?);
headers_map.insert("Connection", HeaderValue::from_str("keep-alive")?);
headers_map.insert("Accept-Encoding", HeaderValue::from_static("gzip, br"));
headers_map.insert("Connection", HeaderValue::from_static("keep-alive"));
headers_map.insert("User-Agent", HeaderValue::from_str(&self.useragent)?);
headers_map.insert("Host", HeaderValue::from_str(uri_host)?);
if body_bytes.is_empty() {
headers_map.insert("Content-Length", HeaderValue::from_str("0")?);
headers_map.insert("Content-Length", HeaderValue::from_static("0"));
}
if let Some(headers) = headers {
@ -314,7 +309,7 @@ impl EpoxyClient {
headers_map.insert(
HeaderName::from_bytes(hdr[0].as_bytes())
.replace_err("Failed to get hdr name")?,
HeaderValue::from_str(hdr[1].clone().as_ref())
HeaderValue::from_bytes(hdr[1].as_bytes())
.replace_err("Failed to get hdr value")?,
);
}
@ -387,45 +382,41 @@ impl EpoxyClient {
Object::define_property(
&resp,
&jval!("url"),
&jvali!("url"),
&utils::define_property_obj(jval!(resp_uri.to_string()), false)
.replace_err("Failed to make define_property object for url")?,
);
Object::define_property(
&resp,
&jval!("redirected"),
&jvali!("redirected"),
&utils::define_property_obj(jval!(req_redirected), false)
.replace_err("Failed to make define_property object for redirected")?,
);
let raw_headers = Object::new();
for (k, v) in resp_headers_raw.iter() {
if let Ok(jv) = Reflect::get(&raw_headers, &jval!(k.to_string())) {
let k = jval!(k.to_string());
let v = jval!(v.to_str()?.to_string());
if let Ok(jv) = Reflect::get(&raw_headers, &k) {
if jv.is_array() {
let arr = Array::from(&jv);
arr.push(&jval!(v.to_str()?.to_string()));
let _ = Reflect::set(&raw_headers, &jval!(k.to_string()), &arr);
arr.push(&v);
Reflect::set(&raw_headers, &k, &arr).flatten("Failed to set rawHeader")?;
} else if jv.is_truthy() {
let _ = Reflect::set(
&raw_headers,
&jval!(k.to_string()),
&Array::of2(&jv, &jval!(v.to_str()?.to_string())),
);
Reflect::set(&raw_headers, &k, &Array::of2(&jv, &v))
.flatten("Failed to set rawHeader")?;
} else {
let _ = Reflect::set(
&raw_headers,
&jval!(k.to_string()),
&jval!(v.to_str()?.to_string()),
);
Reflect::set(&raw_headers, &k, &v).flatten("Failed to set rawHeader")?;
}
}
}
Object::define_property(
&resp,
&jval!("rawHeaders"),
&utils::define_property_obj(jval!(&raw_headers), false).replace_err("wjat!!")?,
&jvali!("rawHeaders"),
&utils::define_property_obj(jval!(&raw_headers), false)
.replace_err("Failed to make define_property object for rawHeaders")?,
);
Ok(resp)

View file

@ -4,10 +4,12 @@ use js_sys::Function;
use tokio::io::{split, AsyncWriteExt, WriteHalf};
use tokio_util::io::ReaderStream;
#[wasm_bindgen]
#[wasm_bindgen(inspectable)]
pub struct EpxTlsStream {
tx: WriteHalf<EpxIoTlsStream>,
onerror: Function,
#[wasm_bindgen(readonly, getter_with_clone)]
pub url: String,
}
#[wasm_bindgen]
@ -51,7 +53,7 @@ impl EpxTlsStream {
.call0(&Object::default())
.replace_err("Failed to call onopen")?;
Ok(Self { tx, onerror })
Ok(Self { tx, onerror, url: url.to_string() })
}
.await;
if let Err(ret) = ret {

View file

@ -1,4 +1,4 @@
use wasm_bindgen::prelude::*;
use wasm_bindgen::{intern, prelude::*};
use hyper::rt::Executor;
use hyper::{header::HeaderValue, Uri};
@ -15,7 +15,6 @@ extern "C" {
pub fn console_error(s: &str);
}
#[allow(unused_macros)]
macro_rules! debug {
($($t:tt)*) => (utils::console_debug(&format_args!($($t)*).to_string()))
}
@ -25,25 +24,34 @@ macro_rules! log {
($($t:tt)*) => (utils::console_log(&format_args!($($t)*).to_string()))
}
#[allow(unused_macros)]
macro_rules! error {
($($t:tt)*) => (utils::console_error(&format_args!($($t)*).to_string()))
}
#[allow(unused_macros)]
macro_rules! jerr {
($expr:expr) => {
JsError::new($expr)
};
}
#[allow(unused_macros)]
macro_rules! jval {
($expr:expr) => {
JsValue::from($expr)
};
}
macro_rules! jerri {
($expr:expr) => {
JsError::new(intern($expr))
};
}
macro_rules! jvali {
($expr:expr) => {
JsValue::from(intern($expr))
};
}
pub trait ReplaceErr {
type Ok;
@ -55,11 +63,11 @@ impl<T, E: std::fmt::Debug> ReplaceErr for Result<T, E> {
type Ok = T;
fn replace_err(self, err: &str) -> Result<<Self as ReplaceErr>::Ok, JsError> {
self.map_err(|oe| jerr!(&format!("{}, original error: {:?}", err, oe)))
self.map_err(|_| jerri!(err))
}
fn replace_err_jv(self, err: &str) -> Result<<Self as ReplaceErr>::Ok, JsValue> {
self.map_err(|oe| jval!(&format!("{}, original error: {:?}", err, oe)))
self.map_err(|_| jvali!(err))
}
}
@ -67,17 +75,52 @@ impl<T> ReplaceErr for Option<T> {
type Ok = T;
fn replace_err(self, err: &str) -> Result<<Self as ReplaceErr>::Ok, JsError> {
self.ok_or_else(|| jerr!(err))
self.ok_or_else(|| jerri!(err))
}
fn replace_err_jv(self, err: &str) -> Result<<Self as ReplaceErr>::Ok, JsValue> {
self.ok_or_else(|| jval!(err))
self.ok_or_else(|| jvali!(err))
}
}
// the... BOOLINATOR!
impl ReplaceErr for bool {
type Ok = ();
fn replace_err(self, err: &str) -> Result<(), JsError> {
if !self {
Err(jerri!(err))
} else {
Ok(())
}
}
fn replace_err_jv(self, err: &str) -> Result<(), JsValue> {
if !self {
Err(jvali!(err))
} else {
Ok(())
}
}
}
// the... BOOLINATOR!
pub trait Boolinator {
fn flatten(self, err: &str) -> Result<(), JsError>;
}
impl Boolinator for Result<bool, JsValue> {
fn flatten(self, err: &str) -> Result<(), JsError> {
if !self.replace_err(err)? {
Err(jerri!(err))
} else {
Ok(())
}
}
}
pub trait UriExt {
fn get_redirect(&self, location: &HeaderValue) -> Result<Uri, JsError>;
fn is_same_host(&self, other: &Uri) -> bool;
}
impl UriExt for Uri {
@ -93,9 +136,6 @@ impl UriExt for Uri {
Ok(Uri::from_parts(new_parts)?)
}
fn is_same_host(&self, other: &Uri) -> bool {
self.host() == other.host() && self.port() == other.port()
}
}
#[derive(Clone)]
@ -129,8 +169,8 @@ pub fn entries_of_object(obj: &Object) -> Vec<Vec<String>> {
pub fn define_property_obj(value: JsValue, writable: bool) -> Result<Object, JsValue> {
let entries: Array = [
Array::of2(&jval!("value"), &jval!(value)),
Array::of2(&jval!("writable"), &jval!(writable)),
Array::of2(&jval!(intern("value")), &value),
Array::of2(&jval!(intern("writable")), &jval!(writable)),
]
.iter()
.collect::<Array>();

View file

@ -15,10 +15,16 @@ use js_sys::Function;
use std::str::from_utf8;
use tokio::io::WriteHalf;
#[wasm_bindgen]
#[wasm_bindgen(inspectable)]
pub struct EpxWebSocket {
tx: Arc<Mutex<WebSocketWrite<WriteHalf<TokioIo<Upgraded>>>>>,
onerror: Function,
#[wasm_bindgen(readonly, getter_with_clone)]
pub url: String,
#[wasm_bindgen(readonly, getter_with_clone)]
pub protocols: Vec<String>,
#[wasm_bindgen(readonly, getter_with_clone)]
pub origin: String,
}
#[wasm_bindgen]
@ -53,7 +59,7 @@ impl EpxWebSocket {
.method("GET")
.uri(url.clone())
.header("Host", host)
.header("Origin", origin)
.header("Origin", origin.clone())
.header(UPGRADE, "websocket")
.header(CONNECTION, "upgrade")
.header("Sec-WebSocket-Key", key)
@ -109,7 +115,7 @@ impl EpxWebSocket {
.call0(&Object::default())
.replace_err("Failed to call onopen")?;
Ok(Self { tx, onerror })
Ok(Self { tx, onerror, origin, protocols, url: url.to_string() })
}
.await;
if let Err(ret) = ret {

View file

@ -9,8 +9,7 @@ use fastwebsockets::{
};
use futures_util::{SinkExt, StreamExt, TryFutureExt};
use hyper::{
body::Incoming, header::HeaderValue, server::conn::http1, service::service_fn, Request,
Response, StatusCode,
body::Incoming, server::conn::http1, service::service_fn, Request, Response, StatusCode,
};
use hyper_util::rt::TokioIo;
use tokio::net::{TcpListener, TcpStream, UdpSocket};
@ -88,24 +87,10 @@ async fn accept_http(
if upgrade::is_upgrade_request(&req)
&& let Some(uri) = uri.strip_prefix(&prefix)
{
let (mut res, fut) = upgrade::upgrade(&mut req)?;
let (res, fut) = upgrade::upgrade(&mut req)?;
if let Some(protocols) = req.headers().get("Sec-Websocket-Protocol").and_then(|x| {
Some(
x.to_str()
.ok()?
.split(',')
.map(|x| x.trim())
.collect::<Vec<&str>>(),
)
}) && protocols.contains(&"wisp-v1")
&& (uri.is_empty() || uri == "/")
{
if uri.is_empty() || uri == "/" {
tokio::spawn(async move { accept_ws(fut, addr.clone()).await });
res.headers_mut().insert(
"Sec-Websocket-Protocol",
HeaderValue::from_str("wisp-v1").unwrap(),
);
} else {
let uri = uri.strip_prefix('/').unwrap_or(uri).to_string();
tokio::spawn(async move { accept_wsproxy(fut, uri, addr.clone()).await });
@ -119,7 +104,7 @@ async fn accept_http(
println!("random request to path {:?}", uri);
Ok(Response::builder()
.status(StatusCode::OK)
.body(HttpBody::new(":3".to_string().into()))
.body(HttpBody::new(":3".into()))
.unwrap())
}
}

View file

@ -74,7 +74,7 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
};
let req = Request::builder()
.method("GET")
.uri(format!("wss://{}:{}/", &addr, addr_port))
.uri("/")
.header("Host", &addr)
.header(UPGRADE, "websocket")
.header(CONNECTION, "upgrade")