0.3.5-alpha.7

This commit is contained in:
2026-09-22 06:36:54 +02:00
parent 0ef5e1a233
commit 1764eb9bed
20 changed files with 1021 additions and 55 deletions

View File

@@ -0,0 +1,30 @@
# file: crates/apps/game-realtime-webtransport-browser-smoke/Cargo.toml
# version: 1
[package]
name = "game-realtime-webtransport-browser-smoke"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
authors.workspace = true
publish.workspace = true
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
game-realtime-transport-lib = { path = "../../common/game-realtime-transport-lib" }
game-realtime-webtransport-lib = { path = "../../common/game-realtime-webtransport-lib" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
game-logging-lib = { path = "../../common/game-logging-lib" }
tokio = { workspace = true, features = ["macros", "rt", "time"] }
tracing.workspace = true
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen.workspace = true
wasm-bindgen-futures.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,96 @@
// file: crates/apps/game-realtime-webtransport-browser-smoke/src/browser.rs
// version: 1
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 CERTIFICATE_HASH_HEX_SIZE: usize = 64;
const CERTIFICATE_HASH_SIZE: usize = 32;
/// Runs one real-browser WebTransport round-trip against the supplied pinned endpoint.
#[wasm_bindgen::prelude::wasm_bindgen]
pub async fn run_browser_smoke(endpoint: String, certificate_sha256_hex: String) -> Result<String, wasm_bindgen::JsValue> {
let certificate_hash = match decode_certificate_hash(certificate_sha256_hex.as_str()) {
Ok(value) => game_realtime_webtransport_lib::WebTransportCertificateHash::from_sha256(value),
Err(error) => return Err(wasm_bindgen::JsValue::from_str(error.as_str())),
};
let transport = game_realtime_webtransport_lib::WebTransportConfig::default()
.with_connect_timeout(std::time::Duration::from_secs(5))
.with_primary_stream_timeout(std::time::Duration::from_secs(5))
.with_send_timeout(std::time::Duration::from_secs(5));
let config = match game_realtime_webtransport_lib::WebTransportClientConfig::new(endpoint.as_str(), certificate_hash) {
Ok(value) => value.with_transport_config(transport),
Err(error) => return Err(js_transport_error("browser client configuration failed", error)),
};
let session = match game_realtime_webtransport_lib::connect(&config).await {
Ok(value) => value,
Err(error) => return Err(js_transport_error("browser client connection failed", error)),
};
let connection = match session.open_primary_connection().await {
Ok(value) => value,
Err(error) => return Err(js_transport_error("browser primary stream open failed", error)),
};
let (mut sender, mut receiver) = connection.split();
if let Err(error) = sender.send(game_realtime_transport_lib::TransportMessage::new(crate::shared::BROWSER_PAYLOAD.to_vec())).await {
return Err(js_transport_error("browser send failed", error));
}
let received = match receiver.receive().await {
Ok(value) => value,
Err(error) => return Err(js_transport_error("browser receive failed", error)),
};
if !receive_matches(received, crate::shared::SERVER_PAYLOAD) {
return Err(wasm_bindgen::JsValue::from_str("browser did not receive the expected server payload"));
}
if let Err(error) = sender.close().await {
return Err(js_transport_error("browser close failed", error));
}
let remote_close = match receiver.receive().await {
Ok(value) => value,
Err(error) => return Err(js_transport_error("browser close observation failed", error)),
};
if remote_close != game_realtime_transport_lib::TransportReceive::Closed {
return Err(wasm_bindgen::JsValue::from_str("browser did not observe the server FIN"));
}
return Ok(String::from("game-realtime-webtransport-browser-smoke: PASS"));
}
fn decode_certificate_hash(value: &str) -> Result<[u8; CERTIFICATE_HASH_SIZE], String> {
let encoded = value.as_bytes();
if encoded.len() != CERTIFICATE_HASH_HEX_SIZE {
return Err(String::from("certificate SHA-256 hash must contain exactly 64 hexadecimal characters"));
}
let mut decoded = [0_u8; CERTIFICATE_HASH_SIZE];
for index in 0..CERTIFICATE_HASH_SIZE {
let high = match hex_nibble(encoded[index * 2]) {
Some(value) => value,
None => return Err(String::from("certificate SHA-256 hash contains a non-hexadecimal character")),
};
let low = match hex_nibble(encoded[index * 2 + 1]) {
Some(value) => value,
None => return Err(String::from("certificate SHA-256 hash contains a non-hexadecimal character")),
};
decoded[index] = (high << 4) | low;
}
return Ok(decoded);
}
fn hex_nibble(value: u8) -> Option<u8> {
return match value {
b'0'..=b'9' => Some(value - b'0'),
b'a'..=b'f' => Some(value - b'a' + 10),
b'A'..=b'F' => Some(value - b'A' + 10),
_ => None,
};
}
fn js_transport_error(context: &str, error: game_realtime_transport_lib::TransportError) -> wasm_bindgen::JsValue {
return wasm_bindgen::JsValue::from_str(format!("{context}: {error}").as_str());
}
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,
};
}

View File

@@ -0,0 +1,17 @@
// file: crates/apps/game-realtime-webtransport-browser-smoke/src/lib.rs
// version: 1
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! WebAssembly adapter for the real-browser WebTransport smoke.
#[cfg(target_arch = "wasm32")]
mod browser;
#[cfg(target_arch = "wasm32")]
mod shared;
#[cfg(target_arch = "wasm32")]
/// Re-export of the browser smoke entry point exposed through wasm-bindgen.
pub use self::browser::run_browser_smoke;

View File

@@ -0,0 +1,144 @@
// file: crates/apps/game-realtime-webtransport-browser-smoke/src/main.rs
// version: 1
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Native peer for the real-browser WebTransport smoke.
#[cfg(not(target_arch = "wasm32"))]
use game_realtime_transport_lib::RealtimeConnection; // rust-rules: trait-import
#[cfg(not(target_arch = "wasm32"))]
use game_realtime_transport_lib::RealtimeReceiver; // rust-rules: trait-import
#[cfg(not(target_arch = "wasm32"))]
use game_realtime_transport_lib::RealtimeSender; // rust-rules: trait-import
#[cfg(not(target_arch = "wasm32"))]
mod shared;
#[cfg(not(target_arch = "wasm32"))]
const BROWSER_HOST_URL: &str = "http://127.0.0.1:1435/main.html";
#[cfg(not(target_arch = "wasm32"))]
const BROWSER_SMOKE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(120);
#[cfg(not(target_arch = "wasm32"))]
const TRACING_TARGET: &str = "games::realtime::webtransport::browser-smoke";
#[cfg(not(target_arch = "wasm32"))]
#[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 browser WebTransport smoke tracing: {error}");
return std::process::ExitCode::FAILURE;
},
};
let prepared = match prepare_listener() {
Ok(value) => value,
Err(error) => {
tracing::error!(target: TRACING_TARGET, detail = error.as_str(), "browser WebTransport smoke preparation failed");
eprintln!("game-realtime-webtransport-browser-smoke: FAIL: {error}");
return std::process::ExitCode::FAILURE;
},
};
let (mut listener, endpoint, certificate_hash_hex) = prepared;
let browser_url = format!("{BROWSER_HOST_URL}?endpoint={endpoint}&sha256={certificate_hash_hex}");
tracing::info!(target: TRACING_TARGET, endpoint = endpoint.as_str(), "browser WebTransport smoke peer ready");
println!("game-realtime-webtransport-browser-smoke: ENDPOINT={endpoint}");
println!("game-realtime-webtransport-browser-smoke: CERT_SHA256={certificate_hash_hex}");
println!("game-realtime-webtransport-browser-smoke: OPEN={browser_url}");
let result = tokio::time::timeout(BROWSER_SMOKE_TIMEOUT, serve_browser(&mut listener)).await;
return match result {
Ok(Ok(())) => {
tracing::info!(target: TRACING_TARGET, "browser WebTransport smoke passed");
println!("game-realtime-webtransport-browser-smoke: PASS");
std::process::ExitCode::SUCCESS
},
Ok(Err(error)) => {
tracing::error!(target: TRACING_TARGET, detail = error.as_str(), "browser WebTransport smoke failed");
eprintln!("game-realtime-webtransport-browser-smoke: FAIL: {error}");
std::process::ExitCode::FAILURE
},
Err(_) => {
tracing::error!(target: TRACING_TARGET, timeout_ms = BROWSER_SMOKE_TIMEOUT.as_millis(), "browser WebTransport smoke timed out");
eprintln!("game-realtime-webtransport-browser-smoke: FAIL: browser did not complete the smoke before the launcher deadline");
std::process::ExitCode::FAILURE
},
};
}
#[cfg(target_arch = "wasm32")]
fn main() {
return;
}
#[cfg(not(target_arch = "wasm32"))]
fn prepare_listener() -> Result<(game_realtime_webtransport_lib::WebTransportListener, String, String), String> {
let identity = match game_realtime_webtransport_lib::WebTransportServerIdentity::generate_loopback() {
Ok(value) => value,
Err(error) => return Err(format!("loopback identity generation failed: {error}")),
};
let certificate_hash_hex = encode_hex(identity.certificate_hash().as_bytes());
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 listener = match game_realtime_webtransport_lib::WebTransportListener::bind(server_config) {
Ok(value) => value,
Err(error) => return Err(format!("listener bind failed: {error}")),
};
let endpoint = format!("https://{}/browser-smoke", listener.local_addr());
return Ok((listener, endpoint, certificate_hash_hex));
}
#[cfg(not(target_arch = "wasm32"))]
async fn serve_browser(listener: &mut game_realtime_webtransport_lib::WebTransportListener) -> Result<(), String> {
let session = match listener.accept().await {
Ok(value) => value,
Err(error) => return Err(format!("server accept failed: {error}")),
};
let connection = match session.accept_primary_connection().await {
Ok(value) => value,
Err(error) => return Err(format!("server primary stream accept failed: {error}")),
};
let (mut sender, mut receiver) = connection.split();
let browser_received = match receiver.receive().await {
Ok(value) => value,
Err(error) => return Err(format!("server receive failed: {error}")),
};
if !receive_matches(browser_received, shared::BROWSER_PAYLOAD) {
return Err(String::from("server did not receive the expected browser payload"));
}
if let Err(error) = sender.send(game_realtime_transport_lib::TransportMessage::new(shared::SERVER_PAYLOAD.to_vec())).await {
return Err(format!("server send failed: {error}"));
}
let browser_close = match receiver.receive().await {
Ok(value) => value,
Err(error) => return Err(format!("server close observation failed: {error}")),
};
if browser_close != game_realtime_transport_lib::TransportReceive::Closed {
return Err(String::from("server did not observe the browser FIN"));
}
if let Err(error) = sender.close().await {
return Err(format!("server close failed: {error}"));
}
return Ok(());
}
#[cfg(not(target_arch = "wasm32"))]
fn encode_hex(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut encoded = String::with_capacity(bytes.len() * 2);
for byte in bytes {
encoded.push(char::from(HEX[usize::from(byte >> 4)]));
encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
return encoded;
}
#[cfg(not(target_arch = "wasm32"))]
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,
};
}

View File

@@ -0,0 +1,7 @@
// file: crates/apps/game-realtime-webtransport-browser-smoke/src/shared.rs
// version: 1
/// Binary payload sent from the real browser to the native smoke peer.
pub(crate) const BROWSER_PAYLOAD: &[u8] = b"games.sasedev-webtransport-browser-client-smoke";
/// Binary payload sent from the native smoke peer back to the real browser.
pub(crate) const SERVER_PAYLOAD: &[u8] = b"games.sasedev-webtransport-browser-server-smoke";