mod helpers;
use std::{
error::Error,
sync::{
atomic::{AtomicUsize, Ordering},
Arc, Mutex,
},
time::Duration,
};
use futures_util::{SinkExt, StreamExt};
use tokio::{
net::{TcpListener, TcpStream},
time::{Instant, Interval},
};
use tokio_tungstenite::{
accept_hdr_async,
tungstenite::{
handshake::server::{ErrorResponse, Request, Response},
http::{HeaderName, HeaderValue, StatusCode},
protocol::{frame::coding::CloseCode, CloseFrame},
Message,
},
WebSocketStream,
};
pub use helpers::{drain, fast, next_event, next_item, WINDOW};
const WAIT_FOR: Duration = Duration::from_secs(5);
const CLOSE_REPLY_WAIT: Duration = Duration::from_secs(1);
#[derive(Debug, Clone, Default)]
pub struct Script {
pub reject: bool,
pub stall: bool,
pub refuse_upgrade: Option<u16>,
pub refuse_headers: Vec<(String, String)>,
pub first_frame_first: bool,
pub send: Vec<Message>,
pub push_every: Option<Duration>,
pub ping_every: Option<Duration>,
pub close_after: bool,
pub close_code: Option<(u16, String)>,
pub drop_after: Option<Duration>,
pub go_silent: bool,
pub cut: bool,
pub cut_after: Option<Duration>,
}
impl Script {
pub fn hold() -> Self {
Self::default()
}
pub fn frames(frames: &[&str]) -> Self {
Self::messages(
frames
.iter()
.map(|frame| Message::Text((*frame).into()))
.collect(),
)
}
pub fn messages(send: Vec<Message>) -> Self {
Self {
send,
..Self::default()
}
}
pub fn pings_only(every: Duration) -> Self {
Self::hold().ping_every(every)
}
pub fn close_at_once() -> Self {
Self::hold().then_close()
}
pub fn reject() -> Self {
Self {
reject: true,
..Self::default()
}
}
pub fn stall() -> Self {
Self {
stall: true,
..Self::default()
}
}
pub fn refuse_upgrade(status: u16) -> Self {
Self {
refuse_upgrade: Some(status),
..Self::default()
}
}
pub fn with_header(mut self, name: &str, value: &str) -> Self {
self.refuse_headers
.push((name.to_owned(), value.to_owned()));
self
}
pub fn after_first_frame(mut self) -> Self {
self.first_frame_first = true;
self
}
pub fn then_close(mut self) -> Self {
self.close_after = true;
self
}
pub fn close_with(mut self, code: u16, reason: &str) -> Self {
self.close_code = Some((code, reason.to_owned()));
self
}
pub fn push_every(mut self, every: Duration) -> Self {
self.push_every = Some(every);
self
}
pub fn ping_every(mut self, every: Duration) -> Self {
self.ping_every = Some(every);
self
}
pub fn drop_after(mut self, after: Duration) -> Self {
self.drop_after = Some(after);
self
}
pub fn go_silent(mut self) -> Self {
self.go_silent = true;
self
}
pub fn then_cut(mut self) -> Self {
self.cut = true;
self
}
pub fn cut_after(mut self, after: Duration) -> Self {
self.cut_after = Some(after);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Received {
pub connection: usize,
pub at: Instant,
pub text: String,
}
#[derive(Default)]
struct Recorder {
accepted_at: Mutex<Vec<Instant>>,
handshakes: AtomicUsize,
paths: Mutex<Vec<String>>,
received: Mutex<Vec<Received>>,
first_frames: Mutex<Vec<String>>,
pongs: Mutex<Vec<Vec<u8>>>,
pings: AtomicUsize,
closes: AtomicUsize,
close_replies: AtomicUsize,
client_ended: AtomicUsize,
ended: AtomicUsize,
}
pub struct ScriptedServer {
pub url: String,
recorder: Arc<Recorder>,
}
fn no_answer(_connection: usize) -> impl FnMut(&str) -> Vec<Message> + Send {
|_| Vec::new()
}
impl ScriptedServer {
pub async fn start(scripts: Vec<Script>) -> Self {
Self::start_answering(scripts, no_answer).await
}
pub async fn start_answering<F, A>(scripts: Vec<Script>, answer: F) -> Self
where
F: Fn(usize) -> A + Send + 'static,
A: FnMut(&str) -> Vec<Message> + Send + 'static,
{
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let url = format!("ws://{}", listener.local_addr().expect("local address"));
let recorder = Arc::new(Recorder::default());
let shared = Arc::clone(&recorder);
tokio::spawn(async move {
let mut index = 0;
while let Ok((stream, _)) = listener.accept().await {
shared.accepted_at.lock().unwrap().push(Instant::now());
let script = scripts
.get(index)
.or_else(|| scripts.last())
.cloned()
.unwrap_or_default();
let answerer = answer(index);
let recorder = Arc::clone(&shared);
tokio::spawn(async move {
let _ = serve(stream, index, script, answerer, &recorder).await;
recorder.ended.fetch_add(1, Ordering::SeqCst);
});
index += 1;
}
});
Self { url, recorder }
}
pub fn connection_count(&self) -> usize {
self.recorder.accepted_at.lock().unwrap().len()
}
pub fn accepted_at(&self) -> Vec<Instant> {
self.recorder.accepted_at.lock().unwrap().clone()
}
pub fn handshake_count(&self) -> usize {
self.recorder.handshakes.load(Ordering::SeqCst)
}
pub fn paths(&self) -> Vec<String> {
self.recorder.paths.lock().unwrap().clone()
}
pub fn received(&self) -> Vec<Received> {
self.recorder.received.lock().unwrap().clone()
}
pub fn client_frames(&self) -> Vec<String> {
self.received().into_iter().map(|r| r.text).collect()
}
pub fn first_frames(&self) -> Vec<String> {
self.recorder.first_frames.lock().unwrap().clone()
}
pub fn count_frames(&self, predicate: impl Fn(&str) -> bool) -> usize {
self.recorder
.received
.lock()
.unwrap()
.iter()
.filter(|r| predicate(&r.text))
.count()
}
pub fn pongs(&self) -> Vec<Vec<u8>> {
self.recorder.pongs.lock().unwrap().clone()
}
pub fn ping_count(&self) -> usize {
self.recorder.pings.load(Ordering::SeqCst)
}
pub fn close_count(&self) -> usize {
self.recorder.closes.load(Ordering::SeqCst)
}
pub fn close_reply_count(&self) -> usize {
self.recorder.close_replies.load(Ordering::SeqCst)
}
pub fn client_ended_count(&self) -> usize {
self.recorder.client_ended.load(Ordering::SeqCst)
}
pub fn ended_count(&self) -> usize {
self.recorder.ended.load(Ordering::SeqCst)
}
pub async fn wait_for(&self, label: &str, predicate: impl Fn(&Self) -> bool) {
let deadline = Instant::now() + WAIT_FOR;
while Instant::now() < deadline {
if predicate(self) {
return;
}
tokio::time::sleep(Duration::from_millis(5)).await;
}
panic!("timed out waiting for {label}");
}
}
type Socket = WebSocketStream<TcpStream>;
type Failure = Box<dyn Error + Send + Sync>;
async fn tick(interval: &mut Option<Interval>) {
match interval {
Some(interval) => {
interval.tick().await;
}
None => std::future::pending().await,
}
}
async fn until(deadline: Option<Instant>) {
match deadline {
Some(at) => tokio::time::sleep_until(at).await,
None => std::future::pending().await,
}
}
struct Tape<'a> {
recorder: &'a Recorder,
connection: usize,
first: bool,
}
impl Tape<'_> {
fn record(&mut self, message: &Message) {
match message {
Message::Text(text) => {
let text = text.to_string();
if self.first {
self.recorder
.first_frames
.lock()
.unwrap()
.push(text.clone());
self.first = false;
}
self.recorder.received.lock().unwrap().push(Received {
connection: self.connection,
at: Instant::now(),
text,
});
}
Message::Ping(_) => {
self.recorder.pings.fetch_add(1, Ordering::SeqCst);
}
Message::Pong(payload) => {
self.recorder.pongs.lock().unwrap().push(payload.to_vec());
}
_ => {}
}
}
}
async fn close(ws: &mut Socket, frame: Option<CloseFrame>, tape: &mut Tape<'_>) {
if ws.close(frame).await.is_err() {
return;
}
let deadline = Instant::now() + CLOSE_REPLY_WAIT;
while let Ok(Some(Ok(message))) = tokio::time::timeout_at(deadline, ws.next()).await {
if let Message::Close(_) = message {
tape.recorder.close_replies.fetch_add(1, Ordering::SeqCst);
return;
}
tape.record(&message);
}
}
async fn send_scripted(
ws: &mut Socket,
script: &Script,
close_frame: Option<CloseFrame>,
drop_at: Option<Instant>,
tape: &mut Tape<'_>,
) -> Result<bool, Failure> {
for message in &script.send {
ws.send(message.clone()).await?;
}
if script.cut {
return Ok(false);
}
if script.close_after {
close(ws, close_frame, tape).await;
return Ok(false);
}
if script.go_silent {
until(drop_at).await;
close(ws, close_frame, tape).await;
return Ok(false);
}
Ok(true)
}
async fn handshake(stream: TcpStream, script: &Script) -> Option<(Socket, String)> {
let path = Arc::new(Mutex::new(String::new()));
let seen = Arc::clone(&path);
let refusal = script.refuse_upgrade.map(|status| {
let headers = script.refuse_headers.clone();
(status, headers)
});
#[allow(clippy::result_large_err)]
let callback =
move |request: &Request, response: Response| -> Result<Response, ErrorResponse> {
*seen.lock().unwrap() = request.uri().path().to_owned();
let Some((status, headers)) = refusal else {
return Ok(response);
};
let mut refused = ErrorResponse::new(None);
*refused.status_mut() = StatusCode::from_u16(status).expect("an HTTP status");
for (name, value) in headers {
refused.headers_mut().insert(
HeaderName::from_bytes(name.as_bytes()).expect("a header name"),
HeaderValue::from_str(&value).expect("a header value"),
);
}
Err(refused)
};
let ws = accept_hdr_async(stream, callback).await.ok()?;
let path = path.lock().unwrap().clone();
Some((ws, path))
}
async fn serve<A>(
stream: TcpStream,
index: usize,
script: Script,
mut answer: A,
recorder: &Recorder,
) -> Result<(), Failure>
where
A: FnMut(&str) -> Vec<Message>,
{
if script.reject {
drop(stream);
return Ok(());
}
if script.stall {
let _held = stream;
std::future::pending::<()>().await;
return Ok(());
}
let Some((mut ws, path)) = handshake(stream, &script).await else {
return Ok(());
};
recorder.handshakes.fetch_add(1, Ordering::SeqCst);
recorder.paths.lock().unwrap().push(path);
let close_frame = script.close_code.clone().map(|(code, reason)| CloseFrame {
code: CloseCode::from(code),
reason: reason.into(),
});
let drop_at = script.drop_after.map(|after| Instant::now() + after);
let cut_at = script.cut_after.map(|after| Instant::now() + after);
let mut tape = Tape {
recorder,
connection: index,
first: true,
};
let mut sent = false;
if !script.first_frame_first {
if !send_scripted(&mut ws, &script, close_frame.clone(), drop_at, &mut tape).await? {
return Ok(());
}
sent = true;
}
let mut pushes = sent
.then(|| script.push_every.map(tokio::time::interval))
.flatten();
let mut pings = script
.ping_every
.map(|every| tokio::time::interval_at(Instant::now() + every, every));
let mut sequence: u32 = 0;
loop {
tokio::select! {
() = until(drop_at) => {
close(&mut ws, close_frame.clone(), &mut tape).await;
return Ok(());
}
() = until(cut_at) => return Ok(()),
() = tick(&mut pings) => {
sequence += 1;
ws.send(Message::Ping(sequence.to_be_bytes().to_vec().into())).await?;
}
() = tick(&mut pushes) => {
if let Some(push) = script.send.first() {
ws.send(push.clone()).await?;
}
}
message = ws.next() => match message {
Some(Ok(message)) => {
tape.record(&message);
match message {
Message::Text(text) => {
for reply in answer(text.as_str()) {
ws.send(reply).await?;
}
if !sent {
sent = true;
if !send_scripted(&mut ws, &script, close_frame.clone(), drop_at, &mut tape)
.await?
{
return Ok(());
}
pushes = script.push_every.map(tokio::time::interval);
}
}
Message::Close(_) => {
recorder.closes.fetch_add(1, Ordering::SeqCst);
recorder.client_ended.fetch_add(1, Ordering::SeqCst);
let _ = ws.flush().await;
return Ok(());
}
_ => {}
}
}
Some(Err(_)) | None => {
recorder.client_ended.fetch_add(1, Ordering::SeqCst);
return Ok(());
}
},
}
}
}
#[cfg(test)]
mod tests;