use std::net::TcpStream;
use std::time::Duration;
use liminal::protocol::{Frame as LiminalFrame, ProtocolVersion, encode, encoded_len};
use tungstenite::Message;
use tungstenite::protocol::frame::coding::{CloseCode, Data, OpCode};
use tungstenite::protocol::frame::{CloseFrame, Frame};
use tungstenite::protocol::{Role, WebSocket};
use super::super::super::pre_admission::{PRE_ADMISSION_FRAME_BOUND, PreAdmissionOversize};
use super::SliceStep;
use super::tests::scheduler_free_process;
const GUARD: Duration = Duration::from_secs(60);
const FRAGMENT: usize = PRE_ADMISSION_FRAME_BOUND / 4;
fn client_socket(stream: TcpStream) -> Result<WebSocket<TcpStream>, String> {
stream
.set_read_timeout(Some(GUARD))
.map_err(|error| format!("client guard: {error}"))?;
Ok(WebSocket::from_raw_socket(stream, Role::Client, None))
}
fn write_message(
mut client: WebSocket<TcpStream>,
payloads: &[usize],
) -> Result<WebSocket<TcpStream>, String> {
for (index, length) in payloads.iter().enumerate() {
let opcode = if index == 0 {
OpCode::Data(Data::Binary)
} else {
OpCode::Data(Data::Continue)
};
let is_final = index + 1 == payloads.len();
client
.write(Message::Frame(Frame::message(
vec![0_u8; *length],
opcode,
is_final,
)))
.map_err(|error| format!("write fragment {index}: {error}"))?;
}
client.flush().map_err(|error| format!("flush: {error}"))?;
Ok(client)
}
fn read_close(client: &mut WebSocket<TcpStream>) -> Result<CloseFrame, String> {
loop {
match client.read() {
Ok(Message::Close(Some(frame))) => return Ok(frame),
Ok(Message::Close(None)) => return Err("close frame carried no code".to_owned()),
Ok(_) => {}
Err(error) => return Err(format!("client read before close: {error}")),
}
}
}
fn refuse(payloads: &[usize]) -> Result<CloseFrame, String> {
let (mut process, client) = scheduler_free_process()?;
let server_stream = process
.socket
.as_ref()
.ok_or("process has no socket")?
.get_ref();
server_stream
.set_read_timeout(Some(GUARD))
.map_err(|error| format!("server guard: {error}"))?;
let client = client_socket(client)?;
let (step, client) = std::thread::scope(|scope| {
let writer = scope.spawn(|| write_message(client, payloads));
let step = process.service_socket(1);
(step, writer.join())
});
let mut client = client.map_err(|_| "writer thread panicked".to_owned())??;
if !matches!(step, SliceStep::Stop(_)) {
return Err("the oversize message did not stop the connection".to_owned());
}
read_close(&mut client)
}
#[test]
fn fragmented_message_over_the_bound_before_connect_is_refused_by_name() -> Result<(), String> {
let close = refuse(&[FRAGMENT; 6])?;
let expected = PreAdmissionOversize {
attempted: FRAGMENT * 5,
bound: PRE_ADMISSION_FRAME_BOUND,
};
assert_eq!(close.code, CloseCode::Size, "{close:?}");
assert_eq!(close.reason.as_str(), expected.to_string());
Ok(())
}
#[test]
fn single_frame_declaring_more_than_the_bound_before_connect_is_refused_from_its_header()
-> Result<(), String> {
let close = refuse(&[PRE_ADMISSION_FRAME_BOUND + 1])?;
let expected = PreAdmissionOversize {
attempted: PRE_ADMISSION_FRAME_BOUND + 1,
bound: PRE_ADMISSION_FRAME_BOUND,
};
assert_eq!(close.code, CloseCode::Size, "{close:?}");
assert_eq!(close.reason.as_str(), expected.to_string());
Ok(())
}
#[test]
fn a_successful_connect_installs_the_acceptor_bound() -> Result<(), String> {
let (mut process, _client) = scheduler_free_process()?;
let limit = |process: &super::WebSocketConnectionProcess| {
process
.socket
.as_ref()
.map(|socket| socket.get_config().max_message_size)
};
assert_eq!(limit(&process), Some(Some(PRE_ADMISSION_FRAME_BOUND)));
let connect = LiminalFrame::Connect {
flags: 0,
min_version: ProtocolVersion::new(1, 0),
max_version: ProtocolVersion::new(1, 0),
auth_token: Vec::new(),
};
let mut bytes = vec![0_u8; encoded_len(&connect).map_err(|error| error.to_string())?];
let written = encode(&connect, &mut bytes).map_err(|error| error.to_string())?;
bytes.truncate(written);
let step = process.handle_inbound_message(1, Message::Binary(bytes.into()));
assert!(step.is_none(), "the connect was accepted");
assert!(process.state.authenticated);
assert_eq!(limit(&process), Some(Some(process.admitted_bound)));
Ok(())
}
#[test]
fn both_transports_refuse_with_the_same_name() -> Result<(), String> {
let refusal = PreAdmissionOversize {
attempted: PRE_ADMISSION_FRAME_BOUND + 1,
bound: PRE_ADMISSION_FRAME_BOUND,
};
let close = super::pre_admission_close_frame(refusal);
let LiminalFrame::ConnectError {
message: Some(tcp_message),
..
} = super::super::super::pre_admission::connect_error_frame(refusal)
else {
return Err("the byte-stream refusal is a ConnectError carrying a message".to_owned());
};
assert_eq!(close.code, CloseCode::Size);
assert_eq!(close.reason.as_str(), tcp_message);
assert!(tcp_message.starts_with(super::super::super::pre_admission::REFUSAL_NAME));
assert!(tcp_message.starts_with("FrameTooLarge"));
Ok(())
}