135 lines
7.6 KiB
Rust
135 lines
7.6 KiB
Rust
// file: crates/apps/game-realtime-webtransport-smoke/src/main.rs
|
|
// version: 1
|
|
|
|
#![warn(missing_docs)]
|
|
#![deny(unreachable_pub)]
|
|
#![forbid(unsafe_code)]
|
|
|
|
//! Executable localhost smoke for the public games.sasedev native WebTransport realtime path.
|
|
|
|
use game_realtime_transport_lib::RealtimeConnection; // rust-rules: trait-import
|
|
use game_realtime_transport_lib::RealtimeReceiver; // rust-rules: trait-import
|
|
use game_realtime_transport_lib::RealtimeSender; // rust-rules: trait-import
|
|
|
|
const CLIENT_PAYLOAD: &[u8] = b"games.sasedev-webtransport-client-smoke";
|
|
const SERVER_PAYLOAD: &[u8] = b"games.sasedev-webtransport-server-smoke";
|
|
const SMOKE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
|
const TRACING_TARGET: &str = "games::realtime::webtransport::smoke";
|
|
|
|
#[tokio::main(flavor = "current_thread")]
|
|
async fn main() -> std::process::ExitCode {
|
|
let _logging_guard = match game_logging_lib::init_console_tracing() {
|
|
std::result::Result::Ok(guard) => guard,
|
|
std::result::Result::Err(error) => {
|
|
eprintln!("failed to initialize realtime WebTransport smoke tracing: {error}");
|
|
return std::process::ExitCode::FAILURE;
|
|
},
|
|
};
|
|
tracing::info!(target: TRACING_TARGET, "realtime WebTransport smoke started");
|
|
let result = tokio::time::timeout(SMOKE_TIMEOUT, run_smoke()).await;
|
|
return match result {
|
|
std::result::Result::Ok(std::result::Result::Ok(())) => {
|
|
tracing::info!(target: TRACING_TARGET, "realtime WebTransport smoke passed");
|
|
println!("game-realtime-webtransport-smoke: PASS");
|
|
std::process::ExitCode::SUCCESS
|
|
},
|
|
std::result::Result::Ok(std::result::Result::Err(error)) => {
|
|
tracing::error!(target: TRACING_TARGET, detail = error.as_str(), "realtime WebTransport smoke failed");
|
|
eprintln!("game-realtime-webtransport-smoke: FAIL: {error}");
|
|
std::process::ExitCode::FAILURE
|
|
},
|
|
std::result::Result::Err(_) => {
|
|
tracing::error!(target: TRACING_TARGET, timeout_ms = SMOKE_TIMEOUT.as_millis(), "realtime WebTransport smoke timed out");
|
|
eprintln!("game-realtime-webtransport-smoke: FAIL: smoke timed out");
|
|
std::process::ExitCode::FAILURE
|
|
},
|
|
};
|
|
}
|
|
|
|
async fn run_smoke() -> std::result::Result<(), String> {
|
|
let identity = match game_realtime_webtransport_lib::WebTransportServerIdentity::generate_loopback() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(format!("loopback identity generation failed: {error}")),
|
|
};
|
|
let certificate_hash = identity.certificate_hash().clone();
|
|
let bind_address = std::net::SocketAddr::from(([127, 0, 0, 1], 0));
|
|
let server_config = game_realtime_webtransport_lib::WebTransportServerConfig::new(bind_address, identity);
|
|
let mut listener = match game_realtime_webtransport_lib::WebTransportListener::bind(server_config) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(format!("listener bind failed: {error}")),
|
|
};
|
|
let endpoint = format!("https://{}/smoke", listener.local_addr());
|
|
let client_config = match game_realtime_webtransport_lib::WebTransportClientConfig::new(endpoint.as_str(), certificate_hash) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(format!("client configuration failed: {error}")),
|
|
};
|
|
tracing::info!(target: TRACING_TARGET, endpoint = endpoint.as_str(), "loopback endpoint bound with exact certificate pin");
|
|
let (server_result, client_result) = tokio::join!(listener.accept(), game_realtime_webtransport_lib::connect(&client_config));
|
|
let server_session = match server_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(format!("server accept failed: {error}")),
|
|
};
|
|
let client_session = match client_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(format!("client connect failed: {error}")),
|
|
};
|
|
let client_connection = match client_session.open_primary_connection().await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(format!("client primary stream open failed: {error}")),
|
|
};
|
|
let server_connection = match server_session.accept_primary_connection().await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(format!("server primary stream accept failed: {error}")),
|
|
};
|
|
let (mut server_sender, mut server_receiver) = server_connection.split();
|
|
let (mut client_sender, mut client_receiver) = client_connection.split();
|
|
if let std::result::Result::Err(error) = client_sender.send(game_realtime_transport_lib::TransportMessage::new(CLIENT_PAYLOAD.to_vec())).await {
|
|
return std::result::Result::Err(format!("client send failed: {error}"));
|
|
}
|
|
let server_received = match server_receiver.receive().await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(format!("server receive failed: {error}")),
|
|
};
|
|
if !receive_matches(server_received, CLIENT_PAYLOAD) {
|
|
return std::result::Result::Err(String::from("server did not receive the expected client payload"));
|
|
}
|
|
if let std::result::Result::Err(error) = server_sender.send(game_realtime_transport_lib::TransportMessage::new(SERVER_PAYLOAD.to_vec())).await {
|
|
return std::result::Result::Err(format!("server send failed: {error}"));
|
|
}
|
|
let client_received = match client_receiver.receive().await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(format!("client receive failed: {error}")),
|
|
};
|
|
if !receive_matches(client_received, SERVER_PAYLOAD) {
|
|
return std::result::Result::Err(String::from("client did not receive the expected server payload"));
|
|
}
|
|
if let std::result::Result::Err(error) = client_sender.close().await {
|
|
return std::result::Result::Err(format!("client close failed: {error}"));
|
|
}
|
|
let server_close = match server_receiver.receive().await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(format!("server close observation failed: {error}")),
|
|
};
|
|
if server_close != game_realtime_transport_lib::TransportReceive::Closed {
|
|
return std::result::Result::Err(String::from("server did not observe the client FIN"));
|
|
}
|
|
if let std::result::Result::Err(error) = server_sender.close().await {
|
|
return std::result::Result::Err(format!("server close failed: {error}"));
|
|
}
|
|
let client_close = match client_receiver.receive().await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(format!("client close observation failed: {error}")),
|
|
};
|
|
if client_close != game_realtime_transport_lib::TransportReceive::Closed {
|
|
return std::result::Result::Err(String::from("client did not observe the server FIN"));
|
|
}
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
fn receive_matches(receive: game_realtime_transport_lib::TransportReceive, expected: &[u8]) -> bool {
|
|
return match receive {
|
|
game_realtime_transport_lib::TransportReceive::Message(message) => message.as_bytes() == expected,
|
|
game_realtime_transport_lib::TransportReceive::Closed => false,
|
|
};
|
|
}
|