0.3.5-alpha.3

This commit is contained in:
2026-09-21 22:55:21 +02:00
parent 5b7939cbd8
commit fd6ffecdf5
11 changed files with 750 additions and 37 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: crates/common/game-realtime-webtransport-lib/README.md -->
<!-- version: 1 -->
<!-- version: 2 -->
# game-realtime-webtransport-lib
@@ -9,13 +9,18 @@ Backend WebTransport/QUIC candidat pour le realtime de `games.sasedev`.
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 première frontière disponible couvre :
La frontière native disponible couvre désormais :
- configuration client HTTPS avec pin SHA-256 exact ;
- identité serveur X.509 DER + clé privée PKCS#8 DER injectables ;
- génération locale d'une identité self-signed ECDSA P-256 à validité courte pour `localhost`, IPv4 loopback et IPv6 loopback ;
- bind UDP/QUIC sur adresse explicite ou port éphémère ;
- établissement HTTP/3 WebTransport client/server natif ;
- sélection d'un unique stream bidirectionnel fiable comme chemin realtime principal ;
- framing privé `u32` big-endian + payload binaire ;
- borne POC de 1 MiB vérifiée avant allocation côté réception et avant écriture côté émission ;
- adaptation `RealtimeConnection` / `RealtimeSender` / `RealtimeReceiver` ;
- fermeture propre du chemin logique par FIN du stream primaire ;
- tracing sous `games::realtime::webtransport`.
## TLS de développement
@@ -26,10 +31,42 @@ Une identité préexistante peut être injectée en DER avec `WebTransportServer
Aucune option de désactivation globale de la vérification TLS n'est exposée.
## Stream fiable principal
Une session WebTransport établie n'est pas encore le contrat realtime lui-même. Le client appelle `WebTransportSession::open_primary_connection()` ; le serveur appelle `WebTransportSession::accept_primary_connection()`.
Le chemin logique devient ensuite :
```text
one WebTransport session
-> one primary bidirectional reliable stream
-> u32 big-endian payload length
-> payload bytes
```
`open_primary_connection()` écrit déjà l'en-tête de stream WebTransport requis par HTTP/3 avant de retourner. Le pair peut donc terminer `accept_primary_connection()` avant l'envoi de la première frame applicative ; aucun préambule propre à games.sasedev n'est nécessaire.
`RealtimeConnection::split()` conserve la session WebTransport dans les deux moitiés afin que la session ne soit pas fermée au moment où l'objet connexion est consommé.
## Limite de frame POC
La borne actuelle du framing fiable est volontairement interne à cette tranche : 1 MiB par message. Elle empêche une longueur `u32` hostile de provoquer une allocation arbitraire et rejette aussi l'émission hors limite avec `TransportErrorKind::MessageTooLarge`.
Cette valeur n'est pas encore une configuration produit. La tranche de robustesse suivante doit décider si la limite devient configurable avec les deadlines, la backpressure, les resets, l'abort/cancellation et le mapping d'erreurs détaillé.
## Frontières actuelles
Cette crate n'adapte pas encore une session vers `RealtimeConnection`. Elle n'ouvre pas encore le stream bidirectionnel applicatif principal et ne définit donc ni framing message, ni limites de payload, ni deadlines applicatives, ni datagram API commune.
La crate ne possède toujours pas :
Ces responsabilités doivent rester séparées de la seule preuve d'établissement natif afin que l'introduction de QUIC/TLS demeure testable indépendamment du futur framing fiable.
- d'API datagram transport-neutral ;
- de deadlines applicatives WebTransport ;
- de politique de backpressure explicite ;
- de reset/abort/cancellation produit ;
- de mapping fin de toutes les erreurs Quinn/WebTransport ;
- de chemin navigateur/WASM ;
- de smoke executable public WebTransport ;
- de benchmark WebSocket/WebTransport.
Le chemin natif sexé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.
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.

View File

@@ -0,0 +1,89 @@
<!-- file: crates/common/game-realtime-webtransport-lib/USAGE.md -->
<!-- version: 1 -->
# Utilisation de game-realtime-webtransport-lib
Ce guide décrit le chemin natif fiable actuellement exposé par `game-realtime-webtransport-lib`. Il ne décrit ni gameplay, ni protocole wire métier, ni datagrams.
## Serveur natif
Créer d'abord l'identité TLS et le listener :
```rust
let identity = match game_realtime_webtransport_lib::WebTransportServerIdentity::generate_loopback() {
Ok(value) => value,
Err(error) => return Err(error),
};
let config = game_realtime_webtransport_lib::WebTransportServerConfig::new(
std::net::SocketAddr::from(([127, 0, 0, 1], 4433)),
identity,
);
let mut listener = match game_realtime_webtransport_lib::WebTransportListener::bind(config) {
Ok(value) => value,
Err(error) => return Err(error),
};
let session = match listener.accept().await {
Ok(value) => value,
Err(error) => return Err(error),
};
let connection = match session.accept_primary_connection().await {
Ok(value) => value,
Err(error) => return Err(error),
};
```
`open_primary_connection()` écrit l'en-tête WebTransport requis pour identifier le stream avant de retourner. Le serveur peut donc attendre `accept_primary_connection()` puis commencer les échanges applicatifs ; aucune frame artificielle n'est nécessaire pour rendre le stream visible.
## Client natif
Le client doit connaître le SHA-256 exact du certificat serveur :
```rust
let config = match game_realtime_webtransport_lib::WebTransportClientConfig::new(
"https://127.0.0.1:4433/game",
certificate_hash,
) {
Ok(value) => value,
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 pinning est obligatoire dans cette API native ; il n'existe pas de variante qui désactive globalement la vérification TLS.
## Contrat realtime
Une fois le stream primaire sélectionné, utiliser uniquement les traits de `game-realtime-transport-lib` :
```rust
let (mut sender, mut receiver) = game_realtime_transport_lib::RealtimeConnection::split(connection);
let message = game_realtime_transport_lib::TransportMessage::new(vec![1, 2, 3, 4]);
if let Err(error) = game_realtime_transport_lib::RealtimeSender::send(&mut sender, message).await {
return Err(error);
}
let received = match game_realtime_transport_lib::RealtimeReceiver::receive(&mut receiver).await {
Ok(value) => value,
Err(error) => return Err(error),
};
if let Err(error) = game_realtime_transport_lib::RealtimeSender::close(&mut sender).await {
return Err(error);
}
```
Le backend encode chaque `TransportMessage` sous la forme `u32` big-endian + payload. Le consommateur ne doit pas reproduire ce framing lui-même.
## Fermeture actuelle
`RealtimeSender::close()` termine proprement la direction d'émission du stream primaire. Le pair observe ensuite `TransportReceive::Closed` lorsqu'il atteint le FIN après les messages déjà écrits.
La fermeture de session complète, les resets, les aborts, les timeouts et les cas d'annulation appartiennent à la tranche de robustesse suivante et ne doivent pas être simulés par le consommateur.

View File

@@ -1,25 +1,31 @@
// file: crates/common/game-realtime-webtransport-lib/src/lib.rs
// version: 1
// version: 2
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Native Quinn-backed WebTransport establishment for the games.sasedev realtime transport POC.
//! Native WebTransport/QUIC backend candidate for the transport-neutral realtime contract.
mod webtransport;
/// Re-export of one SHA-256 certificate fingerprint accepted by the native WebTransport client.
/// Re-export of the pinned SHA-256 certificate fingerprint used by the native client.
pub use self::webtransport::WebTransportCertificateHash;
/// Re-export of native client establishment configuration.
/// Re-export of native WebTransport client configuration.
pub use self::webtransport::WebTransportClientConfig;
/// Re-export of a bound native WebTransport server listener.
/// Re-export of an established WebTransport connection adapted to the transport-neutral realtime contract.
pub use self::webtransport::WebTransportConnection;
/// Re-export of the bound native WebTransport listener.
pub use self::webtransport::WebTransportListener;
/// Re-export of native WebTransport server establishment configuration.
/// Re-export of the receive half of the primary reliable WebTransport stream.
pub use self::webtransport::WebTransportReceiver;
/// Re-export of the send half of the primary reliable WebTransport stream.
pub use self::webtransport::WebTransportSender;
/// Re-export of native WebTransport server configuration.
pub use self::webtransport::WebTransportServerConfig;
/// Re-export of a generated or injected WebTransport server identity.
/// Re-export of native WebTransport server TLS identity material.
pub use self::webtransport::WebTransportServerIdentity;
/// Re-export of an established native WebTransport session.
pub use self::webtransport::WebTransportSession;
/// Re-export of the native WebTransport client session constructor.
/// Re-export of the native WebTransport client establishment function.
pub use self::webtransport::connect;

View File

@@ -1,9 +1,11 @@
// file: crates/common/game-realtime-webtransport-lib/src/webtransport.rs
// version: 1
// version: 2
const CERTIFICATE_HASH_SIZE: usize = 32;
const LOCAL_CERTIFICATE_CLOCK_SKEW: std::time::Duration = std::time::Duration::from_secs(60);
const LOCAL_CERTIFICATE_VALIDITY: std::time::Duration = std::time::Duration::from_secs(7 * 24 * 60 * 60);
const PRIMARY_FRAME_HEADER_SIZE: usize = 4;
const PRIMARY_FRAME_MAX_PAYLOAD_SIZE: usize = 1024 * 1024;
const TRACING_TARGET: &str = "games::realtime::webtransport";
/// SHA-256 fingerprint of one certificate accepted by the native WebTransport client.
@@ -151,7 +153,7 @@ impl WebTransportServerConfig {
}
}
/// Established native WebTransport session before application-stream adaptation.
/// Established native WebTransport session before or while the single primary application stream is selected.
pub struct WebTransportSession {
inner: web_transport_quinn::Session,
}
@@ -161,6 +163,37 @@ impl WebTransportSession {
return Self { inner };
}
/// Accepts the peer-created primary bidirectional stream and adapts it to the transport-neutral realtime contract.
///
/// The native WebTransport wrapper writes the required stream/session header while opening the stream, so the peer can
/// accept it before the first application frame is sent.
pub async fn accept_primary_connection(self) -> Result<WebTransportConnection, game_realtime_transport_lib::TransportError> {
let (sender, receiver) = match self.inner.accept_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(), "WebTransport primary bidirectional stream accept failed");
return Err(mapped);
},
};
tracing::debug!(target: TRACING_TARGET, peer = %self.inner.remote_address(), "WebTransport primary bidirectional stream accepted");
return Ok(WebTransportConnection::new(self.inner, sender, receiver));
}
/// 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(), "WebTransport primary bidirectional stream open failed");
return Err(mapped);
},
};
tracing::debug!(target: TRACING_TARGET, peer = %self.inner.remote_address(), "WebTransport primary bidirectional stream opened");
return Ok(WebTransportConnection::new(self.inner, sender, receiver));
}
/// Returns the remote UDP socket address backing the established QUIC connection.
#[must_use]
pub fn remote_addr(&self) -> std::net::SocketAddr {
@@ -177,6 +210,121 @@ impl WebTransportSession {
}
}
/// Established WebTransport realtime connection carried by one primary reliable bidirectional stream.
pub struct WebTransportConnection {
receiver: web_transport_quinn::RecvStream,
sender: web_transport_quinn::SendStream,
session: web_transport_quinn::Session,
}
impl WebTransportConnection {
fn new(session: web_transport_quinn::Session, sender: web_transport_quinn::SendStream, receiver: web_transport_quinn::RecvStream) -> Self {
return Self { receiver, sender, session };
}
}
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 },
crate::WebTransportReceiver { inner: self.receiver, _session: receiver_session },
);
}
}
/// Receive half of the primary reliable WebTransport stream.
pub struct WebTransportReceiver {
inner: web_transport_quinn::RecvStream,
_session: web_transport_quinn::Session,
}
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 {
let payload_len = match read_frame_payload_len(&mut self.inner).await {
Ok(Some(value)) => value,
Ok(None) => {
tracing::debug!(target: TRACING_TARGET, "remote WebTransport primary stream closed cleanly");
return Ok(game_realtime_transport_lib::TransportReceive::Closed);
},
Err(error) => return Err(error),
};
let payload = match read_frame_payload(&mut self.inner, payload_len).await {
Ok(value) => value,
Err(error) => return Err(error),
};
tracing::trace!(target: TRACING_TARGET, payload_len = payload.len(), "framed WebTransport payload received");
return Ok(game_realtime_transport_lib::TransportReceive::Message(game_realtime_transport_lib::TransportMessage::new(payload)));
});
}
}
/// Send half of the primary reliable WebTransport stream.
pub struct WebTransportSender {
inner: web_transport_quinn::SendStream,
_session: web_transport_quinn::Session,
}
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 {
return match self.inner.finish() {
Ok(()) => {
tracing::debug!(target: TRACING_TARGET, "local WebTransport primary stream close initiated");
Ok(())
},
Err(error) => {
let mapped = transport_error(game_realtime_transport_lib::TransportErrorKind::Closed, error.to_string());
tracing::warn!(target: TRACING_TARGET, detail = mapped.detail(), "WebTransport primary stream close failed");
Err(mapped)
},
};
});
}
fn send(&mut self, message: game_realtime_transport_lib::TransportMessage) -> Self::SendFuture<'_> {
return Box::pin(async move {
let payload_len = message.len();
let frame_header = match frame_header(payload_len) {
Ok(value) => value,
Err(error) => return Err(error),
};
if let Err(error) = self.inner.write_all(&frame_header).await {
let mapped = transport_error(game_realtime_transport_lib::TransportErrorKind::Protocol, error.to_string());
tracing::warn!(target: TRACING_TARGET, payload_len = payload_len, detail = mapped.detail(), "WebTransport frame header send failed");
return Err(mapped);
}
if let Err(error) = self.inner.write_all(message.as_bytes()).await {
let mapped = transport_error(game_realtime_transport_lib::TransportErrorKind::Protocol, error.to_string());
tracing::warn!(target: TRACING_TARGET, payload_len = payload_len, detail = mapped.detail(), "WebTransport frame payload send failed");
return Err(mapped);
}
tracing::trace!(target: TRACING_TARGET, payload_len = payload_len, "framed WebTransport payload sent");
return Ok(());
});
}
}
/// Bound native WebTransport server endpoint that accepts HTTP/3 WebTransport sessions.
pub struct WebTransportListener {
server: web_transport_quinn::Server,
@@ -270,10 +418,75 @@ fn certificate_hash(certificate_der: &[u8]) -> Result<WebTransportCertificateHas
return Ok(WebTransportCertificateHash::from_sha256(bytes));
}
fn frame_header(payload_len: usize) -> Result<[u8; PRIMARY_FRAME_HEADER_SIZE], game_realtime_transport_lib::TransportError> {
if payload_len > PRIMARY_FRAME_MAX_PAYLOAD_SIZE {
return Err(message_too_large(payload_len));
}
let payload_len = match u32::try_from(payload_len) {
Ok(value) => value,
Err(_) => return Err(message_too_large(payload_len)),
};
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 message_too_large(payload_len: usize) -> game_realtime_transport_lib::TransportError {
return transport_error(
game_realtime_transport_lib::TransportErrorKind::MessageTooLarge,
format!("WebTransport primary frame payload size {payload_len} exceeds {PRIMARY_FRAME_MAX_PAYLOAD_SIZE} bytes"),
);
}
async fn read_frame_payload(stream: &mut web_transport_quinn::RecvStream, payload_len: usize) -> Result<Vec<u8>, game_realtime_transport_lib::TransportError> {
let mut payload = vec![0_u8; payload_len];
let mut offset = 0_usize;
while offset < payload_len {
let read = match stream.read(&mut payload[offset..]).await {
Ok(Some(value)) => value,
Ok(None) => return Err(protocol_error("WebTransport primary stream closed in the middle of a frame payload")),
Err(error) => return Err(protocol_error(error.to_string())),
};
if read == 0 {
return Err(protocol_error("WebTransport primary stream returned an empty read in the middle of a frame payload"));
}
offset += read;
}
return Ok(payload);
}
async fn read_frame_payload_len(stream: &mut web_transport_quinn::RecvStream) -> Result<Option<usize>, game_realtime_transport_lib::TransportError> {
let mut header = [0_u8; PRIMARY_FRAME_HEADER_SIZE];
let mut offset = 0_usize;
while offset < PRIMARY_FRAME_HEADER_SIZE {
let read = match stream.read(&mut header[offset..]).await {
Ok(Some(value)) => value,
Ok(None) => {
if offset == 0 {
return Ok(None);
}
return Err(protocol_error("WebTransport primary stream closed in the middle of a frame header"));
},
Err(error) => return Err(protocol_error(error.to_string())),
};
if read == 0 {
return Err(protocol_error("WebTransport primary stream returned an empty read in the middle of a frame header"));
}
offset += read;
}
let payload_len = u32::from_be_bytes(header) as usize;
if payload_len > PRIMARY_FRAME_MAX_PAYLOAD_SIZE {
return Err(message_too_large(payload_len));
}
return Ok(Some(payload_len));
}
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);
}

View File

@@ -0,0 +1,89 @@
// file: crates/common/game-realtime-webtransport-lib/tests/realtime_connection.rs
// version: 1
//! Deterministic loopback proof for the primary reliable WebTransport stream and transport-neutral framing contract.
const TEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
#[tokio::test(flavor = "current_thread")]
async fn primary_stream_round_trip_is_binary_ordered_and_closes_cleanly() {
let identity = match game_realtime_webtransport_lib::WebTransportServerIdentity::generate_loopback() {
Ok(value) => value,
Err(error) => panic!("loopback identity generation failed: {error}"),
};
let certificate_hash = identity.certificate_hash().clone();
let server_config = game_realtime_webtransport_lib::WebTransportServerConfig::new(std::net::SocketAddr::from(([127, 0, 0, 1], 0)), identity);
let mut listener = match game_realtime_webtransport_lib::WebTransportListener::bind(server_config) {
Ok(value) => value,
Err(error) => panic!("WebTransport listener bind failed: {error}"),
};
let endpoint = format!("https://{}/realtime", listener.local_addr());
let client_config = match game_realtime_webtransport_lib::WebTransportClientConfig::new(endpoint.as_str(), certificate_hash) {
Ok(value) => value,
Err(error) => panic!("WebTransport client configuration failed: {error}"),
};
let sessions = tokio::time::timeout(TEST_TIMEOUT, async {
return tokio::join!(listener.accept(), game_realtime_webtransport_lib::connect(&client_config));
})
.await;
let (server_session, client_session) = match sessions {
Ok((Ok(server), Ok(client))) => (server, client),
Ok((Err(error), _)) => panic!("WebTransport server establishment failed: {error}"),
Ok((_, Err(error))) => panic!("WebTransport client establishment failed: {error}"),
Err(_) => panic!("WebTransport loopback establishment timed out"),
};
let client_connection = match client_session.open_primary_connection().await {
Ok(value) => value,
Err(error) => panic!("client primary stream open failed: {error}"),
};
let (mut client_sender, mut client_receiver) = game_realtime_transport_lib::RealtimeConnection::split(client_connection);
let server_connection = match tokio::time::timeout(TEST_TIMEOUT, server_session.accept_primary_connection()).await {
Ok(Ok(connection)) => connection,
Ok(Err(error)) => panic!("server primary stream accept failed: {error}"),
Err(_) => panic!("server primary stream accept timed out"),
};
let (mut server_sender, mut server_receiver) = game_realtime_transport_lib::RealtimeConnection::split(server_connection);
let first_payload = Vec::new();
send_payload(&mut client_sender, first_payload.clone(), "client first send").await;
assert_received_payload(&mut server_receiver, first_payload.clone(), "server first receive").await;
send_payload(&mut server_sender, first_payload.clone(), "server first echo").await;
assert_received_payload(&mut client_receiver, first_payload, "client first echo receive").await;
let remaining_payloads = [vec![0x00, 0x7f, 0x80, 0xff], b"third-message".to_vec()];
for payload in remaining_payloads {
send_payload(&mut client_sender, payload.clone(), "client ordered send").await;
assert_received_payload(&mut server_receiver, payload.clone(), "server ordered receive").await;
send_payload(&mut server_sender, payload.clone(), "server ordered echo").await;
assert_received_payload(&mut client_receiver, payload, "client ordered echo receive").await;
}
if let Err(error) = game_realtime_transport_lib::RealtimeSender::close(&mut client_sender).await {
panic!("client sender close failed: {error}");
}
assert_closed(&mut server_receiver, "server remote close").await;
if let Err(error) = game_realtime_transport_lib::RealtimeSender::close(&mut server_sender).await {
panic!("server sender close failed: {error}");
}
assert_closed(&mut client_receiver, "client remote close").await;
}
async fn assert_closed(receiver: &mut game_realtime_webtransport_lib::WebTransportReceiver, label: &str) {
let received = match game_realtime_transport_lib::RealtimeReceiver::receive(receiver).await {
Ok(value) => value,
Err(error) => panic!("{label} failed: {error}"),
};
assert_eq!(received, game_realtime_transport_lib::TransportReceive::Closed);
}
async fn assert_received_payload(receiver: &mut game_realtime_webtransport_lib::WebTransportReceiver, payload: Vec<u8>, label: &str) {
let received = match game_realtime_transport_lib::RealtimeReceiver::receive(receiver).await {
Ok(value) => value,
Err(error) => panic!("{label} failed: {error}"),
};
assert_eq!(received, game_realtime_transport_lib::TransportReceive::Message(game_realtime_transport_lib::TransportMessage::new(payload)));
}
async fn send_payload(sender: &mut game_realtime_webtransport_lib::WebTransportSender, payload: Vec<u8>, label: &str) {
let message = game_realtime_transport_lib::TransportMessage::new(payload);
if let Err(error) = game_realtime_transport_lib::RealtimeSender::send(sender, message).await {
panic!("{label} failed: {error}");
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/common/game-realtime-webtransport-lib/unit_tests/webtransport.rs
// version: 1
// version: 2
#[test]
fn certificate_hash_preserves_exact_sha256_bytes() {
@@ -25,6 +25,29 @@ fn client_config_accepts_https_and_rejects_non_secure_schemes() {
}
}
#[test]
fn frame_header_is_big_endian_and_payload_bound_is_enforced() {
let header = match super::frame_header(0x00_01_02_03) {
Ok(value) => value,
Err(error) => panic!("valid frame header rejected: {error}"),
};
assert_eq!(header, [0x00, 0x01, 0x02, 0x03]);
let oversized = super::frame_header(super::PRIMARY_FRAME_MAX_PAYLOAD_SIZE + 1);
match oversized {
Ok(_) => panic!("oversized WebTransport frame unexpectedly accepted"),
Err(error) => assert_eq!(error.kind(), game_realtime_transport_lib::TransportErrorKind::MessageTooLarge),
}
}
#[test]
fn generated_loopback_identity_has_sha256_fingerprint() {
let identity = match super::WebTransportServerIdentity::generate_loopback() {
Ok(value) => value,
Err(error) => panic!("loopback identity generation failed: {error}"),
};
assert_eq!(identity.certificate_hash().as_bytes().len(), super::CERTIFICATE_HASH_SIZE);
}
#[test]
fn injected_identity_rejects_empty_certificate_or_key() {
let missing_certificate = super::WebTransportServerIdentity::from_pkcs8_der(Vec::new(), vec![1]);
@@ -38,12 +61,3 @@ fn injected_identity_rejects_empty_certificate_or_key() {
Err(error) => assert_eq!(error.kind(), game_realtime_transport_lib::TransportErrorKind::InvalidConfiguration),
}
}
#[test]
fn generated_loopback_identity_has_sha256_fingerprint() {
let identity = match super::WebTransportServerIdentity::generate_loopback() {
Ok(value) => value,
Err(error) => panic!("loopback identity generation failed: {error}"),
};
assert_eq!(identity.certificate_hash().as_bytes().len(), super::CERTIFICATE_HASH_SIZE);
}