Skip to content

Commit d8e4c23

Browse files
authored
Detect abnormal close messages for websocket streams
For #13
1 parent 38e6729 commit d8e4c23

3 files changed

Lines changed: 138 additions & 4 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/integration_tests.rs

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ mod test {
1313
use rand::Rng;
1414
use tokio::{io::{AsyncReadExt, AsyncWriteExt}, join, net::TcpStream, select};
1515
use tokio_util::sync::CancellationToken;
16-
use tokio_websockets::{ClientBuilder, Message};
16+
use tokio_websockets::{ClientBuilder, CloseCode, Message};
1717

1818
use crate::{config::test::default_config, server::server::server_with_real_deps, util::test::{compare_bufs, get_file, setup_logging, sleep_ms}};
1919

@@ -89,6 +89,32 @@ mod test {
8989
}
9090
}
9191

92+
async fn websocket_writer_end_with_normal_closure(port: u16, header: &[u8], file: Vec<u8>) {
93+
let uri = format!("ws://127.0.0.1:{}", port).parse::<Uri>().unwrap();
94+
let (mut client, _) = ClientBuilder::from_uri(uri).connect().await.unwrap();
95+
96+
random_sleep().await;
97+
client.send(Message::binary(header.to_owned())).await.unwrap();
98+
random_sleep().await;
99+
for data in file.chunks(1000) {
100+
client.send(Message::binary(data.to_owned())).await.unwrap();
101+
random_sleep().await;
102+
}
103+
// Add a ping and pong we should ignore for good measure.
104+
client.send(Message::ping("foo")).await.unwrap();
105+
client.send(Message::pong("bar")).await.unwrap();
106+
client.send(Message::close(Some(CloseCode::NORMAL_CLOSURE), "")).await.unwrap();
107+
}
108+
109+
async fn websocket_writer_abnormal_closure(port: u16, header: &[u8]) {
110+
let uri = format!("ws://127.0.0.1:{}", port).parse::<Uri>().unwrap();
111+
let (mut client, _) = ClientBuilder::from_uri(uri).connect().await.unwrap();
112+
random_sleep().await;
113+
client.send(Message::binary(header.to_owned())).await.unwrap();
114+
random_sleep().await;
115+
client.send(Message::close(Some(CloseCode::GOING_AWAY), "")).await.unwrap();
116+
}
117+
92118
async fn websocket_reader(port: u16, header: &[u8]) -> Vec<u8> {
93119
let uri = format!("ws://127.0.0.1:{}", port).parse::<Uri>().unwrap();
94120
let (mut client, _) = ClientBuilder::from_uri(uri).connect().await.unwrap();
@@ -216,4 +242,100 @@ mod test {
216242

217243
// TODO: verify stats saved to the database.
218244
}
245+
246+
// See issue #13.
247+
#[cfg_attr(not(feature = "local_db_tests"), ignore)]
248+
#[tokio::test(flavor = "multi_thread")]
249+
async fn integration_test_server_websocket_normal_closure_message() {
250+
251+
setup_logging();
252+
253+
let replay_dir = tempfile::tempdir().unwrap();
254+
let mut config = default_config();
255+
config.server.port = None;
256+
config.server.websocket_port = Some(0);
257+
config.database.host = std::env::var("DB_HOST").unwrap();
258+
config.database.port = str::parse::<u16>(&std::env::var("DB_PORT").unwrap()).unwrap();
259+
config.storage.vault_path = replay_dir.path().to_str().unwrap().into();
260+
config.server.connection_accept_timeout_s = Duration::from_millis(50);
261+
262+
let token = CancellationToken::new();
263+
let (server, port_info) = server_with_real_deps(Arc::new(config), token.child_token()).await;
264+
265+
let sent_replay = get_file("example");
266+
let replay_writer = websocket_writer_end_with_normal_closure(port_info.websocket.unwrap(), b"P/2001/foo\0", sent_replay.clone());
267+
268+
let exit_server = async move || {
269+
tokio::time::sleep(Duration::from_secs(1)).await;
270+
token.cancel();
271+
};
272+
273+
let run = async move || {
274+
let (_, _, _) = join! {
275+
server.run(),
276+
exit_server(),
277+
replay_writer,
278+
};
279+
};
280+
tokio::time::timeout(Duration::from_secs(2), run()).await.unwrap();
281+
282+
let mut replay_path: PathBuf = replay_dir.path().to_owned();
283+
replay_path.push("0");
284+
replay_path.push("0");
285+
replay_path.push("0");
286+
replay_path.push("20");
287+
replay_path.push("2001.fafreplay");
288+
let saved_replay = std::fs::read(&replay_path).unwrap();
289+
290+
// This file was copied from this test and manually verified to be correct.
291+
// Zstd is deterministic for the same data + params + version, let's rely on that.
292+
let reference_replay = get_file("integration/game_2001.fafreplay");
293+
compare_bufs(saved_replay, reference_replay);
294+
}
295+
296+
#[cfg_attr(not(feature = "local_db_tests"), ignore)]
297+
#[tokio::test(flavor = "multi_thread")]
298+
async fn integration_test_server_websocket_abnormal_closure() {
299+
300+
setup_logging();
301+
302+
let replay_dir = tempfile::tempdir().unwrap();
303+
let mut config = default_config();
304+
config.server.port = None;
305+
config.server.websocket_port = Some(0);
306+
config.database.host = std::env::var("DB_HOST").unwrap();
307+
config.database.port = str::parse::<u16>(&std::env::var("DB_PORT").unwrap()).unwrap();
308+
config.storage.vault_path = replay_dir.path().to_str().unwrap().into();
309+
config.server.connection_accept_timeout_s = Duration::from_millis(50);
310+
311+
let token = CancellationToken::new();
312+
let (server, port_info) = server_with_real_deps(Arc::new(config), token.child_token()).await;
313+
314+
let replay_writer = websocket_writer_abnormal_closure(port_info.websocket.unwrap(), b"P/2001/foo\0");
315+
316+
let exit_server = async move || {
317+
tokio::time::sleep(Duration::from_secs(1)).await;
318+
token.cancel();
319+
};
320+
321+
let run = async move || {
322+
let (_, _, _) = join! {
323+
server.run(),
324+
exit_server(),
325+
replay_writer,
326+
};
327+
};
328+
tokio::time::timeout(Duration::from_secs(2), run()).await.unwrap();
329+
330+
// The abnormal closure should've been interpreted as an error and not data.
331+
// FIXME this doesn't check that the connection ended with an error...
332+
// FIXME a test in replay.rs might be better.
333+
let mut replay_path: PathBuf = replay_dir.path().to_owned();
334+
replay_path.push("0");
335+
replay_path.push("0");
336+
replay_path.push("0");
337+
replay_path.push("20");
338+
replay_path.push("2001.fafreplay");
339+
let _saved_replay = std::fs::read(&replay_path).expect_err("Replay should have been empty and not saved.");
340+
}
219341
}

src/server/websocket_stream.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use tokio_util::{
77
bytes::Bytes,
88
io::{CopyToBytes, SinkWriter, StreamReader},
99
};
10-
use tokio_websockets::{Error, Message, ServerBuilder, WebSocketStream};
10+
use tokio_websockets::{CloseCode, Error, Message, ServerBuilder, WebSocketStream};
1111

1212
use super::connection::{ReaderType, WriterType};
1313

@@ -51,7 +51,19 @@ fn map_websocket_sink_to_bytes(sink: impl WebsocketSink) -> impl for<'a> Sink<&'
5151
fn map_websocket_stream_to_bytes(stream: impl WebsocketStream) -> impl Stream<Item = Result<Bytes, std::io::Error>> {
5252
stream.map(|i| {
5353
i.map_err(map_message_error_to_io_error)
54-
.map(|mes| mes.into_payload().into())
54+
.and_then(|mes| {
55+
if let Some((code, reason)) = mes.as_close() {
56+
if code != CloseCode::NORMAL_CLOSURE {
57+
Err(std::io::Error::other(reason))
58+
} else {
59+
Ok(Bytes::new())
60+
}
61+
} else if mes.is_ping() || mes.is_pong() {
62+
Ok(Bytes::new())
63+
} else {
64+
Ok(mes.into_payload().into())
65+
}
66+
})
5567
})
5668
}
5769

0 commit comments

Comments
 (0)