General Upgrade, NEW: WebServers, Better Docs
Some checks failed
CI / checks (push) Failing after 2m23s

This commit is contained in:
Alex Emmet 2026-07-18 03:08:03 +02:00
commit 08aa193fd1
119 changed files with 10029 additions and 4883 deletions

2
transport/Cargo.lock generated
View file

@ -4,4 +4,4 @@ version = 4
[[package]]
name = "transport"
version = "0.1.0"
version = "0.2.0"

View file

@ -1,11 +1,11 @@
[package]
name = "mtp-transport"
version = "0.1.0"
version = "0.2.0"
edition = "2024"
[dependencies]
mtp-codec = { version = "0.1.0", path = "../codec" }
mtp-common = { version = "0.1.0", path = "../common" }
mtp-codec = { version = "0.2.0", path = "../codec" }
mtp-common = { version = "0.2.0", path = "../common" }
wtransport = { version = "0.7.1", default-features = false, features = [
"aws-lc-rs",
"quinn",
@ -14,18 +14,25 @@ wtransport = { version = "0.7.1", default-features = false, features = [
rustls = { version = "0.23.41" }
tokio = { version = "1", features = ["full"] }
rustls-native-certs = "0.8.4"
log = "0.4"
rcgen = "0.14"
tracing = "0.1"
async-trait = "0.1"
sha2 = "0.11"
[dev-dependencies]
[[test]]
name = "integration"
required-features = ["host"]
required-features = ["host", "insecure-tls"]
[features]
# Enables hosting a MTP server
host = []
pipes = ["mtp-codec/pipes"]
# Compiles the insecure certificate verifier (NoopCertVerifier).
# Even with this feature enabled, the verifier requires the environment
# variable MTP_INSECURE_TLS=1 at runtime. Intended for local development
# only; never enable in release builds.
insecure-tls = []

View file

@ -1,61 +1,124 @@
use std::sync::Arc;
use mtp_common::CommunicationError;
use rustls::{
ClientConfig as RustlsClientConfig, DigitallySignedStruct, RootCertStore, SignatureScheme,
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
pki_types::{ServerName, UnixTime, pem::PemObject},
};
use wtransport::{ClientConfig, Endpoint};
use rustls::{ClientConfig as RustlsClientConfig, RootCertStore, pki_types::pem::PemObject};
use wtransport::{ClientConfig as WTransportClientConfig, Endpoint};
use crate::{ConnectionHandle, Policy, Receiver, Sender};
#[derive(Debug)]
struct NoopCertVerifier;
#[cfg(feature = "insecure-tls")]
mod noop_verifier {
use rustls::{
DigitallySignedStruct, SignatureScheme,
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
pki_types::{CertificateDer, ServerName, UnixTime},
};
impl ServerCertVerifier for NoopCertVerifier {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
Ok(ServerCertVerified::assertion())
#[derive(Debug)]
pub(super) struct NoopCertVerifier;
impl ServerCertVerifier for NoopCertVerifier {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
vec![
SignatureScheme::RSA_PKCS1_SHA1,
SignatureScheme::RSA_PKCS1_SHA256,
SignatureScheme::RSA_PKCS1_SHA384,
SignatureScheme::RSA_PKCS1_SHA512,
SignatureScheme::ECDSA_NISTP256_SHA256,
SignatureScheme::ECDSA_NISTP384_SHA384,
SignatureScheme::RSA_PSS_SHA256,
SignatureScheme::RSA_PSS_SHA384,
SignatureScheme::RSA_PSS_SHA512,
SignatureScheme::ED25519,
]
}
}
}
/// TLS and transport settings for a native client connection.
///
/// Certificate verification uses system roots by default. Disabling verification
/// requires an explicit call to [`Self::with_insecure_certificate_verification`]
/// and the `insecure-tls` compile-time feature.
pub struct ClientConfig {
server_cert: Option<Vec<u8>>,
pinned_hash: Option<[u8; 32]>,
insecure_certificate_verification: bool,
policy: Policy,
}
impl ClientConfig {
pub fn new(policy: Policy) -> Self {
Self {
server_cert: None,
pinned_hash: None,
insecure_certificate_verification: false,
policy,
}
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
pub fn with_server_certificate(mut self, cert_pem: Vec<u8>) -> Self {
self.server_cert = Some(cert_pem);
self.insecure_certificate_verification = false;
self.pinned_hash = None;
self
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
/// Pin the connection to a specific SPKI SHA-256 hash.
///
/// The client will only accept server certificates whose DER-encoded Subject
/// Public Key Info matches the given 32-byte hash. This is the recommended
/// approach for trusting self-signed certificates without disabling
/// verification entirely.
pub fn with_pinned_certificate_hash(mut self, hash: [u8; 32]) -> Self {
self.pinned_hash = Some(hash);
self.server_cert = None;
self.insecure_certificate_verification = false;
self
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
vec![
SignatureScheme::RSA_PKCS1_SHA1,
SignatureScheme::RSA_PKCS1_SHA256,
SignatureScheme::RSA_PKCS1_SHA384,
SignatureScheme::RSA_PKCS1_SHA512,
SignatureScheme::ECDSA_NISTP256_SHA256,
SignatureScheme::ECDSA_NISTP384_SHA384,
SignatureScheme::RSA_PSS_SHA256,
SignatureScheme::RSA_PSS_SHA384,
SignatureScheme::RSA_PSS_SHA512,
SignatureScheme::ED25519,
]
/// Disable server certificate verification.
///
/// Requires the `insecure-tls` feature at compile time and the environment
/// variable `MTP_INSECURE_TLS=1` at runtime. Returns an error if either
/// condition is not met.
///
/// Intended only for local development with a self-signed host.
#[cfg(feature = "insecure-tls")]
pub fn with_insecure_certificate_verification(mut self) -> Self {
self.server_cert = None;
self.pinned_hash = None;
self.insecure_certificate_verification = true;
self
}
}
@ -63,13 +126,47 @@ pub async fn connect(
url: &str,
server_cert: Option<Vec<u8>>,
policy: Policy,
) -> Result<(Sender, Receiver), CommunicationError> {
let config = match server_cert {
Some(cert_pem) => ClientConfig::new(policy).with_server_certificate(cert_pem),
None => ClientConfig::new(policy),
};
connect_with_config(url, config).await
}
/// Connect using explicit TLS and transport configuration.
pub async fn connect_with_config(
url: &str,
config: ClientConfig,
) -> Result<(Sender, Receiver), CommunicationError> {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let client_config = if let Some(cert_pem) = server_cert {
configure_client_with_cert(cert_pem, &policy)?
let client_config = if config.insecure_certificate_verification {
#[cfg(feature = "insecure-tls")]
{
let env_val = std::env::var("MTP_INSECURE_TLS")
.map(|v| v == "1")
.unwrap_or(false);
if !env_val {
return Err(CommunicationError::Other(
"insecure TLS requires MTP_INSECURE_TLS=1 in the environment".into(),
));
}
client_config_insecure(&config.policy)?
}
#[cfg(not(feature = "insecure-tls"))]
{
unreachable!(
"insecure_certificate_verification is only set when \
the insecure-tls feature is enabled"
)
}
} else if let Some(hash) = config.pinned_hash {
crate::pinning::configure_client_pinned_hash(hash, &config.policy)?
} else if let Some(cert_pem) = config.server_cert {
configure_client_with_cert(cert_pem, &config.policy)?
} else {
configure_client_system_roots(&policy)?
configure_client_system_roots(&config.policy)?
};
let endpoint = Endpoint::client(client_config)
@ -81,7 +178,7 @@ pub async fn connect(
.map_err(|e| CommunicationError::ConnectingError(e.to_string()))?;
let handle = Arc::new(ConnectionHandle::new());
let policy = Arc::new(policy);
let policy = Arc::new(config.policy);
let sender = Sender::new(connection.clone(), handle.clone(), policy.clone());
let receiver = Receiver::new(connection, handle, policy);
@ -92,7 +189,7 @@ pub async fn connect(
fn configure_client_with_cert(
server_cert: Vec<u8>,
policy: &Policy,
) -> Result<ClientConfig, CommunicationError> {
) -> Result<WTransportClientConfig, CommunicationError> {
let mut root_store = RootCertStore::empty();
let certs = rustls::pki_types::CertificateDer::pem_slice_iter(&server_cert)
@ -108,7 +205,10 @@ fn configure_client_with_cert(
client_config_from_roots(root_store, policy)
}
fn client_config_insecure(policy: &Policy) -> Result<ClientConfig, CommunicationError> {
#[cfg(feature = "insecure-tls")]
fn client_config_insecure(policy: &Policy) -> Result<WTransportClientConfig, CommunicationError> {
use noop_verifier::NoopCertVerifier;
let mut tls_config = RustlsClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(NoopCertVerifier))
@ -116,7 +216,7 @@ fn client_config_insecure(policy: &Policy) -> Result<ClientConfig, Communication
tls_config.alpn_protocols = vec![b"h3".to_vec()];
Ok(ClientConfig::builder()
Ok(WTransportClientConfig::builder()
.with_bind_default()
.with_custom_tls(tls_config)
.keep_alive_interval(policy.keep_alive_interval)
@ -125,16 +225,11 @@ fn client_config_insecure(policy: &Policy) -> Result<ClientConfig, Communication
.build())
}
fn configure_client_system_roots(policy: &Policy) -> Result<ClientConfig, CommunicationError> {
// Check if insecure mode is enabled via env variable MTP_INSECURE
let insecure = std::env::var("MTP_INSECURE").is_ok();
if insecure {
// Insecure mode: skip certificate verification entirely
return client_config_insecure(policy);
}
fn configure_client_system_roots(
policy: &Policy,
) -> Result<WTransportClientConfig, CommunicationError> {
let mut root_store = RootCertStore::empty();
// Load native certs
let certs = rustls_native_certs::load_native_certs().certs;
for cert in certs {
@ -147,14 +242,14 @@ fn configure_client_system_roots(policy: &Policy) -> Result<ClientConfig, Commun
fn client_config_from_roots(
root_store: RootCertStore,
policy: &Policy,
) -> Result<ClientConfig, CommunicationError> {
) -> Result<WTransportClientConfig, CommunicationError> {
let mut tls_config = RustlsClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
tls_config.alpn_protocols = vec![b"h3".to_vec()];
Ok(ClientConfig::builder()
Ok(WTransportClientConfig::builder()
.with_bind_default()
.with_custom_tls(tls_config)
.keep_alive_interval(policy.keep_alive_interval)

View file

@ -4,16 +4,17 @@ use crate::pipe::PipeReader;
use mtp_codec::CommunicationValue;
use mtp_common::CommunicationError;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::sync::{Mutex, Notify, RwLock, Semaphore, mpsc};
use tokio::time::{Duration, sleep, timeout};
use tracing::{debug, info, instrument, trace};
use tracing::{debug, info, instrument, trace, warn};
use wtransport::Connection;
#[cfg(feature = "pipes")]
#[derive(Debug)]
pub enum TransportEvent {
pub enum TransportEvent<R = wtransport::RecvStream> {
Message(CommunicationValue),
Pipe(PipeReader),
Pipe(PipeReader<R>),
}
const APPLICATION_CLOSE_REASON: &str = "mtp-close";
@ -28,6 +29,8 @@ pub enum SendMode {
pub struct Policy {
pub send_mode: SendMode,
pub max_message_size: u64,
/// Receive limit used until the application-level handshake completes.
pub handshake_max_message_size: u64,
pub close_frame_len: u32,
pub application_close_code: u32,
pub open_stream_timeout: Duration,
@ -37,8 +40,6 @@ pub struct Policy {
pub keep_alive_interval: Option<Duration>,
pub max_idle_timeout: Option<Duration>,
pub force_close_delay: Duration,
pub max_transient_recv_errors: usize,
pub transient_recv_backoff: Duration,
pub persistent_stream_max_retries: usize,
pub persistent_stream_retry_backoff: Duration,
pub receiver_queue_capacity: usize,
@ -50,7 +51,8 @@ impl Default for Policy {
fn default() -> Self {
Self {
send_mode: SendMode::PersistentStream,
max_message_size: 1_000_000_000,
max_message_size: 16 * 1024 * 1024,
handshake_max_message_size: 64 * 1024,
close_frame_len: u32::MAX,
application_close_code: 0,
open_stream_timeout: Duration::from_millis(2_000),
@ -60,8 +62,6 @@ impl Default for Policy {
keep_alive_interval: Some(Duration::from_secs(3)),
max_idle_timeout: Some(Duration::from_secs(30)),
force_close_delay: Duration::from_millis(300),
max_transient_recv_errors: 20,
transient_recv_backoff: Duration::from_millis(100),
persistent_stream_max_retries: 4,
persistent_stream_retry_backoff: Duration::from_millis(20),
receiver_queue_capacity: 1000,
@ -82,6 +82,11 @@ impl Policy {
self
}
pub fn with_handshake_max_message_size(mut self, max_message_size: u64) -> Self {
self.handshake_max_message_size = max_message_size;
self
}
pub fn with_timeouts(
mut self,
open_stream_timeout: Duration,
@ -179,15 +184,15 @@ impl Sender {
match timeout(policy.write_timeout, write_result).await {
Ok(Ok(())) => Ok(()),
Ok(Err(wtransport::error::StreamWriteError::Stopped(code))) => {
log::warn!("[Sender] write failed: peer sent STOP_SENDING (error code {code})");
warn!("[Sender] write failed: peer sent STOP_SENDING (error code {code})");
Err(CommunicationError::StreamClosed)
}
Ok(Err(other)) => {
log::warn!("[Sender] write failed: {other}");
warn!("[Sender] write failed: {other}");
Err(CommunicationError::StreamError)
}
Err(_) => {
log::warn!("[Sender] write timed out (len={})", bytes.len());
warn!("[Sender] write timed out (len={})", bytes.len());
Err(CommunicationError::StreamError)
}
}
@ -260,6 +265,12 @@ impl Sender {
}
let err = res.err().unwrap_or(CommunicationError::StreamError);
if !matches!(
err,
CommunicationError::StreamError | CommunicationError::StreamClosed
) {
return Err(err);
}
*stream_opt = None;
tries += 1;
if tries > policy.persistent_stream_max_retries {
@ -283,15 +294,15 @@ impl Sender {
match timeout(policy.write_timeout, stream.finish()).await {
Ok(Ok(())) => Ok(()),
Ok(Err(wtransport::error::StreamWriteError::Stopped(code))) => {
log::warn!("[Sender] finish failed: peer sent STOP_SENDING (error code {code})");
warn!("[Sender] finish failed: peer sent STOP_SENDING (error code {code})");
Err(CommunicationError::StreamClosed)
}
Ok(Err(other)) => {
log::warn!("[Sender] finish failed: {other}");
warn!("[Sender] finish failed: {other}");
Err(CommunicationError::StreamError)
}
Err(_) => {
log::warn!("[Sender] finish timed out");
warn!("[Sender] finish timed out");
Err(CommunicationError::StreamError)
}
}
@ -308,30 +319,30 @@ impl Sender {
match timeout(policy.write_timeout, stream.write_all(&len_bytes)).await {
Ok(Ok(())) => {}
Ok(Err(wtransport::error::StreamWriteError::Stopped(code))) => {
log::warn!(
warn!(
"[Sender] close frame write failed: peer sent STOP_SENDING (error code {code})"
);
}
Ok(Err(other)) => {
log::warn!("[Sender] close frame write failed: {other}");
warn!("[Sender] close frame write failed: {other}");
}
Err(_) => {
log::warn!("[Sender] close frame write timed out");
warn!("[Sender] close frame write timed out");
}
}
match timeout(policy.write_timeout, stream.finish()).await {
Ok(Ok(())) => {}
Ok(Err(wtransport::error::StreamWriteError::Stopped(code))) => {
log::warn!(
warn!(
"[Sender] close frame finish failed: peer sent STOP_SENDING (error code {code})"
);
}
Ok(Err(other)) => {
log::warn!("[Sender] close frame finish failed: {other}");
warn!("[Sender] close frame finish failed: {other}");
}
Err(_) => {
log::warn!("[Sender] close frame finish timed out");
warn!("[Sender] close frame finish timed out");
}
}
@ -402,17 +413,15 @@ impl Sender {
match timeout(self.policy.write_timeout, stream.finish()).await {
Ok(Ok(())) => {}
Ok(Err(wtransport::error::StreamWriteError::Stopped(code))) => {
log::warn!(
"[Sender] finish_stream: peer sent STOP_SENDING (error code {code})"
);
warn!("[Sender] finish_stream: peer sent STOP_SENDING (error code {code})");
return Err(CommunicationError::StreamClosed);
}
Ok(Err(other)) => {
log::warn!("[Sender] finish_stream failed: {other}");
warn!("[Sender] finish_stream failed: {other}");
return Err(CommunicationError::StreamError);
}
Err(_) => {
log::warn!("[Sender] finish_stream timed out");
warn!("[Sender] finish_stream timed out");
return Err(CommunicationError::StreamError);
}
}
@ -478,13 +487,13 @@ impl Sender {
if let Some(mut stream) = stream_guard.lock().await.take() {
match timeout(policy.write_timeout, stream.finish()).await {
Ok(Ok(())) => {}
Ok(Err(wtransport::error::StreamWriteError::Stopped(code))) => log::warn!(
Ok(Err(wtransport::error::StreamWriteError::Stopped(code))) => warn!(
"[Sender] persistent stream finish failed: peer sent STOP_SENDING (error code {code})"
),
Ok(Err(e)) => {
log::warn!("[Sender] persistent stream finish failed: {e}")
warn!("[Sender] persistent stream finish failed: {e}")
}
Err(_) => log::warn!("[Sender] persistent stream finish timed out"),
Err(_) => warn!("[Sender] persistent stream finish timed out"),
}
}
@ -526,11 +535,11 @@ impl Sender {
match timeout(policy.write_timeout, close_write).await {
Ok(Ok(())) => {}
Ok(Err(wtransport::error::StreamWriteError::Stopped(code))) => log::warn!(
Ok(Err(wtransport::error::StreamWriteError::Stopped(code))) => warn!(
"[Sender] close_and_wait failed: peer sent STOP_SENDING (error code {code})"
),
Ok(Err(e)) => log::warn!("[Sender] close_and_wait failed: {e}"),
Err(_) => log::warn!("[Sender] close_and_wait timed out"),
Ok(Err(e)) => warn!("[Sender] close_and_wait failed: {e}"),
Err(_) => warn!("[Sender] close_and_wait timed out"),
}
} else {
let _ = Self::send_close_frame(&connection, &policy).await;
@ -585,6 +594,7 @@ struct ReceiverInner {
handle: Arc<ConnectionHandle>,
ping_control: Arc<RwLock<PingControl>>,
queue_notify: Arc<Notify>,
max_message_size: Arc<AtomicU64>,
}
impl Clone for Receiver {
@ -611,6 +621,26 @@ struct PingControl {
impl Receiver {
pub fn new(connection: Connection, handle: Arc<ConnectionHandle>, policy: Arc<Policy>) -> Self {
Self::new_with_max_message_size(connection, handle, policy.clone(), policy.max_message_size)
}
pub(crate) fn new_for_handshake(
connection: Connection,
handle: Arc<ConnectionHandle>,
policy: Arc<Policy>,
) -> Self {
let initial_max = policy
.handshake_max_message_size
.min(policy.max_message_size);
Self::new_with_max_message_size(connection, handle, policy, initial_max)
}
fn new_with_max_message_size(
connection: Connection,
handle: Arc<ConnectionHandle>,
policy: Arc<Policy>,
initial_max_message_size: u64,
) -> Self {
#[cfg(feature = "pipes")]
let (msg_tx, msg_rx) = mpsc::channel::<Result<CommunicationValue, CommunicationError>>(
policy.receiver_queue_capacity,
@ -629,6 +659,8 @@ impl Receiver {
let accept_ping_control = ping_control.clone();
let queue_notify = Arc::new(Notify::new());
let accept_queue_notify = queue_notify.clone();
let max_message_size = Arc::new(AtomicU64::new(initial_max_message_size));
let accept_max_message_size = max_message_size.clone();
let stream_limit = Arc::new(Semaphore::new(policy.max_concurrent_stream_tasks.max(1)));
let accept_stream_limit = stream_limit.clone();
debug!(
@ -695,6 +727,7 @@ impl Receiver {
let stream_handle = conn_handle.clone();
let stream_policy = accept_policy.clone();
let stream_ping_control = accept_ping_control.clone();
let stream_max_message_size = accept_max_message_size.clone();
tokio::spawn(async move {
let _permit = permit;
@ -713,7 +746,8 @@ impl Receiver {
break;
}
match Self::read_one_frame(&mut s, &stream_policy).await {
let frame_limit = stream_max_message_size.load(Ordering::Relaxed);
match Self::read_one_frame(&mut s, &stream_policy, frame_limit).await {
Ok(ReceivedFrame::Message(msg)) => {
frame_count += 1;
@ -721,8 +755,8 @@ impl Receiver {
{
let pipe_request_type =
mtp_codec::CommunicationType::PipeRequest
.to_id(&mtp_codec::TypeMap::latest());
if msg.get_type() == pipe_request_type
.try_to_id(&mtp_codec::TypeMap::latest());
if Some(msg.get_type()) == pipe_request_type
&& frame_count == 1
{
let pipe_id = msg.get_id();
@ -751,17 +785,17 @@ impl Receiver {
}
let ping_type = mtp_codec::CommunicationType::Ping
.to_id(&mtp_codec::TypeMap::latest());
.try_to_id(&mtp_codec::TypeMap::latest());
let pong_type = mtp_codec::CommunicationType::Pong
.to_id(&mtp_codec::TypeMap::latest());
.try_to_id(&mtp_codec::TypeMap::latest());
let control = {
let control = stream_ping_control.read().await;
if msg.get_type() == ping_type {
if Some(msg.get_type()) == ping_type {
control
.pong_sender
.clone()
.map(|sender| (Some(sender), None))
} else if msg.get_type() == pong_type {
} else if Some(msg.get_type()) == pong_type {
control
.pong_observer
.clone()
@ -781,7 +815,7 @@ impl Receiver {
);
}
if let Err(e) = sender.send(&pong).await {
log::warn!("[Receiver] failed to send Pong: {e}");
warn!("[Receiver] failed to send Pong: {e}");
}
continue;
}
@ -882,16 +916,24 @@ impl Receiver {
handle,
ping_control,
queue_notify,
max_message_size,
}),
}
}
/// Change the receive cap for subsequently parsed frames.
pub fn set_max_message_size(&self, max_message_size: u64) {
self.inner
.max_message_size
.store(max_message_size, Ordering::Relaxed);
}
/* Respond to reserved Ping frames without exposing them to application I/O. */
pub fn respond_to_pings(&self, sender: Sender) {
if let Ok(mut control) = self.inner.ping_control.try_write() {
control.pong_sender = Some(sender);
} else {
log::warn!("[Receiver] could not register Ping responder: control lock busy");
warn!("[Receiver] could not register Ping responder: control lock busy");
}
}
@ -900,7 +942,7 @@ impl Receiver {
if let Ok(mut control) = self.inner.ping_control.try_write() {
control.pong_observer = Some(observer);
} else {
log::warn!("[Receiver] could not register Pong observer: control lock busy");
warn!("[Receiver] could not register Pong observer: control lock busy");
}
}
@ -908,6 +950,7 @@ impl Receiver {
async fn read_one_frame(
stream: &mut wtransport::RecvStream,
policy: &Policy,
max_message_size: u64,
) -> Result<ReceivedFrame, CommunicationError> {
use wtransport::error::{StreamReadError, StreamReadExactError};
@ -918,23 +961,23 @@ impl Receiver {
return Ok(ReceivedFrame::Idle);
}
Ok(Err(StreamReadExactError::FinishedEarly(n))) => {
log::warn!(
warn!(
"[Receiver] length-prefix read ended early ({n}/4 bytes): stream closed by peer"
);
return Err(CommunicationError::StreamError);
}
Ok(Err(StreamReadExactError::Read(StreamReadError::Reset(code)))) => {
log::warn!(
warn!(
"[Receiver] length-prefix read failed: peer sent RESET_STREAM (error code {code})"
);
return Err(CommunicationError::StreamError);
}
Ok(Err(other)) => {
log::warn!("[Receiver] length-prefix read failed: {other}");
warn!("[Receiver] length-prefix read failed: {other}");
return Err(CommunicationError::StreamError);
}
Err(_) => {
log::warn!("[Receiver] length-prefix read timed out");
warn!("[Receiver] length-prefix read timed out");
return Err(CommunicationError::StreamError);
}
}
@ -945,32 +988,50 @@ impl Receiver {
}
let len_usize = len as usize;
if len as u64 > policy.max_message_size {
if len as u64 > max_message_size {
return Err(CommunicationError::MessageTooLarge);
}
let mut buf = vec![0u8; len_usize];
match timeout(policy.read_timeout, stream.read_exact(&mut buf)).await {
Ok(Ok(())) => {}
Ok(Err(StreamReadExactError::FinishedEarly(n))) => {
log::warn!(
"[Receiver] body read ended early ({n}/{len_usize} bytes): stream closed by peer"
);
return Err(CommunicationError::StreamError);
}
Ok(Err(StreamReadExactError::Read(StreamReadError::Reset(code)))) => {
log::warn!(
"[Receiver] body read failed: peer sent RESET_STREAM (error code {code})"
);
return Err(CommunicationError::StreamError);
}
Ok(Err(other)) => {
log::warn!("[Receiver] body read failed: {other}");
return Err(CommunicationError::StreamError);
}
Err(_) => {
log::warn!("[Receiver] body read timed out (len={len_usize})");
return Err(CommunicationError::StreamError);
// Grow in bounded chunks instead of trusting the peer's length prefix
// enough to allocate the complete frame up front.
let mut buf = Vec::new();
buf.try_reserve(len_usize.min(16 * 1024))
.map_err(|_| CommunicationError::MessageTooLarge)?;
while buf.len() < len_usize {
let chunk_len = (len_usize - buf.len()).min(16 * 1024);
let mut chunk = [0u8; 16 * 1024];
match timeout(
policy.read_timeout,
stream.read_exact(&mut chunk[..chunk_len]),
)
.await
{
Ok(Ok(())) => {
buf.try_reserve(chunk_len)
.map_err(|_| CommunicationError::MessageTooLarge)?;
buf.extend_from_slice(&chunk[..chunk_len]);
}
Ok(Err(StreamReadExactError::FinishedEarly(n))) => {
warn!(
"[Receiver] body read ended early ({}/{len_usize} bytes): stream closed by peer",
buf.len() + n
);
return Err(CommunicationError::StreamError);
}
Ok(Err(StreamReadExactError::Read(StreamReadError::Reset(code)))) => {
warn!(
"[Receiver] body read failed: peer sent RESET_STREAM (error code {code})"
);
return Err(CommunicationError::StreamError);
}
Ok(Err(other)) => {
warn!("[Receiver] body read failed: {other}");
return Err(CommunicationError::StreamError);
}
Err(_) => {
warn!("[Receiver] body read timed out (len={len_usize})");
return Err(CommunicationError::StreamError);
}
}
}
@ -1155,7 +1216,8 @@ mod tests {
fn test_policy_default_values() {
let p = Policy::default();
assert_eq!(p.send_mode, SendMode::PersistentStream);
assert_eq!(p.max_message_size, 1_000_000_000);
assert_eq!(p.max_message_size, 16 * 1024 * 1024);
assert_eq!(p.handshake_max_message_size, 64 * 1024);
assert_eq!(p.close_frame_len, u32::MAX);
assert_eq!(p.application_close_code, 0);
assert_eq!(p.open_stream_timeout, Duration::from_millis(2_000));
@ -1165,8 +1227,6 @@ mod tests {
assert_eq!(p.keep_alive_interval, Some(Duration::from_secs(3)));
assert_eq!(p.max_idle_timeout, Some(Duration::from_secs(30)));
assert_eq!(p.force_close_delay, Duration::from_millis(300));
assert_eq!(p.max_transient_recv_errors, 20);
assert_eq!(p.transient_recv_backoff, Duration::from_millis(100));
assert_eq!(p.persistent_stream_max_retries, 4);
assert_eq!(p.persistent_stream_retry_backoff, Duration::from_millis(20));
assert_eq!(p.receiver_queue_capacity, 1000);

21
transport/src/framing.rs Normal file
View file

@ -0,0 +1,21 @@
use crate::{Policy, TransportSendStream};
use mtp_codec::CommunicationValue;
use mtp_common::CommunicationError;
/// Writes the canonical length-prefixed MTP frame used by every transport.
pub(crate) async fn write_frame<S: TransportSendStream>(
stream: &mut S,
value: &CommunicationValue,
policy: &Policy,
) -> Result<(), CommunicationError> {
let bytes = value.to_bytes().map_err(|_| CommunicationError::Encode)?;
if bytes.len() as u64 > policy.max_message_size
|| bytes.len() as u64 >= policy.close_frame_len as u64
{
return Err(CommunicationError::MessageTooLarge);
}
stream
.write_all(&(bytes.len() as u32).to_be_bytes())
.await?;
stream.write_all(&bytes).await
}

View file

@ -0,0 +1,403 @@
//! Transport-neutral MTP framing.
//!
//! These types are used by non-wtransport backends. The established
//! [`crate::Sender`] and [`crate::Receiver`] remain source compatible native
//! wrappers while the framing implementation below is shared by adapters.
use crate::{
Policy, TransportConnection, TransportRecvStream, TransportSendStream, framing::write_frame,
};
use mtp_codec::CommunicationValue;
use mtp_common::CommunicationError;
use std::sync::Arc;
use tokio::sync::{Mutex, RwLock, Semaphore, mpsc};
use tokio::time::timeout;
#[cfg(feature = "pipes")]
use crate::pipe::{PipeReader, PipeWriter};
pub struct GenericSender<C: TransportConnection> {
connection: C,
policy: Arc<Policy>,
persistent: Arc<Mutex<Option<C::SendStream>>>,
send_lock: Arc<Mutex<()>>,
}
impl<C: TransportConnection> Clone for GenericSender<C> {
fn clone(&self) -> Self {
Self {
connection: self.connection.clone(),
policy: self.policy.clone(),
persistent: self.persistent.clone(),
send_lock: self.send_lock.clone(),
}
}
}
impl<C: TransportConnection> GenericSender<C> {
pub fn new(connection: C, policy: Arc<Policy>) -> Self {
Self {
connection,
policy,
persistent: Arc::new(Mutex::new(None)),
send_lock: Arc::new(Mutex::new(())),
}
}
async fn open(&self) -> Result<C::SendStream, CommunicationError> {
timeout(self.policy.open_stream_timeout, self.connection.open_uni())
.await
.map_err(|_| CommunicationError::StreamError)?
}
pub async fn send(&self, value: &CommunicationValue) -> Result<(), CommunicationError> {
let _lock = self.send_lock.lock().await;
if self.connection.close_reason().is_some() {
return Err(CommunicationError::StreamClosed);
}
match self.policy.send_mode {
crate::SendMode::SingleStreamPerMessage => {
let mut stream = self.open().await?;
timeout(
self.policy.write_timeout,
write_frame(&mut stream, value, &self.policy),
)
.await
.map_err(|_| CommunicationError::StreamError)??;
timeout(self.policy.write_timeout, stream.finish())
.await
.map_err(|_| CommunicationError::StreamError)?
}
crate::SendMode::PersistentStream => {
let mut stream = self.persistent.lock().await;
let mut attempts = 0;
loop {
if stream.is_none() {
*stream = Some(self.open().await?);
}
let result = timeout(
self.policy.write_timeout,
write_frame(stream.as_mut().unwrap(), value, &self.policy),
)
.await
.map_err(|_| CommunicationError::StreamError)
.and_then(|r| r);
if result.is_ok() {
return result;
}
*stream = None;
attempts += 1;
if attempts > self.policy.persistent_stream_max_retries {
return result;
}
tokio::time::sleep(
self.policy.persistent_stream_retry_backoff * attempts as u32,
)
.await;
}
}
}
}
#[cfg(feature = "pipes")]
pub async fn open_pipe(
&self,
pipe_id: u32,
description: &str,
) -> Result<PipeWriter<C::SendStream>, CommunicationError> {
if self.connection.close_reason().is_some() {
return Err(CommunicationError::StreamClosed);
}
let mut stream = self.open().await?;
let request = CommunicationValue::new(mtp_codec::CommunicationType::PipeRequest)
.with_id(pipe_id)
.add_typed_default(
mtp_codec::DataType::Description,
mtp_codec::DataValue::Str(description.to_string()),
);
timeout(
self.policy.write_timeout,
write_frame(&mut stream, &request, &self.policy),
)
.await
.map_err(|_| CommunicationError::StreamError)??;
Ok(PipeWriter { stream })
}
pub fn close(&self) {
self.connection
.close(self.policy.application_close_code, b"mtp-close");
}
pub fn is_closed(&self) -> bool {
self.connection.close_reason().is_some()
}
pub fn is_open(&self) -> bool {
!self.is_closed()
}
pub fn close_reason(&self) -> Option<CommunicationError> {
self.connection.close_reason()
}
}
pub struct GenericReceiver<C: TransportConnection> {
incoming: Arc<Mutex<mpsc::Receiver<Result<CommunicationValue, CommunicationError>>>>,
#[cfg(feature = "pipes")]
pipes: Arc<Mutex<mpsc::Receiver<PipeReader<C::RecvStream>>>>,
connection: C,
ping_sender: Arc<RwLock<Option<GenericSender<C>>>>,
}
impl<C: TransportConnection> Clone for GenericReceiver<C> {
fn clone(&self) -> Self {
Self {
incoming: self.incoming.clone(),
#[cfg(feature = "pipes")]
pipes: self.pipes.clone(),
connection: self.connection.clone(),
ping_sender: self.ping_sender.clone(),
}
}
}
impl<C: TransportConnection> GenericReceiver<C> {
pub fn new(connection: C, policy: Arc<Policy>) -> Self {
let (tx, rx) = mpsc::channel(policy.receiver_queue_capacity);
#[cfg(feature = "pipes")]
let (pipe_tx, pipe_rx) = mpsc::channel(policy.receiver_queue_capacity);
let ping_sender: Arc<RwLock<Option<GenericSender<C>>>> = Arc::new(RwLock::new(None));
let task_ping_sender = ping_sender.clone();
let task_connection = connection.clone();
let task_policy = policy.clone();
tokio::spawn(async move {
let limit = Arc::new(Semaphore::new(
task_policy.max_concurrent_stream_tasks.max(1),
));
loop {
let stream = match timeout(
task_policy.accept_stream_timeout,
task_connection.accept_uni(),
)
.await
{
Ok(Ok(stream)) => stream,
Ok(Err(error)) => {
let _ = tx.send(Err(error)).await;
break;
}
Err(_) => {
if task_connection.close_reason().is_some() {
let _ = tx.send(Err(CommunicationError::StreamClosed)).await;
break;
} else {
continue;
}
}
};
let tx = tx.clone();
#[cfg(feature = "pipes")]
let pipe_tx = pipe_tx.clone();
let policy = task_policy.clone();
let permit = limit.clone();
let ping_sender = task_ping_sender.clone();
tokio::spawn(async move {
let Ok(_permit) = permit.acquire_owned().await else {
return;
};
let mut stream = stream;
let mut frames = 0usize;
loop {
if policy
.max_frames_per_stream
.is_some_and(|max| frames >= max)
{
let _ = tx.send(Err(CommunicationError::StreamError)).await;
break;
}
let mut len = [0; 4];
match timeout(policy.read_timeout, stream.read_exact(&mut len)).await {
Ok(Ok(())) => {}
Ok(Err(_)) | Err(_) => break,
}
let len = u32::from_be_bytes(len);
if len == policy.close_frame_len {
let _ = tx.send(Err(CommunicationError::StreamClosed)).await;
break;
}
if len as u64 > policy.max_message_size {
let _ = tx.send(Err(CommunicationError::MessageTooLarge)).await;
break;
}
let target_len = len as usize;
let mut body = Vec::new();
if body.try_reserve(target_len.min(16 * 1024)).is_err() {
let _ = tx.send(Err(CommunicationError::MessageTooLarge)).await;
break;
}
while body.len() < target_len {
let chunk_len = (target_len - body.len()).min(16 * 1024);
let mut chunk = [0u8; 16 * 1024];
if !matches!(
timeout(
policy.read_timeout,
stream.read_exact(&mut chunk[..chunk_len]),
)
.await,
Ok(Ok(()))
) || body.try_reserve(chunk_len).is_err()
{
let _ = tx.send(Err(CommunicationError::StreamError)).await;
break;
}
body.extend_from_slice(&chunk[..chunk_len]);
}
if body.len() != target_len {
break;
}
frames += 1;
let message = match CommunicationValue::from_bytes(&body) {
Ok(message) => message,
Err(_) => {
let _ = tx
.send(Err(CommunicationError::ParseCommunicationValue))
.await;
break;
}
};
#[cfg(feature = "pipes")]
{
let pipe_request_type = mtp_codec::CommunicationType::PipeRequest
.try_to_id(&mtp_codec::TypeMap::latest());
if Some(message.get_type()) == pipe_request_type && frames == 1 {
let pipe_id = message.get_id();
let description = message
.get_str(mtp_codec::DataType::Description)
.unwrap_or("")
.to_string();
let pipe_reader = PipeReader {
stream,
description,
pipe_id,
};
if pipe_tx.send(pipe_reader).await.is_err() {
break;
}
return;
}
}
if message.is_type(mtp_codec::CommunicationType::Ping) {
if let Some(sender) = ping_sender.read().await.clone() {
let mut pong =
CommunicationValue::new(mtp_codec::CommunicationType::Pong)
.with_id(message.get_id());
if let Some(timestamp) =
message.get_data_opt(mtp_codec::DataType::Timestamp)
{
pong = pong.add_typed_default(
mtp_codec::DataType::Timestamp,
timestamp.clone(),
);
}
let _ = sender.send(&pong).await;
}
continue;
}
if tx.send(Ok(message)).await.is_err() {
break;
}
}
});
}
});
Self {
incoming: Arc::new(Mutex::new(rx)),
#[cfg(feature = "pipes")]
pipes: Arc::new(Mutex::new(pipe_rx)),
connection,
ping_sender,
}
}
pub async fn respond_to_pings(&self, sender: GenericSender<C>) {
*self.ping_sender.write().await = Some(sender);
}
pub async fn receive(&self) -> Result<CommunicationValue, CommunicationError> {
self.incoming
.lock()
.await
.recv()
.await
.unwrap_or(Err(CommunicationError::StreamClosed))
}
#[cfg(feature = "pipes")]
pub async fn receive_event(
&self,
) -> Result<crate::TransportEvent<C::RecvStream>, CommunicationError> {
let mut incoming = self.incoming.lock().await;
let mut pipes = self.pipes.lock().await;
tokio::select! {
msg = incoming.recv() => {
match msg {
Some(Ok(val)) => Ok(crate::TransportEvent::Message(val)),
Some(Err(e)) => Err(e),
None => Err(self
.connection
.close_reason()
.unwrap_or(CommunicationError::StreamClosed)),
}
}
pipe = pipes.recv() => {
match pipe {
Some(reader) => Ok(crate::TransportEvent::Pipe(reader)),
None => Err(self
.connection
.close_reason()
.unwrap_or(CommunicationError::StreamClosed)),
}
}
}
}
#[cfg(feature = "pipes")]
pub async fn receive_pipe(&self) -> Result<PipeReader<C::RecvStream>, CommunicationError> {
self.pipes
.lock()
.await
.recv()
.await
.ok_or(CommunicationError::StreamClosed)
}
#[cfg(feature = "pipes")]
pub fn try_receive_pipe(
&self,
) -> Result<Option<PipeReader<C::RecvStream>>, CommunicationError> {
match self.pipes.try_lock() {
Ok(mut rx) => match rx.try_recv() {
Ok(reader) => Ok(Some(reader)),
Err(mpsc::error::TryRecvError::Empty) => Ok(None),
Err(mpsc::error::TryRecvError::Disconnected) => {
Err(CommunicationError::StreamClosed)
}
},
Err(_) => Ok(None),
}
}
pub fn is_closed(&self) -> bool {
self.connection.close_reason().is_some()
}
pub fn is_open(&self) -> bool {
!self.is_closed()
}
pub fn close_reason(&self) -> Option<CommunicationError> {
self.connection.close_reason()
}
}

View file

@ -3,24 +3,53 @@ use mtp_common::CommunicationError;
use rustls::pki_types::{PrivateKeyDer, pem::PemObject};
use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use tracing::debug;
use wtransport::{Connection as WTConnection, Endpoint, ServerConfig};
fn generate_self_signed_cert() -> (Vec<u8>, Vec<u8>) {
let key_pair = match rcgen::KeyPair::generate() {
Ok(key_pair) => key_pair,
Err(e) => panic!("failed to generate self-signed key pair: {e}"),
};
let params = match rcgen::CertificateParams::new(vec!["localhost".into(), "127.0.0.1".into()]) {
Ok(params) => params,
Err(e) => panic!("failed to build self-signed certificate params: {e}"),
};
let cert = match params.self_signed(&key_pair) {
Ok(cert) => cert,
Err(e) => panic!("failed to self-sign certificate: {e}"),
};
fn generate_self_signed_cert() -> Result<(Vec<u8>, Vec<u8>), CommunicationError> {
let key_pair = rcgen::KeyPair::generate().map_err(|e| {
CommunicationError::Other(format!("failed to generate self-signed key pair: {e}"))
})?;
let params = rcgen::CertificateParams::new(vec!["localhost".into(), "127.0.0.1".into()])
.map_err(|e| {
CommunicationError::Other(format!(
"failed to build self-signed certificate params: {e}"
))
})?;
let cert = params
.self_signed(&key_pair)
.map_err(|e| CommunicationError::Other(format!("failed to self-sign certificate: {e}")))?;
let cert_pem = cert.pem();
let key_pem = key_pair.serialize_pem();
(cert_pem.into_bytes(), key_pem.into_bytes())
Ok((cert_pem.into_bytes(), key_pem.into_bytes()))
}
enum HostCredentials {
Pem { cert_pem: Vec<u8>, key_pem: Vec<u8> },
SelfSigned,
}
/// TLS and transport settings for a native host.
pub struct HostConfig {
credentials: HostCredentials,
policy: Policy,
}
impl HostConfig {
pub fn new(cert_pem: Vec<u8>, key_pem: Vec<u8>, policy: Policy) -> Self {
Self {
credentials: HostCredentials::Pem { cert_pem, key_pem },
policy,
}
}
/// Generate a self-signed certificate for local development.
pub fn self_signed(policy: Policy) -> Self {
Self {
credentials: HostCredentials::SelfSigned,
policy,
}
}
}
pub struct Host {
@ -61,19 +90,24 @@ pub async fn host(
cert_pem: Vec<u8>,
key_pem: Vec<u8>,
policy: Policy,
) -> Result<Host, CommunicationError> {
host_with_config(ip, port, HostConfig::new(cert_pem, key_pem, policy)).await
}
/// Start a host using explicit TLS and transport configuration.
pub async fn host_with_config(
ip: IpAddr,
port: u16,
config: HostConfig,
) -> Result<Host, CommunicationError> {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
// When MTP_INSECURE is set, generate a self-signed cert so the host can
// run without externally-provided TLS credentials.
let is_insecure = std::env::var("MTP_INSECURE").is_ok();
let (use_cert_pem, use_key_pem) = if is_insecure {
generate_self_signed_cert()
} else {
(cert_pem, key_pem)
let (cert_pem, key_pem) = match config.credentials {
HostCredentials::Pem { cert_pem, key_pem } => (cert_pem, key_pem),
HostCredentials::SelfSigned => generate_self_signed_cert()?,
};
let server_config = configure_server(ip, port, use_cert_pem, use_key_pem, &policy).await?;
let server_config = configure_server(ip, port, cert_pem, key_pem, &config.policy).await?;
let endpoint = Endpoint::server(server_config)
.map_err(|e| CommunicationError::Other(format!("Endpoint creation failed: {}", e)))?;
@ -83,7 +117,7 @@ pub async fn host(
let (incoming_tx, incoming_rx) = tokio::sync::mpsc::channel(16);
let policy = Arc::new(policy);
let policy = Arc::new(config.policy);
let task = tokio::spawn(async move {
loop {
@ -92,7 +126,7 @@ pub async fn host(
let request = match incoming_session.await {
Ok(req) => req,
Err(e) => {
log::debug!("incoming WebTransport session failed: {e}");
debug!("incoming WebTransport session failed: {e}");
continue;
}
};
@ -103,7 +137,7 @@ pub async fn host(
{
Ok(conn) => conn,
Err(e) => {
log::debug!("WebTransport request accept failed: {e}");
debug!("WebTransport request accept failed: {e}");
continue;
}
};
@ -128,7 +162,7 @@ async fn handle_connection(
let handle = Arc::new(ConnectionHandle::new());
let sender = Sender::new(connection.clone(), handle.clone(), policy.clone());
let receiver = Receiver::new(connection, handle, policy);
let receiver = Receiver::new_for_handshake(connection, handle, policy);
let _ = tx.send((sender, receiver)).await;
}

View file

@ -1,21 +1,27 @@
pub mod client;
pub mod connection;
pub mod connection_handle;
mod framing;
pub mod generic_connection;
pub mod pinning;
pub mod transport_traits;
#[cfg(feature = "pipes")]
pub mod pipe;
pub use connection::{Policy, Receiver, SendMode, Sender};
pub use generic_connection::{GenericReceiver, GenericSender};
#[cfg(feature = "pipes")]
pub use connection::TransportEvent;
#[cfg(feature = "pipes")]
pub use pipe::{PipeReader, PipeWriter};
pub use client::connect;
pub use client::{ClientConfig, connect, connect_with_config};
pub use connection_handle::ConnectionHandle;
pub use transport_traits::{TransportConnection, TransportRecvStream, TransportSendStream};
#[cfg(feature = "host")]
pub mod host;
#[cfg(feature = "host")]
pub use host::{Host, host};
pub use host::{Host, HostConfig, host, host_with_config};

175
transport/src/pinning.rs Normal file
View file

@ -0,0 +1,175 @@
use rustls::{
ClientConfig as RustlsClientConfig, DigitallySignedStruct, SignatureScheme,
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
pki_types::{CertificateDer, ServerName, UnixTime},
};
use sha2::{Digest, Sha256};
use wtransport::ClientConfig as WTransportClientConfig;
use crate::Policy;
use mtp_common::CommunicationError;
use std::sync::Arc;
/// A certificate verifier that pins a connection to a specific SPKI
/// (Subject Public Key Info) SHA-256 hash. The client will only accept
/// server certificates whose DER-encoded SPKI matches the provided hash.
#[derive(Debug)]
pub struct PinnedCertVerifier {
expected_hash: [u8; 32],
}
impl PinnedCertVerifier {
pub fn new(expected_hash: [u8; 32]) -> Self {
Self { expected_hash }
}
}
impl ServerCertVerifier for PinnedCertVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
let der = end_entity.as_ref();
let spki = extract_spki(der).map_err(|_| {
rustls::Error::General("failed to extract SPKI from certificate".into())
})?;
let computed = Sha256::digest(&spki);
if computed.as_slice() != self.expected_hash.as_slice() {
return Err(rustls::Error::General(format!(
"certificate SPKI hash mismatch: expected {:02x?}, got {:02x?}",
self.expected_hash, computed
)));
}
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
vec![
SignatureScheme::RSA_PKCS1_SHA256,
SignatureScheme::ECDSA_NISTP256_SHA256,
SignatureScheme::RSA_PSS_SHA256,
SignatureScheme::ED25519,
]
}
}
/// Extract the Subject Public Key Info (SPKI) field from a DER-encoded X.509
/// certificate. Returns the raw bytes of the SPKI sequence.
fn extract_spki(der: &[u8]) -> Result<Vec<u8>, ()> {
let (_, outer) = parse_der_sequence(der).map_err(|_| ())?;
let (_, tbs) = parse_der_sequence(outer).map_err(|_| ())?;
// Skip version (context [0]), serial number, signature algorithm, issuer,
// validity, subject to reach subjectPublicKeyInfo (index 6).
let mut offset = 0;
let mut element_index = 0;
while offset < tbs.len() && element_index < 6 {
let (len, _) = parse_der_element(&tbs[offset..]).map_err(|_| ())?;
offset += len;
element_index += 1;
}
if element_index != 6 {
return Err(());
}
let (spki_len, spki) = parse_der_element(&tbs[offset..]).map_err(|_| ())?;
if spki_len == 0 {
return Err(());
}
Ok(spki.to_vec())
}
fn parse_der_element(data: &[u8]) -> Result<(usize, &[u8]), ()> {
if data.len() < 2 {
return Err(());
}
let mut offset = 1;
let len_byte = data[offset];
offset += 1;
let content_len = if len_byte & 0x80 == 0 {
len_byte as usize
} else {
let num_bytes = (len_byte & 0x7F) as usize;
if offset + num_bytes > data.len() {
return Err(());
}
let mut len = 0usize;
for i in 0..num_bytes {
len = (len << 8) | data[offset + i] as usize;
}
offset += num_bytes;
len
};
if offset + content_len > data.len() {
return Err(());
}
let total_len = offset + content_len;
Ok((total_len, &data[offset..offset + content_len]))
}
fn parse_der_sequence(data: &[u8]) -> Result<(usize, &[u8]), ()> {
if data.is_empty() || data[0] != 0x30 {
return Err(());
}
parse_der_element(data)
}
/// Build a [`WTransportClientConfig`] that verifies the server certificate
/// against a pinned SPKI SHA-256 hash.
pub fn configure_client_pinned_hash(
expected_hash: [u8; 32],
policy: &Policy,
) -> Result<WTransportClientConfig, CommunicationError> {
let verifier = PinnedCertVerifier::new(expected_hash);
let mut tls_config = RustlsClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(verifier))
.with_no_client_auth();
tls_config.alpn_protocols = vec![b"h3".to_vec()];
Ok(WTransportClientConfig::builder()
.with_bind_default()
.with_custom_tls(tls_config)
.keep_alive_interval(policy.keep_alive_interval)
.max_idle_timeout(policy.max_idle_timeout)
.map_err(|e| CommunicationError::Other(e.to_string()))?
.build())
}

View file

@ -1,16 +1,17 @@
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tracing::warn;
#[derive(Debug)]
pub struct PipeWriter {
pub(crate) stream: wtransport::SendStream,
pub struct PipeWriter<S = wtransport::SendStream> {
pub(crate) stream: S,
}
impl PipeWriter {
pub async fn finish(mut self) -> Result<(), mtp_common::CommunicationError> {
self.stream.finish().await.map_err(|e| {
log::warn!("[PipeWriter] finish failed: {e}");
warn!("[PipeWriter] finish failed: {e}");
mtp_common::CommunicationError::StreamWriteError(e)
})
}
@ -20,7 +21,18 @@ impl PipeWriter {
}
}
impl AsyncWrite for PipeWriter {
impl<S: tokio::io::AsyncWrite + Send + Unpin> PipeWriter<S> {
pub async fn finish_async(mut self) -> Result<(), mtp_common::CommunicationError> {
tokio::io::AsyncWriteExt::shutdown(&mut self)
.await
.map_err(|e| {
warn!("[PipeWriter] finish_async failed: {e}");
mtp_common::CommunicationError::StreamError
})
}
}
impl<S: tokio::io::AsyncWrite + Send + Unpin> AsyncWrite for PipeWriter<S> {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
@ -39,13 +51,13 @@ impl AsyncWrite for PipeWriter {
}
#[derive(Debug)]
pub struct PipeReader {
pub(crate) stream: wtransport::RecvStream,
pub struct PipeReader<R = wtransport::RecvStream> {
pub(crate) stream: R,
pub(crate) description: String,
pub(crate) pipe_id: u32,
}
impl PipeReader {
impl<R> PipeReader<R> {
pub fn description(&self) -> &str {
&self.description
}
@ -55,7 +67,7 @@ impl PipeReader {
}
}
impl AsyncRead for PipeReader {
impl<R: tokio::io::AsyncRead + Send + Unpin> AsyncRead for PipeReader<R> {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,

View file

@ -0,0 +1,108 @@
//! Transport-neutral primitives used by alternative MTP hosts.
//!
//! The existing public `Sender` and `Receiver` remain backed by
//! `wtransport`. These traits are deliberately introduced separately so new
//! QUIC/WebTransport backends can be added without changing that API in one
//! breaking step.
use async_trait::async_trait;
use mtp_common::CommunicationError;
/// A writable unidirectional stream suitable for MTP frames.
///
/// Implementors must also implement [`tokio::io::AsyncWrite`] so that the
/// stream can back a raw pipe via [`crate::PipeWriter`] when the `pipes`
/// feature is enabled.
#[async_trait]
pub trait TransportSendStream: tokio::io::AsyncWrite + Send + Sync {
async fn write_all(&mut self, buf: &[u8]) -> Result<(), CommunicationError>;
async fn finish(&mut self) -> Result<(), CommunicationError>;
}
/// A readable unidirectional stream suitable for MTP frames.
///
/// Implementors must also implement [`tokio::io::AsyncRead`] so that the
/// stream can back a raw pipe via [`crate::PipeReader`] when the `pipes`
/// feature is enabled.
#[async_trait]
pub trait TransportRecvStream: tokio::io::AsyncRead + Send + Sync {
async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), CommunicationError>;
async fn read_chunk(&mut self, max: usize) -> Result<Option<Vec<u8>>, CommunicationError>;
}
/// A QUIC/WebTransport connection that provides MTP's unidirectional streams.
#[async_trait]
pub trait TransportConnection: Clone + Send + Sync + 'static {
type SendStream: TransportSendStream + 'static;
type RecvStream: TransportRecvStream + 'static;
async fn open_uni(&self) -> Result<Self::SendStream, CommunicationError>;
async fn accept_uni(&self) -> Result<Self::RecvStream, CommunicationError>;
fn close_reason(&self) -> Option<CommunicationError>;
fn close(&self, code: u32, reason: &[u8]);
}
#[async_trait]
impl TransportSendStream for wtransport::SendStream {
async fn write_all(&mut self, buf: &[u8]) -> Result<(), CommunicationError> {
wtransport::SendStream::write_all(self, buf)
.await
.map_err(|_| CommunicationError::StreamError)
}
async fn finish(&mut self) -> Result<(), CommunicationError> {
wtransport::SendStream::finish(self)
.await
.map_err(|_| CommunicationError::StreamError)
}
}
#[async_trait]
impl TransportRecvStream for wtransport::RecvStream {
async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), CommunicationError> {
wtransport::RecvStream::read_exact(self, buf)
.await
.map_err(|_| CommunicationError::StreamError)
}
async fn read_chunk(&mut self, max: usize) -> Result<Option<Vec<u8>>, CommunicationError> {
let mut buf = vec![0; max];
match wtransport::RecvStream::read(self, &mut buf).await {
Ok(Some(size)) => {
buf.truncate(size);
Ok(Some(buf))
}
Ok(None) => Ok(None),
Err(_) => Err(CommunicationError::StreamError),
}
}
}
#[async_trait]
impl TransportConnection for wtransport::Connection {
type SendStream = wtransport::SendStream;
type RecvStream = wtransport::RecvStream;
async fn open_uni(&self) -> Result<Self::SendStream, CommunicationError> {
let opening = wtransport::Connection::open_uni(self)
.await
.map_err(CommunicationError::ConnectionError)?;
opening.await.map_err(|_| CommunicationError::StreamError)
}
async fn accept_uni(&self) -> Result<Self::RecvStream, CommunicationError> {
wtransport::Connection::accept_uni(self)
.await
.map_err(|_| CommunicationError::StreamError)
}
fn close_reason(&self) -> Option<CommunicationError> {
self.quic_connection()
.close_reason()
.map(|_| CommunicationError::StreamClosed)
}
fn close(&self, code: u32, reason: &[u8]) {
self.quic_connection().close(code.into(), reason);
}
}

View file

@ -0,0 +1,321 @@
#![cfg(feature = "pipes")]
use async_trait::async_trait;
use mtp_codec::CommunicationValue;
use mtp_common::CommunicationError;
use mtp_transport::{
GenericReceiver, GenericSender, Policy, TransportConnection, TransportEvent,
TransportRecvStream, TransportSendStream,
};
use std::sync::Arc;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, duplex};
use tokio::sync::{Mutex, mpsc};
struct MockSendStream {
inner: DuplexStream,
}
impl AsyncWrite for MockSendStream {
fn poll_write(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
std::pin::Pin::new(&mut self.inner).poll_write(cx, buf)
}
fn poll_flush(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::pin::Pin::new(&mut self.inner).poll_flush(cx)
}
fn poll_shutdown(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::pin::Pin::new(&mut self.inner).poll_shutdown(cx)
}
}
#[async_trait]
impl TransportSendStream for MockSendStream {
async fn write_all(&mut self, buf: &[u8]) -> Result<(), CommunicationError> {
AsyncWriteExt::write_all(&mut self.inner, buf)
.await
.map_err(|_| CommunicationError::StreamError)
}
async fn finish(&mut self) -> Result<(), CommunicationError> {
self.inner
.shutdown()
.await
.map_err(|_| CommunicationError::StreamError)
}
}
struct MockRecvStream {
inner: DuplexStream,
}
impl AsyncRead for MockRecvStream {
fn poll_read(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::pin::Pin::new(&mut self.inner).poll_read(cx, buf)
}
}
#[async_trait]
impl TransportRecvStream for MockRecvStream {
async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), CommunicationError> {
AsyncReadExt::read_exact(&mut self.inner, buf)
.await
.map(|_| ())
.map_err(|_| CommunicationError::StreamError)
}
async fn read_chunk(&mut self, max: usize) -> Result<Option<Vec<u8>>, CommunicationError> {
let mut buf = vec![0u8; max];
match AsyncReadExt::read(&mut self.inner, &mut buf).await {
Ok(0) => Ok(None),
Ok(n) => {
buf.truncate(n);
Ok(Some(buf))
}
Err(_) => Err(CommunicationError::StreamError),
}
}
}
#[derive(Clone)]
struct MockTransportConnection {
pair_tx: mpsc::Sender<DuplexStream>,
pair_rx: Arc<Mutex<mpsc::Receiver<DuplexStream>>>,
}
impl MockTransportConnection {
fn pair() -> (Self, Self) {
let (tx_a, rx_a) = mpsc::channel(16);
let (tx_b, rx_b) = mpsc::channel(16);
(
Self {
pair_tx: tx_a,
pair_rx: Arc::new(Mutex::new(rx_b)),
},
Self {
pair_tx: tx_b,
pair_rx: Arc::new(Mutex::new(rx_a)),
},
)
}
}
#[async_trait]
impl TransportConnection for MockTransportConnection {
type SendStream = MockSendStream;
type RecvStream = MockRecvStream;
async fn open_uni(&self) -> Result<Self::SendStream, CommunicationError> {
let (local, remote) = duplex(65536);
self.pair_tx
.send(remote)
.await
.map_err(|_| CommunicationError::StreamError)?;
Ok(MockSendStream { inner: local })
}
async fn accept_uni(&self) -> Result<Self::RecvStream, CommunicationError> {
let remote = self
.pair_rx
.lock()
.await
.recv()
.await
.ok_or(CommunicationError::StreamClosed)?;
Ok(MockRecvStream { inner: remote })
}
fn close_reason(&self) -> Option<CommunicationError> {
None
}
fn close(&self, _code: u32, _reason: &[u8]) {}
}
async fn mock_connected_pair() -> (MockTransportConnection, MockTransportConnection) {
MockTransportConnection::pair()
}
#[tokio::test]
async fn test_open_pipe_and_receive_reader() -> Result<(), Box<dyn std::error::Error>> {
let (conn_a, conn_b) = mock_connected_pair().await;
let policy = Arc::new(Policy::default());
let sender = GenericSender::new(conn_a, policy.clone());
let receiver = GenericReceiver::new(conn_b, policy);
let pipe_writer = sender.open_pipe(42, "test-pipe").await?;
let pipe_reader = receiver.receive_pipe().await?;
assert_eq!(pipe_reader.pipe_id(), 42);
assert_eq!(pipe_reader.description(), "test-pipe");
drop(pipe_writer);
Ok(())
}
#[tokio::test]
async fn test_pipe_raw_data_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
let (conn_a, conn_b) = mock_connected_pair().await;
let policy = Arc::new(Policy::default());
let sender = GenericSender::new(conn_a, policy.clone());
let receiver = GenericReceiver::new(conn_b, policy);
let mut pipe_writer = sender.open_pipe(1, "data-pipe").await?;
let data = b"hello through the pipe";
AsyncWriteExt::write_all(&mut pipe_writer, data).await?;
pipe_writer.finish_async().await?;
let mut pipe_reader = receiver.receive_pipe().await?;
let mut buf = vec![0u8; data.len()];
AsyncReadExt::read_exact(&mut pipe_reader, &mut buf).await?;
assert_eq!(&buf, data);
Ok(())
}
#[tokio::test]
async fn test_pipe_large_payload() -> Result<(), Box<dyn std::error::Error>> {
let (conn_a, conn_b) = mock_connected_pair().await;
let policy = Arc::new(Policy::default());
let sender = GenericSender::new(conn_a, policy.clone());
let receiver = GenericReceiver::new(conn_b, policy);
let mut pipe_writer = sender.open_pipe(7, "big-pipe").await?;
let data: Vec<u8> = (0..256 * 1024).map(|i| (i % 256) as u8).collect();
let data_clone = data.clone();
let write_handle = tokio::spawn(async move {
AsyncWriteExt::write_all(&mut pipe_writer, &data_clone)
.await
.map_err(|_| CommunicationError::StreamError)?;
pipe_writer.finish_async().await
});
let mut pipe_reader = receiver.receive_pipe().await?;
let mut buf = Vec::new();
AsyncReadExt::read_to_end(&mut pipe_reader, &mut buf).await?;
assert_eq!(buf, data);
write_handle.await??;
Ok(())
}
#[tokio::test]
async fn test_receive_event_dispatches_pipe() -> Result<(), Box<dyn std::error::Error>> {
let (conn_a, conn_b) = mock_connected_pair().await;
let policy = Arc::new(Policy::default());
let sender = GenericSender::new(conn_a, policy.clone());
let receiver = GenericReceiver::new(conn_b, policy);
let mut pipe_writer = sender.open_pipe(99, "event-pipe").await?;
match receiver.receive_event().await? {
TransportEvent::Pipe(mut reader) => {
assert_eq!(reader.pipe_id(), 99);
assert_eq!(reader.description(), "event-pipe");
let data = b"event dispatch test";
AsyncWriteExt::write_all(&mut pipe_writer, data).await?;
pipe_writer.finish_async().await?;
let mut buf = vec![0u8; data.len()];
AsyncReadExt::read_exact(&mut reader, &mut buf).await?;
assert_eq!(&buf, data);
}
TransportEvent::Message(_) => panic!("expected Pipe event, got Message"),
}
Ok(())
}
#[tokio::test]
async fn test_try_receive_pipe_returns_none_when_empty() -> Result<(), Box<dyn std::error::Error>> {
let (conn_a, conn_b) = mock_connected_pair().await;
let policy = Arc::new(Policy::default());
let _sender = GenericSender::new(conn_a, policy.clone());
let receiver = GenericReceiver::new(conn_b, policy);
let result = receiver.try_receive_pipe()?;
assert!(result.is_none());
Ok(())
}
#[tokio::test]
async fn test_regular_messages_still_work_alongside_pipes() -> Result<(), Box<dyn std::error::Error>>
{
let (conn_a, conn_b) = mock_connected_pair().await;
let policy = Arc::new(Policy::default());
let sender = GenericSender::new(conn_a, policy.clone());
let receiver = GenericReceiver::new(conn_b, policy);
let msg = CommunicationValue::new(mtp_codec::CommunicationType::Pong);
sender.send(&msg).await?;
let _pipe_writer = sender.open_pipe(1, "mixed-pipe").await?;
let received = receiver.receive().await?;
assert_eq!(
received.get_type(),
mtp_codec::CommunicationType::Pong
.try_to_id(&mtp_codec::TypeMap::latest())
.unwrap()
);
let pipe_reader = receiver.receive_pipe().await?;
assert_eq!(pipe_reader.pipe_id(), 1);
Ok(())
}
#[tokio::test]
async fn test_multiple_pipes() -> Result<(), Box<dyn std::error::Error>> {
let (conn_a, conn_b) = mock_connected_pair().await;
let policy = Arc::new(Policy::default());
let sender = GenericSender::new(conn_a, policy.clone());
let receiver = GenericReceiver::new(conn_b, policy);
let mut pw1 = sender.open_pipe(10, "first").await?;
let mut pw2 = sender.open_pipe(20, "second").await?;
let r1 = receiver.receive_pipe().await?;
assert_eq!(r1.pipe_id(), 10);
let r2 = receiver.receive_pipe().await?;
assert_eq!(r2.pipe_id(), 20);
let data1 = b"pipe-one-data";
AsyncWriteExt::write_all(&mut pw1, data1).await?;
pw1.finish_async().await?;
let data2 = b"pipe-two-data";
AsyncWriteExt::write_all(&mut pw2, data2).await?;
pw2.finish_async().await?;
let mut buf1 = vec![0u8; data1.len()];
let mut reader1 = r1;
AsyncReadExt::read_exact(&mut reader1, &mut buf1).await?;
assert_eq!(&buf1, data1);
let mut buf2 = vec![0u8; data2.len()];
let mut reader2 = r2;
AsyncReadExt::read_exact(&mut reader2, &mut buf2).await?;
assert_eq!(&buf2, data2);
Ok(())
}

View file

@ -1,7 +1,10 @@
use std::net::{IpAddr, Ipv4Addr};
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue, TypeMap};
use mtp_transport::{Host, Policy, Receiver, Sender, connect, host};
use mtp_transport::{
ClientConfig as TransportClientConfig, Host, HostConfig as TransportHostConfig, Policy,
Receiver, Sender, connect, connect_with_config, host, host_with_config,
};
fn generate_self_signed_cert() -> (Vec<u8>, Vec<u8>) {
let key_pair = rcgen::KeyPair::generate().expect("failed to generate self-signed key pair");
@ -15,7 +18,10 @@ fn generate_self_signed_cert() -> (Vec<u8>, Vec<u8>) {
(cert_pem.into_bytes(), key_pem.into_bytes())
}
async fn start_test_host(cert_pem: Vec<u8>, key_pem: Vec<u8>) -> Result<Host, Box<dyn std::error::Error>> {
async fn start_test_host(
cert_pem: Vec<u8>,
key_pem: Vec<u8>,
) -> Result<Host, Box<dyn std::error::Error>> {
Ok(host(
IpAddr::V4(Ipv4Addr::LOCALHOST),
0,
@ -26,12 +32,16 @@ async fn start_test_host(cert_pem: Vec<u8>, key_pem: Vec<u8>) -> Result<Host, Bo
.await?)
}
async fn connect_to_host(h: &Host, cert_pem: Vec<u8>) -> Result<(Sender, Receiver), Box<dyn std::error::Error>> {
async fn connect_to_host(
h: &Host,
cert_pem: Vec<u8>,
) -> Result<(Sender, Receiver), Box<dyn std::error::Error>> {
let url = format!("https://127.0.0.1:{}", h.local_addr().port());
Ok(connect(&url, Some(cert_pem), Policy::default()).await?)
}
async fn connected_pair() -> Result<(Host, Sender, Receiver, Sender, Receiver), Box<dyn std::error::Error>> {
async fn connected_pair()
-> Result<(Host, Sender, Receiver, Sender, Receiver), Box<dyn std::error::Error>> {
let (cert_pem, key_pem) = generate_self_signed_cert();
let mut h = start_test_host(cert_pem.clone(), key_pem).await?;
let (client_tx, client_rx) = connect_to_host(&h, cert_pem).await?;
@ -41,7 +51,9 @@ async fn connected_pair() -> Result<(Host, Sender, Receiver, Sender, Receiver),
fn numbered_message(comm_type: CommunicationType, value: u128, tm: &TypeMap) -> CommunicationValue {
CommunicationValue::new(comm_type).add_data(
DataType::PqSignature.to_id(tm),
DataType::PqSignature
.try_to_id(tm)
.expect("test type must be mapped"),
DataValue::UnsignedNumber(value),
)
}
@ -52,7 +64,10 @@ fn assert_numbered_message(
value: u128,
tm: &TypeMap,
) {
assert_eq!(message.get_type(), comm_type.to_id(tm));
assert_eq!(
message.get_type(),
comm_type.try_to_id(tm).expect("test type must be mapped")
);
assert_eq!(
message.get_data(DataType::PqSignature).clone(),
DataValue::UnsignedNumber(value)
@ -69,6 +84,32 @@ async fn test_host_start_and_stop() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
#[tokio::test]
async fn test_explicit_development_tls() -> Result<(), Box<dyn std::error::Error>> {
// The insecure-tls feature requires MTP_INSECURE_TLS=1 at runtime.
// SAFETY: test is single-threaded; no concurrent readers of this env var.
unsafe {
std::env::set_var("MTP_INSECURE_TLS", "1");
}
let mut h = host_with_config(
IpAddr::V4(Ipv4Addr::LOCALHOST),
0,
TransportHostConfig::self_signed(Policy::default()),
)
.await?;
let url = format!("https://127.0.0.1:{}", h.local_addr().port());
let client_config =
TransportClientConfig::new(Policy::default()).with_insecure_certificate_verification();
let (client_tx, _client_rx) = connect_with_config(&url, client_config).await?;
let (_host_tx, _host_rx) = h.next().await.ok_or("host did not accept connection")?;
client_tx.close();
h.shutdown();
Ok(())
}
#[tokio::test]
async fn test_send_receive_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
let (_h, client_tx, client_rx, host_tx, host_rx) = connected_pair().await?;
@ -142,7 +183,12 @@ async fn test_close_detection() -> Result<(), Box<dyn std::error::Error>> {
// Host should still receive the message
let tm = TypeMap::latest();
let received = host_rx.receive().await?;
assert_eq!(received.get_type(), CommunicationType::Ping.to_id(&tm));
assert_eq!(
received.get_type(),
CommunicationType::Ping
.try_to_id(&tm)
.expect("test type must be mapped")
);
// Host should get an error or closed signal on next receive
let result = host_rx.receive().await;
@ -204,7 +250,8 @@ async fn test_drop_receiver_keeps_sender_alive() -> Result<(), Box<dyn std::erro
}
#[tokio::test]
async fn test_persistent_stream_reopens_after_local_finish() -> Result<(), Box<dyn std::error::Error>> {
async fn test_persistent_stream_reopens_after_local_finish()
-> Result<(), Box<dyn std::error::Error>> {
let (_h, client_tx, client_rx, host_tx, host_rx) = connected_pair().await?;
let tm = TypeMap::latest();
@ -239,14 +286,12 @@ async fn test_receiver_backpressure_with_small_queue() -> Result<(), Box<dyn std
for i in 0..8u128 {
client_tx
.send(&numbered_message(CommunicationType::Ping, i, &tm))
.await
?;
.await?;
}
for i in 0..8u128 {
let received = tokio::time::timeout(std::time::Duration::from_secs(5), host_rx.receive())
.await?
?;
let received =
tokio::time::timeout(std::time::Duration::from_secs(5), host_rx.receive()).await??;
assert_numbered_message(&received, CommunicationType::Ping, i, &tm);
}
@ -269,9 +314,7 @@ async fn test_max_frames_per_stream_enforced() -> Result<(), Box<dyn std::error:
)
.await?;
let url = format!("https://127.0.0.1:{}", h.local_addr().port());
let (client_tx, _client_rx) = connect(&url, Some(cert_pem), Policy::default())
.await
?;
let (client_tx, _client_rx) = connect(&url, Some(cert_pem), Policy::default()).await?;
let (_host_tx, host_rx) = h.next().await.ok_or("host did not accept connection")?;
let tm = TypeMap::latest();
@ -293,7 +336,8 @@ async fn test_max_frames_per_stream_enforced() -> Result<(), Box<dyn std::error:
}
#[tokio::test]
async fn test_semaphore_saturation_with_concurrent_streams() -> Result<(), Box<dyn std::error::Error>> {
async fn test_semaphore_saturation_with_concurrent_streams()
-> Result<(), Box<dyn std::error::Error>> {
let (cert_pem, key_pem) = generate_self_signed_cert();
let policy = Policy::default()
.with_send_mode(mtp_transport::SendMode::SingleStreamPerMessage)
@ -308,8 +352,7 @@ async fn test_semaphore_saturation_with_concurrent_streams() -> Result<(), Box<d
)
.await?;
let url = format!("https://127.0.0.1:{}", h.local_addr().port());
let (client_tx, _client_rx) = connect(&url, Some(cert_pem), Policy::default())
.await?;
let (client_tx, _client_rx) = connect(&url, Some(cert_pem), Policy::default()).await?;
let (_host_tx, host_rx) = h.next().await.ok_or("host did not accept connection")?;
let tm = TypeMap::latest();
@ -327,9 +370,8 @@ async fn test_semaphore_saturation_with_concurrent_streams() -> Result<(), Box<d
}
for i in 0..6u128 {
let received = tokio::time::timeout(std::time::Duration::from_secs(5), host_rx.receive())
.await?
?;
let received =
tokio::time::timeout(std::time::Duration::from_secs(5), host_rx.receive()).await??;
assert_numbered_message(&received, CommunicationType::Ping, i, &tm);
}