General Upgrade, NEW: WebServers, Better Docs
Some checks failed
CI / checks (push) Failing after 2m23s
Some checks failed
CI / checks (push) Failing after 2m23s
This commit is contained in:
parent
5f11d476b6
commit
3afc75b45d
120 changed files with 10032 additions and 4886 deletions
2
transport/Cargo.lock
generated
2
transport/Cargo.lock
generated
|
|
@ -4,4 +4,4 @@ version = 4
|
|||
|
||||
[[package]]
|
||||
name = "transport"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
21
transport/src/framing.rs
Normal 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
|
||||
}
|
||||
403
transport/src/generic_connection.rs
Normal file
403
transport/src/generic_connection.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
175
transport/src/pinning.rs
Normal 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())
|
||||
}
|
||||
|
|
@ -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<'_>,
|
||||
|
|
|
|||
108
transport/src/transport_traits.rs
Normal file
108
transport/src/transport_traits.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
321
transport/tests/generic_pipe.rs
Normal file
321
transport/tests/generic_pipe.rs
Normal 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(())
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue