[Fix] Harden MTP codec, transport, and SDK security

This commit is contained in:
Alex Emmet 2026-08-18 20:57:45 +02:00
commit a7e804c603
No known key found for this signature in database
73 changed files with 11892 additions and 5756 deletions

184
wasm/src/client/dispatch.rs Normal file
View file

@ -0,0 +1,184 @@
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, VecDeque};
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use crate::client::ConnectionState;
use crate::client_pipe::{self, PendingRequest};
pub(super) struct PingTimer {
pub(super) id: i32,
pub(super) closure: Closure<dyn FnMut()>,
}
pub(super) struct PendingPing {
pub(super) generation: u32,
pub(super) sent_at: f64,
}
pub(super) fn frame_property(frame: &JsValue, key: &str) -> Option<JsValue> {
js_sys::Reflect::get(frame, &JsValue::from_str(key))
.ok()
.filter(|value| !value.is_null() && !value.is_undefined())
}
pub(super) fn frame_id(frame: &JsValue) -> Option<u32> {
frame_property(frame, "id")
.and_then(|value| value.as_f64())
.filter(|value| {
value.is_finite() && value.fract() == 0.0 && (0.0..=u32::MAX as f64).contains(value)
})
.and_then(|value| u32::try_from(value as u64).ok())
}
pub(super) fn frame_type(frame: &JsValue) -> Option<String> {
frame_property(frame, "type").and_then(|value| value.as_string())
}
pub(super) fn route_incoming_frame(
frame: &JsValue,
generation: u32,
on_message: &js_sys::Function,
subscriptions: &Rc<RefCell<HashMap<u32, (String, js_sys::Function)>>>,
pending_requests: &Rc<RefCell<HashMap<u32, PendingRequest>>>,
expired_requests: &Rc<RefCell<HashMap<u32, f64>>>,
pending_pings: &Rc<RefCell<HashMap<u32, PendingPing>>>,
ping_ms: &Rc<Cell<Option<f64>>>,
) {
let message_type = frame_type(frame);
if message_type.as_deref() == Some("Pong")
&& let Some(ping_id) = frame_id(frame)
{
let sent_at = pending_pings
.borrow()
.get(&ping_id)
.filter(|ping| ping.generation == generation)
.map(|ping| ping.sent_at);
if let Some(sent_at) = sent_at {
pending_pings.borrow_mut().remove(&ping_id);
ping_ms.set(Some(js_sys::Date::now() - sent_at));
return;
}
}
if let Some(request_id) = frame_id(frame) {
let pending = {
let mut requests = pending_requests.borrow_mut();
if requests
.get(&request_id)
.is_some_and(|request| request.generation == generation)
{
requests.remove(&request_id)
} else {
None
}
};
if let Some(pending) = pending {
let type_matches = pending
.response_type
.as_ref()
.zip(message_type.as_ref())
.map(|(expected, actual)| expected == actual)
.unwrap_or(true);
if type_matches {
let _ = pending.sender.send(Ok(frame.clone()));
} else {
let actual = message_type.clone().unwrap_or_else(|| "unknown".into());
let _ = pending.sender.send(Err(crate::error::js_error(format!(
"unexpected response type: expected {}, got {}",
pending.response_type.unwrap_or_else(|| "unknown".into()),
actual
))));
}
return;
}
if client_pipe::consume_expired_request(expired_requests, request_id) {
return;
}
}
let _ = on_message.call1(&JsValue::NULL, frame);
let Some(message_type) = message_type else {
return;
};
let callbacks: Vec<js_sys::Function> = subscriptions
.borrow()
.iter()
.filter(|(_, (t, _))| t == &message_type)
.map(|(_, (_, cb))| cb.clone())
.collect();
for callback in callbacks {
let _ = callback.call1(&JsValue::NULL, frame);
}
}
pub(super) fn stop_ping_timer(ping_timer: &Rc<RefCell<Option<PingTimer>>>) {
let Some(timer) = ping_timer.borrow_mut().take() else {
return;
};
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>())
{
let _ = clear_interval.call1(&JsValue::NULL, &JsValue::from_f64(timer.id as f64));
}
drop(timer.closure);
}
pub(super) fn reject_pending_requests(
pending_requests: &Rc<RefCell<HashMap<u32, PendingRequest>>>,
message: &str,
) {
let pending = std::mem::take(&mut *pending_requests.borrow_mut());
for (_, pending) in pending {
let _ = pending.sender.send(Err(crate::error::js_error(message)));
}
}
pub(super) 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(())
}
pub(super) fn set_shared_state(
state: &Rc<Cell<ConnectionState>>,
pending_state_callbacks: &Rc<RefCell<VecDeque<ConnectionState>>>,
state_callback: &JsValue,
new_state: ConnectionState,
) {
state.set(new_state);
pending_state_callbacks.borrow_mut().push_back(new_state);
let global = js_sys::global();
let qmt = js_sys::Reflect::get(&global, &JsValue::from_str("queueMicrotask"))
.and_then(|f| f.dyn_into::<js_sys::Function>());
let scheduled = qmt
.and_then(|qmt| qmt.call1(&global, state_callback))
.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, state_callback, &JsValue::from_f64(0.0))
})
.is_err()
{
pending_state_callbacks.borrow_mut().pop_back();
}
}