General Upgrade, NEW: WebServers, Better Docs
Some checks failed
CI / checks (push) Failing after 5m18s
Some checks failed
CI / checks (push) Failing after 5m18s
This commit is contained in:
parent
5f11d476b6
commit
6e5c985719
122 changed files with 10309 additions and 5206 deletions
|
|
@ -1,10 +1,10 @@
|
|||
[package]
|
||||
name = "mtp-wasm"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["getrandom-v02"]
|
||||
ignored = ["getrandom"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
|
@ -13,22 +13,26 @@ crate-type = ["cdylib"]
|
|||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
js-sys = "0.3"
|
||||
web-sys = { version = "0.3", features = ["console"] }
|
||||
futures-channel = "0.3"
|
||||
futures-util = "0.3"
|
||||
console_error_panic_hook = "0.1"
|
||||
tracing = "0.1"
|
||||
wasm-tracing = "2.1"
|
||||
|
||||
hex = "0.4"
|
||||
|
||||
getrandom = { version = "0.4", features = ["wasm_js"] }
|
||||
getrandom-v02 = { package = "getrandom", version = "0.2", features = ["js"] }
|
||||
getrandom = { version = "0.2.17", features = ["js"] }
|
||||
getrandom-v04 = { package = "getrandom", version = "0.4.3", features = ["wasm_js"] }
|
||||
|
||||
mtp-common = { version = "0.2.0", path = "../common" }
|
||||
mtp-type-map = { version = "0.2.0", path = "../type-map" }
|
||||
mtp-codec = { version = "0.2.0", path = "../codec", features = ["crypto", "pipes"] }
|
||||
mtp-crypto = { version = "0.2.0", path = "../crypto", features = ["wasm"] }
|
||||
zeroize = "1.9"
|
||||
wasm-bindgen-test = "0.3.76"
|
||||
|
||||
mtp-common = { version = "0.1.0", path = "../common" }
|
||||
mtp-type-map = { version = "0.1.0", path = "../type-map" }
|
||||
mtp-codec = { version = "0.1.0", path = "../codec", features = ["crypto", "pipes"] }
|
||||
mtp-crypto = { version = "0.1.0", path = "../crypto", features = ["wasm"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
hex = "0.4"
|
||||
|
||||
[features]
|
||||
|
|
|
|||
146
wasm/src/auth.rs
Normal file
146
wasm/src/auth.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||
use mtp_type_map::CommunicationTypeId;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::error::js_error;
|
||||
|
||||
pub(crate) fn raw_frame_preview(bytes: &[u8]) -> String {
|
||||
let shown = bytes.len().min(256);
|
||||
let mut preview = hex::encode(&bytes[..shown]);
|
||||
if bytes.len() > shown {
|
||||
preview.push_str("...");
|
||||
}
|
||||
format!("{} bytes, hex={preview}", bytes.len())
|
||||
}
|
||||
|
||||
pub(crate) fn unexpected_response_type_error(
|
||||
context: &str,
|
||||
expected_type: CommunicationTypeId,
|
||||
response_type: CommunicationTypeId,
|
||||
response: &[u8],
|
||||
parsed: &CommunicationValue,
|
||||
) -> JsValue {
|
||||
js_error(format!(
|
||||
"unexpected response type during {context}: expected {:?}, got {:?}; raw {}; parsed {}",
|
||||
expected_type,
|
||||
response_type,
|
||||
raw_frame_preview(response),
|
||||
parsed
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn verify_host_challenge(
|
||||
challenge: &CommunicationValue,
|
||||
_tm: &mtp_codec::TypeMap,
|
||||
host_pk: &mtp_crypto::PublicKeyBundle,
|
||||
id: u64,
|
||||
server_challenge: u128,
|
||||
require_pq: bool,
|
||||
) -> Result<(), JsValue> {
|
||||
let sig = match challenge.get_data(DataType::Signature) {
|
||||
DataValue::Bytes(b) => b.clone(),
|
||||
_ => return Err(js_error("missing host challenge signature")),
|
||||
};
|
||||
let pq_sig = match challenge.get_data(DataType::PqSignature) {
|
||||
DataValue::Bytes(b) => b.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
let host_requires_pq = challenge.get_data(DataType::RequirePq) == &DataValue::BoolTrue;
|
||||
if host_requires_pq && host_pk.sig_pq_public_key.as_bytes().is_empty() {
|
||||
return Err(js_error(
|
||||
"host requires post-quantum authentication but its PQ public key is absent",
|
||||
));
|
||||
}
|
||||
if require_pq && pq_sig.is_empty() {
|
||||
return Err(js_error(
|
||||
"host challenge is missing the required PQ signature",
|
||||
));
|
||||
}
|
||||
|
||||
let payload = mtp_crypto::auth::challenge_payload(id, server_challenge);
|
||||
mtp_crypto::verify_ed25519(&host_pk.sig_cl_public_key, &payload, &sig)
|
||||
.map_err(|_| js_error("host challenge signature invalid"))?;
|
||||
if !pq_sig.is_empty() {
|
||||
mtp_crypto::verify_ml_dsa(&host_pk.sig_pq_public_key, &payload, &pq_sig)
|
||||
.map_err(|_| js_error("host challenge PQ signature invalid"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn verify_host_final(
|
||||
resp: &CommunicationValue,
|
||||
_tm: &mtp_codec::TypeMap,
|
||||
host_pk: &mtp_crypto::PublicKeyBundle,
|
||||
id: u64,
|
||||
client_nonce: u128,
|
||||
server_challenge: u128,
|
||||
require_pq: bool,
|
||||
) -> Result<(), JsValue> {
|
||||
if *resp.get_data(DataType::ClientNonce) != DataValue::UnsignedNumber(client_nonce) {
|
||||
return Err(js_error("nonce mismatch"));
|
||||
}
|
||||
let host_sig = match resp.get_data(DataType::Signature) {
|
||||
DataValue::Bytes(b) => b.clone(),
|
||||
_ => return Err(js_error("missing host signature")),
|
||||
};
|
||||
let host_pq_sig = match resp.get_data(DataType::PqSignature) {
|
||||
DataValue::Bytes(b) => b.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
if require_pq && host_pq_sig.is_empty() {
|
||||
return Err(js_error(
|
||||
"host confirmation is missing the required PQ signature",
|
||||
));
|
||||
}
|
||||
|
||||
let payload = mtp_crypto::auth::host_final_payload(id, client_nonce, server_challenge);
|
||||
mtp_crypto::verify_ed25519(&host_pk.sig_cl_public_key, &payload, &host_sig)
|
||||
.map_err(|_| js_error("host signature invalid"))?;
|
||||
if !host_pq_sig.is_empty() {
|
||||
mtp_crypto::verify_ml_dsa(&host_pk.sig_pq_public_key, &payload, &host_pq_sig)
|
||||
.map_err(|_| js_error("host PQ signature invalid"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn random_nonce() -> Result<u128, JsValue> {
|
||||
let mut nonce_bytes = [0u8; 16];
|
||||
getrandom_v04::fill(&mut nonce_bytes).map_err(|_| js_error("rng failed"))?;
|
||||
Ok(u128::from_be_bytes(nonce_bytes))
|
||||
}
|
||||
|
||||
pub(crate) fn signed_challenge_response_bytes(
|
||||
keyring: &mtp_crypto::Keyring,
|
||||
proof_payload: &[u8],
|
||||
client_nonce: u128,
|
||||
) -> Result<Vec<u8>, JsValue> {
|
||||
use mtp_crypto::SignatureScheme;
|
||||
|
||||
let signer = mtp_crypto::Ed25519Signer::new(&keyring.sig_cl_secret_key)
|
||||
.map_err(|e| js_error(format!("signer creation failed: {}", e)))?;
|
||||
let signature = signer
|
||||
.sign(proof_payload)
|
||||
.map_err(|e| js_error(format!("signature failed: {}", e)))?;
|
||||
|
||||
let mut proof = CommunicationValue::new(CommunicationType::ChallengeResponse)
|
||||
.add_typed_default(
|
||||
DataType::ClientNonce,
|
||||
DataValue::UnsignedNumber(client_nonce),
|
||||
)
|
||||
.add_typed_default(DataType::Signature, DataValue::Bytes(signature));
|
||||
|
||||
if !keyring.sig_pq_secret_key.as_bytes().is_empty() {
|
||||
let pq_signer =
|
||||
mtp_crypto::MlDsaSigner::new(&keyring.sig_pq_secret_key, &keyring.sig_pq_public_key)
|
||||
.map_err(|e| js_error(format!("PQ signer creation failed: {}", e)))?;
|
||||
let pq_signature = pq_signer
|
||||
.sign(proof_payload)
|
||||
.map_err(|e| js_error(format!("PQ signature failed: {}", e)))?;
|
||||
proof = proof.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature));
|
||||
}
|
||||
|
||||
proof
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(format!("encode failed: {}", e)))
|
||||
}
|
||||
|
|
@ -1,84 +1,27 @@
|
|||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
|
||||
use futures_channel::oneshot;
|
||||
use wasm_bindgen::JsCast;
|
||||
use futures_util::{FutureExt, pin_mut, select};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue, PROTOCOL_VERSION};
|
||||
use mtp_type_map::CommunicationTypeId;
|
||||
|
||||
use mtp_crypto::SignatureScheme;
|
||||
|
||||
use crate::auth;
|
||||
use crate::client_pipe::{self, PendingRequest};
|
||||
use crate::config::ConnectionConfig;
|
||||
use crate::error::js_error;
|
||||
use crate::pipe::PipeReader;
|
||||
use crate::transport::WasmTransport;
|
||||
|
||||
struct PendingRequest {
|
||||
response_type: Option<String>,
|
||||
sender: oneshot::Sender<Result<JsValue, JsValue>>,
|
||||
}
|
||||
|
||||
struct PingTimer {
|
||||
id: i32,
|
||||
closure: Closure<dyn FnMut()>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const PIPE_HANDLE_TS: &str = r#"
|
||||
export interface WasmPipeHandle {
|
||||
wait(): Promise<PipeWriter | null>;
|
||||
readonly pipeId: number;
|
||||
readonly description: string;
|
||||
}
|
||||
"#;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmPipeHandle {
|
||||
pipe_id: u32,
|
||||
description: String,
|
||||
transport: WasmTransport,
|
||||
response_rx: Rc<RefCell<Option<oneshot::Receiver<Result<bool, JsValue>>>>>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmPipeHandle {
|
||||
pub async fn wait(&self) -> Result<JsValue, JsValue> {
|
||||
let rx = self
|
||||
.response_rx
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.ok_or_else(|| js_error("handle already consumed"))?;
|
||||
|
||||
let accepted = rx
|
||||
.await
|
||||
.map_err(|_| js_error("pipe handle channel closed"))?;
|
||||
|
||||
match accepted {
|
||||
Ok(true) => {
|
||||
let writer = self
|
||||
.transport
|
||||
.open_pipe(self.pipe_id, &self.description)
|
||||
.await?;
|
||||
Ok(JsValue::from(writer))
|
||||
}
|
||||
Ok(false) => Ok(JsValue::NULL),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn pipe_id(&self) -> u32 {
|
||||
self.pipe_id
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn description(&self) -> String {
|
||||
self.description.clone()
|
||||
}
|
||||
}
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS: u32 = 30_000;
|
||||
|
||||
fn frame_property(frame: &JsValue, key: &str) -> Option<JsValue> {
|
||||
js_sys::Reflect::get(frame, &JsValue::from_str(key))
|
||||
|
|
@ -117,12 +60,13 @@ fn route_incoming_frame(
|
|||
let _ = pending.sender.send(Ok(frame.clone()));
|
||||
} else {
|
||||
let actual = message_type.clone().unwrap_or_else(|| "unknown".into());
|
||||
let _ = pending.sender.send(Err(js_error(&format!(
|
||||
let _ = pending.sender.send(Err(js_error(format!(
|
||||
"unexpected response type: expected {}, got {}",
|
||||
pending.response_type.unwrap_or_else(|| "unknown".into()),
|
||||
actual
|
||||
))));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -148,7 +92,7 @@ fn stop_ping_timer(ping_timer: &Rc<RefCell<Option<PingTimer>>>) {
|
|||
};
|
||||
if let Ok(clear_interval) =
|
||||
js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("clearInterval"))
|
||||
.and_then(|value| value.dyn_into::<js_sys::Function>().map_err(Into::into))
|
||||
.and_then(|value| value.dyn_into::<js_sys::Function>())
|
||||
{
|
||||
let _ = clear_interval.call1(&JsValue::NULL, &JsValue::from_f64(timer.id as f64));
|
||||
}
|
||||
|
|
@ -165,153 +109,25 @@ fn reject_pending_requests(
|
|||
}
|
||||
}
|
||||
|
||||
fn reject_pending_pipe_creations(
|
||||
pending: &Rc<RefCell<HashMap<u32, oneshot::Sender<Result<bool, JsValue>>>>>,
|
||||
message: &str,
|
||||
) {
|
||||
let pending = std::mem::take(&mut *pending.borrow_mut());
|
||||
for (_, tx) in pending {
|
||||
let _ = tx.send(Err(js_error(message)));
|
||||
}
|
||||
}
|
||||
|
||||
fn random_pipe_id() -> Result<u32, JsValue> {
|
||||
let mut bytes = [0u8; 4];
|
||||
getrandom::fill(&mut bytes).map_err(|_| js_error("rng failed"))?;
|
||||
Ok(u32::from_be_bytes(bytes))
|
||||
}
|
||||
|
||||
fn raw_frame_preview(bytes: &[u8]) -> String {
|
||||
let shown = bytes.len().min(256);
|
||||
let mut preview = hex::encode(&bytes[..shown]);
|
||||
if bytes.len() > shown {
|
||||
preview.push_str("...");
|
||||
}
|
||||
format!("{} bytes, hex={preview}", bytes.len())
|
||||
}
|
||||
|
||||
fn unexpected_response_type_error(
|
||||
context: &str,
|
||||
expected_type: CommunicationTypeId,
|
||||
response_type: CommunicationTypeId,
|
||||
response: &[u8],
|
||||
parsed: &CommunicationValue,
|
||||
) -> JsValue {
|
||||
js_error(&format!(
|
||||
"unexpected response type during {context}: expected {:?}, got {:?}; raw {}; parsed {}",
|
||||
expected_type,
|
||||
response_type,
|
||||
raw_frame_preview(response),
|
||||
parsed
|
||||
))
|
||||
}
|
||||
|
||||
/*
|
||||
* Verify the host's signature over the challenge it issued (step 2), mirroring
|
||||
* the native client (`client/src/lib.rs`). `id` is the client id for a login or
|
||||
* `0` for a registration. The Ed25519 signature is mandatory; the ML-DSA
|
||||
* signature is verified only when the host included one.
|
||||
*/
|
||||
fn verify_host_challenge(
|
||||
challenge: &CommunicationValue,
|
||||
_tm: &mtp_codec::TypeMap,
|
||||
host_pk: &mtp_crypto::PublicKeyBundle,
|
||||
id: u64,
|
||||
server_challenge: u128,
|
||||
) -> Result<(), JsValue> {
|
||||
let sig = match challenge.get_data(DataType::Signature) {
|
||||
DataValue::Bytes(b) => b.clone(),
|
||||
_ => return Err(js_error("missing host challenge signature")),
|
||||
};
|
||||
let pq_sig = match challenge.get_data(DataType::PqSignature) {
|
||||
DataValue::Bytes(b) => b.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
let payload = mtp_crypto::auth::challenge_payload(id, server_challenge);
|
||||
mtp_crypto::verify_ed25519(&host_pk.sig_cl_public_key, &payload, &sig)
|
||||
.map_err(|_| js_error("host challenge signature invalid"))?;
|
||||
if !pq_sig.is_empty() {
|
||||
mtp_crypto::verify_ml_dsa(&host_pk.sig_pq_public_key, &payload, &pq_sig)
|
||||
.map_err(|_| js_error("host challenge PQ signature invalid"))?;
|
||||
}
|
||||
async fn wait_for_timeout(timeout_ms: u32) -> Result<(), JsValue> {
|
||||
let promise = js_sys::Promise::new(&mut |resolve, reject| {
|
||||
let result = js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("setTimeout"))
|
||||
.and_then(|value| value.dyn_into::<js_sys::Function>())
|
||||
.and_then(|set_timeout| {
|
||||
set_timeout.call2(
|
||||
&JsValue::NULL,
|
||||
&resolve,
|
||||
&JsValue::from_f64(timeout_ms as f64),
|
||||
)
|
||||
});
|
||||
if let Err(error) = result {
|
||||
let _ = reject.call1(&JsValue::NULL, &error);
|
||||
}
|
||||
});
|
||||
wasm_bindgen_futures::JsFuture::from(promise).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/*
|
||||
* Verify the host's final confirmation (step 4): the echoed `client_nonce` and
|
||||
* the host signature over the handshake transcript. `id` is the client id for a
|
||||
* login and the host-assigned id for a register.
|
||||
*/
|
||||
fn verify_host_final(
|
||||
resp: &CommunicationValue,
|
||||
_tm: &mtp_codec::TypeMap,
|
||||
host_pk: &mtp_crypto::PublicKeyBundle,
|
||||
id: u64,
|
||||
client_nonce: u128,
|
||||
server_challenge: u128,
|
||||
) -> Result<(), JsValue> {
|
||||
if *resp.get_data(DataType::ClientNonce) != DataValue::UnsignedNumber(client_nonce) {
|
||||
return Err(js_error("nonce mismatch"));
|
||||
}
|
||||
let host_sig = match resp.get_data(DataType::Signature) {
|
||||
DataValue::Bytes(b) => b.clone(),
|
||||
_ => return Err(js_error("missing host signature")),
|
||||
};
|
||||
let host_pq_sig = match resp.get_data(DataType::PqSignature) {
|
||||
DataValue::Bytes(b) => b.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
let payload = mtp_crypto::auth::host_final_payload(id, client_nonce, server_challenge);
|
||||
mtp_crypto::verify_ed25519(&host_pk.sig_cl_public_key, &payload, &host_sig)
|
||||
.map_err(|_| js_error("host signature invalid"))?;
|
||||
if !host_pq_sig.is_empty() {
|
||||
mtp_crypto::verify_ml_dsa(&host_pk.sig_pq_public_key, &payload, &host_pq_sig)
|
||||
.map_err(|_| js_error("host PQ signature invalid"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn random_nonce() -> Result<u128, JsValue> {
|
||||
let mut nonce_bytes = [0u8; 16];
|
||||
getrandom::fill(&mut nonce_bytes).map_err(|_| js_error("rng failed"))?;
|
||||
Ok(u128::from_be_bytes(nonce_bytes))
|
||||
}
|
||||
|
||||
fn signed_challenge_response_bytes(
|
||||
keyring: &mtp_crypto::Keyring,
|
||||
proof_payload: &[u8],
|
||||
client_nonce: u128,
|
||||
) -> Result<Vec<u8>, JsValue> {
|
||||
let signer = mtp_crypto::Ed25519Signer::new(&keyring.sig_cl_secret_key)
|
||||
.map_err(|e| js_error(&format!("signer creation failed: {}", e)))?;
|
||||
let signature = signer
|
||||
.sign(proof_payload)
|
||||
.map_err(|e| js_error(&format!("signature failed: {}", e)))?;
|
||||
|
||||
let mut proof = CommunicationValue::new(CommunicationType::ChallengeResponse)
|
||||
.add_typed_default(
|
||||
DataType::ClientNonce,
|
||||
DataValue::UnsignedNumber(client_nonce),
|
||||
)
|
||||
.add_typed_default(DataType::Signature, DataValue::Bytes(signature));
|
||||
|
||||
if !keyring.sig_pq_secret_key.as_bytes().is_empty() {
|
||||
let pq_signer =
|
||||
mtp_crypto::MlDsaSigner::new(&keyring.sig_pq_secret_key, &keyring.sig_pq_public_key)
|
||||
.map_err(|e| js_error(&format!("PQ signer creation failed: {}", e)))?;
|
||||
let pq_signature = pq_signer
|
||||
.sign(proof_payload)
|
||||
.map_err(|e| js_error(&format!("PQ signature failed: {}", e)))?;
|
||||
proof = proof.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature));
|
||||
}
|
||||
|
||||
proof
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConnectionState {
|
||||
|
|
@ -325,7 +141,8 @@ pub enum ConnectionState {
|
|||
pub struct WasmClient {
|
||||
transport: Rc<RefCell<Option<WasmTransport>>>,
|
||||
state: Rc<Cell<ConnectionState>>,
|
||||
on_state_change: js_sys::Function,
|
||||
pending_state_callbacks: Rc<RefCell<VecDeque<ConnectionState>>>,
|
||||
state_callback: Closure<dyn FnMut()>,
|
||||
pub(crate) on_message: js_sys::Function,
|
||||
pub(crate) on_error: js_sys::Function,
|
||||
subscriptions: Rc<RefCell<HashMap<u32, (String, js_sys::Function)>>>,
|
||||
|
|
@ -346,10 +163,21 @@ impl WasmClient {
|
|||
on_error: Option<js_sys::Function>,
|
||||
) -> Self {
|
||||
let noop = || js_sys::Function::new_no_args("");
|
||||
let on_state_change = on_state_change.unwrap_or_else(noop);
|
||||
let pending_state_callbacks = Rc::new(RefCell::new(VecDeque::new()));
|
||||
let callback_queue = pending_state_callbacks.clone();
|
||||
let callback = on_state_change.clone();
|
||||
let state_callback = Closure::wrap(Box::new(move || {
|
||||
let state = callback_queue.borrow_mut().pop_front();
|
||||
if let Some(state) = state {
|
||||
let _ = callback.call1(&JsValue::NULL, &JsValue::from(state as u8));
|
||||
}
|
||||
}) as Box<dyn FnMut()>);
|
||||
Self {
|
||||
transport: Rc::new(RefCell::new(None)),
|
||||
state: Rc::new(Cell::new(ConnectionState::Disconnected)),
|
||||
on_state_change: on_state_change.unwrap_or_else(noop),
|
||||
pending_state_callbacks,
|
||||
state_callback,
|
||||
on_message: on_message.unwrap_or_else(noop),
|
||||
on_error: on_error.unwrap_or_else(noop),
|
||||
subscriptions: Rc::new(RefCell::new(HashMap::new())),
|
||||
|
|
@ -372,7 +200,6 @@ impl WasmClient {
|
|||
self.state.get() as u8
|
||||
}
|
||||
|
||||
/// Unauthenticated connect (sends basic Identification, enables receive loop).
|
||||
#[wasm_bindgen]
|
||||
pub async fn connect(&self, config: &ConnectionConfig) -> Result<(), JsValue> {
|
||||
self.set_state(ConnectionState::Connecting);
|
||||
|
|
@ -395,21 +222,46 @@ impl WasmClient {
|
|||
}
|
||||
let ident_bytes = ident
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
||||
.map_err(|e| js_error(format!("encode failed: {}", e)))?;
|
||||
transport.send_frame(&ident_bytes).await?;
|
||||
|
||||
let outcome_bytes = transport.read_one_frame().await?;
|
||||
let outcome = CommunicationValue::from_bytes(&outcome_bytes)
|
||||
.map_err(|e| js_error(format!("parse handshake outcome: {e}")))?;
|
||||
let tm = mtp_codec::TypeMap::latest();
|
||||
if Some(outcome.get_type()) == CommunicationType::ErrorBadVersion.try_to_id(&tm) {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(js_error(
|
||||
outcome
|
||||
.get_str(DataType::ErrorMessage)
|
||||
.unwrap_or("host does not support this protocol version"),
|
||||
));
|
||||
}
|
||||
let expected = CommunicationType::IdentificationResponse
|
||||
.try_to_id(&tm)
|
||||
.ok_or_else(|| js_error("IdentificationResponse is absent from the type map"))?;
|
||||
if outcome.get_type() != expected
|
||||
|| outcome.get_data(DataType::Connected) != &DataValue::BoolTrue
|
||||
{
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(js_error(
|
||||
outcome
|
||||
.get_str(DataType::ErrorMessage)
|
||||
.unwrap_or("host rejected the connection"),
|
||||
));
|
||||
}
|
||||
match outcome.get_data(DataType::Version) {
|
||||
DataValue::Str(version) if mtp_codec::Version::parse(version).is_some() => {}
|
||||
_ => {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(js_error("host omitted a valid negotiated protocol version"));
|
||||
}
|
||||
}
|
||||
|
||||
self.start_receive_loop(transport);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Authenticated login with an existing client ID.
|
||||
/// Exchanges Identification + signatures and verifies the host response.
|
||||
///
|
||||
/// - `host_public_key_bytes`: serialized PublicKeyBundle from the server
|
||||
/// - `keyring_bytes`: serialized Keyring of this client (must match `client_id`)
|
||||
/// - `client_id`: previously assigned client ID
|
||||
///
|
||||
/// Returns the confirmed (same) client ID on success.
|
||||
#[wasm_bindgen]
|
||||
pub async fn auth_connect(
|
||||
&self,
|
||||
|
|
@ -421,9 +273,9 @@ impl WasmClient {
|
|||
self.set_state(ConnectionState::Connecting);
|
||||
|
||||
let host_pk = mtp_crypto::PublicKeyBundle::from_bytes(host_public_key_bytes)
|
||||
.map_err(|e| js_error(&format!("invalid host public key: {}", e)))?;
|
||||
.map_err(|e| js_error(format!("invalid host public key: {}", e)))?;
|
||||
let keyring = mtp_crypto::Keyring::from_bytes(keyring_bytes)
|
||||
.map_err(|e| js_error(&format!("invalid keyring: {}", e)))?;
|
||||
.map_err(|e| js_error(format!("invalid keyring: {}", e)))?;
|
||||
|
||||
let tm = mtp_codec::TypeMap::latest();
|
||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||
|
|
@ -435,7 +287,6 @@ impl WasmClient {
|
|||
)
|
||||
.await?;
|
||||
|
||||
// 1. Send the unsigned Identification hello.
|
||||
let mut hello = CommunicationValue::new(CommunicationType::Identification)
|
||||
.add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
|
||||
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(client_id as u128));
|
||||
|
|
@ -444,10 +295,9 @@ impl WasmClient {
|
|||
}
|
||||
let hello_bytes = hello
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
||||
.map_err(|e| js_error(format!("encode failed: {}", e)))?;
|
||||
transport.send_frame(&hello_bytes).await?;
|
||||
|
||||
// 2. Receive and verify the host's challenge.
|
||||
let server_challenge = self
|
||||
.read_verified_challenge(
|
||||
&transport,
|
||||
|
|
@ -455,11 +305,12 @@ impl WasmClient {
|
|||
&host_pk,
|
||||
client_id,
|
||||
"auth_connect challenge",
|
||||
config.require_pq,
|
||||
!keyring.sig_pq_secret_key.as_bytes().is_empty(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 3. Sign the host's challenge and send the proof.
|
||||
let client_nonce = random_nonce()?;
|
||||
let client_nonce = auth::random_nonce()?;
|
||||
|
||||
let proof_payload = mtp_crypto::auth::login_proof_payload(
|
||||
&version_str,
|
||||
|
|
@ -467,18 +318,19 @@ impl WasmClient {
|
|||
server_challenge,
|
||||
client_nonce,
|
||||
);
|
||||
let proof = signed_challenge_response_bytes(&keyring, &proof_payload, client_nonce)?;
|
||||
let proof = auth::signed_challenge_response_bytes(&keyring, &proof_payload, client_nonce)?;
|
||||
transport.send_frame(&proof).await?;
|
||||
|
||||
// 4. Receive and verify the host's final confirmation.
|
||||
let response = transport.read_one_frame().await?;
|
||||
let resp_comm = CommunicationValue::from_bytes(&response)
|
||||
.map_err(|e| js_error(&format!("parse response: {}", e)))?;
|
||||
.map_err(|e| js_error(format!("parse response: {}", e)))?;
|
||||
let resp_type = resp_comm.get_type();
|
||||
let expected_type = CommunicationType::IdentificationResponse.to_id(&tm);
|
||||
let expected_type = CommunicationType::IdentificationResponse
|
||||
.try_to_id(&tm)
|
||||
.ok_or_else(|| js_error("IdentificationResponse is absent from the type map"))?;
|
||||
if resp_type != expected_type {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(unexpected_response_type_error(
|
||||
return Err(auth::unexpected_response_type_error(
|
||||
"auth_connect",
|
||||
expected_type,
|
||||
resp_type,
|
||||
|
|
@ -489,23 +341,26 @@ impl WasmClient {
|
|||
|
||||
if resp_comm.get_data(DataType::Connected) != &DataValue::BoolTrue {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(js_error("host rejected authentication"));
|
||||
return Err(js_error(
|
||||
resp_comm
|
||||
.get_str(DataType::ErrorMessage)
|
||||
.unwrap_or("host rejected authentication"),
|
||||
));
|
||||
}
|
||||
|
||||
// Verify echoed nonce + host signature (login: id is client_id).
|
||||
if let Err(e) = verify_host_final(
|
||||
if let Err(e) = auth::verify_host_final(
|
||||
&resp_comm,
|
||||
&tm,
|
||||
&host_pk,
|
||||
client_id,
|
||||
client_nonce,
|
||||
server_challenge,
|
||||
config.require_pq,
|
||||
) {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// Extract assigned ID
|
||||
let assigned_id = match resp_comm.get_data(DataType::Id) {
|
||||
DataValue::UnsignedNumber(n) => *n as u64,
|
||||
_ => {
|
||||
|
|
@ -519,13 +374,6 @@ impl WasmClient {
|
|||
Ok(assigned_id)
|
||||
}
|
||||
|
||||
/// Authenticated registration with a fresh keyring.
|
||||
/// The server assigns a new client ID.
|
||||
///
|
||||
/// - `host_public_key_bytes`: serialized PublicKeyBundle from the server
|
||||
/// - `keyring_bytes`: serialized Keyring (must include ed25519 secret key)
|
||||
///
|
||||
/// Returns the newly assigned client ID.
|
||||
#[wasm_bindgen]
|
||||
pub async fn auth_register(
|
||||
&self,
|
||||
|
|
@ -536,9 +384,9 @@ impl WasmClient {
|
|||
self.set_state(ConnectionState::Connecting);
|
||||
|
||||
let host_pk = mtp_crypto::PublicKeyBundle::from_bytes(host_public_key_bytes)
|
||||
.map_err(|e| js_error(&format!("invalid host public key: {}", e)))?;
|
||||
.map_err(|e| js_error(format!("invalid host public key: {}", e)))?;
|
||||
let keyring = mtp_crypto::Keyring::from_bytes(keyring_bytes)
|
||||
.map_err(|e| js_error(&format!("invalid keyring: {}", e)))?;
|
||||
.map_err(|e| js_error(format!("invalid keyring: {}", e)))?;
|
||||
|
||||
let tm = mtp_codec::TypeMap::latest();
|
||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||
|
|
@ -551,7 +399,6 @@ impl WasmClient {
|
|||
)
|
||||
.await?;
|
||||
|
||||
// 1. Send the unsigned Register hello (version + public-key bundle).
|
||||
let mut hello = CommunicationValue::new(CommunicationType::Register)
|
||||
.add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
|
||||
.add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes.clone()));
|
||||
|
|
@ -560,16 +407,22 @@ impl WasmClient {
|
|||
}
|
||||
let hello_bytes = hello
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
||||
.map_err(|e| js_error(format!("encode failed: {}", e)))?;
|
||||
transport.send_frame(&hello_bytes).await?;
|
||||
|
||||
// 2. Receive and verify the host's challenge (register binds id = 0).
|
||||
let server_challenge = self
|
||||
.read_verified_challenge(&transport, &tm, &host_pk, 0, "auth_register challenge")
|
||||
.read_verified_challenge(
|
||||
&transport,
|
||||
&tm,
|
||||
&host_pk,
|
||||
0,
|
||||
"auth_register challenge",
|
||||
config.require_pq,
|
||||
!keyring.sig_pq_secret_key.as_bytes().is_empty(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 3. Sign the host's challenge over the bundle and send the proof.
|
||||
let client_nonce = random_nonce()?;
|
||||
let client_nonce = auth::random_nonce()?;
|
||||
|
||||
let proof_payload = mtp_crypto::auth::register_proof_payload(
|
||||
&version_str,
|
||||
|
|
@ -577,18 +430,19 @@ impl WasmClient {
|
|||
server_challenge,
|
||||
client_nonce,
|
||||
);
|
||||
let proof = signed_challenge_response_bytes(&keyring, &proof_payload, client_nonce)?;
|
||||
let proof = auth::signed_challenge_response_bytes(&keyring, &proof_payload, client_nonce)?;
|
||||
transport.send_frame(&proof).await?;
|
||||
|
||||
// 4. Receive the host's final confirmation; extract + verify assigned id.
|
||||
let response = transport.read_one_frame().await?;
|
||||
let resp_comm = CommunicationValue::from_bytes(&response)
|
||||
.map_err(|e| js_error(&format!("parse response: {}", e)))?;
|
||||
.map_err(|e| js_error(format!("parse response: {}", e)))?;
|
||||
let resp_type = resp_comm.get_type();
|
||||
let expected_type = CommunicationType::RegisterResponse.to_id(&tm);
|
||||
let expected_type = CommunicationType::RegisterResponse
|
||||
.try_to_id(&tm)
|
||||
.ok_or_else(|| js_error("RegisterResponse is absent from the type map"))?;
|
||||
if resp_type != expected_type {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(unexpected_response_type_error(
|
||||
return Err(auth::unexpected_response_type_error(
|
||||
"auth_register",
|
||||
expected_type,
|
||||
resp_type,
|
||||
|
|
@ -599,7 +453,11 @@ impl WasmClient {
|
|||
|
||||
if resp_comm.get_data(DataType::Connected) != &DataValue::BoolTrue {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(js_error("host rejected registration"));
|
||||
return Err(js_error(
|
||||
resp_comm
|
||||
.get_str(DataType::ErrorMessage)
|
||||
.unwrap_or("host rejected registration"),
|
||||
));
|
||||
}
|
||||
|
||||
let assigned_id = match resp_comm.get_data(DataType::Id) {
|
||||
|
|
@ -610,14 +468,14 @@ impl WasmClient {
|
|||
}
|
||||
};
|
||||
|
||||
// Verify echoed nonce + host signature (register: id is host-assigned).
|
||||
if let Err(e) = verify_host_final(
|
||||
if let Err(e) = auth::verify_host_final(
|
||||
&resp_comm,
|
||||
&tm,
|
||||
&host_pk,
|
||||
assigned_id,
|
||||
client_nonce,
|
||||
server_challenge,
|
||||
config.require_pq,
|
||||
) {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(e);
|
||||
|
|
@ -641,9 +499,10 @@ impl WasmClient {
|
|||
&self,
|
||||
frame: Vec<u8>,
|
||||
response_type: Option<String>,
|
||||
timeout_ms: Option<u32>,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
let request = CommunicationValue::from_bytes(&frame)
|
||||
.map_err(|e| js_error(&format!("parse request: {}", e)))?;
|
||||
.map_err(|e| js_error(format!("parse request: {}", e)))?;
|
||||
let request_id = request.get_id();
|
||||
if request_id == 0 {
|
||||
return Err(js_error("request frame must have a non-zero id"));
|
||||
|
|
@ -654,22 +513,49 @@ impl WasmClient {
|
|||
};
|
||||
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
self.pending_requests.borrow_mut().insert(
|
||||
request_id,
|
||||
PendingRequest {
|
||||
response_type,
|
||||
sender,
|
||||
},
|
||||
);
|
||||
|
||||
if let Err(error) = transport.send_frame(&frame).await {
|
||||
self.pending_requests.borrow_mut().remove(&request_id);
|
||||
return Err(error);
|
||||
let token = Rc::new(());
|
||||
{
|
||||
let mut pending = self.pending_requests.borrow_mut();
|
||||
if pending.contains_key(&request_id) {
|
||||
return Err(js_error(format!(
|
||||
"request id {request_id} is already pending"
|
||||
)));
|
||||
}
|
||||
pending.insert(
|
||||
request_id,
|
||||
PendingRequest {
|
||||
token: token.clone(),
|
||||
response_type,
|
||||
sender,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
match receiver.await {
|
||||
Ok(result) => result,
|
||||
Err(_) => Err(js_error("request cancelled")),
|
||||
let timeout_ms = timeout_ms.unwrap_or(DEFAULT_REQUEST_TIMEOUT_MS);
|
||||
let response = async {
|
||||
transport.send_frame(&frame).await?;
|
||||
match receiver.await {
|
||||
Ok(result) => result,
|
||||
Err(_) => Err(js_error("request cancelled")),
|
||||
}
|
||||
}
|
||||
.fuse();
|
||||
let timeout = wait_for_timeout(timeout_ms).fuse();
|
||||
pin_mut!(response, timeout);
|
||||
select! {
|
||||
result = response => {
|
||||
if result.is_err() {
|
||||
client_pipe::remove_pending_request(&self.pending_requests, request_id, &token);
|
||||
}
|
||||
result
|
||||
},
|
||||
result = timeout => {
|
||||
client_pipe::remove_pending_request(&self.pending_requests, request_id, &token);
|
||||
result?;
|
||||
Err(js_error(format!(
|
||||
"request {request_id} timed out after {timeout_ms}ms"
|
||||
)))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -712,7 +598,7 @@ impl WasmClient {
|
|||
)
|
||||
.with_sender(client_id)
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(&format!("encode ping failed: {}", e)));
|
||||
.map_err(|e| js_error(format!("encode ping failed: {}", e)));
|
||||
match frame {
|
||||
Ok(frame) => {
|
||||
if let Err(error) = transport.send_frame(&frame).await {
|
||||
|
|
@ -748,7 +634,7 @@ impl WasmClient {
|
|||
};
|
||||
if let Ok(clear_interval) =
|
||||
js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("clearInterval"))
|
||||
.and_then(|value| value.dyn_into::<js_sys::Function>().map_err(Into::into))
|
||||
.and_then(|value| value.dyn_into::<js_sys::Function>())
|
||||
{
|
||||
let _ = clear_interval.call1(&JsValue::NULL, &JsValue::from_f64(timer.id as f64));
|
||||
}
|
||||
|
|
@ -763,64 +649,36 @@ impl WasmClient {
|
|||
}
|
||||
self.subscriptions.borrow_mut().clear();
|
||||
self.reject_pending_requests("disconnected");
|
||||
reject_pending_pipe_creations(&self.pending_pipe_creations, "disconnected");
|
||||
client_pipe::reject_pending_pipe_creations(&self.pending_pipe_creations, "disconnected");
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
}
|
||||
|
||||
/// Set the callback invoked when a remote peer opens a pipe request.
|
||||
/// The callback receives a plain JS object `{ pipeId: number, description: string }`.
|
||||
#[wasm_bindgen]
|
||||
pub fn set_on_pipe_request(&self, callback: Option<js_sys::Function>) {
|
||||
*self.on_pipe_request.borrow_mut() = callback;
|
||||
}
|
||||
|
||||
/// Initiate an outgoing pipe. Returns a `WasmPipeHandle` whose `wait()`
|
||||
/// method resolves after the remote peer accepts (or denies) the request.
|
||||
#[wasm_bindgen]
|
||||
pub async fn create_pipe(&self, description: &str) -> Result<WasmPipeHandle, JsValue> {
|
||||
pub async fn create_pipe(
|
||||
&self,
|
||||
description: &str,
|
||||
) -> Result<client_pipe::WasmPipeHandle, JsValue> {
|
||||
let transport = self
|
||||
.transport
|
||||
.borrow()
|
||||
.clone()
|
||||
.ok_or_else(|| js_error("not connected"))?;
|
||||
|
||||
let pipe_id = random_pipe_id()?;
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.pending_pipe_creations.borrow_mut().insert(pipe_id, tx);
|
||||
|
||||
let request = CommunicationValue::new(CommunicationType::PipeRequest)
|
||||
.with_id(pipe_id)
|
||||
.add_typed_default(
|
||||
DataType::Description,
|
||||
DataValue::Str(description.to_string()),
|
||||
);
|
||||
let request_bytes = request
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
||||
web_sys::console::log_1(
|
||||
&format!(
|
||||
"[mtp wasm] create_pipe sending PipeRequest id={} description={description} bytes={}",
|
||||
pipe_id,
|
||||
request_bytes.len()
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
transport.send_frame(&request_bytes).await?;
|
||||
web_sys::console::log_1(
|
||||
&format!("[mtp wasm] create_pipe sent PipeRequest id={pipe_id}").into(),
|
||||
);
|
||||
|
||||
Ok(WasmPipeHandle {
|
||||
let pipe_id = client_pipe::random_pipe_id()?;
|
||||
client_pipe::wasm_create_pipe(
|
||||
&transport,
|
||||
description,
|
||||
pipe_id,
|
||||
description: description.to_string(),
|
||||
transport,
|
||||
response_rx: Rc::new(RefCell::new(Some(rx))),
|
||||
})
|
||||
&self.pending_pipe_creations,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Accept an incoming pipe request (identified by `pipe_id`). Sends a
|
||||
/// `PipeResponse` with `Accepted = true` and returns a `PipeReader` once
|
||||
/// the remote peer opens the pipe stream.
|
||||
#[wasm_bindgen]
|
||||
pub async fn accept_pipe(&self, pipe_id: u32) -> Result<PipeReader, JsValue> {
|
||||
let transport = self
|
||||
|
|
@ -829,29 +687,9 @@ impl WasmClient {
|
|||
.clone()
|
||||
.ok_or_else(|| js_error("not connected"))?;
|
||||
|
||||
let resp = CommunicationValue::new(CommunicationType::PipeResponse)
|
||||
.with_id(pipe_id)
|
||||
.add_typed_default(DataType::Accepted, DataValue::BoolTrue);
|
||||
let resp_bytes = resp
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
||||
web_sys::console::log_1(
|
||||
&format!("[mtp wasm] accept_pipe sending PipeResponse id={pipe_id} accepted=true bytes={}", resp_bytes.len()).into(),
|
||||
);
|
||||
transport.send_frame(&resp_bytes).await?;
|
||||
web_sys::console::log_1(
|
||||
&format!("[mtp wasm] accept_pipe sent PipeResponse id={pipe_id}").into(),
|
||||
);
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.pending_pipes.borrow_mut().insert(pipe_id, tx);
|
||||
|
||||
rx.await
|
||||
.map_err(|_| js_error("pipe closed before stream arrived"))
|
||||
client_pipe::wasm_accept_pipe(&transport, pipe_id, &self.pending_pipes).await
|
||||
}
|
||||
|
||||
/// Deny an incoming pipe request. Sends a `PipeResponse` with
|
||||
/// `Accepted = false`.
|
||||
#[wasm_bindgen]
|
||||
pub async fn deny_pipe(&self, pipe_id: u32) -> Result<(), JsValue> {
|
||||
let transport = self
|
||||
|
|
@ -860,49 +698,34 @@ impl WasmClient {
|
|||
.clone()
|
||||
.ok_or_else(|| js_error("not connected"))?;
|
||||
|
||||
let resp = CommunicationValue::new(CommunicationType::PipeResponse)
|
||||
.with_id(pipe_id)
|
||||
.add_typed_default(DataType::Accepted, DataValue::BoolFalse);
|
||||
let resp_bytes = resp
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
||||
transport.send_frame(&resp_bytes).await
|
||||
client_pipe::wasm_deny_pipe(&transport, pipe_id).await
|
||||
}
|
||||
|
||||
fn set_state(&self, new_state: ConnectionState) {
|
||||
self.state.set(new_state);
|
||||
|
||||
// Defer the callback to a microtask so re-entrant &mut self calls don't alias.
|
||||
let cb = self.on_state_change.clone();
|
||||
let val = JsValue::from(new_state as u8);
|
||||
let closure = Closure::wrap(Box::new(move || {
|
||||
let _ = cb.call1(&JsValue::NULL, &val);
|
||||
}) as Box<dyn FnMut()>);
|
||||
self.pending_state_callbacks
|
||||
.borrow_mut()
|
||||
.push_back(new_state);
|
||||
|
||||
let global = js_sys::global();
|
||||
let mut closure_opt = Some(closure);
|
||||
|
||||
let qmt = js_sys::Reflect::get(&global, &JsValue::from_str("queueMicrotask"))
|
||||
.and_then(|f| f.dyn_into::<js_sys::Function>().map_err(Into::into));
|
||||
let scheduled = match qmt {
|
||||
Ok(qmt) => {
|
||||
if let Some(c) = closure_opt.take() {
|
||||
let _ = qmt.call1(&global, c.as_ref());
|
||||
c.forget();
|
||||
}
|
||||
true
|
||||
}
|
||||
Err(_) => false,
|
||||
};
|
||||
if !scheduled {
|
||||
if let Ok(set_timeout) = js_sys::Reflect::get(&global, &JsValue::from_str("setTimeout"))
|
||||
.and_then(|f| f.dyn_into::<js_sys::Function>().map_err(Into::into))
|
||||
{
|
||||
if let Some(c) = closure_opt.take() {
|
||||
let _ = set_timeout.call2(&global, c.as_ref(), &JsValue::from_f64(0.0));
|
||||
c.forget();
|
||||
}
|
||||
}
|
||||
.and_then(|f| f.dyn_into::<js_sys::Function>());
|
||||
let scheduled = qmt
|
||||
.and_then(|qmt| qmt.call1(&global, self.state_callback.as_ref()))
|
||||
.is_ok();
|
||||
if !scheduled
|
||||
&& js_sys::Reflect::get(&global, &JsValue::from_str("setTimeout"))
|
||||
.and_then(|f| f.dyn_into::<js_sys::Function>())
|
||||
.and_then(|set_timeout| {
|
||||
set_timeout.call2(
|
||||
&global,
|
||||
self.state_callback.as_ref(),
|
||||
&JsValue::from_f64(0.0),
|
||||
)
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
self.pending_state_callbacks.borrow_mut().pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1000,7 +823,7 @@ impl WasmClient {
|
|||
state.set(ConnectionState::Disconnected);
|
||||
stop_ping_timer(&ping_timer);
|
||||
reject_pending_requests(&pending_requests, "disconnected");
|
||||
reject_pending_pipe_creations(&pending_pipe_creations, "disconnected");
|
||||
client_pipe::reject_pending_pipe_creations(&pending_pipe_creations, "disconnected");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1015,14 +838,18 @@ impl WasmClient {
|
|||
host_pk: &mtp_crypto::PublicKeyBundle,
|
||||
bound_id: u64,
|
||||
context: &str,
|
||||
require_pq: bool,
|
||||
client_has_pq_key: bool,
|
||||
) -> Result<u128, JsValue> {
|
||||
let challenge_bytes = transport.read_one_frame().await?;
|
||||
let challenge = CommunicationValue::from_bytes(&challenge_bytes)
|
||||
.map_err(|e| js_error(&format!("parse challenge: {}", e)))?;
|
||||
let expected = CommunicationType::Challenge.to_id(tm);
|
||||
.map_err(|e| js_error(format!("parse challenge: {}", e)))?;
|
||||
let expected = CommunicationType::Challenge
|
||||
.try_to_id(tm)
|
||||
.ok_or_else(|| js_error("Challenge is absent from the type map"))?;
|
||||
if challenge.get_type() != expected {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(unexpected_response_type_error(
|
||||
return Err(auth::unexpected_response_type_error(
|
||||
context,
|
||||
expected,
|
||||
challenge.get_type(),
|
||||
|
|
@ -1039,7 +866,21 @@ impl WasmClient {
|
|||
}
|
||||
};
|
||||
|
||||
if let Err(e) = verify_host_challenge(&challenge, tm, host_pk, bound_id, server_challenge) {
|
||||
if challenge.get_data(DataType::RequirePq) == &DataValue::BoolTrue && !client_has_pq_key {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(js_error(
|
||||
"host requires post-quantum authentication but the client PQ key is absent",
|
||||
));
|
||||
}
|
||||
|
||||
if let Err(e) = auth::verify_host_challenge(
|
||||
&challenge,
|
||||
tm,
|
||||
host_pk,
|
||||
bound_id,
|
||||
server_challenge,
|
||||
require_pq,
|
||||
) {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(e);
|
||||
}
|
||||
|
|
|
|||
175
wasm/src/client_pipe.rs
Normal file
175
wasm/src/client_pipe.rs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use futures_channel::oneshot;
|
||||
use tracing::debug;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||
|
||||
use crate::error::js_error;
|
||||
use crate::pipe::PipeReader;
|
||||
use crate::transport::WasmTransport;
|
||||
|
||||
pub(crate) struct PendingRequest {
|
||||
pub(crate) token: Rc<()>,
|
||||
pub(crate) response_type: Option<String>,
|
||||
pub(crate) sender: oneshot::Sender<Result<JsValue, JsValue>>,
|
||||
}
|
||||
|
||||
pub(crate) fn remove_pending_request(
|
||||
pending_requests: &Rc<RefCell<HashMap<u32, PendingRequest>>>,
|
||||
request_id: u32,
|
||||
token: &Rc<()>,
|
||||
) {
|
||||
let mut pending = pending_requests.borrow_mut();
|
||||
if pending
|
||||
.get(&request_id)
|
||||
.is_some_and(|entry| Rc::ptr_eq(&entry.token, token))
|
||||
{
|
||||
pending.remove(&request_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const PIPE_HANDLE_TS: &str = r#"
|
||||
export interface WasmPipeHandle {
|
||||
wait(): Promise<PipeWriter | null>;
|
||||
readonly pipeId: number;
|
||||
readonly description: string;
|
||||
}
|
||||
"#;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmPipeHandle {
|
||||
pipe_id: u32,
|
||||
description: String,
|
||||
transport: WasmTransport,
|
||||
response_rx: Rc<RefCell<Option<oneshot::Receiver<Result<bool, JsValue>>>>>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmPipeHandle {
|
||||
pub async fn wait(&self) -> Result<JsValue, JsValue> {
|
||||
let rx = self
|
||||
.response_rx
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.ok_or_else(|| js_error("handle already consumed"))?;
|
||||
|
||||
let accepted = rx
|
||||
.await
|
||||
.map_err(|_| js_error("pipe handle channel closed"))?;
|
||||
|
||||
match accepted {
|
||||
Ok(true) => {
|
||||
let writer = self
|
||||
.transport
|
||||
.open_pipe(self.pipe_id, &self.description)
|
||||
.await?;
|
||||
Ok(JsValue::from(writer))
|
||||
}
|
||||
Ok(false) => Ok(JsValue::NULL),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn pipe_id(&self) -> u32 {
|
||||
self.pipe_id
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn description(&self) -> String {
|
||||
self.description.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn random_pipe_id() -> Result<u32, JsValue> {
|
||||
let mut bytes = [0u8; 4];
|
||||
getrandom_v04::fill(&mut bytes).map_err(|_| js_error("rng failed"))?;
|
||||
Ok(u32::from_be_bytes(bytes))
|
||||
}
|
||||
|
||||
pub(crate) fn reject_pending_pipe_creations(
|
||||
pending: &Rc<RefCell<HashMap<u32, oneshot::Sender<Result<bool, JsValue>>>>>,
|
||||
message: &str,
|
||||
) {
|
||||
let pending = std::mem::take(&mut *pending.borrow_mut());
|
||||
for (_, tx) in pending {
|
||||
let _ = tx.send(Err(js_error(message)));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn wasm_create_pipe(
|
||||
transport: &WasmTransport,
|
||||
description: &str,
|
||||
pipe_id: u32,
|
||||
pending_pipe_creations: &Rc<RefCell<HashMap<u32, oneshot::Sender<Result<bool, JsValue>>>>>,
|
||||
) -> Result<WasmPipeHandle, JsValue> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
pending_pipe_creations.borrow_mut().insert(pipe_id, tx);
|
||||
|
||||
let request = CommunicationValue::new(CommunicationType::PipeRequest)
|
||||
.with_id(pipe_id)
|
||||
.add_typed_default(
|
||||
DataType::Description,
|
||||
DataValue::Str(description.to_string()),
|
||||
);
|
||||
let request_bytes = request
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(format!("encode failed: {}", e)))?;
|
||||
debug!(
|
||||
target = "mtp.wasm",
|
||||
pipe_id,
|
||||
description,
|
||||
frame_len = request_bytes.len(),
|
||||
"sending pipe request"
|
||||
);
|
||||
transport.send_frame(&request_bytes).await?;
|
||||
|
||||
Ok(WasmPipeHandle {
|
||||
pipe_id,
|
||||
description: description.to_string(),
|
||||
transport: transport.clone(),
|
||||
response_rx: Rc::new(RefCell::new(Some(rx))),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn wasm_accept_pipe(
|
||||
transport: &WasmTransport,
|
||||
pipe_id: u32,
|
||||
pending_pipes: &Rc<RefCell<HashMap<u32, oneshot::Sender<PipeReader>>>>,
|
||||
) -> Result<PipeReader, JsValue> {
|
||||
let resp = CommunicationValue::new(CommunicationType::PipeResponse)
|
||||
.with_id(pipe_id)
|
||||
.add_typed_default(DataType::Accepted, DataValue::BoolTrue);
|
||||
let resp_bytes = resp
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(format!("encode failed: {}", e)))?;
|
||||
debug!(
|
||||
target = "mtp.wasm",
|
||||
pipe_id,
|
||||
accepted = true,
|
||||
frame_len = resp_bytes.len(),
|
||||
"sending pipe response"
|
||||
);
|
||||
transport.send_frame(&resp_bytes).await?;
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
pending_pipes.borrow_mut().insert(pipe_id, tx);
|
||||
|
||||
rx.await
|
||||
.map_err(|_| js_error("pipe closed before stream arrived"))
|
||||
}
|
||||
|
||||
pub(crate) async fn wasm_deny_pipe(transport: &WasmTransport, pipe_id: u32) -> Result<(), JsValue> {
|
||||
let resp = CommunicationValue::new(CommunicationType::PipeResponse)
|
||||
.with_id(pipe_id)
|
||||
.add_typed_default(DataType::Accepted, DataValue::BoolFalse);
|
||||
let resp_bytes = resp
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(format!("encode failed: {}", e)))?;
|
||||
transport.send_frame(&resp_bytes).await
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ pub struct ConnectionConfig {
|
|||
pub(crate) server_certificate_hashes: Option<Vec<String>>,
|
||||
pub(crate) client_id: u64,
|
||||
pub(crate) max_message_size: u32,
|
||||
pub(crate) require_pq: bool,
|
||||
pub(crate) description: Option<String>,
|
||||
}
|
||||
|
||||
|
|
@ -24,7 +25,8 @@ impl ConnectionConfig {
|
|||
url,
|
||||
server_certificate_hashes: None,
|
||||
client_id: 0,
|
||||
max_message_size: 1_000_000_000,
|
||||
max_message_size: 16 * 1024 * 1024,
|
||||
require_pq: true,
|
||||
description: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -59,6 +61,16 @@ impl ConnectionConfig {
|
|||
self.max_message_size
|
||||
}
|
||||
|
||||
#[wasm_bindgen(setter)]
|
||||
pub fn set_require_pq(&mut self, require_pq: bool) {
|
||||
self.require_pq = require_pq;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn require_pq(&self) -> bool {
|
||||
self.require_pq
|
||||
}
|
||||
|
||||
#[wasm_bindgen(setter)]
|
||||
pub fn set_description(&mut self, description: String) {
|
||||
self.description = Some(description);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use wasm_bindgen::prelude::*;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use mtp_crypto::{
|
||||
AeadDecrypt, AeadEncrypt, ChaCha20Poly1305, Ed25519Signer, HybridKem, KemPrivateKey,
|
||||
|
|
@ -22,14 +23,14 @@ impl WasmKeyring {
|
|||
/// Serialise the keyring to bytes.
|
||||
#[wasm_bindgen]
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
self.inner.to_bytes()
|
||||
self.inner.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
/// Deserialise a keyring from bytes.
|
||||
#[wasm_bindgen]
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<WasmKeyring, JsValue> {
|
||||
let inner = Keyring::from_bytes(bytes)
|
||||
.map_err(|e| js_error(&format!("Keyring::from_bytes: {}", e)))?;
|
||||
.map_err(|e| js_error(format!("Keyring::from_bytes: {}", e)))?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
|
|
@ -45,7 +46,7 @@ impl WasmKeyring {
|
|||
/// Generate a full keyring with KEM, ML-DSA, and Ed25519 keys.
|
||||
#[wasm_bindgen]
|
||||
pub fn keyring_generate() -> Vec<u8> {
|
||||
Keyring::generate().to_bytes()
|
||||
Keyring::generate().to_bytes().to_vec()
|
||||
}
|
||||
|
||||
/// Build a [`Keyring`] containing only an Ed25519 keypair (no KEM, no ML-DSA).
|
||||
|
|
@ -68,7 +69,7 @@ pub fn keyring_from_ed25519(secret_key: &[u8], public_key: &[u8]) -> Result<Vec<
|
|||
SignaturePublicKey::new(public_key.to_vec()),
|
||||
SignaturePrivateKey::new(secret_key.to_vec()),
|
||||
);
|
||||
Ok(keyring.to_bytes())
|
||||
Ok(keyring.to_bytes().to_vec())
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
|
@ -105,7 +106,7 @@ impl WasmPublicKeyBundle {
|
|||
#[wasm_bindgen]
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<WasmPublicKeyBundle, JsValue> {
|
||||
let inner = PublicKeyBundle::from_bytes(bytes)
|
||||
.map_err(|e| js_error(&format!("PublicKeyBundle::from_bytes: {}", e)))?;
|
||||
.map_err(|e| js_error(format!("PublicKeyBundle::from_bytes: {}", e)))?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
}
|
||||
|
|
@ -121,7 +122,7 @@ impl WasmPublicKeyBundle {
|
|||
/// decapsulate and recover the same shared secret.
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmEncapsulated {
|
||||
inner_shared_secret: Vec<u8>,
|
||||
inner_shared_secret: Zeroizing<Vec<u8>>,
|
||||
inner_ciphertext: Vec<u8>,
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +131,7 @@ impl WasmEncapsulated {
|
|||
/// Symmetric secret derived during encapsulation.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn shared_secret(&self) -> Vec<u8> {
|
||||
self.inner_shared_secret.clone()
|
||||
self.inner_shared_secret.to_vec()
|
||||
}
|
||||
|
||||
/// KEM ciphertext to transmit to the recipient.
|
||||
|
|
@ -149,7 +150,7 @@ impl WasmEncapsulated {
|
|||
pub fn wasm_kem_encapsulate(recipient_public_key: &[u8]) -> Result<WasmEncapsulated, JsValue> {
|
||||
let pk = KemPublicKey::new(recipient_public_key.to_vec());
|
||||
let enc = HybridKem::encapsulate(&pk)
|
||||
.map_err(|e| js_error(&format!("kem_encapsulate failed: {}", e)))?;
|
||||
.map_err(|e| js_error(format!("kem_encapsulate failed: {}", e)))?;
|
||||
Ok(WasmEncapsulated {
|
||||
inner_shared_secret: enc.shared_secret,
|
||||
inner_ciphertext: enc.ciphertext,
|
||||
|
|
@ -167,7 +168,8 @@ pub fn wasm_kem_decapsulate(
|
|||
) -> Result<Vec<u8>, JsValue> {
|
||||
let sk = KemPrivateKey::new(recipient_private_key.to_vec());
|
||||
HybridKem::decapsulate(&sk, ciphertext)
|
||||
.map_err(|e| js_error(&format!("kem_decapsulate failed: {}", e)))
|
||||
.map(|secret| secret.to_vec())
|
||||
.map_err(|e| js_error(format!("kem_decapsulate failed: {}", e)))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
|
@ -200,7 +202,7 @@ impl WasmChaCha20Poly1305 {
|
|||
pub fn encrypt(&self, plaintext: &[u8], aad: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||
self.inner
|
||||
.encrypt(plaintext, aad)
|
||||
.map_err(|e| js_error(&format!("encrypt failed: {}", e)))
|
||||
.map_err(|e| js_error(format!("encrypt failed: {}", e)))
|
||||
}
|
||||
|
||||
/// Decrypt `nonce || ciphertext` with `aad`.
|
||||
|
|
@ -208,7 +210,7 @@ impl WasmChaCha20Poly1305 {
|
|||
pub fn decrypt(&self, ciphertext: &[u8], aad: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||
self.inner
|
||||
.decrypt(ciphertext, aad)
|
||||
.map_err(|e| js_error(&format!("decrypt failed: {}", e)))
|
||||
.map_err(|e| js_error(format!("decrypt failed: {}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,7 +230,7 @@ impl WasmEd25519Signer {
|
|||
pub fn new(secret_key: Vec<u8>) -> Result<WasmEd25519Signer, JsValue> {
|
||||
let sk = SignaturePrivateKey::new(secret_key);
|
||||
let inner =
|
||||
Ed25519Signer::new(&sk).map_err(|e| js_error(&format!("Ed25519Signer::new: {}", e)))?;
|
||||
Ed25519Signer::new(&sk).map_err(|e| js_error(format!("Ed25519Signer::new: {}", e)))?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
|
|
@ -237,7 +239,7 @@ impl WasmEd25519Signer {
|
|||
pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||
self.inner
|
||||
.sign(message)
|
||||
.map_err(|e| js_error(&format!("sign failed: {}", e)))
|
||||
.map_err(|e| js_error(format!("sign failed: {}", e)))
|
||||
}
|
||||
|
||||
/// Verify `signature` against `message`.
|
||||
|
|
@ -245,7 +247,7 @@ impl WasmEd25519Signer {
|
|||
pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), JsValue> {
|
||||
self.inner
|
||||
.verify(message, signature)
|
||||
.map_err(|e| js_error(&format!("verify failed: {}", e)))
|
||||
.map_err(|e| js_error(format!("verify failed: {}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -290,7 +292,7 @@ pub fn ed25519_verify(
|
|||
) -> Result<(), JsValue> {
|
||||
let pk = SignaturePublicKey::new(public_key);
|
||||
mtp_crypto::verify_ed25519(&pk, message, signature)
|
||||
.map_err(|e| js_error(&format!("verify_ed25519 failed: {}", e)))
|
||||
.map_err(|e| js_error(format!("verify_ed25519 failed: {}", e)))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
|
@ -322,7 +324,7 @@ pub fn wasm_hkdf_expand(
|
|||
len: usize,
|
||||
) -> Result<Vec<u8>, JsValue> {
|
||||
mtp_crypto::hkdf_expand(ikm, salt, info, len)
|
||||
.map_err(|e| js_error(&format!("hkdf_expand failed: {}", e)))
|
||||
.map_err(|e| js_error(format!("hkdf_expand failed: {}", e)))
|
||||
}
|
||||
|
||||
/// Derive a 32-byte encryption key from `ikm` with `salt` and `context`.
|
||||
|
|
@ -334,7 +336,7 @@ pub fn wasm_derive_encryption_key(
|
|||
) -> Result<Vec<u8>, JsValue> {
|
||||
mtp_crypto::derive_encryption_key(ikm, salt, context)
|
||||
.map(|key| key.to_vec())
|
||||
.map_err(|e| js_error(&format!("derive_encryption_key failed: {}", e)))
|
||||
.map_err(|e| js_error(format!("derive_encryption_key failed: {}", e)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
use wasm_bindgen::{JsCast, prelude::*};
|
||||
|
||||
use mtp_codec::{
|
||||
CommunicationType, CommunicationValue, DataType, DataValue, communication_type_name,
|
||||
data_type_name,
|
||||
};
|
||||
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue, PROTOCOL_VERSION};
|
||||
use mtp_type_map::TypeMap;
|
||||
|
||||
use crate::error::js_error;
|
||||
|
|
@ -25,40 +22,40 @@ fn set_prop(obj: &js_sys::Object, key: &str, value: &JsValue) -> Result<(), JsVa
|
|||
}
|
||||
|
||||
fn integer_value(value: &str) -> JsValue {
|
||||
if let Ok(number) = value.parse::<f64>() {
|
||||
if number.fract() == 0.0 && number.abs() <= 9_007_199_254_740_991.0 {
|
||||
return JsValue::from_f64(number);
|
||||
}
|
||||
if let Ok(number) = value.parse::<f64>()
|
||||
&& number.fract() == 0.0
|
||||
&& number.abs() <= 9_007_199_254_740_991.0
|
||||
{
|
||||
return JsValue::from_f64(number);
|
||||
}
|
||||
JsValue::from_str(value)
|
||||
}
|
||||
|
||||
fn data_value_to_js(value: &DataValue) -> Result<JsValue, JsValue> {
|
||||
fn data_value_to_js(value: &DataValue, tm: &TypeMap) -> Result<JsValue, JsValue> {
|
||||
match value {
|
||||
DataValue::BoolTrue => Ok(JsValue::TRUE),
|
||||
DataValue::BoolFalse => Ok(JsValue::FALSE),
|
||||
DataValue::Bool(v) => Ok(JsValue::from_bool(*v)),
|
||||
DataValue::SignedNumber(n) => Ok(integer_value(&n.to_string())),
|
||||
DataValue::UnsignedNumber(n) => Ok(integer_value(&n.to_string())),
|
||||
DataValue::Float(exp, mant) => {
|
||||
Ok(JsValue::from_f64((*mant as f64) * 10f64.powi(*exp as i32)))
|
||||
}
|
||||
DataValue::Float(value) => Ok(JsValue::from_f64(*value)),
|
||||
DataValue::Str(s) => Ok(JsValue::from_str(s)),
|
||||
DataValue::Bytes(bytes) => Ok(js_sys::Uint8Array::from(&bytes[..]).into()),
|
||||
DataValue::Array(values) => {
|
||||
let arr = js_sys::Array::new();
|
||||
for value in values {
|
||||
arr.push(&data_value_to_js(value)?);
|
||||
arr.push(&data_value_to_js(value, tm)?);
|
||||
}
|
||||
Ok(arr.into())
|
||||
}
|
||||
DataValue::Container(entries) => {
|
||||
let obj = js_sys::Object::new();
|
||||
for (key, value) in entries {
|
||||
let name = data_type_name(key.0)
|
||||
let name = tm
|
||||
.data_type_name(key.0)
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| key.0.to_string());
|
||||
set_prop(&obj, &name, &data_value_to_js(value)?)?;
|
||||
set_prop(&obj, &name, &data_value_to_js(value, tm)?)?;
|
||||
}
|
||||
Ok(obj.into())
|
||||
}
|
||||
|
|
@ -71,7 +68,8 @@ fn data_value_to_js(value: &DataValue) -> Result<JsValue, JsValue> {
|
|||
}
|
||||
}
|
||||
|
||||
fn js_to_data_value(value: &JsValue) -> Result<DataValue, JsValue> {
|
||||
const MAX_SAFE_INT: f64 = 9007199254740991.0; // 2^53 - 1
|
||||
fn js_to_data_value(value: &JsValue, tm: &TypeMap) -> Result<DataValue, JsValue> {
|
||||
if value.is_null() || value.is_undefined() {
|
||||
return Ok(DataValue::Null);
|
||||
}
|
||||
|
|
@ -88,19 +86,25 @@ fn js_to_data_value(value: &JsValue) -> Result<DataValue, JsValue> {
|
|||
let array = js_sys::Array::from(value);
|
||||
let mut values = Vec::with_capacity(array.length() as usize);
|
||||
for item in array.iter() {
|
||||
values.push(js_to_data_value(&item)?);
|
||||
values.push(js_to_data_value(&item, tm)?);
|
||||
}
|
||||
return Ok(DataValue::Array(values));
|
||||
}
|
||||
if let Some(v) = value.as_f64() {
|
||||
if v.fract() == 0.0 {
|
||||
if v >= 0.0 {
|
||||
return Ok(DataValue::UnsignedNumber(v as u128));
|
||||
if let Some(v) = value.as_f64() {
|
||||
if v.is_finite()
|
||||
&& v.fract() == 0.0
|
||||
&& (-(MAX_SAFE_INT + 1.0)..=MAX_SAFE_INT).contains(&v)
|
||||
{
|
||||
if v >= 0.0 {
|
||||
return Ok(DataValue::UnsignedNumber(v as u128));
|
||||
} else {
|
||||
return Ok(DataValue::SignedNumber(v as i128));
|
||||
}
|
||||
}
|
||||
return Ok(DataValue::SignedNumber(v as i128));
|
||||
return Ok(DataValue::Float(v));
|
||||
}
|
||||
let mantissa = (v * 1_000_000.0).round() as u32;
|
||||
return Ok(DataValue::Float(246, mantissa));
|
||||
return Ok(DataValue::Float(v));
|
||||
}
|
||||
|
||||
let type_name = value.js_typeof().as_string().unwrap_or_default();
|
||||
|
|
@ -131,12 +135,15 @@ fn js_to_data_value(value: &JsValue) -> Result<DataValue, JsValue> {
|
|||
.as_string()
|
||||
.ok_or_else(|| js_error("object key must be a string"))?;
|
||||
let data_type = DataType::from_name(&key)
|
||||
.ok_or_else(|| js_error(&format!("unknown data type: {key}")))?;
|
||||
.ok_or_else(|| js_error(format!("unknown data type: {key}")))?;
|
||||
let value = js_sys::Reflect::get(&object, &JsValue::from_str(&key))?;
|
||||
entries.push((
|
||||
data_type.to_id(&TypeMap::latest()),
|
||||
js_to_data_value(&value)?,
|
||||
));
|
||||
let id = data_type.try_to_id(tm).ok_or_else(|| {
|
||||
js_error(format!(
|
||||
"data type {key} is not available in protocol version {}",
|
||||
tm.version
|
||||
))
|
||||
})?;
|
||||
entries.push((id, js_to_data_value(&value, tm)?));
|
||||
}
|
||||
return Ok(DataValue::Container(entries));
|
||||
}
|
||||
|
|
@ -150,7 +157,7 @@ fn option_u32(options: &JsValue, key: &str) -> Result<Option<u32>, JsValue> {
|
|||
return Ok(None);
|
||||
}
|
||||
let Some(n) = value.as_f64() else {
|
||||
return Err(js_error(&format!("{key} must be a number")));
|
||||
return Err(js_error(format!("{key} must be a number")));
|
||||
};
|
||||
Ok(Some(n as u32))
|
||||
}
|
||||
|
|
@ -173,14 +180,18 @@ fn option_u64(options: &JsValue, key: &str) -> Result<Option<u64>, JsValue> {
|
|||
return as_string
|
||||
.parse::<u64>()
|
||||
.map(Some)
|
||||
.map_err(|_| js_error(&format!("{key} out of range")));
|
||||
.map_err(|_| js_error(format!("{key} out of range")));
|
||||
}
|
||||
Err(js_error(&format!("{key} must be a number or bigint")))
|
||||
Err(js_error(format!("{key} must be a number or bigint")))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_frame_value(frame: &[u8]) -> Result<JsValue, JsValue> {
|
||||
let comm = CommunicationValue::from_bytes(frame)
|
||||
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
|
||||
.map_err(|e| js_error(format!("parse failed: {}", e)))?;
|
||||
let tm = comm
|
||||
.type_map()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| TypeMap::new(PROTOCOL_VERSION));
|
||||
let obj = js_sys::Object::new();
|
||||
let data = js_sys::Object::new();
|
||||
|
||||
|
|
@ -188,7 +199,8 @@ pub(crate) fn parse_frame_value(frame: &[u8]) -> Result<JsValue, JsValue> {
|
|||
set_prop(&obj, "id", &JsValue::from_f64(comm.get_id() as f64))?;
|
||||
}
|
||||
|
||||
let frame_type = communication_type_name(comm.get_type().0)
|
||||
let frame_type = tm
|
||||
.communication_type_name(comm.get_type().0)
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| comm.get_type().0.to_string());
|
||||
set_prop(&obj, "type", &JsValue::from_str(&frame_type))?;
|
||||
|
|
@ -209,10 +221,11 @@ pub(crate) fn parse_frame_value(frame: &[u8]) -> Result<JsValue, JsValue> {
|
|||
}
|
||||
|
||||
for (key, value) in comm.data() {
|
||||
let name = data_type_name(key.0)
|
||||
let name = tm
|
||||
.data_type_name(key.0)
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| key.0.to_string());
|
||||
set_prop(&data, &name, &data_value_to_js(value)?)?;
|
||||
set_prop(&data, &name, &data_value_to_js(value, &tm)?)?;
|
||||
}
|
||||
set_prop(&obj, "data", &data.into())?;
|
||||
set_prop(&obj, "raw", &js_sys::Uint8Array::from(frame).into())?;
|
||||
|
|
@ -244,14 +257,14 @@ pub fn build_ping_frame(
|
|||
}
|
||||
|
||||
msg.to_bytes()
|
||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))
|
||||
.map_err(|e| js_error(format!("encode failed: {}", e)))
|
||||
}
|
||||
|
||||
/// Parse an auth response frame into a JS object.
|
||||
#[wasm_bindgen]
|
||||
#[wasm_bindgen(unchecked_return_type = "AuthResponse")]
|
||||
pub fn parse_auth_response(response: &[u8]) -> Result<JsValue, JsValue> {
|
||||
let comm = CommunicationValue::from_bytes(response)
|
||||
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
|
||||
.map_err(|e| js_error(format!("parse failed: {}", e)))?;
|
||||
|
||||
let connected = matches!(comm.get_data(DataType::Connected), DataValue::BoolTrue);
|
||||
|
||||
|
|
@ -282,10 +295,20 @@ pub fn parse_auth_response(response: &[u8]) -> Result<JsValue, JsValue> {
|
|||
js_sys::Reflect::set(&obj, &"clientNonce".into(), &arr).ok();
|
||||
}
|
||||
if let Some(id) = assigned_id {
|
||||
js_sys::Reflect::set(&obj, &"assignedId".into(), &JsValue::from(id as f64)).ok();
|
||||
js_sys::Reflect::set(
|
||||
&obj,
|
||||
&"assignedId".into(),
|
||||
&JsValue::bigint_from_str(&id.to_string()),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
if let Some(ts) = timestamp {
|
||||
js_sys::Reflect::set(&obj, &"timestamp".into(), &JsValue::from(ts as f64)).ok();
|
||||
js_sys::Reflect::set(
|
||||
&obj,
|
||||
&"timestamp".into(),
|
||||
&JsValue::bigint_from_str(&ts.to_string()),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
if let Some(sig) = signature {
|
||||
let arr = js_sys::Uint8Array::from(&sig[..]);
|
||||
|
|
@ -299,7 +322,7 @@ pub fn parse_auth_response(response: &[u8]) -> Result<JsValue, JsValue> {
|
|||
#[wasm_bindgen]
|
||||
pub fn format_frame(frame: &[u8]) -> Result<String, JsValue> {
|
||||
let comm = CommunicationValue::from_bytes(frame)
|
||||
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
|
||||
.map_err(|e| js_error(format!("parse failed: {}", e)))?;
|
||||
Ok(comm.to_string())
|
||||
}
|
||||
|
||||
|
|
@ -317,7 +340,8 @@ pub fn build_frame(
|
|||
options: JsValue,
|
||||
) -> Result<Vec<u8>, JsValue> {
|
||||
let comm_type = CommunicationType::from_name(message_type)
|
||||
.ok_or_else(|| js_error(&format!("unknown communication type: {message_type}")))?;
|
||||
.ok_or_else(|| js_error(format!("unknown communication type: {message_type}")))?;
|
||||
let tm = TypeMap::new(PROTOCOL_VERSION);
|
||||
let mut msg = CommunicationValue::new(comm_type);
|
||||
|
||||
if !options.is_null() && !options.is_undefined() {
|
||||
|
|
@ -341,12 +365,15 @@ pub fn build_frame(
|
|||
.as_string()
|
||||
.ok_or_else(|| js_error("object key must be a string"))?;
|
||||
let data_type = DataType::from_name(&key)
|
||||
.ok_or_else(|| js_error(&format!("unknown data type: {key}")))?;
|
||||
.ok_or_else(|| js_error(format!("unknown data type: {key}")))?;
|
||||
let value = js_sys::Reflect::get(&object, &JsValue::from_str(&key))?;
|
||||
msg = msg.add_data(
|
||||
data_type.to_id(&TypeMap::latest()),
|
||||
js_to_data_value(&value)?,
|
||||
);
|
||||
let id = data_type.try_to_id(&tm).ok_or_else(|| {
|
||||
js_error(format!(
|
||||
"data type {key} is not available in protocol version {}",
|
||||
tm.version
|
||||
))
|
||||
})?;
|
||||
msg = msg.add_data(id, js_to_data_value(&value, &tm)?);
|
||||
}
|
||||
} else if !data.is_null() && !data.is_undefined() {
|
||||
return Err(js_error(
|
||||
|
|
@ -355,7 +382,7 @@ pub fn build_frame(
|
|||
}
|
||||
|
||||
msg.to_bytes()
|
||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))
|
||||
.map_err(|e| js_error(format!("encode failed: {}", e)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -370,7 +397,10 @@ mod tests {
|
|||
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
|
||||
let tm = TypeMap::latest();
|
||||
|
||||
assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm));
|
||||
assert_eq!(
|
||||
cv.get_type(),
|
||||
CommunicationType::Ping.try_to_id(&tm).unwrap()
|
||||
);
|
||||
assert_eq!(cv.get_sender(), 42);
|
||||
assert_eq!(
|
||||
cv.get_data(DataType::Description),
|
||||
|
|
@ -389,7 +419,10 @@ mod tests {
|
|||
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
|
||||
let tm = TypeMap::latest();
|
||||
|
||||
assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm));
|
||||
assert_eq!(
|
||||
cv.get_type(),
|
||||
CommunicationType::Ping.try_to_id(&tm).unwrap()
|
||||
);
|
||||
assert_eq!(cv.get_sender(), 99);
|
||||
assert_eq!(
|
||||
cv.get_data(DataType::Description),
|
||||
|
|
@ -412,13 +445,27 @@ mod tests {
|
|||
assert_eq!(cv.get_sender(), 0);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn fractional_float_roundtrips_without_corruption() {
|
||||
let tm = TypeMap::new(PROTOCOL_VERSION);
|
||||
for expected in [-12.5, 0.125, 1.5e200] {
|
||||
let encoded = js_to_data_value(&JsValue::from_f64(expected), &tm)
|
||||
.expect("JS float should encode");
|
||||
assert_eq!(encoded, DataValue::Float(expected));
|
||||
let decoded = data_value_to_js(&encoded, &tm).expect("float should decode");
|
||||
assert_eq!(decoded.as_f64(), Some(expected));
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn parse_auth_response_success() {
|
||||
const ASSIGNED_ID: u128 = 9_007_199_254_740_993;
|
||||
const TIMESTAMP: u128 = 9_007_199_254_740_995;
|
||||
let resp = CommunicationValue::new(CommunicationType::IdentificationResponse)
|
||||
.add_typed_default(DataType::Connected, DataValue::BoolTrue)
|
||||
.add_typed_default(DataType::ClientNonce, DataValue::UnsignedNumber(999))
|
||||
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(42))
|
||||
.add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(12345))
|
||||
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(ASSIGNED_ID))
|
||||
.add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(TIMESTAMP))
|
||||
.to_bytes()
|
||||
.expect("encode failed");
|
||||
|
||||
|
|
@ -430,9 +477,20 @@ mod tests {
|
|||
assert_eq!(connected, Some(true));
|
||||
|
||||
let id = js_sys::Reflect::get(&result, &"assignedId".into())
|
||||
.ok()
|
||||
.and_then(|v| v.as_f64());
|
||||
assert_eq!(id, Some(42.0));
|
||||
.expect("assignedId should be present")
|
||||
.unchecked_into::<js_sys::BigInt>()
|
||||
.to_string(10)
|
||||
.expect("assignedId should stringify")
|
||||
.as_string();
|
||||
assert_eq!(id.as_deref(), Some("9007199254740993"));
|
||||
|
||||
let timestamp = js_sys::Reflect::get(&result, &"timestamp".into())
|
||||
.expect("timestamp should be present")
|
||||
.unchecked_into::<js_sys::BigInt>()
|
||||
.to_string(10)
|
||||
.expect("timestamp should stringify")
|
||||
.as_string();
|
||||
assert_eq!(timestamp.as_deref(), Some("9007199254740995"));
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
pub mod auth;
|
||||
pub mod client;
|
||||
pub mod client_pipe;
|
||||
pub mod config;
|
||||
pub mod crypto;
|
||||
pub mod error;
|
||||
|
|
@ -18,4 +20,5 @@ use wasm_bindgen::prelude::*;
|
|||
#[wasm_bindgen(start)]
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
logging::init_tracing();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub(crate) fn init_tracing() {
|
||||
use tracing::Level;
|
||||
use wasm_tracing::prelude::WasmLayerConfig;
|
||||
|
||||
let config = WasmLayerConfig::new()
|
||||
.set_max_level(Level::DEBUG)
|
||||
.to_owned();
|
||||
let _ = wasm_tracing::set_as_global_default_with_config(config);
|
||||
}
|
||||
|
||||
/// Log severity used by the public SDK when translating raw WASM events.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ impl PipeWriter {
|
|||
.map_err(|_| js_error("write not a function"))?;
|
||||
let write_promise = write_fn
|
||||
.call1(&self.writer, &chunk)
|
||||
.map_err(|e| js_error(&format!("write failed: {:?}", e)))?;
|
||||
.map_err(|e| js_error(format!("write failed: {:?}", e)))?;
|
||||
JsFuture::from(write_promise.unchecked_into::<js_sys::Promise>()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -55,7 +55,7 @@ impl PipeWriter {
|
|||
.map_err(|_| js_error("close not a function"))?;
|
||||
let close_promise = close_fn
|
||||
.call0(&self.writer)
|
||||
.map_err(|e| js_error(&format!("close failed: {:?}", e)))?;
|
||||
.map_err(|e| js_error(format!("close failed: {:?}", e)))?;
|
||||
if let Err(e) = JsFuture::from(close_promise.unchecked_into::<js_sys::Promise>()).await {
|
||||
crate::transport::log_stream_error_code(&e, "pipe writer close");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,12 +46,11 @@ pub(crate) fn log_stream_error_code(error: &JsValue, context: &str) {
|
|||
None => format!("[WasmTransport] {context}: stream error ({message})"),
|
||||
};
|
||||
|
||||
if let Ok(console) = js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("console")) {
|
||||
if let Ok(warn) = js_sys::Reflect::get(&console, &JsValue::from_str("warn"))
|
||||
if let Ok(console) = js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("console"))
|
||||
&& let Ok(warn) = js_sys::Reflect::get(&console, &JsValue::from_str("warn"))
|
||||
.and_then(|f| f.dyn_into::<js_sys::Function>())
|
||||
{
|
||||
let _ = warn.call1(&console, &JsValue::from_str(&formatted));
|
||||
}
|
||||
{
|
||||
let _ = warn.call1(&console, &JsValue::from_str(&formatted));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +77,7 @@ fn resolve_stream_readable(recv_stream: &JsValue) -> Result<JsValue, JsValue> {
|
|||
/// Releases a writer's lock so an abandoned writer isn't treated as an abort (which sends STOP_SENDING).
|
||||
pub(crate) fn release_writer_lock(writer: &JsValue) {
|
||||
if let Ok(release) = js_sys::Reflect::get(writer, &JsValue::from_str("releaseLock"))
|
||||
.and_then(|f| f.dyn_into::<js_sys::Function>().map_err(Into::into))
|
||||
.and_then(|f| f.dyn_into::<js_sys::Function>())
|
||||
{
|
||||
let _ = release.call0(writer);
|
||||
}
|
||||
|
|
@ -87,7 +86,7 @@ pub(crate) fn release_writer_lock(writer: &JsValue) {
|
|||
/// Releases a reader's lock so an abandoned reader isn't treated as a cancel (which sends STOP_SENDING).
|
||||
pub(crate) fn release_reader_lock(reader: &JsValue) {
|
||||
if let Ok(release) = js_sys::Reflect::get(reader, &JsValue::from_str("releaseLock"))
|
||||
.and_then(|f| f.dyn_into::<js_sys::Function>().map_err(Into::into))
|
||||
.and_then(|f| f.dyn_into::<js_sys::Function>())
|
||||
{
|
||||
let _ = release.call0(reader);
|
||||
}
|
||||
|
|
@ -181,7 +180,7 @@ impl WasmTransport {
|
|||
.map_err(|_| js_error("WebTransport.ready is not a Promise"))?;
|
||||
JsFuture::from(ready)
|
||||
.await
|
||||
.map_err(|e| js_error(&format!("WebTransport ready failed: {:?}", e)))?;
|
||||
.map_err(|e| js_error(format!("WebTransport ready failed: {:?}", e)))?;
|
||||
Ok(Self {
|
||||
inner: transport,
|
||||
max_message_size,
|
||||
|
|
@ -237,7 +236,7 @@ impl WasmTransport {
|
|||
.map_err(|_| js_error("write not a function"))?;
|
||||
let write_promise = write_fn
|
||||
.call1(&writer_val, &chunk)
|
||||
.map_err(|e| js_error(&format!("write failed: {:?}", e)))?;
|
||||
.map_err(|e| js_error(format!("write failed: {:?}", e)))?;
|
||||
if let Err(e) = JsFuture::from(write_promise.unchecked_into::<js_sys::Promise>()).await {
|
||||
log_stream_error_code(&e, "send_frame write");
|
||||
release_writer_lock(&writer_val);
|
||||
|
|
@ -250,7 +249,7 @@ impl WasmTransport {
|
|||
.map_err(|_| js_error("close not a function"))?;
|
||||
let close_promise = close_fn
|
||||
.call0(&writer_val)
|
||||
.map_err(|e| js_error(&format!("close failed: {:?}", e)))?;
|
||||
.map_err(|e| js_error(format!("close failed: {:?}", e)))?;
|
||||
if let Err(e) = JsFuture::from(close_promise.unchecked_into::<js_sys::Promise>()).await {
|
||||
// Write succeeded; STOP_SENDING on close just means peer stopped reading before FIN.
|
||||
log_stream_error_code(&e, "send_frame close");
|
||||
|
|
@ -298,7 +297,7 @@ impl WasmTransport {
|
|||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
log_stream_error_code(&e, "open_next_stream accept");
|
||||
return Err(js_error(&format!("accept stream failed: {:?}", e)));
|
||||
return Err(js_error(format!("accept stream failed: {:?}", e)));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -344,7 +343,7 @@ impl WasmTransport {
|
|||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
log_stream_error_code(&e, "read_chunk");
|
||||
return Err(js_error(&format!("read failed: {:?}", e)));
|
||||
return Err(js_error(format!("read failed: {:?}", e)));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -362,7 +361,7 @@ impl WasmTransport {
|
|||
}
|
||||
|
||||
/// Try to pull one complete frame out of the buffer without reading more.
|
||||
fn parse_buffer(&self) -> Result<Option<FrameOutcome>, JsValue> {
|
||||
fn parse_buffer(&self, max_message_size: u32) -> Result<Option<FrameOutcome>, JsValue> {
|
||||
let buf = self.buffer.borrow();
|
||||
if buf.len() < 4 {
|
||||
return Ok(None);
|
||||
|
|
@ -371,7 +370,7 @@ impl WasmTransport {
|
|||
if frame_len == CLOSE_FRAME_LEN {
|
||||
return Ok(Some(FrameOutcome::Closed));
|
||||
}
|
||||
if frame_len > self.max_message_size {
|
||||
if frame_len > max_message_size {
|
||||
return Err(js_error("message too large"));
|
||||
}
|
||||
let frame_len = frame_len as usize;
|
||||
|
|
@ -393,9 +392,9 @@ impl WasmTransport {
|
|||
* persistent uni stream) or one-per-stream; both are handled by buffering
|
||||
* across reads and advancing to the next stream when the current one ends.
|
||||
*/
|
||||
async fn next_frame(&self) -> Result<FrameOutcome, JsValue> {
|
||||
async fn next_frame(&self, max_message_size: u32) -> Result<FrameOutcome, JsValue> {
|
||||
loop {
|
||||
if let Some(outcome) = self.parse_buffer()? {
|
||||
if let Some(outcome) = self.parse_buffer(max_message_size)? {
|
||||
return Ok(outcome);
|
||||
}
|
||||
|
||||
|
|
@ -407,7 +406,15 @@ impl WasmTransport {
|
|||
match self.read_chunk().await? {
|
||||
Some(chunk) => {
|
||||
if !chunk.is_empty() {
|
||||
self.buffer.borrow_mut().extend_from_slice(&chunk);
|
||||
let mut buffer = self.buffer.borrow_mut();
|
||||
let maximum_buffer = max_message_size as usize + 4;
|
||||
if buffer.len().saturating_add(chunk.len()) > maximum_buffer {
|
||||
return Err(js_error("message too large"));
|
||||
}
|
||||
buffer
|
||||
.try_reserve(chunk.len())
|
||||
.map_err(|_| js_error("message allocation failed"))?;
|
||||
buffer.extend_from_slice(&chunk);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
|
@ -430,7 +437,10 @@ impl WasmTransport {
|
|||
|
||||
/// Read exactly one application frame (used during the auth handshake).
|
||||
pub async fn read_one_frame(&self) -> Result<Vec<u8>, JsValue> {
|
||||
match self.next_frame().await? {
|
||||
match self
|
||||
.next_frame(self.max_message_size.min(64 * 1024))
|
||||
.await?
|
||||
{
|
||||
FrameOutcome::Frame(frame) => Ok(frame),
|
||||
FrameOutcome::Closed => Err(js_error("connection closed before frame")),
|
||||
FrameOutcome::Ended => Err(js_error("stream ended before frame")),
|
||||
|
|
@ -445,7 +455,7 @@ impl WasmTransport {
|
|||
F: FnMut(JsValue),
|
||||
{
|
||||
loop {
|
||||
match self.next_frame().await {
|
||||
match self.next_frame(self.max_message_size).await {
|
||||
Ok(FrameOutcome::Frame(frame)) => match parse_frame_value(&frame) {
|
||||
Ok(parsed) => {
|
||||
on_message(parsed);
|
||||
|
|
@ -477,16 +487,16 @@ impl WasmTransport {
|
|||
G: FnMut(crate::pipe::PipeReader),
|
||||
{
|
||||
let pipe_request_type =
|
||||
mtp_codec::CommunicationType::PipeRequest.to_id(&mtp_codec::TypeMap::latest());
|
||||
mtp_codec::CommunicationType::PipeRequest.try_to_id(&mtp_codec::TypeMap::latest());
|
||||
|
||||
loop {
|
||||
match self.next_frame().await {
|
||||
match self.next_frame(self.max_message_size).await {
|
||||
Ok(FrameOutcome::Frame(frame)) => {
|
||||
let is_first = self.new_stream_frame.get();
|
||||
if is_first {
|
||||
self.new_stream_frame.set(false);
|
||||
if let Ok(comm) = mtp_codec::CommunicationValue::from_bytes(&frame)
|
||||
&& comm.get_type() == pipe_request_type
|
||||
&& Some(comm.get_type()) == pipe_request_type
|
||||
{
|
||||
let pipe_id = comm.get_id();
|
||||
let description = comm
|
||||
|
|
@ -567,7 +577,7 @@ impl WasmTransport {
|
|||
);
|
||||
let frame_bytes = request
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
||||
.map_err(|e| js_error(format!("encode failed: {}", e)))?;
|
||||
|
||||
let len = frame_bytes.len() as u32;
|
||||
let mut wire = Vec::with_capacity(4 + frame_bytes.len());
|
||||
|
|
@ -581,7 +591,7 @@ impl WasmTransport {
|
|||
.map_err(|_| js_error("write not a function"))?;
|
||||
let write_promise = write_fn
|
||||
.call1(&writer_val, &chunk)
|
||||
.map_err(|e| js_error(&format!("write failed: {:?}", e)))?;
|
||||
.map_err(|e| js_error(format!("write failed: {:?}", e)))?;
|
||||
if let Err(e) = JsFuture::from(write_promise.unchecked_into::<js_sys::Promise>()).await {
|
||||
log_stream_error_code(&e, "open_pipe write");
|
||||
release_writer_lock(&writer_val);
|
||||
|
|
@ -601,7 +611,7 @@ impl WasmTransport {
|
|||
}
|
||||
|
||||
if let Ok(close) = js_sys::Reflect::get(&self.inner, &JsValue::from_str("close"))
|
||||
.and_then(|value| value.dyn_into::<js_sys::Function>().map_err(Into::into))
|
||||
.and_then(|value| value.dyn_into::<js_sys::Function>())
|
||||
{
|
||||
let _ = close.call1(&self.inner, &js_sys::Object::new());
|
||||
}
|
||||
|
|
|
|||
11
wasm/types/mtp_wasm.d.ts
vendored
11
wasm/types/mtp_wasm.d.ts
vendored
|
|
@ -23,8 +23,8 @@ export interface Ed25519GenerateResult {
|
|||
export interface AuthResponse {
|
||||
connected: boolean;
|
||||
clientNonce?: Uint8Array;
|
||||
assignedId?: number;
|
||||
timestamp?: number;
|
||||
assignedId?: bigint;
|
||||
timestamp?: bigint;
|
||||
signature?: Uint8Array;
|
||||
}
|
||||
|
||||
|
|
@ -50,6 +50,7 @@ export class ConnectionConfig implements DisposableWasmObject {
|
|||
client_id: bigint;
|
||||
description: string | undefined;
|
||||
max_message_size: number;
|
||||
require_pq: boolean;
|
||||
server_certificate_hashes: string[];
|
||||
readonly url: string;
|
||||
}
|
||||
|
|
@ -109,7 +110,11 @@ export class WasmClient implements DisposableWasmObject {
|
|||
): Promise<bigint>;
|
||||
connect(config: ConnectionConfig): Promise<void>;
|
||||
disconnect(): void;
|
||||
request(frame: Uint8Array, response_type?: string | null): Promise<ParsedFrame>;
|
||||
request(
|
||||
frame: Uint8Array,
|
||||
response_type?: string | null,
|
||||
timeout_ms?: number | null,
|
||||
): Promise<ParsedFrame>;
|
||||
send(frame: Uint8Array): Promise<void>;
|
||||
start_protocol_pings(interval_ms: number, client_id: bigint): void;
|
||||
stop_protocol_pings(): void;
|
||||
|
|
|
|||
Loading…
Reference in a new issue