Merge branch 'master' of ssh://git.methanium.net/methanium/mtp
All checks were successful
CI / checks (push) Successful in 5m56s

This commit is contained in:
Alex 2026-07-28 18:50:04 +02:00
commit 590810ce59
Signed by: alex
SSH key fingerprint: SHA256:D1+Ub8o0v4K5y1JNivW8IxEOelqLSvPmUzBbDIoZkRQ
10 changed files with 195 additions and 158 deletions

View file

@ -3,6 +3,7 @@ use mtp_codec::{CommunicationType, DataType, DataValue};
use mtp_codec::{CommunicationValue, Version, registry::VersionedCodec};
use mtp_common::CommunicationError;
use std::net::SocketAddr;
#[cfg(feature = "pipes")]
use std::sync::Arc;
#[cfg(feature = "pipes")]
use tokio::sync::{Mutex, mpsc};
@ -11,6 +12,7 @@ use tokio::sync::{Mutex, mpsc};
use crate::error::random_client_id;
#[cfg(feature = "pipes")]
use crate::pipe::{PipeDispatcher, PipeReceiver, PipeRequest, PipeSender, run_dispatcher};
#[cfg(feature = "pipes")]
use mtp_transport::Policy;
mod connection_capability {
@ -161,6 +163,9 @@ where
}
/// Construct an MTP connection with an explicit policy for pipe dispatch.
// The shared transport constructor keeps its argument order aligned with
// `from_transport_parts_with_remote_addr`; policy is required only here.
#[allow(clippy::too_many_arguments)]
pub fn from_transport_parts_with_policy(
version: Version,
codec: VersionedCodec,
@ -255,35 +260,6 @@ impl<S, R, P> MTPConnection<S, R, P> {
client_public_key: None,
}
}
pub fn from_transport_parts_with_policy(
version: Version,
codec: VersionedCodec,
sender: S,
receiver: R,
path: String,
description: Option<String>,
remote_addr: Option<SocketAddr>,
_policy: Arc<Policy>,
) -> Self {
Self {
version,
codec,
sender,
receiver,
path,
remote_addr,
description,
_pipe_stream: std::marker::PhantomData,
_dispatcher_task: tokio::spawn(async {}),
#[cfg(feature = "crypto")]
auth_state: crate::error::AuthState::Unauthenticated,
#[cfg(feature = "crypto")]
client_id: random_client_id(),
#[cfg(feature = "crypto")]
client_public_key: None,
}
}
}
#[cfg(not(feature = "pipes"))]

View file

@ -33,8 +33,7 @@ pub trait HandshakeSender: Send + Sync {
pub trait HandshakeReceiver: Send + Sync {
fn receive(
&self,
) -> impl std::future::Future<Output = Result<CommunicationValue, CommunicationError>>
+ Send;
) -> impl std::future::Future<Output = Result<CommunicationValue, CommunicationError>> + Send;
}
/// The result of a successful handshake, containing everything needed to
@ -136,7 +135,10 @@ impl HandshakeEngine {
}
};
let negotiated = match self.registry.negotiate(std::slice::from_ref(&client_version)) {
let negotiated = match self
.registry
.negotiate(std::slice::from_ref(&client_version))
{
Some(v) => v,
None => {
send_rejection_generic(
@ -300,8 +302,8 @@ impl HandshakeEngine {
_ => 0,
};
if cid > 0 {
if let Some(bundle) =
if cid > 0
&& let Some(bundle) =
(self.config.get_existing_client)(cid, description.clone()).await
{
return self
@ -318,7 +320,6 @@ impl HandshakeEngine {
)
.await;
}
}
// Unknown or zero ID: fall back to guest
let guest_id = self.assign_guest_id().await?;
@ -634,8 +635,8 @@ impl HandshakeEngine {
DataValue::UnsignedNumber(client_nonce),
)
.add_typed_default(DataType::Signature, DataValue::Bytes(host_sig));
response = response
.add_typed_default(DataType::Version, DataValue::Str(negotiated.to_string()));
response =
response.add_typed_default(DataType::Version, DataValue::Str(negotiated.to_string()));
if pq_enabled {
response =
response.add_typed_default(DataType::PqSignature, DataValue::Bytes(host_pq_sig));
@ -722,19 +723,15 @@ fn extract_register_bundle(
msg: &CommunicationValue,
) -> Result<mtp_crypto::PublicKeyBundle, AcceptError> {
match msg.get_data(DataType::PublicKeys) {
DataValue::Bytes(b) => mtp_crypto::PublicKeyBundle::from_bytes(b).map_err(|_| {
AcceptError::AuthenticationFailed("invalid public key bundle".into())
}),
DataValue::Bytes(b) => mtp_crypto::PublicKeyBundle::from_bytes(b)
.map_err(|_| AcceptError::AuthenticationFailed("invalid public key bundle".into())),
_ => Err(AcceptError::AuthenticationFailed(
"missing public keys".into(),
)),
}
}
async fn send_rejection_generic<S: HandshakeSender>(
sender: &S,
reason: RejectionReason,
) {
async fn send_rejection_generic<S: HandshakeSender>(sender: &S, reason: RejectionReason) {
let response = match &reason {
RejectionReason::BadVersion { supported_versions } => {
CommunicationValue::new(CommunicationType::ErrorBadVersion)
@ -815,7 +812,9 @@ impl<C: mtp_transport::TransportConnection> HandshakeSender for mtp_transport::G
}
}
impl<C: mtp_transport::TransportConnection> HandshakeReceiver for mtp_transport::GenericReceiver<C> {
impl<C: mtp_transport::TransportConnection> HandshakeReceiver
for mtp_transport::GenericReceiver<C>
{
fn receive(
&self,
) -> impl std::future::Future<Output = Result<CommunicationValue, CommunicationError>> + Send

View file

@ -1,7 +1,10 @@
#[cfg(not(feature = "crypto"))]
use mtp_codec::{Version, registry::{Registry, VersionedCodec}};
#[cfg(feature = "crypto")]
use mtp_codec::registry::Registry;
#[cfg(not(feature = "crypto"))]
use mtp_codec::{
Version,
registry::{Registry, VersionedCodec},
};
use mtp_transport::{Receiver, Sender};
use std::sync::Arc;
use std::time::Instant;
@ -136,9 +139,7 @@ impl HandshakeContext {
#[cfg(feature = "crypto")]
{
Ok(Some(self.connection_from_handshake_result(
sender,
receiver,
result,
sender, receiver, result,
)))
}
#[cfg(not(feature = "crypto"))]

View file

@ -11,7 +11,7 @@ pub use MTPHost as Host;
pub use config::HostConfig;
pub use config::Policy;
pub use connection::{MTPConnection, MtpReceiverLike, MtpSenderLike};
pub use engine::{HandshakeEngine, HandshakeResult, HandshakeReceiver, HandshakeSender};
pub use engine::{HandshakeEngine, HandshakeReceiver, HandshakeResult, HandshakeSender};
pub use error::AcceptError;
pub use handshake::MTPHost;
pub use mtp_transport::Receiver;

View file

@ -352,9 +352,11 @@ fn build_endpoint(
transport.keep_alive_interval(Some(keep_alive));
}
if let Some(idle_timeout) = config.policy.max_idle_timeout {
transport.max_idle_timeout(Some(idle_timeout.try_into().map_err(
|error| CommunicationError::Other(format!("{error}")),
)?));
transport.max_idle_timeout(Some(
idle_timeout
.try_into()
.map_err(|error| CommunicationError::Other(format!("{error}")))?,
));
}
Arc::new(transport)
});

View file

@ -267,15 +267,11 @@ pub(crate) async fn accept_web_connection(
send_pongs: bool,
policy: Policy,
host_config: Arc<HostConfig>,
auth_semaphore: Arc<tokio::sync::Semaphore>,
_auth_semaphore: Arc<tokio::sync::Semaphore>,
) -> Result<WebMTPConnection, AcceptError> {
#[cfg(feature = "crypto")]
{
let permit = auth_semaphore
.clone()
.acquire_owned()
.await
.map_err(|_| {
let permit = _auth_semaphore.clone().acquire_owned().await.map_err(|_| {
AcceptError::AuthenticationFailed("authentication service stopped".into())
})?;
let result = tokio::time::timeout(
@ -327,8 +323,8 @@ async fn accept_web_connection_inner(
if send_pongs {
receiver.respond_to_pings(sender.clone()).await;
}
let connection: WebMTPConnection =
mtp_host::MTPConnection::from_transport_parts_with_policy(
#[cfg(feature = "pipes")]
let connection: WebMTPConnection = mtp_host::MTPConnection::from_transport_parts_with_policy(
negotiated,
codec,
sender,
@ -338,10 +334,21 @@ async fn accept_web_connection_inner(
Some(remote_addr),
policy,
);
#[cfg(not(feature = "pipes"))]
let connection: WebMTPConnection =
mtp_host::MTPConnection::from_transport_parts_with_remote_addr(
negotiated,
codec,
sender,
receiver,
path,
description.clone(),
Some(remote_addr),
);
#[cfg(not(feature = "crypto"))]
{
connection.receiver.set_max_message_size(max_message_size);
return Ok(connection);
Ok(connection)
}
#[cfg(feature = "crypto")]
let mut connection = connection;
@ -364,7 +371,8 @@ async fn accept_web_connection_inner(
// Unauthenticated: send accepted response with guest ID (or ID 0)
if !is_allow_auth && !is_force_auth {
let response = mtp_codec::CommunicationValue::new(CommunicationType::IdentificationResponse)
let response =
mtp_codec::CommunicationValue::new(CommunicationType::IdentificationResponse)
.add_typed_default(DataType::Connected, DataValue::BoolTrue)
.add_typed_default(
DataType::Version,
@ -405,16 +413,31 @@ async fn accept_web_connection_inner(
if let Some(bundle) =
(_host_config.get_existing_client)(id, description.clone()).await
{
(id, Some(bundle), CommunicationType::IdentificationResponse, false)
(
id,
Some(bundle),
CommunicationType::IdentificationResponse,
false,
)
} else {
// Unknown client: fall back to guest
let guest_id = H3TransportConnection::assign_guest_id(&_host_config).await?;
(guest_id, None, CommunicationType::IdentificationResponse, true)
(
guest_id,
None,
CommunicationType::IdentificationResponse,
true,
)
}
} else {
// ID zero or missing: fall back to guest
let guest_id = H3TransportConnection::assign_guest_id(&_host_config).await?;
(guest_id, None, CommunicationType::IdentificationResponse, true)
(
guest_id,
None,
CommunicationType::IdentificationResponse,
true,
)
}
} else if first_type_opt == reg_type {
// Registration: always authenticate (both AllowAuth and ForceAuth)
@ -442,7 +465,12 @@ async fn accept_web_connection_inner(
let bundle = (_host_config.get_existing_client)(id, description.clone())
.await
.ok_or_else(|| AcceptError::AuthenticationFailed("unknown client id".into()))?;
(id, Some(bundle), CommunicationType::IdentificationResponse, false)
(
id,
Some(bundle),
CommunicationType::IdentificationResponse,
false,
)
} else {
return Err(AcceptError::AuthenticationFailed(
"unexpected authentication message".into(),
@ -453,9 +481,13 @@ async fn accept_web_connection_inner(
// Guest path: skip challenge/response, send accepted with guest ID
if is_guest {
let guest_id = client_id;
let response = mtp_codec::CommunicationValue::new(CommunicationType::IdentificationResponse)
let response =
mtp_codec::CommunicationValue::new(CommunicationType::IdentificationResponse)
.add_typed_default(DataType::Connected, DataValue::BoolTrue)
.add_typed_default(DataType::Version, DataValue::Str(connection.version.to_string()))
.add_typed_default(
DataType::Version,
DataValue::Str(connection.version.to_string()),
)
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(guest_id as u128));
connection
.sender
@ -513,14 +545,11 @@ async fn accept_web_connection_inner(
let host_config = _host_config.clone();
let pq_signer = host_pq_signer.clone();
async move {
let signer =
Ed25519Signer::new(&host_config.host_keyring.sig_cl_secret_key)
let signer = Ed25519Signer::new(&host_config.host_keyring.sig_cl_secret_key)
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?;
if let Some(pq_signer) = pq_signer {
mtp_crypto::sign_parallel::sign_dual_parallel_shared_pq(
signer,
pq_signer,
payload,
signer, pq_signer, payload,
)
.await
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))
@ -535,8 +564,7 @@ async fn accept_web_connection_inner(
let sign_challenge_started = Instant::now();
let (sig, pq_sig) = host_sign(auth::challenge_payload(client_id, server_challenge)).await?;
tracing::debug!(elapsed = ?sign_challenge_started.elapsed(), "web authentication handshake: sign challenge");
let mut challenge =
mtp_codec::CommunicationValue::new(CommunicationType::Challenge)
let mut challenge = mtp_codec::CommunicationValue::new(CommunicationType::Challenge)
.add_typed_default(
DataType::ServerNonce,
DataValue::UnsignedNumber(server_challenge),
@ -579,8 +607,7 @@ async fn accept_web_connection_inner(
}
};
tracing::debug!(elapsed = ?receive_proof_started.elapsed(), "web authentication handshake: receive client proof");
if Some(proof.get_type()) != CommunicationType::ChallengeResponse.try_to_id(&tm)
{
if Some(proof.get_type()) != CommunicationType::ChallengeResponse.try_to_id(&tm) {
return Err(AcceptError::AuthenticationFailed(
"missing challenge response".into(),
));
@ -605,11 +632,7 @@ async fn accept_web_connection_inner(
DataValue::Bytes(bytes) => bytes.as_slice(),
_ => &[],
};
let payload = if first.get_type()
== CommunicationType::Register
.try_to_id(&tm)
.unwrap()
{
let payload = if first.get_type() == CommunicationType::Register.try_to_id(&tm).unwrap() {
auth::register_proof_payload(
&version.to_string(),
&client_bundle.as_bytes(),
@ -643,9 +666,13 @@ async fn accept_web_connection_inner(
tracing::debug!(elapsed = ?verify_proof_started.elapsed(), "web authentication handshake: verify client proof");
if !proof_ok {
let rejection = mtp_codec::CommunicationValue::new(CommunicationType::IdentificationResponse)
let rejection =
mtp_codec::CommunicationValue::new(CommunicationType::IdentificationResponse)
.add_typed_default(DataType::Connected, DataValue::BoolFalse)
.add_typed_default(DataType::ErrorMessage, DataValue::Str("client proof signature invalid".into()));
.add_typed_default(
DataType::ErrorMessage,
DataValue::Str("client proof signature invalid".into()),
);
let _ = connection.sender.send(&rejection).await;
connection.sender.close();
return Err(AcceptError::AuthenticationFailed(
@ -698,5 +725,3 @@ async fn accept_web_connection_inner(
Ok(connection)
}
}

View file

@ -857,6 +857,10 @@ export class MTPClient {
return this.raw.client.state;
}
get pingMs(): number | null {
return this.raw.client.ping_ms ?? null;
}
async #loadStoredCredentials() {
if (this.#credentials || !this.#options.storage) {
return;

View file

@ -198,7 +198,9 @@ impl<C: TransportConnection> GenericReceiver<C> {
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 max_message_size = Arc::new(AtomicU64::new(
policy.handshake_max_message_size.min(policy.max_message_size),
policy
.handshake_max_message_size
.min(policy.max_message_size),
));
let task_ping_sender = ping_sender.clone();
let task_connection = connection.clone();
@ -214,8 +216,8 @@ impl<C: TransportConnection> GenericReceiver<C> {
loop {
// Backpressure: stop accepting new streams if the output queue is full.
#[cfg(feature = "pipes")]
let cap_full = task_accept_task_tx.capacity() == 0
|| task_accept_task_pipe_tx.capacity() == 0;
let cap_full =
task_accept_task_tx.capacity() == 0 || task_accept_task_pipe_tx.capacity() == 0;
#[cfg(not(feature = "pipes"))]
let cap_full = task_accept_task_tx.capacity() == 0;
@ -269,10 +271,7 @@ impl<C: TransportConnection> GenericReceiver<C> {
{
let close_error = CommunicationError::StreamError;
let _ = tx.send(Err(close_error.clone())).await;
connection.close(
policy.application_close_code,
b"max frames exceeded",
);
connection.close(policy.application_close_code, b"max frames exceeded");
break;
}
let mut len = [0; 4];
@ -291,12 +290,12 @@ impl<C: TransportConnection> GenericReceiver<C> {
break;
}
Err(_) => {
tracing::warn!("MTP receive stream timed out while reading frame header");
let _ = tx.send(Err(CommunicationError::StreamError)).await;
connection.close(
policy.application_close_code,
b"frame header timeout",
tracing::warn!(
"MTP receive stream timed out while reading frame header"
);
let _ = tx.send(Err(CommunicationError::StreamError)).await;
connection
.close(policy.application_close_code, b"frame header timeout");
break;
}
}
@ -308,10 +307,7 @@ impl<C: TransportConnection> GenericReceiver<C> {
if len as u64 > frame_limit {
tracing::warn!(len, "MTP receive stream frame is too large");
let _ = tx.send(Err(CommunicationError::MessageTooLarge)).await;
connection.close(
policy.application_close_code,
b"frame too large",
);
connection.close(policy.application_close_code, b"frame too large");
break;
}
let target_len = len as usize;
@ -322,10 +318,8 @@ impl<C: TransportConnection> GenericReceiver<C> {
"MTP receive stream could not reserve frame body"
);
let _ = tx.send(Err(CommunicationError::MessageTooLarge)).await;
connection.close(
policy.application_close_code,
b"frame allocation failed",
);
connection
.close(policy.application_close_code, b"frame allocation failed");
break;
}
while body.len() < target_len {
@ -345,10 +339,8 @@ impl<C: TransportConnection> GenericReceiver<C> {
"MTP receive stream failed while reading frame body"
);
let _ = tx.send(Err(CommunicationError::StreamError)).await;
connection.close(
policy.application_close_code,
b"frame body read error",
);
connection
.close(policy.application_close_code, b"frame body read error");
break;
}
body.extend_from_slice(&chunk[..chunk_len]);
@ -361,12 +353,10 @@ impl<C: TransportConnection> GenericReceiver<C> {
Ok(message) => message,
Err(_) => {
tracing::warn!("MTP receive stream contained an invalid frame");
let _ = tx.send(Err(CommunicationError::ParseCommunicationValue))
let _ = tx
.send(Err(CommunicationError::ParseCommunicationValue))
.await;
connection.close(
policy.application_close_code,
b"invalid frame",
);
connection.close(policy.application_close_code, b"invalid frame");
break;
}
};

View file

@ -44,9 +44,19 @@ fn route_incoming_frame(
on_message: &js_sys::Function,
subscriptions: &Rc<RefCell<HashMap<u32, (String, js_sys::Function)>>>,
pending_requests: &Rc<RefCell<HashMap<u32, PendingRequest>>>,
pending_pings: &Rc<RefCell<HashMap<u32, f64>>>,
ping_ms: &Rc<Cell<Option<f64>>>,
) {
let message_type = frame_type(frame);
if message_type.as_deref() == Some("Pong") {
if let Some(sent_at) = frame_id(frame).and_then(|id| pending_pings.borrow_mut().remove(&id))
{
ping_ms.set(Some(js_sys::Date::now() - sent_at));
}
return;
}
if let Some(request_id) = frame_id(frame) {
let pending = pending_requests.borrow_mut().remove(&request_id);
if let Some(pending) = pending {
@ -149,6 +159,8 @@ pub struct WasmClient {
next_subscription_id: Rc<Cell<u32>>,
pending_requests: Rc<RefCell<HashMap<u32, PendingRequest>>>,
ping_timer: Rc<RefCell<Option<PingTimer>>>,
pending_pings: Rc<RefCell<HashMap<u32, f64>>>,
ping_ms: Rc<Cell<Option<f64>>>,
pending_pipe_creations: Rc<RefCell<HashMap<u32, oneshot::Sender<Result<bool, JsValue>>>>>,
pending_pipes: Rc<RefCell<HashMap<u32, oneshot::Sender<PipeReader>>>>,
on_pipe_request: Rc<RefCell<Option<js_sys::Function>>>,
@ -184,6 +196,8 @@ impl WasmClient {
next_subscription_id: Rc::new(Cell::new(1)),
pending_requests: Rc::new(RefCell::new(HashMap::new())),
ping_timer: Rc::new(RefCell::new(None)),
pending_pings: Rc::new(RefCell::new(HashMap::new())),
ping_ms: Rc::new(Cell::new(None)),
pending_pipe_creations: Rc::new(RefCell::new(HashMap::new())),
pending_pipes: Rc::new(RefCell::new(HashMap::new())),
on_pipe_request: Rc::new(RefCell::new(None)),
@ -200,6 +214,11 @@ impl WasmClient {
self.state.get() as u8
}
#[wasm_bindgen(getter)]
pub fn ping_ms(&self) -> Option<f64> {
self.ping_ms.get()
}
#[wasm_bindgen]
pub async fn connect(&self, config: &ConnectionConfig) -> Result<(), JsValue> {
self.set_state(ConnectionState::Connecting);
@ -582,11 +601,17 @@ impl WasmClient {
};
let interval_ms = interval_ms.max(1_000) as i32;
let on_error = self.on_error.clone();
let pending_pings = self.pending_pings.clone();
let closure = Closure::wrap(Box::new(move || {
let transport = transport.clone();
let on_error = on_error.clone();
let pending_pings = pending_pings.clone();
wasm_bindgen_futures::spawn_local(async move {
let timestamp = js_sys::Date::now() as u64;
let sent_at = js_sys::Date::now();
pending_pings
.borrow_mut()
.retain(|_, pending_at| sent_at - *pending_at < interval_ms as f64 * 3.0);
let timestamp = sent_at as u64;
let frame = CommunicationValue::new(CommunicationType::Ping)
.add_typed_default(
DataType::Description,
@ -596,12 +621,16 @@ impl WasmClient {
DataType::Timestamp,
DataValue::UnsignedNumber(timestamp as u128),
)
.with_sender(client_id)
.with_sender(client_id);
let ping_id = frame.get_id();
let frame = frame
.to_bytes()
.map_err(|e| js_error(format!("encode ping failed: {}", e)));
match frame {
Ok(frame) => {
pending_pings.borrow_mut().insert(ping_id, sent_at);
if let Err(error) = transport.send_frame(&frame).await {
pending_pings.borrow_mut().remove(&ping_id);
let _ = on_error.call1(&JsValue::NULL, &error);
}
}
@ -629,6 +658,8 @@ impl WasmClient {
#[wasm_bindgen]
pub fn stop_protocol_pings(&self) {
self.pending_pings.borrow_mut().clear();
self.ping_ms.set(None);
let Some(timer) = self.ping_timer.borrow_mut().take() else {
return;
};
@ -741,6 +772,10 @@ impl WasmClient {
let pending_requests = self.pending_requests.clone();
let loop_pending_requests = pending_requests.clone();
let ping_timer = self.ping_timer.clone();
let pending_pings = self.pending_pings.clone();
let loop_pending_pings = pending_pings.clone();
let ping_ms = self.ping_ms.clone();
let loop_ping_ms = ping_ms.clone();
let pending_pipe_creations = self.pending_pipe_creations.clone();
let pending_pipes = self.pending_pipes.clone();
let on_pipe_request = self.on_pipe_request.clone();
@ -808,6 +843,8 @@ impl WasmClient {
&on_msg,
&subscriptions,
&loop_pending_requests,
&loop_pending_pings,
&loop_ping_ms,
);
},
on_err.clone(),
@ -822,6 +859,8 @@ impl WasmClient {
.await;
state.set(ConnectionState::Disconnected);
stop_ping_timer(&ping_timer);
pending_pings.borrow_mut().clear();
ping_ms.set(None);
reject_pending_requests(&pending_requests, "disconnected");
client_pipe::reject_pending_pipe_creations(&pending_pipe_creations, "disconnected");
});

View file

@ -121,6 +121,7 @@ export class WasmClient implements DisposableWasmObject {
subscribe(message_type: string, callback: MessageCallback): number;
unsubscribe(id: number): boolean;
static is_supported(): boolean;
readonly ping_ms: number | undefined;
readonly state: ConnectionState;
}