[WIP] Security work While on holiday
This commit is contained in:
parent
a81ac4efca
commit
7f0231e3f1
109 changed files with 19694 additions and 5210 deletions
|
|
@ -1,12 +1,14 @@
|
|||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
|
||||
use futures_util::lock::Mutex as AsyncMutex;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
|
||||
use crate::error::js_error;
|
||||
use crate::frame::parse_frame_value;
|
||||
use crate::frame::parse_frame_value_with_type_map;
|
||||
use mtp_codec::TypeMap;
|
||||
|
||||
const CLOSE_FRAME_LEN: u32 = u32::MAX;
|
||||
|
||||
|
|
@ -126,6 +128,11 @@ pub struct WasmTransport {
|
|||
buffer: Rc<RefCell<Vec<u8>>>,
|
||||
/// Set to `true` when `open_next_stream` succeeds; cleared after the first frame is parsed.
|
||||
new_stream_frame: Rc<Cell<bool>>,
|
||||
/// A single ordered browser send stream shared by all cloned transports.
|
||||
outgoing_writer: Rc<RefCell<Option<JsValue>>>,
|
||||
/// Serializes stream creation and writes across concurrent callers.
|
||||
send_lock: Rc<AsyncMutex<()>>,
|
||||
type_map: Rc<RefCell<TypeMap>>,
|
||||
}
|
||||
|
||||
impl WasmTransport {
|
||||
|
|
@ -188,6 +195,9 @@ impl WasmTransport {
|
|||
stream_reader: Rc::new(RefCell::new(None)),
|
||||
buffer: Rc::new(RefCell::new(Vec::new())),
|
||||
new_stream_frame: Rc::new(Cell::new(false)),
|
||||
outgoing_writer: Rc::new(RefCell::new(None)),
|
||||
send_lock: Rc::new(AsyncMutex::new(())),
|
||||
type_map: Rc::new(RefCell::new(TypeMap::latest())),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -195,40 +205,48 @@ impl WasmTransport {
|
|||
&self.inner
|
||||
}
|
||||
|
||||
pub fn set_type_map(&self, type_map: &TypeMap) {
|
||||
*self.type_map.borrow_mut() = type_map.clone();
|
||||
}
|
||||
|
||||
pub fn type_map(&self) -> TypeMap {
|
||||
self.type_map.borrow().clone()
|
||||
}
|
||||
|
||||
pub async fn send_frame(&self, frame: &[u8]) -> Result<(), JsValue> {
|
||||
let _send_guard = self.send_lock.lock().await;
|
||||
if frame.len() as u64 > self.max_message_size as u64
|
||||
|| frame.len() as u64 >= CLOSE_FRAME_LEN as u64
|
||||
{
|
||||
return Err(js_error("message too large"));
|
||||
}
|
||||
|
||||
let create_stream = js_sys::Reflect::get(
|
||||
&self.inner,
|
||||
&JsValue::from_str("createUnidirectionalStream"),
|
||||
)?
|
||||
.dyn_into::<js_sys::Function>()
|
||||
.map_err(|_| js_error("createUnidirectionalStream not a function"))?;
|
||||
let stream_promise = create_stream
|
||||
.call0(&self.inner)?
|
||||
.dyn_into::<js_sys::Promise>()
|
||||
.map_err(|_| js_error("createUnidirectionalStream did not return a Promise"))?;
|
||||
let stream = JsFuture::from(stream_promise).await?;
|
||||
|
||||
let writable_or_stream = resolve_stream_writable(&stream)?;
|
||||
|
||||
let writer_val = js_sys::Reflect::get(&writable_or_stream, &JsValue::from_str("getWriter"))
|
||||
.map_err(|_| js_error("missing getWriter"))?
|
||||
let writer_val = if let Some(writer) = self.outgoing_writer.borrow().clone() {
|
||||
writer
|
||||
} else {
|
||||
let create_stream = js_sys::Reflect::get(
|
||||
&self.inner,
|
||||
&JsValue::from_str("createUnidirectionalStream"),
|
||||
)?
|
||||
.dyn_into::<js_sys::Function>()
|
||||
.map_err(|_| js_error("getWriter not a function"))?
|
||||
.call0(&writable_or_stream)
|
||||
.map_err(|_| js_error("getWriter call failed"))?;
|
||||
.map_err(|_| js_error("createUnidirectionalStream not a function"))?;
|
||||
let stream_promise = create_stream
|
||||
.call0(&self.inner)?
|
||||
.dyn_into::<js_sys::Promise>()
|
||||
.map_err(|_| js_error("createUnidirectionalStream did not return a Promise"))?;
|
||||
let stream = JsFuture::from(stream_promise).await?;
|
||||
let writable_or_stream = resolve_stream_writable(&stream)?;
|
||||
let writer = js_sys::Reflect::get(&writable_or_stream, &JsValue::from_str("getWriter"))
|
||||
.map_err(|_| js_error("missing getWriter"))?
|
||||
.dyn_into::<js_sys::Function>()
|
||||
.map_err(|_| js_error("getWriter not a function"))?
|
||||
.call0(&writable_or_stream)
|
||||
.map_err(|_| js_error("getWriter call failed"))?;
|
||||
*self.outgoing_writer.borrow_mut() = Some(writer.clone());
|
||||
writer
|
||||
};
|
||||
|
||||
let len = frame.len() as u32;
|
||||
let mut wire = Vec::with_capacity(4 + frame.len());
|
||||
wire.extend_from_slice(&len.to_be_bytes());
|
||||
wire.extend_from_slice(frame);
|
||||
|
||||
let chunk = js_sys::Uint8Array::from(&wire[..]);
|
||||
let chunk = js_sys::Uint8Array::from(frame);
|
||||
|
||||
let write_fn = js_sys::Reflect::get(&writer_val, &JsValue::from_str("write"))
|
||||
.map_err(|_| js_error("missing write"))?
|
||||
|
|
@ -239,25 +257,11 @@ impl WasmTransport {
|
|||
.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");
|
||||
self.outgoing_writer.borrow_mut().take();
|
||||
release_writer_lock(&writer_val);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
let close_fn = js_sys::Reflect::get(&writer_val, &JsValue::from_str("close"))
|
||||
.map_err(|_| js_error("missing close"))?
|
||||
.dyn_into::<js_sys::Function>()
|
||||
.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)))?;
|
||||
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");
|
||||
}
|
||||
|
||||
// Release the lock so the writer isn't treated as an abort.
|
||||
release_writer_lock(&writer_val);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -366,21 +370,21 @@ impl WasmTransport {
|
|||
if buf.len() < 4 {
|
||||
return Ok(None);
|
||||
}
|
||||
let frame_len = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
|
||||
if frame_len == CLOSE_FRAME_LEN {
|
||||
let body_len = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
|
||||
if body_len == CLOSE_FRAME_LEN {
|
||||
return Ok(Some(FrameOutcome::Closed));
|
||||
}
|
||||
let frame_len = body_len
|
||||
.checked_add(4)
|
||||
.ok_or_else(|| js_error("invalid frame length"))?;
|
||||
if frame_len > max_message_size {
|
||||
return Err(js_error("message too large"));
|
||||
}
|
||||
let frame_len = frame_len as usize;
|
||||
let Some(frame_end) = 4usize.checked_add(frame_len) else {
|
||||
return Err(js_error("invalid frame length"));
|
||||
};
|
||||
let frame_end = frame_len as usize;
|
||||
if frame_end > buf.len() {
|
||||
return Ok(None);
|
||||
}
|
||||
let frame = buf[4..frame_end].to_vec();
|
||||
let frame = buf[..frame_end].to_vec();
|
||||
drop(buf);
|
||||
self.buffer.borrow_mut().drain(..frame_end);
|
||||
Ok(Some(FrameOutcome::Frame(frame)))
|
||||
|
|
@ -456,15 +460,18 @@ impl WasmTransport {
|
|||
{
|
||||
loop {
|
||||
match self.next_frame(self.max_message_size).await {
|
||||
Ok(FrameOutcome::Frame(frame)) => match parse_frame_value(&frame) {
|
||||
Ok(parsed) => {
|
||||
on_message(parsed);
|
||||
Ok(FrameOutcome::Frame(frame)) => {
|
||||
let type_map = self.type_map();
|
||||
match parse_frame_value_with_type_map(&frame, &type_map) {
|
||||
Ok(parsed) => {
|
||||
on_message(parsed);
|
||||
}
|
||||
Err(e) => {
|
||||
let message = e.as_string().unwrap_or_else(|| format!("{:?}", e));
|
||||
let _ = on_error.call1(&JsValue::NULL, &JsValue::from_str(&message));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let message = e.as_string().unwrap_or_else(|| format!("{:?}", e));
|
||||
let _ = on_error.call1(&JsValue::NULL, &JsValue::from_str(&message));
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(FrameOutcome::Closed) | Ok(FrameOutcome::Ended) => break,
|
||||
Err(e) => {
|
||||
let _ = on_error.call1(&JsValue::NULL, &e);
|
||||
|
|
@ -487,19 +494,28 @@ impl WasmTransport {
|
|||
G: FnMut(crate::pipe::PipeReader),
|
||||
H: FnMut(JsValue),
|
||||
{
|
||||
let pipe_request_type =
|
||||
mtp_codec::CommunicationType::PipeRequest.try_to_id(&mtp_codec::TypeMap::latest());
|
||||
|
||||
loop {
|
||||
match self.next_frame(self.max_message_size).await {
|
||||
Ok(FrameOutcome::Frame(frame)) => {
|
||||
let type_map = self.type_map();
|
||||
let pipe_request_type =
|
||||
mtp_codec::CommunicationType::PipeRequest.try_to_id(&type_map);
|
||||
let pipe_response_type =
|
||||
mtp_codec::CommunicationType::PipeResponse.try_to_id(&type_map);
|
||||
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)
|
||||
if let Ok(comm) =
|
||||
mtp_codec::CommunicationValue::from_bytes_with(&frame, &type_map)
|
||||
&& Some(comm.get_type()) == pipe_request_type
|
||||
{
|
||||
let pipe_id = comm.get_id();
|
||||
let Some(pipe_id) = comm.id().filter(|id| *id != 0) else {
|
||||
on_error(JsValue::from_str(
|
||||
"PipeRequest frame must contain a non-zero id",
|
||||
));
|
||||
self.close();
|
||||
break;
|
||||
};
|
||||
let description = comm
|
||||
.get_str(mtp_codec::DataType::Description)
|
||||
.unwrap_or("")
|
||||
|
|
@ -523,7 +539,33 @@ impl WasmTransport {
|
|||
}
|
||||
}
|
||||
|
||||
match parse_frame_value(&frame) {
|
||||
if let Ok(comm) =
|
||||
mtp_codec::CommunicationValue::from_bytes_with(&frame, &type_map)
|
||||
&& Some(comm.get_type()) == pipe_response_type
|
||||
&& !matches!(comm.id(), Some(id) if id != 0)
|
||||
{
|
||||
on_error(JsValue::from_str(
|
||||
"PipeResponse frame must contain a non-zero id",
|
||||
));
|
||||
self.close();
|
||||
break;
|
||||
}
|
||||
|
||||
if let Ok(comm) =
|
||||
mtp_codec::CommunicationValue::from_bytes_with(&frame, &type_map)
|
||||
&& !matches!(comm.id(), Some(id) if id != 0)
|
||||
&& comm
|
||||
.get_type_name()
|
||||
.is_some_and(|name| name.ends_with("Response"))
|
||||
{
|
||||
on_error(JsValue::from_str(
|
||||
"response frame must contain a non-zero id",
|
||||
));
|
||||
self.close();
|
||||
break;
|
||||
}
|
||||
|
||||
match parse_frame_value_with_type_map(&frame, &type_map) {
|
||||
Ok(parsed) => {
|
||||
on_message(parsed);
|
||||
}
|
||||
|
|
@ -550,6 +592,7 @@ impl WasmTransport {
|
|||
pipe_id: u32,
|
||||
description: &str,
|
||||
) -> Result<crate::pipe::PipeWriter, JsValue> {
|
||||
let _send_guard = self.send_lock.lock().await;
|
||||
let create_stream = js_sys::Reflect::get(
|
||||
&self.inner,
|
||||
&JsValue::from_str("createUnidirectionalStream"),
|
||||
|
|
@ -570,22 +613,21 @@ impl WasmTransport {
|
|||
.call0(&writable_or_stream)
|
||||
.map_err(|_| js_error("getWriter call failed"))?;
|
||||
|
||||
let request = mtp_codec::CommunicationValue::new(mtp_codec::CommunicationType::PipeRequest)
|
||||
.with_id(pipe_id)
|
||||
.add_typed_default(
|
||||
mtp_codec::DataType::Description,
|
||||
mtp_codec::DataValue::Str(description.to_string()),
|
||||
);
|
||||
let type_map = self.type_map();
|
||||
let request = mtp_codec::CommunicationValue::new_with_type_map(
|
||||
mtp_codec::CommunicationType::PipeRequest,
|
||||
&type_map,
|
||||
)
|
||||
.with_id(pipe_id)
|
||||
.add_typed_default(
|
||||
mtp_codec::DataType::Description,
|
||||
mtp_codec::DataValue::Str(description.to_string()),
|
||||
);
|
||||
let frame_bytes = request
|
||||
.to_bytes()
|
||||
.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());
|
||||
wire.extend_from_slice(&len.to_be_bytes());
|
||||
wire.extend_from_slice(&frame_bytes);
|
||||
|
||||
let chunk = js_sys::Uint8Array::from(&wire[..]);
|
||||
let chunk = js_sys::Uint8Array::from(&frame_bytes[..]);
|
||||
let write_fn = js_sys::Reflect::get(&writer_val, &JsValue::from_str("write"))
|
||||
.map_err(|_| js_error("missing write"))?
|
||||
.dyn_into::<js_sys::Function>()
|
||||
|
|
@ -603,6 +645,12 @@ impl WasmTransport {
|
|||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
if let Some(writer) = self.outgoing_writer.borrow_mut().take() {
|
||||
// The WebTransport session close below terminates the stream. The
|
||||
// lock must be released first so dropping it is not interpreted as
|
||||
// an application abort.
|
||||
release_writer_lock(&writer);
|
||||
}
|
||||
// Release reader locks before closing so they aren't treated as cancels.
|
||||
if let Some(reader) = self.stream_reader.borrow_mut().take() {
|
||||
release_reader_lock(&reader);
|
||||
|
|
|
|||
Loading…
Reference in a new issue