0.3.5-alpha.6
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: crates/common/game-realtime-webtransport-lib/Cargo.toml
|
||||
# version: 2
|
||||
# version: 3
|
||||
|
||||
[package]
|
||||
name = "game-realtime-webtransport-lib"
|
||||
@@ -12,13 +12,18 @@ publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
game-realtime-transport-lib = { path = "../game-realtime-transport-lib" }
|
||||
rcgen = { workspace = true, features = ["ring"] }
|
||||
tokio = { workspace = true, features = ["time"] }
|
||||
tracing.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
rcgen = { workspace = true, features = ["ring"] }
|
||||
tokio = { workspace = true, features = ["time"] }
|
||||
web-transport-quinn = { workspace = true, features = ["ring"] }
|
||||
|
||||
[dev-dependencies]
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
web-transport-wasm.workspace = true
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
tokio = { workspace = true, features = ["macros", "rt", "time"] }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/common/game-realtime-webtransport-lib/README.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# game-realtime-webtransport-lib
|
||||
|
||||
@@ -7,9 +7,9 @@ Backend WebTransport/QUIC candidat pour le realtime de `games.sasedev`.
|
||||
|
||||
## Responsabilité
|
||||
|
||||
La crate possède le transport WebTransport concret sans introduire de sémantique gameplay, room, joueur, tick ou snapshot. Son chemin natif repose sur `web-transport-quinn` et conserve les erreurs publiques dans `game-realtime-transport-lib`.
|
||||
La crate possède le transport WebTransport concret sans introduire de sémantique gameplay, room, joueur, tick ou snapshot. Le chemin natif repose sur `web-transport-quinn`; le chemin client `wasm32-unknown-unknown` repose sur `web-transport-wasm` et l'API WebTransport du navigateur. Les deux conservent les erreurs publiques dans `game-realtime-transport-lib`.
|
||||
|
||||
La frontière native disponible couvre désormais :
|
||||
La frontière fiable disponible couvre désormais :
|
||||
|
||||
- configuration client HTTPS avec pin SHA-256 exact ;
|
||||
- identité serveur X.509 DER + clé privée PKCS#8 DER injectables ;
|
||||
@@ -19,14 +19,26 @@ La frontière native disponible couvre désormais :
|
||||
- sélection d'un unique stream bidirectionnel fiable comme chemin realtime principal ;
|
||||
- framing privé `u32` big-endian + payload binaire ;
|
||||
- limite de message configurable, 1 MiB par défaut, vérifiée avant allocation côté réception et avant écriture côté émission ;
|
||||
- deadlines configurables pour la connexion, l'ouverture/accept du stream primaire et un envoi complet ;
|
||||
- deadlines configurables sur le chemin natif pour la connexion, l'ouverture/accept du stream primaire et un envoi complet ;
|
||||
- adaptation `RealtimeConnection` / `RealtimeSender` / `RealtimeReceiver` ;
|
||||
- FIN propre via `RealtimeSender::close()` ;
|
||||
- reset/STOP_SENDING backend-spécifiques via `WebTransportSender::abort(...)` et `WebTransportReceiver::abort(...)` ;
|
||||
- cancellation/drop terminale : un sender abandonné est reset plutôt que transformé implicitement en FIN ;
|
||||
- parseur de framing réception incrémental conservant son état si une future `receive()` est annulée ;
|
||||
- mapping stable des erreurs reset/close/session/protocole vers `TransportErrorKind` ;
|
||||
- tracing sous `games::realtime::webtransport`.
|
||||
- tracing sous `games::realtime::webtransport`;
|
||||
- client WASM avec endpoint HTTPS, hash certificat SHA-256 explicite, établissement de session navigateur et ouverture du stream bidirectionnel primaire;
|
||||
- adaptation WASM du framing `u32` big-endian et des traits realtime, y compris FIN, reset/STOP et réception incrémentale.
|
||||
|
||||
## Chemin navigateur/WASM
|
||||
|
||||
Pour `wasm32-unknown-unknown`, la crate remplace les dépendances natives Quinn/Tokio/rcgen par `web-transport-wasm`. Le build final reçoit `--cfg=web_sys_unstable_apis` uniquement pour cette cible via `.cargo/config.toml`, conformément à l'exigence actuelle des bindings WebTransport de `web-sys`.
|
||||
|
||||
L'API client garde les mêmes noms de surface que le client natif : `WebTransportCertificateHash`, `WebTransportClientConfig`, `connect`, `WebTransportSession`, `WebTransportConnection`, `WebTransportSender` et `WebTransportReceiver`. Le navigateur ouvre toujours le stream bidirectionnel primaire côté client ; le serveur reste le backend Rust natif.
|
||||
|
||||
Le pin SHA-256 est transmis à `WebTransportOptions.serverCertificateHashes`; aucune variante navigateur sans validation TLS n'est ajoutée. Le framing applicatif reste strictement identique au natif.
|
||||
|
||||
`alpha.6` ferme uniquement la preuve de compilation de ce chemin. La limite de message de `WebTransportConfig` est appliquée immédiatement, mais les deadlines opérationnelles de cette configuration restent une différence explicite : le wrapper navigateur conserve les opérations Web API abandonnées de manière cancel-safe, et la tranche `alpha.7` doit décider puis prouver la politique de timer/runtime avant de déclarer la parité comportementale navigateur. Aucun smoke runtime navigateur n'est attribué à `alpha.6`.
|
||||
|
||||
## TLS de développement
|
||||
|
||||
@@ -57,7 +69,7 @@ one WebTransport session
|
||||
|
||||
`WebTransportConfig::default()` conserve la baseline de 1 MiB par message. La limite peut être réduite ou augmentée tant qu'elle reste strictement positive et représentable dans le champ de longueur `u32` du framing.
|
||||
|
||||
Les deadlines configurables couvrent :
|
||||
Sur le chemin natif, les deadlines configurables couvrent :
|
||||
|
||||
- connexion client et réponse finale à une requête WebTransport déjà surfacée côté serveur ;
|
||||
- ouverture ou accept du stream bidirectionnel principal ;
|
||||
@@ -65,7 +77,7 @@ Les deadlines configurables couvrent :
|
||||
|
||||
L'attente d'un nouveau pair sur le listener reste volontairement non bornée : un serveur inactif ne doit pas produire périodiquement une erreur uniquement parce qu'aucun client ne se présente.
|
||||
|
||||
QUIC applique sa propre flow-control. Le backend n'ajoute pas une seconde file applicative : si un envoi reste bloqué par flow-control/réseau au-delà de `send_timeout`, l'opération retourne `TransportErrorKind::Timeout` et le stream est reset afin qu'une frame partiellement transmise ne puisse pas être suivie d'une nouvelle frame invalide.
|
||||
QUIC applique sa propre flow-control. Le backend natif n'ajoute pas une seconde file applicative : si un envoi reste bloqué par flow-control/réseau au-delà de `send_timeout`, l'opération retourne `TransportErrorKind::Timeout` et le stream est reset afin qu'une frame partiellement transmise ne puisse pas être suivie d'une nouvelle frame invalide. Le chemin navigateur conserve la même politique de reset sur cancellation, mais son timer `send_timeout` reste explicitement différé à `alpha.7`.
|
||||
|
||||
## Lifecycle, abort et cancellation
|
||||
|
||||
@@ -91,7 +103,7 @@ Le backend distingue notamment :
|
||||
- fermeture de session WebTransport explicite -> `Closed` ;
|
||||
- erreur de session/connexion non classée comme fermeture propre -> `Io` ;
|
||||
- reset/stop invalide ou framing tronqué -> `Protocol` ;
|
||||
- deadline dépassée -> `Timeout`.
|
||||
- deadline dépassée -> `Timeout` sur le chemin natif ; la matérialisation des timers navigateur reste différée à `alpha.7`.
|
||||
|
||||
Une longueur entrante hors limite ou un framing tronqué provoque aussi l'arrêt de la direction de réception afin d'éviter de poursuivre sur un flux désynchronisé.
|
||||
|
||||
@@ -100,11 +112,11 @@ Une longueur entrante hors limite ou un framing tronqué provoque aussi l'arrêt
|
||||
La crate ne possède toujours pas :
|
||||
|
||||
- d'API datagram transport-neutral ;
|
||||
- de chemin navigateur/WASM ;
|
||||
- de smoke executable public WebTransport ;
|
||||
- de serveur WebTransport WASM ;
|
||||
- de preuve runtime navigateur ;
|
||||
- de fallback WebSocket ;
|
||||
- de benchmark WebSocket/WebTransport.
|
||||
|
||||
Ces responsabilités restent réservées aux tranches suivantes du plan `0.3.5`.
|
||||
|
||||
Le chemin natif s'exécute sous un runtime Tokio fourni par le consommateur ; la crate ne crée ni runtime ni thread privé. Le chemin navigateur/WASM est distinct : aucun `cfg` WASM ni dépendance navigateur n'est requis par le backend natif actuel.
|
||||
Le chemin natif s'exécute sous un runtime Tokio fourni par le consommateur ; la crate ne crée ni runtime ni thread privé. Les dépendances navigateur sont target-specific et ne sont donc pas tirées par le backend natif. Réciproquement, `web-transport-quinn`, Tokio et rcgen ne sont pas requis pour construire la crate en `wasm32-unknown-unknown`.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<!-- file: crates/common/game-realtime-webtransport-lib/USAGE.md -->
|
||||
<!-- version: 2 -->
|
||||
<!-- version: 3 -->
|
||||
|
||||
# Utilisation de game-realtime-webtransport-lib
|
||||
|
||||
Ce guide décrit le chemin natif fiable exposé par `game-realtime-webtransport-lib`. Il ne décrit ni gameplay, ni protocole wire métier, ni datagrams.
|
||||
Ce guide décrit les chemins fiables natif et client navigateur/WASM exposés par `game-realtime-webtransport-lib`. Il ne décrit ni gameplay, ni protocole wire métier, ni datagrams.
|
||||
|
||||
## Configuration transport
|
||||
|
||||
`WebTransportConfig` porte les limites et deadlines du chemin fiable. La configuration par défaut garde une limite de 1 MiB par message et des deadlines bornées pour connexion, stream primaire et send.
|
||||
`WebTransportConfig` porte les limites et deadlines du chemin fiable. La configuration par défaut garde une limite de 1 MiB par message. Les deadlines sont appliquées par le chemin natif ; le client navigateur les valide mais leur matérialisation par timer reste différée à `alpha.7`.
|
||||
|
||||
Exemple de configuration plus stricte :
|
||||
|
||||
@@ -19,7 +19,7 @@ let transport = game_realtime_webtransport_lib::WebTransportConfig::default()
|
||||
.with_send_timeout(std::time::Duration::from_secs(2));
|
||||
```
|
||||
|
||||
La validation effective se fait lors du bind serveur ou de la connexion client. Une limite nulle, une limite non représentable en `u32` ou une deadline nulle est rejetée comme `InvalidConfiguration`.
|
||||
La validation effective se fait lors du bind serveur ou de la connexion client. Une limite nulle, une limite non représentable en `u32` ou une deadline nulle est rejetée comme `InvalidConfiguration`, y compris côté navigateur même lorsque le timer correspondant n'est pas encore matérialisé.
|
||||
|
||||
## Serveur natif
|
||||
|
||||
@@ -77,6 +77,35 @@ let connection = match session.open_primary_connection().await {
|
||||
|
||||
Le pinning est obligatoire dans cette API native ; il n'existe pas de variante qui désactive globalement la vérification TLS.
|
||||
|
||||
## Client navigateur/WASM
|
||||
|
||||
Pour `wasm32-unknown-unknown`, les mêmes noms `WebTransportCertificateHash`, `WebTransportClientConfig`, `connect` et `WebTransportSession::open_primary_connection()` sont disponibles. Le serveur reste natif ; le navigateur est uniquement client.
|
||||
|
||||
Exemple de séquence Rust côté WASM :
|
||||
|
||||
```rust
|
||||
let certificate_hash = game_realtime_webtransport_lib::WebTransportCertificateHash::from_sha256(server_sha256);
|
||||
let config = match game_realtime_webtransport_lib::WebTransportClientConfig::new(
|
||||
"https://127.0.0.1:4433/game",
|
||||
certificate_hash,
|
||||
) {
|
||||
Ok(value) => value.with_transport_config(transport),
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let session = match game_realtime_webtransport_lib::connect(&config).await {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let connection = match session.open_primary_connection().await {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
```
|
||||
|
||||
Le build workspace fournit `web_sys_unstable_apis` uniquement à `wasm32-unknown-unknown`. Le hash SHA-256 est transmis au navigateur comme `serverCertificateHashes`. La limite de message configurée est appliquée au framing WASM. Les deadlines `connect_timeout`, `primary_stream_timeout` et `send_timeout` restent validées mais ne sont pas encore appliquées par un timer navigateur dans la tranche de compilation ; cette politique est fermée avec le smoke runtime de la tranche suivante.
|
||||
|
||||
Aucun `wasm-bindgen` frontend ou host Vite n'est requis pour simplement vérifier la compilation de la bibliothèque.
|
||||
|
||||
## Contrat realtime
|
||||
|
||||
Une fois le stream primaire sélectionné, utiliser les traits de `game-realtime-transport-lib` pour le chemin fiable normal :
|
||||
@@ -101,7 +130,7 @@ if let Err(error) = game_realtime_transport_lib::RealtimeSender::close(&mut send
|
||||
|
||||
Le backend encode chaque `TransportMessage` sous la forme `u32` big-endian + payload. Le consommateur ne doit pas reproduire ce framing lui-même.
|
||||
|
||||
La flow-control QUIC est respectée naturellement par l'écriture asynchrone. Un send qui dépasse sa deadline est considéré terminal : le stream est reset et le même sender ne doit pas être réutilisé.
|
||||
La flow-control QUIC est respectée naturellement par l'écriture asynchrone. Sur le chemin natif, un send qui dépasse sa deadline est considéré terminal : le stream est reset et le même sender ne doit pas être réutilisé. Sur le chemin navigateur de `alpha.6`, une cancellation du send reste terminale et reset le stream, mais le timer `send_timeout` est différé à `alpha.7`.
|
||||
|
||||
## Fermeture et abort
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// file: crates/common/game-realtime-webtransport-lib/src/config.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
const DEFAULT_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
const DEFAULT_MAX_MESSAGE_SIZE: usize = 1024 * 1024;
|
||||
const DEFAULT_PRIMARY_STREAM_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
const DEFAULT_SEND_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
|
||||
/// Product-facing limits and operation deadlines for the native WebTransport reliable path.
|
||||
/// Product-facing limits and operation deadlines for the WebTransport reliable path.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct WebTransportConfig {
|
||||
connect_timeout: std::time::Duration,
|
||||
|
||||
@@ -1,34 +1,68 @@
|
||||
// file: crates/common/game-realtime-webtransport-lib/src/lib.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Native WebTransport/QUIC backend candidate for the transport-neutral realtime contract.
|
||||
//! WebTransport/QUIC backend candidate for the transport-neutral realtime contract.
|
||||
|
||||
mod config;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod webtransport;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod webtransport_wasm;
|
||||
|
||||
/// Re-export of product-facing limits and operation deadlines for the reliable WebTransport path.
|
||||
pub use self::config::WebTransportConfig;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Re-export of the pinned SHA-256 certificate fingerprint used by the native client.
|
||||
pub use self::webtransport::WebTransportCertificateHash;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Re-export of native WebTransport client configuration.
|
||||
pub use self::webtransport::WebTransportClientConfig;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Re-export of an established WebTransport connection adapted to the transport-neutral realtime contract.
|
||||
pub use self::webtransport::WebTransportConnection;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Re-export of the bound native WebTransport listener.
|
||||
pub use self::webtransport::WebTransportListener;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Re-export of the receive half of the primary reliable WebTransport stream.
|
||||
pub use self::webtransport::WebTransportReceiver;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Re-export of the send half of the primary reliable WebTransport stream.
|
||||
pub use self::webtransport::WebTransportSender;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Re-export of native WebTransport server configuration.
|
||||
pub use self::webtransport::WebTransportServerConfig;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Re-export of native WebTransport server TLS identity material.
|
||||
pub use self::webtransport::WebTransportServerIdentity;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Re-export of an established native WebTransport session.
|
||||
pub use self::webtransport::WebTransportSession;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Re-export of the native WebTransport client establishment function.
|
||||
pub use self::webtransport::connect;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
/// Re-export of the pinned SHA-256 certificate fingerprint used by the browser WebTransport client.
|
||||
pub use self::webtransport_wasm::WebTransportCertificateHash;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
/// Re-export of browser WebTransport client configuration.
|
||||
pub use self::webtransport_wasm::WebTransportClientConfig;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
/// Re-export of an established browser WebTransport connection adapted to the transport-neutral realtime contract.
|
||||
pub use self::webtransport_wasm::WebTransportConnection;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
/// Re-export of the receive half of the browser primary reliable WebTransport stream.
|
||||
pub use self::webtransport_wasm::WebTransportReceiver;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
/// Re-export of the send half of the browser primary reliable WebTransport stream.
|
||||
pub use self::webtransport_wasm::WebTransportSender;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
/// Re-export of an established browser WebTransport session.
|
||||
pub use self::webtransport_wasm::WebTransportSession;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
/// Re-export of the browser WebTransport client establishment function.
|
||||
pub use self::webtransport_wasm::connect;
|
||||
|
||||
@@ -0,0 +1,519 @@
|
||||
// file: crates/common/game-realtime-webtransport-lib/src/webtransport_wasm.rs
|
||||
// version: 1
|
||||
|
||||
const CERTIFICATE_HASH_SIZE: usize = 32;
|
||||
const FRAME_PROTOCOL_ERROR_CODE: u32 = 0x10;
|
||||
const FRAME_TOO_LARGE_ERROR_CODE: u32 = 0x11;
|
||||
const PRIMARY_FRAME_HEADER_SIZE: usize = 4;
|
||||
const STREAM_CANCELLED_ERROR_CODE: u32 = 0x14;
|
||||
const TRACING_TARGET: &str = "games::realtime::webtransport";
|
||||
|
||||
/// SHA-256 fingerprint of one certificate accepted by the browser WebTransport client.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct WebTransportCertificateHash {
|
||||
bytes: [u8; CERTIFICATE_HASH_SIZE],
|
||||
}
|
||||
|
||||
impl WebTransportCertificateHash {
|
||||
/// Creates a fingerprint from an already-computed SHA-256 digest.
|
||||
#[must_use]
|
||||
pub fn from_sha256(bytes: [u8; CERTIFICATE_HASH_SIZE]) -> Self {
|
||||
return Self { bytes };
|
||||
}
|
||||
|
||||
/// Returns the exact 32-byte SHA-256 digest.
|
||||
#[must_use]
|
||||
pub fn as_bytes(&self) -> &[u8; CERTIFICATE_HASH_SIZE] {
|
||||
return &self.bytes;
|
||||
}
|
||||
}
|
||||
|
||||
/// Browser WebTransport endpoint, certificate pin and reliable-path configuration.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct WebTransportClientConfig {
|
||||
endpoint: url::Url,
|
||||
certificate_hash: WebTransportCertificateHash,
|
||||
transport: crate::WebTransportConfig,
|
||||
}
|
||||
|
||||
impl WebTransportClientConfig {
|
||||
/// Parses and validates a secure WebTransport endpoint with one pinned SHA-256 certificate fingerprint.
|
||||
pub fn new(endpoint: &str, certificate_hash: WebTransportCertificateHash) -> Result<Self, game_realtime_transport_lib::TransportError> {
|
||||
let parsed = match url::Url::parse(endpoint) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(invalid_configuration(error.to_string())),
|
||||
};
|
||||
if parsed.scheme() != "https" {
|
||||
return Err(invalid_configuration("WebTransport endpoint scheme must be https"));
|
||||
}
|
||||
if parsed.host().is_none() {
|
||||
return Err(invalid_configuration("WebTransport endpoint must contain a host"));
|
||||
}
|
||||
return Ok(Self { endpoint: parsed, certificate_hash, transport: crate::WebTransportConfig::default() });
|
||||
}
|
||||
|
||||
/// Returns a copy with explicit reliable-path limits and deadlines.
|
||||
///
|
||||
/// The browser compile path applies the message-size limit immediately. Browser-side operation deadlines are
|
||||
/// validated here but remain a runtime-interoperability concern for the dedicated browser smoke tranche.
|
||||
#[must_use]
|
||||
pub fn with_transport_config(mut self, transport: crate::WebTransportConfig) -> Self {
|
||||
self.transport = transport;
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Returns the validated WebTransport endpoint URL.
|
||||
#[must_use]
|
||||
pub fn endpoint(&self) -> &str {
|
||||
return self.endpoint.as_str();
|
||||
}
|
||||
|
||||
/// Returns the pinned SHA-256 server-certificate fingerprint.
|
||||
#[must_use]
|
||||
pub fn certificate_hash(&self) -> &WebTransportCertificateHash {
|
||||
return &self.certificate_hash;
|
||||
}
|
||||
|
||||
/// Returns the reliable-path limits and deadlines.
|
||||
#[must_use]
|
||||
pub fn transport_config(&self) -> crate::WebTransportConfig {
|
||||
return self.transport;
|
||||
}
|
||||
}
|
||||
|
||||
/// Established browser WebTransport session before the primary application stream is selected.
|
||||
pub struct WebTransportSession {
|
||||
inner: web_transport_wasm::Session,
|
||||
transport: crate::WebTransportConfig,
|
||||
}
|
||||
|
||||
impl WebTransportSession {
|
||||
fn new(inner: web_transport_wasm::Session, transport: crate::WebTransportConfig) -> Self {
|
||||
return Self { inner, transport };
|
||||
}
|
||||
|
||||
/// Opens the single primary bidirectional stream and adapts it to the transport-neutral realtime contract.
|
||||
pub async fn open_primary_connection(self) -> Result<WebTransportConnection, game_realtime_transport_lib::TransportError> {
|
||||
let (sender, receiver) = match self.inner.open_bi().await {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
let mapped = transport_error(game_realtime_transport_lib::TransportErrorKind::Protocol, error.to_string());
|
||||
tracing::warn!(target: TRACING_TARGET, detail = mapped.detail(), "browser WebTransport primary bidirectional stream open failed");
|
||||
return Err(mapped);
|
||||
},
|
||||
};
|
||||
tracing::debug!(target: TRACING_TARGET, endpoint = self.inner.url().as_str(), "browser WebTransport primary bidirectional stream opened");
|
||||
return Ok(WebTransportConnection::new(self.inner, sender, receiver, self.transport));
|
||||
}
|
||||
|
||||
/// Returns the endpoint URL backing this browser WebTransport session.
|
||||
#[must_use]
|
||||
pub fn endpoint(&self) -> &str {
|
||||
return self.inner.url().as_str();
|
||||
}
|
||||
}
|
||||
|
||||
/// Established browser WebTransport connection carrying the single reliable primary stream.
|
||||
pub struct WebTransportConnection {
|
||||
receiver: web_transport_wasm::RecvStream,
|
||||
sender: web_transport_wasm::SendStream,
|
||||
session: web_transport_wasm::Session,
|
||||
transport: crate::WebTransportConfig,
|
||||
}
|
||||
|
||||
impl WebTransportConnection {
|
||||
fn new(
|
||||
session: web_transport_wasm::Session,
|
||||
sender: web_transport_wasm::SendStream,
|
||||
receiver: web_transport_wasm::RecvStream,
|
||||
transport: crate::WebTransportConfig,
|
||||
) -> Self {
|
||||
return Self { receiver, sender, session, transport };
|
||||
}
|
||||
}
|
||||
|
||||
impl game_realtime_transport_lib::RealtimeConnection for WebTransportConnection {
|
||||
type Receiver = crate::WebTransportReceiver;
|
||||
type Sender = crate::WebTransportSender;
|
||||
|
||||
fn split(self) -> (Self::Sender, Self::Receiver) {
|
||||
let receiver_session = self.session.clone();
|
||||
return (
|
||||
crate::WebTransportSender {
|
||||
inner: self.sender,
|
||||
_session: self.session,
|
||||
max_message_size: self.transport.max_message_size(),
|
||||
terminal: false,
|
||||
},
|
||||
crate::WebTransportReceiver {
|
||||
inner: self.receiver,
|
||||
_session: receiver_session,
|
||||
max_message_size: self.transport.max_message_size(),
|
||||
header: [0_u8; PRIMARY_FRAME_HEADER_SIZE],
|
||||
header_read: 0,
|
||||
payload: Vec::new(),
|
||||
payload_read: 0,
|
||||
clean_closed: false,
|
||||
terminal: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive half of the browser primary reliable WebTransport stream.
|
||||
pub struct WebTransportReceiver {
|
||||
inner: web_transport_wasm::RecvStream,
|
||||
_session: web_transport_wasm::Session,
|
||||
max_message_size: usize,
|
||||
header: [u8; PRIMARY_FRAME_HEADER_SIZE],
|
||||
header_read: usize,
|
||||
payload: Vec<u8>,
|
||||
payload_read: usize,
|
||||
clean_closed: bool,
|
||||
terminal: bool,
|
||||
}
|
||||
|
||||
impl WebTransportReceiver {
|
||||
/// Abruptly stops the reliable receive direction with one WebTransport application error code.
|
||||
pub fn abort(&mut self, code: u32) -> Result<(), game_realtime_transport_lib::TransportError> {
|
||||
if self.clean_closed || self.terminal {
|
||||
return Err(transport_error(game_realtime_transport_lib::TransportErrorKind::Closed, "browser WebTransport receiver is already terminal"));
|
||||
}
|
||||
self.inner.stop(code);
|
||||
self.terminal = true;
|
||||
tracing::debug!(target: TRACING_TARGET, code = code, "browser WebTransport primary receive stream aborted");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
async fn receive_frame(&mut self) -> Result<game_realtime_transport_lib::TransportReceive, game_realtime_transport_lib::TransportError> {
|
||||
if self.clean_closed {
|
||||
return Ok(game_realtime_transport_lib::TransportReceive::Closed);
|
||||
}
|
||||
if self.terminal {
|
||||
return Err(transport_error(
|
||||
game_realtime_transport_lib::TransportErrorKind::Aborted,
|
||||
"browser WebTransport receiver is unavailable after a terminal stream failure or abort",
|
||||
));
|
||||
}
|
||||
loop {
|
||||
if self.header_read < PRIMARY_FRAME_HEADER_SIZE {
|
||||
let remaining = PRIMARY_FRAME_HEADER_SIZE - self.header_read;
|
||||
let read = self.inner.read(remaining).await;
|
||||
match read {
|
||||
Ok(Some(chunk)) => {
|
||||
if chunk.is_empty() {
|
||||
return self.fail_protocol("browser WebTransport primary stream returned an empty read in the middle of a frame header");
|
||||
}
|
||||
let end = self.header_read + chunk.len();
|
||||
self.header[self.header_read..end].copy_from_slice(chunk.as_ref());
|
||||
self.header_read = end;
|
||||
continue;
|
||||
},
|
||||
Ok(None) => {
|
||||
if self.header_read == 0 {
|
||||
self.clean_closed = true;
|
||||
tracing::debug!(target: TRACING_TARGET, "remote browser WebTransport primary stream closed cleanly");
|
||||
return Ok(game_realtime_transport_lib::TransportReceive::Closed);
|
||||
}
|
||||
return self.fail_protocol("browser WebTransport primary stream closed in the middle of a frame header");
|
||||
},
|
||||
Err(error) => return self.fail_read(error),
|
||||
}
|
||||
}
|
||||
if self.payload.is_empty() && self.payload_read == 0 {
|
||||
let payload_len = u32::from_be_bytes(self.header) as usize;
|
||||
if payload_len > self.max_message_size {
|
||||
let error = message_too_large(payload_len, self.max_message_size);
|
||||
self.stop_after_failure(FRAME_TOO_LARGE_ERROR_CODE);
|
||||
return Err(error);
|
||||
}
|
||||
if payload_len == 0 {
|
||||
self.reset_frame_state();
|
||||
tracing::trace!(target: TRACING_TARGET, payload_len = 0, "framed browser WebTransport payload received");
|
||||
return Ok(game_realtime_transport_lib::TransportReceive::Message(game_realtime_transport_lib::TransportMessage::new(Vec::new())));
|
||||
}
|
||||
self.payload = vec![0_u8; payload_len];
|
||||
}
|
||||
if self.payload_read < self.payload.len() {
|
||||
let remaining = self.payload.len() - self.payload_read;
|
||||
let read = self.inner.read(remaining).await;
|
||||
match read {
|
||||
Ok(Some(chunk)) => {
|
||||
if chunk.is_empty() {
|
||||
return self.fail_protocol("browser WebTransport primary stream returned an empty read in the middle of a frame payload");
|
||||
}
|
||||
let end = self.payload_read + chunk.len();
|
||||
self.payload[self.payload_read..end].copy_from_slice(chunk.as_ref());
|
||||
self.payload_read = end;
|
||||
if self.payload_read < self.payload.len() {
|
||||
continue;
|
||||
}
|
||||
},
|
||||
Ok(None) => return self.fail_protocol("browser WebTransport primary stream closed in the middle of a frame payload"),
|
||||
Err(error) => return self.fail_read(error),
|
||||
}
|
||||
}
|
||||
let payload = core::mem::take(&mut self.payload);
|
||||
self.reset_frame_state();
|
||||
tracing::trace!(target: TRACING_TARGET, payload_len = payload.len(), "framed browser WebTransport payload received");
|
||||
return Ok(game_realtime_transport_lib::TransportReceive::Message(game_realtime_transport_lib::TransportMessage::new(payload)));
|
||||
}
|
||||
}
|
||||
|
||||
fn fail_protocol(&mut self, detail: &str) -> Result<game_realtime_transport_lib::TransportReceive, game_realtime_transport_lib::TransportError> {
|
||||
let error = protocol_error(detail);
|
||||
self.stop_after_failure(FRAME_PROTOCOL_ERROR_CODE);
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
fn fail_read(
|
||||
&mut self,
|
||||
error: web_transport_wasm::Error,
|
||||
) -> Result<game_realtime_transport_lib::TransportReceive, game_realtime_transport_lib::TransportError> {
|
||||
self.terminal = true;
|
||||
let mapped = map_read_error(error);
|
||||
tracing::warn!(target: TRACING_TARGET, kind = %mapped.kind(), detail = mapped.detail(), "browser WebTransport primary stream receive failed");
|
||||
return Err(mapped);
|
||||
}
|
||||
|
||||
fn reset_frame_state(&mut self) {
|
||||
self.header = [0_u8; PRIMARY_FRAME_HEADER_SIZE];
|
||||
self.header_read = 0;
|
||||
self.payload.clear();
|
||||
self.payload_read = 0;
|
||||
}
|
||||
|
||||
fn stop_after_failure(&mut self, code: u32) {
|
||||
self.inner.stop(code);
|
||||
self.terminal = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WebTransportReceiver {
|
||||
fn drop(&mut self) {
|
||||
if !self.clean_closed && !self.terminal {
|
||||
self.inner.stop(STREAM_CANCELLED_ERROR_CODE);
|
||||
self.terminal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl game_realtime_transport_lib::RealtimeReceiver for WebTransportReceiver {
|
||||
type ReceiveFuture<'a>
|
||||
= std::pin::Pin<
|
||||
Box<dyn core::future::Future<Output = Result<game_realtime_transport_lib::TransportReceive, game_realtime_transport_lib::TransportError>> + 'a>,
|
||||
>
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
fn receive(&mut self) -> Self::ReceiveFuture<'_> {
|
||||
return Box::pin(async move { return self.receive_frame().await });
|
||||
}
|
||||
}
|
||||
|
||||
/// Send half of the browser primary reliable WebTransport stream.
|
||||
pub struct WebTransportSender {
|
||||
inner: web_transport_wasm::SendStream,
|
||||
_session: web_transport_wasm::Session,
|
||||
max_message_size: usize,
|
||||
terminal: bool,
|
||||
}
|
||||
|
||||
impl WebTransportSender {
|
||||
/// Abruptly resets the reliable send direction with one WebTransport application error code.
|
||||
pub fn abort(&mut self, code: u32) -> Result<(), game_realtime_transport_lib::TransportError> {
|
||||
if self.terminal {
|
||||
return Err(transport_error(game_realtime_transport_lib::TransportErrorKind::Closed, "browser WebTransport sender is already terminal"));
|
||||
}
|
||||
self.inner.reset(code);
|
||||
self.terminal = true;
|
||||
tracing::debug!(target: TRACING_TARGET, code = code, "browser WebTransport primary send stream aborted");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WebTransportSender {
|
||||
fn drop(&mut self) {
|
||||
if !self.terminal {
|
||||
self.inner.reset(STREAM_CANCELLED_ERROR_CODE);
|
||||
self.terminal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl game_realtime_transport_lib::RealtimeSender for WebTransportSender {
|
||||
type CloseFuture<'a>
|
||||
= std::pin::Pin<Box<dyn core::future::Future<Output = Result<(), game_realtime_transport_lib::TransportError>> + 'a>>
|
||||
where
|
||||
Self: 'a;
|
||||
type SendFuture<'a>
|
||||
= std::pin::Pin<Box<dyn core::future::Future<Output = Result<(), game_realtime_transport_lib::TransportError>> + 'a>>
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
fn close(&mut self) -> Self::CloseFuture<'_> {
|
||||
return Box::pin(async move {
|
||||
if self.terminal {
|
||||
return Err(transport_error(game_realtime_transport_lib::TransportErrorKind::Closed, "browser WebTransport sender is already terminal"));
|
||||
}
|
||||
self.terminal = true;
|
||||
return match self.inner.finish() {
|
||||
Ok(()) => {
|
||||
tracing::debug!(target: TRACING_TARGET, "local browser WebTransport primary stream close initiated");
|
||||
Ok(())
|
||||
},
|
||||
Err(error) => {
|
||||
let mapped = map_write_error(error);
|
||||
tracing::warn!(target: TRACING_TARGET, detail = mapped.detail(), "browser WebTransport primary stream close failed");
|
||||
Err(mapped)
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
fn send(&mut self, message: game_realtime_transport_lib::TransportMessage) -> Self::SendFuture<'_> {
|
||||
return Box::pin(async move {
|
||||
if self.terminal {
|
||||
return Err(transport_error(
|
||||
game_realtime_transport_lib::TransportErrorKind::Aborted,
|
||||
"browser WebTransport sender is unavailable after close, abort, cancellation or terminal send failure",
|
||||
));
|
||||
}
|
||||
let payload_len = message.len();
|
||||
let frame_header = match frame_header(payload_len, self.max_message_size) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let mut guard = SendOperationGuard::new(&mut self.inner, &mut self.terminal);
|
||||
if let Err(error) = guard.write_frame(&frame_header, message.as_bytes()).await {
|
||||
let mapped = map_write_error(error);
|
||||
guard.abort();
|
||||
tracing::warn!(target: TRACING_TARGET, kind = %mapped.kind(), detail = mapped.detail(), "browser WebTransport framed send failed");
|
||||
return Err(mapped);
|
||||
}
|
||||
guard.disarm();
|
||||
tracing::trace!(target: TRACING_TARGET, payload_len = payload_len, "framed browser WebTransport payload sent");
|
||||
return Ok(());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct SendOperationGuard<'a> {
|
||||
inner: &'a mut web_transport_wasm::SendStream,
|
||||
terminal: &'a mut bool,
|
||||
armed: bool,
|
||||
}
|
||||
|
||||
impl<'a> SendOperationGuard<'a> {
|
||||
fn new(inner: &'a mut web_transport_wasm::SendStream, terminal: &'a mut bool) -> Self {
|
||||
return Self { inner, terminal, armed: true };
|
||||
}
|
||||
|
||||
fn abort(&mut self) {
|
||||
if self.armed {
|
||||
self.inner.reset(STREAM_CANCELLED_ERROR_CODE);
|
||||
*self.terminal = true;
|
||||
self.armed = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn disarm(&mut self) {
|
||||
self.armed = false;
|
||||
}
|
||||
|
||||
async fn write_frame(&mut self, frame_header: &[u8; PRIMARY_FRAME_HEADER_SIZE], payload: &[u8]) -> Result<(), web_transport_wasm::Error> {
|
||||
match self.inner.write(frame_header).await {
|
||||
Ok(()) => {},
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
if !payload.is_empty() {
|
||||
match self.inner.write(payload).await {
|
||||
Ok(()) => {},
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SendOperationGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.abort();
|
||||
}
|
||||
}
|
||||
|
||||
/// Establishes one browser WebTransport session using an exact SHA-256 certificate pin.
|
||||
pub async fn connect(config: &WebTransportClientConfig) -> Result<WebTransportSession, game_realtime_transport_lib::TransportError> {
|
||||
if let Err(error) = config.transport.validate() {
|
||||
return Err(error);
|
||||
}
|
||||
let client = web_transport_wasm::ClientBuilder::new()
|
||||
.with_pooling(false)
|
||||
.with_unreliable(true)
|
||||
.with_server_certificate_hashes(vec![config.certificate_hash.as_bytes().to_vec()]);
|
||||
let session = match client.connect(config.endpoint.clone()).await {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
let mapped = transport_error(game_realtime_transport_lib::TransportErrorKind::Connect, error.to_string());
|
||||
tracing::warn!(
|
||||
target: TRACING_TARGET,
|
||||
endpoint = config.endpoint.as_str(),
|
||||
detail = mapped.detail(),
|
||||
"browser WebTransport client connection failed"
|
||||
);
|
||||
return Err(mapped);
|
||||
},
|
||||
};
|
||||
tracing::info!(target: TRACING_TARGET, endpoint = config.endpoint.as_str(), "browser WebTransport client connected");
|
||||
return Ok(WebTransportSession::new(session, config.transport));
|
||||
}
|
||||
|
||||
fn frame_header(payload_len: usize, max_message_size: usize) -> Result<[u8; PRIMARY_FRAME_HEADER_SIZE], game_realtime_transport_lib::TransportError> {
|
||||
if payload_len > max_message_size {
|
||||
return Err(message_too_large(payload_len, max_message_size));
|
||||
}
|
||||
let payload_len = match u32::try_from(payload_len) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return Err(message_too_large(payload_len, max_message_size)),
|
||||
};
|
||||
return Ok(payload_len.to_be_bytes());
|
||||
}
|
||||
|
||||
fn invalid_configuration(detail: impl Into<String>) -> game_realtime_transport_lib::TransportError {
|
||||
return transport_error(game_realtime_transport_lib::TransportErrorKind::InvalidConfiguration, detail);
|
||||
}
|
||||
|
||||
fn map_read_error(error: web_transport_wasm::Error) -> game_realtime_transport_lib::TransportError {
|
||||
let kind = match &error {
|
||||
web_transport_wasm::Error::Closed => game_realtime_transport_lib::TransportErrorKind::Closed,
|
||||
web_transport_wasm::Error::Session { .. } => game_realtime_transport_lib::TransportErrorKind::Closed,
|
||||
web_transport_wasm::Error::Stream(_) => game_realtime_transport_lib::TransportErrorKind::Aborted,
|
||||
web_transport_wasm::Error::Unknown(_) => game_realtime_transport_lib::TransportErrorKind::Io,
|
||||
};
|
||||
return transport_error(kind, error.to_string());
|
||||
}
|
||||
|
||||
fn map_write_error(error: web_transport_wasm::Error) -> game_realtime_transport_lib::TransportError {
|
||||
let kind = match &error {
|
||||
web_transport_wasm::Error::Closed => game_realtime_transport_lib::TransportErrorKind::Closed,
|
||||
web_transport_wasm::Error::Session { .. } => game_realtime_transport_lib::TransportErrorKind::Closed,
|
||||
web_transport_wasm::Error::Stream(_) => game_realtime_transport_lib::TransportErrorKind::Aborted,
|
||||
web_transport_wasm::Error::Unknown(_) => game_realtime_transport_lib::TransportErrorKind::Io,
|
||||
};
|
||||
return transport_error(kind, error.to_string());
|
||||
}
|
||||
|
||||
fn message_too_large(payload_len: usize, max_message_size: usize) -> game_realtime_transport_lib::TransportError {
|
||||
return transport_error(
|
||||
game_realtime_transport_lib::TransportErrorKind::MessageTooLarge,
|
||||
format!("WebTransport framed payload length {payload_len} exceeds configured maximum {max_message_size}"),
|
||||
);
|
||||
}
|
||||
|
||||
fn protocol_error(detail: impl Into<String>) -> game_realtime_transport_lib::TransportError {
|
||||
return transport_error(game_realtime_transport_lib::TransportErrorKind::Protocol, detail);
|
||||
}
|
||||
|
||||
fn transport_error(kind: game_realtime_transport_lib::TransportErrorKind, detail: impl Into<String>) -> game_realtime_transport_lib::TransportError {
|
||||
return game_realtime_transport_lib::TransportError::new(kind, detail);
|
||||
}
|
||||
Reference in New Issue
Block a user