0.3.5-alpha.7
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
144
crates/apps/game-realtime-webtransport-browser-smoke/src/main.rs
Normal file
144
crates/apps/game-realtime-webtransport-browser-smoke/src/main.rs
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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";
|
||||
Reference in New Issue
Block a user