mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 00:00:56 +00:00
329 lines
13 KiB
Rust
329 lines
13 KiB
Rust
use crate::{Event, FrameOpcode, spawn_standalone_connection, test_support::*};
|
|
use futures_util::{SinkExt, StreamExt};
|
|
use tokio::{
|
|
io::AsyncWriteExt,
|
|
net::TcpListener,
|
|
sync::{mpsc, oneshot},
|
|
time::{Duration, timeout},
|
|
};
|
|
use tokio_tungstenite::tungstenite::Message;
|
|
|
|
#[tokio::test]
|
|
async fn native_send_accounting_does_not_require_a_server_echo() {
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let url = format!("ws://{}/no-echo", listener.local_addr().unwrap());
|
|
let (received_tx, received_rx) = oneshot::channel();
|
|
let server = tokio::spawn(async move {
|
|
let (stream, _) = listener.accept().await.unwrap();
|
|
let mut socket = tokio_tungstenite::accept_async(stream).await.unwrap();
|
|
assert_eq!(
|
|
socket.next().await.unwrap().unwrap(),
|
|
Message::Text("without echo".into())
|
|
);
|
|
received_tx.send(()).unwrap();
|
|
assert!(matches!(socket.next().await, Some(Ok(Message::Close(_)))));
|
|
let _ = socket.flush().await;
|
|
});
|
|
let (tx, mut rx) = mpsc::channel(8);
|
|
let handle = spawn_standalone_connection(70, url, Vec::new(), test_websocket_context(), tx);
|
|
recv_open_event(&mut rx).await;
|
|
handle.send_text("without echo".to_owned()).unwrap();
|
|
assert_send_completed(&mut rx, 70, FrameOpcode::Text, 12).await;
|
|
|
|
timeout(Duration::from_secs(3), received_rx)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
handle.close(Some(1000), String::new()).unwrap();
|
|
assert_closing(&mut rx, 70).await;
|
|
assert_close(&mut rx, 70, 1000, "", true).await;
|
|
server.await.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn native_local_close_without_peer_close_is_abnormal_once() {
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let url = format!("ws://{}/missing-close", listener.local_addr().unwrap());
|
|
let server = tokio::spawn(async move {
|
|
let (stream, _) = listener.accept().await.unwrap();
|
|
let mut socket = tokio_tungstenite::accept_async(stream).await.unwrap();
|
|
assert!(matches!(socket.next().await, Some(Ok(Message::Close(_)))));
|
|
// Intentionally drop without flushing tungstenite's queued Close reply.
|
|
});
|
|
let (tx, mut rx) = mpsc::channel(8);
|
|
let handle = spawn_standalone_connection(71, url, Vec::new(), test_websocket_context(), tx);
|
|
recv_open_event(&mut rx).await;
|
|
handle
|
|
.close(Some(3001), "not acknowledged".to_owned())
|
|
.unwrap();
|
|
assert_closing(&mut rx, 71).await;
|
|
assert!(matches!(
|
|
timeout(Duration::from_secs(3), rx.recv()).await.unwrap(),
|
|
Some(Event::Error { .. })
|
|
));
|
|
assert_close(&mut rx, 71, 1006, "", false).await;
|
|
assert!(
|
|
timeout(Duration::from_secs(3), rx.recv())
|
|
.await
|
|
.unwrap()
|
|
.is_none(),
|
|
"there is only one terminal Close"
|
|
);
|
|
server.await.unwrap();
|
|
}
|
|
|
|
async fn frames_server<F, Fut>(frames: Vec<u8>, handler: F) -> (String, tokio::task::JoinHandle<()>)
|
|
where
|
|
F: FnOnce(tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>) -> Fut + Send + 'static,
|
|
Fut: std::future::Future<Output = ()> + Send,
|
|
{
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let url = format!("ws://{}/frames", listener.local_addr().unwrap());
|
|
let server = tokio::spawn(async move {
|
|
let (stream, _) = listener.accept().await.unwrap();
|
|
let mut socket = tokio_tungstenite::accept_async(stream).await.unwrap();
|
|
socket.get_mut().write_all(&frames).await.unwrap();
|
|
handler(socket).await;
|
|
});
|
|
(url, server)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn native_fragmented_utf8_and_ping_are_assembled_as_one_message() {
|
|
// UTF-8 U+1F642 spans fragments, with empty continuation and control frames.
|
|
let frames = vec![
|
|
0x01, 2, 0xf0, 0x9f, 0x00, 0, 0x89, 2, b'h', b'i', 0x8a, 0, 0x80, 2, 0x99, 0x82, 0x81, 0,
|
|
0x82, 0,
|
|
];
|
|
let (url, server) = frames_server(frames, async |mut socket| {
|
|
assert_eq!(
|
|
socket.next().await.unwrap().unwrap(),
|
|
Message::Pong(b"hi".to_vec().into())
|
|
);
|
|
assert!(matches!(socket.next().await, Some(Ok(Message::Close(_)))));
|
|
let _ = socket.flush().await;
|
|
})
|
|
.await;
|
|
let (tx, mut rx) = mpsc::channel(8);
|
|
let handle = spawn_standalone_connection(72, url, Vec::new(), test_websocket_context(), tx);
|
|
recv_open_event(&mut rx).await;
|
|
assert_text_message(&mut rx, 72, "🙂").await;
|
|
assert_text_message(&mut rx, 72, "").await;
|
|
assert_binary_message(&mut rx, 72, &[]).await;
|
|
handle.close(Some(1000), String::new()).unwrap();
|
|
assert_closing(&mut rx, 72).await;
|
|
assert_close(&mut rx, 72, 1000, "", true).await;
|
|
server.await.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn native_invalid_frames_and_messages_fail_before_delivery() {
|
|
let cases = [
|
|
(vec![0x83, 0], "WebSocket receive failed:"),
|
|
// A native framing failure must also discard a partially assembled message.
|
|
(
|
|
vec![0x01, 1, b'a', 0x82, 1, b'b'],
|
|
"WebSocket receive failed:",
|
|
),
|
|
(vec![0x81, 2, 0xc0, 0xaf], "text is not valid UTF-8"), // overlong
|
|
(vec![0x81, 1, 0xf0], "text is not valid UTF-8"), // incomplete
|
|
(
|
|
vec![0x01, 1, 0xf0, 0x80, 1, b'a'],
|
|
"text is not valid UTF-8",
|
|
),
|
|
(vec![0x88, 1, 0x03], "close payload has invalid length"),
|
|
(vec![0x88, 2, 0x03, 0xee], "invalid close code 1006"),
|
|
(
|
|
vec![0x88, 3, 0x03, 0xe8, 0xff],
|
|
"close reason is not valid UTF-8",
|
|
),
|
|
// A 16 MiB + 1 frame header followed by one byte is enough to reject it.
|
|
(
|
|
vec![0x82, 127, 0, 0, 0, 0, 1, 0, 0, 1, 7],
|
|
"frame exceeds size limit",
|
|
),
|
|
// The largest legal wire length must also fail before buffering payload.
|
|
(
|
|
vec![0x82, 127, 127, 255, 255, 255, 255, 255, 255, 255, 7],
|
|
"frame exceeds size limit",
|
|
),
|
|
];
|
|
for (frames, expected_error) in cases {
|
|
let (url, server) = frames_server(frames, async |mut socket| {
|
|
assert!(matches!(socket.next().await, None | Some(Err(_))));
|
|
})
|
|
.await;
|
|
let (tx, mut rx) = mpsc::channel(8);
|
|
let _handle =
|
|
spawn_standalone_connection(73, url, Vec::new(), test_websocket_context(), tx);
|
|
recv_open_event(&mut rx).await;
|
|
match timeout(Duration::from_secs(3), rx.recv()).await.unwrap() {
|
|
Some(Event::Error { message, .. }) => {
|
|
assert!(
|
|
message.contains(expected_error),
|
|
"{message}: expected {expected_error}"
|
|
);
|
|
}
|
|
unexpected => panic!("expected {expected_error} before delivery, got {unexpected:?}"),
|
|
}
|
|
assert_close(&mut rx, 73, 1006, "", false).await;
|
|
server.await.unwrap();
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn native_message_size_limit_counts_fragments_and_resets_after_delivery() {
|
|
const FRAME_BYTES: usize = 16 * 1024 * 1024;
|
|
for overflow in [false, true] {
|
|
let (url, server) = frames_server(Vec::new(), async move |mut socket| {
|
|
// Accept exactly 64 MiB; leave 1 MiB available in the rejection case.
|
|
let payload = vec![0xab; FRAME_BYTES];
|
|
for (index, first) in [0x02, 0x00, 0x00, 0x00].into_iter().enumerate() {
|
|
let size = if overflow && index == 3 {
|
|
FRAME_BYTES - 1024 * 1024
|
|
} else {
|
|
FRAME_BYTES
|
|
};
|
|
let mut header = vec![first, 127];
|
|
header.extend_from_slice(&(size as u64).to_be_bytes());
|
|
socket.get_mut().write_all(&header).await.unwrap();
|
|
socket.get_mut().write_all(&payload[..size]).await.unwrap();
|
|
}
|
|
if overflow {
|
|
// Only one byte arrives, but the declared 2 MiB final frame would
|
|
// exceed the message limit. Do not wait for its remaining payload.
|
|
socket
|
|
.get_mut()
|
|
.write_all(&[0x80, 127, 0, 0, 0, 0, 0, 0x20, 0, 0, 7])
|
|
.await
|
|
.unwrap();
|
|
assert!(matches!(socket.next().await, None | Some(Err(_))));
|
|
} else {
|
|
socket.get_mut().write_all(&[0x80, 0]).await.unwrap();
|
|
socket
|
|
.send(Message::Text("next message".into()))
|
|
.await
|
|
.unwrap();
|
|
assert!(matches!(socket.next().await, Some(Ok(Message::Close(_)))));
|
|
let _ = socket.flush().await;
|
|
}
|
|
})
|
|
.await;
|
|
let (tx, mut rx) = mpsc::channel(8);
|
|
let handle = spawn_standalone_connection(76, url, Vec::new(), test_websocket_context(), tx);
|
|
recv_open_event(&mut rx).await;
|
|
let event = timeout(Duration::from_secs(10), rx.recv()).await.unwrap();
|
|
if overflow {
|
|
assert!(matches!(event, Some(Event::Error { message, .. })
|
|
if message == "WebSocket message exceeds size limit"));
|
|
assert_close(&mut rx, 76, 1006, "", false).await;
|
|
} else {
|
|
let Some(Event::BinaryMessage { data, .. }) = event else {
|
|
panic!("expected complete binary message, got {event:?}");
|
|
};
|
|
assert_eq!(data.len(), 4 * FRAME_BYTES);
|
|
assert!(data.iter().all(|byte| *byte == 0xab));
|
|
assert_text_message(&mut rx, 76, "next message").await;
|
|
handle.close(Some(1000), String::new()).unwrap();
|
|
assert_closing(&mut rx, 76).await;
|
|
assert_close(&mut rx, 76, 1000, "", true).await;
|
|
}
|
|
timeout(Duration::from_secs(3), server)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn native_unsolicited_extensions_are_rejected_before_open() {
|
|
let message = websocket_computed_accept_handshake_failure_message(
|
|
"extensions",
|
|
vec![
|
|
"Upgrade: websocket",
|
|
"Connection: Upgrade",
|
|
"Sec-WebSocket-Extensions: permessage-deflate",
|
|
],
|
|
Vec::new(),
|
|
)
|
|
.await;
|
|
assert!(message.contains("unrequested extension"), "{message}");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn native_large_incoming_frame_keeps_chunk_offsets_and_utf8() {
|
|
let message = format!("a{}", "🙂".repeat(20_000));
|
|
let expected = message.clone();
|
|
let (url, server) = frames_server(Vec::new(), async move |mut socket| {
|
|
socket.send(Message::Text(message.into())).await.unwrap();
|
|
assert!(matches!(socket.next().await, Some(Ok(Message::Close(_)))));
|
|
let _ = socket.flush().await;
|
|
})
|
|
.await;
|
|
let (tx, mut rx) = mpsc::channel(8);
|
|
let handle = spawn_standalone_connection(74, url, Vec::new(), test_websocket_context(), tx);
|
|
recv_open_event(&mut rx).await;
|
|
assert_text_message(&mut rx, 74, &expected).await;
|
|
handle.close(Some(1000), String::new()).unwrap();
|
|
assert_closing(&mut rx, 74).await;
|
|
assert_close(&mut rx, 74, 1000, "", true).await;
|
|
server.await.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn native_close_handshake_finishes_while_message_sink_is_blocked() {
|
|
use std::sync::Arc;
|
|
use tokio::{
|
|
io::AsyncReadExt,
|
|
sync::{Notify, Semaphore},
|
|
};
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let url = format!("ws://{}/blocked-delivery", listener.local_addr().unwrap());
|
|
let (released_tx, released_rx) = oneshot::channel();
|
|
let server = tokio::spawn(async move {
|
|
let (stream, _) = listener.accept().await.unwrap();
|
|
let mut socket = tokio_tungstenite::accept_async(stream).await.unwrap();
|
|
socket.send(Message::Text("blocked".into())).await.unwrap();
|
|
assert!(matches!(socket.next().await, Some(Ok(Message::Close(_)))));
|
|
let _ = socket.flush().await;
|
|
assert_eq!(socket.get_mut().read(&mut [0]).await.unwrap(), 0);
|
|
released_tx.send(()).unwrap();
|
|
});
|
|
let blocked = Arc::new(Notify::new());
|
|
let resume = Arc::new(Semaphore::new(0));
|
|
let (events, mut rx) = mpsc::channel(8);
|
|
let sink = crate::EventSender::with_async_sink({
|
|
let blocked = blocked.clone();
|
|
let resume = resume.clone();
|
|
move |event| {
|
|
let blocked = blocked.clone();
|
|
let resume = resume.clone();
|
|
let events = events.clone();
|
|
async move {
|
|
if matches!(event, Event::TextMessage { .. }) {
|
|
blocked.notify_one();
|
|
resume.acquire().await.unwrap().forget();
|
|
}
|
|
events.send(event).await.is_ok()
|
|
}
|
|
}
|
|
});
|
|
let handle = spawn_standalone_connection(75, url, Vec::new(), test_websocket_context(), sink);
|
|
recv_open_event(&mut rx).await;
|
|
timeout(Duration::from_secs(3), blocked.notified())
|
|
.await
|
|
.unwrap();
|
|
handle.close(Some(1000), String::new()).unwrap();
|
|
timeout(Duration::from_secs(3), released_rx)
|
|
.await
|
|
.expect("physical close must not wait for the event sink or closing timeout")
|
|
.unwrap();
|
|
assert!(handle.is_closed());
|
|
assert_eq!(handle.send_binary(vec![1]), Err(crate::SendError::Closed));
|
|
resume.add_permits(1);
|
|
assert_text_message(&mut rx, 75, "blocked").await;
|
|
assert_closing(&mut rx, 75).await;
|
|
assert_close(&mut rx, 75, 1000, "", true).await;
|
|
server.await.unwrap();
|
|
}
|