This commit is contained in:
parent
a5c8d4f0c8
commit
2b0bdc3257
4 changed files with 127 additions and 9 deletions
|
|
@ -143,6 +143,11 @@ pub(crate) async fn run_driver(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
tracing::debug!(
|
||||||
|
remote = %remote_addr,
|
||||||
|
session_id = ?session.session_id(),
|
||||||
|
"accepted WebTransport MTP session"
|
||||||
|
);
|
||||||
tokio::spawn(run_session_requests(
|
tokio::spawn(run_session_requests(
|
||||||
session.clone(),
|
session.clone(),
|
||||||
router.clone(),
|
router.clone(),
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ pub struct H3TransportSender {
|
||||||
|
|
||||||
pub struct H3TransportReceiver {
|
pub struct H3TransportReceiver {
|
||||||
stream: H3RecvStream,
|
stream: H3RecvStream,
|
||||||
|
quinn: quinn::Connection,
|
||||||
|
read_exact_calls: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl H3TransportConnection {
|
impl H3TransportConnection {
|
||||||
|
|
@ -76,18 +78,42 @@ impl TransportSendStream for H3TransportSender {
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl TransportRecvStream for H3TransportReceiver {
|
impl TransportRecvStream for H3TransportReceiver {
|
||||||
async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), CommunicationError> {
|
async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), CommunicationError> {
|
||||||
|
let first_read = self.read_exact_calls == 0;
|
||||||
|
self.read_exact_calls += 1;
|
||||||
self.stream
|
self.stream
|
||||||
.read_exact(buf)
|
.read_exact(buf)
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| {
|
||||||
|
if first_read {
|
||||||
|
tracing::debug!(
|
||||||
|
remote = %self.quinn.remote_address(),
|
||||||
|
bytes = buf.len(),
|
||||||
|
"received first bytes from WebTransport MTP stream"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
if error.kind() == std::io::ErrorKind::UnexpectedEof {
|
if error.kind() == std::io::ErrorKind::UnexpectedEof
|
||||||
// Browser control frames are sent on one-frame uni streams.
|
|| self.quinn.close_reason().is_some()
|
||||||
// Reaching FIN while looking for another frame is normal.
|
{
|
||||||
|
/*
|
||||||
|
* Reaching FIN, or losing the enclosing QUIC connection,
|
||||||
|
* is a normal stream-closure path. Do not turn it into a
|
||||||
|
* frame-header failure and close the connection again.
|
||||||
|
*/
|
||||||
return CommunicationError::StreamClosed;
|
return CommunicationError::StreamClosed;
|
||||||
}
|
}
|
||||||
error!("[mtp-webserver] receive stream read_exact failed ({} bytes): {error}", buf.len());
|
error!(
|
||||||
tracing::warn!(len = buf.len(), %error, "WebTransport receive stream read_exact failed");
|
"[mtp-webserver] receive stream read_exact failed ({} bytes): {error}",
|
||||||
|
buf.len()
|
||||||
|
);
|
||||||
|
tracing::warn!(
|
||||||
|
remote = %self.quinn.remote_address(),
|
||||||
|
first_read,
|
||||||
|
len = buf.len(),
|
||||||
|
%error,
|
||||||
|
"WebTransport receive stream read_exact failed"
|
||||||
|
);
|
||||||
CommunicationError::StreamError
|
CommunicationError::StreamError
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -101,6 +127,9 @@ impl TransportRecvStream for H3TransportReceiver {
|
||||||
Ok(Some(buf))
|
Ok(Some(buf))
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
if self.quinn.close_reason().is_some() {
|
||||||
|
return Err(CommunicationError::StreamClosed);
|
||||||
|
}
|
||||||
error!(
|
error!(
|
||||||
"[mtp-webserver] receive stream read failed (max {} bytes): {error}",
|
"[mtp-webserver] receive stream read failed (max {} bytes): {error}",
|
||||||
max
|
max
|
||||||
|
|
@ -167,10 +196,27 @@ impl TransportConnection for H3TransportConnection {
|
||||||
loop {
|
loop {
|
||||||
match self.session.accept_uni().await {
|
match self.session.accept_uni().await {
|
||||||
Ok(Some((id, stream))) if id == self.session.session_id() => {
|
Ok(Some((id, stream))) if id == self.session.session_id() => {
|
||||||
return Ok(H3TransportReceiver { stream });
|
let stream_id = h3::quic::RecvStream::recv_id(&stream);
|
||||||
|
tracing::debug!(
|
||||||
|
remote = %self.quinn.remote_address(),
|
||||||
|
session_id = ?self.session.session_id(),
|
||||||
|
stream_id = ?stream_id,
|
||||||
|
"accepted WebTransport MTP receive stream"
|
||||||
|
);
|
||||||
|
return Ok(H3TransportReceiver {
|
||||||
|
stream,
|
||||||
|
quinn: self.quinn.clone(),
|
||||||
|
read_exact_calls: 0,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Ok(Some(_)) => {
|
Ok(Some((stream_session_id, _stream))) => {
|
||||||
consecutive_errors = 0;
|
consecutive_errors = 0;
|
||||||
|
tracing::debug!(
|
||||||
|
remote = %self.quinn.remote_address(),
|
||||||
|
session_id = ?self.session.session_id(),
|
||||||
|
stream_session_id = ?stream_session_id,
|
||||||
|
"ignored WebTransport receive stream belonging to another session"
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Ok(None) => return Err(CommunicationError::StreamClosed),
|
Ok(None) => return Err(CommunicationError::StreamClosed),
|
||||||
|
|
@ -306,7 +352,18 @@ async fn accept_web_connection_inner(
|
||||||
connection_id,
|
connection_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await;
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
if let Err(error) = &result {
|
||||||
|
tracing::warn!(
|
||||||
|
remote = %remote_addr,
|
||||||
|
connection_id,
|
||||||
|
%error,
|
||||||
|
"WebTransport MTP handshake failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
let result = result?;
|
||||||
#[cfg(not(feature = "crypto"))]
|
#[cfg(not(feature = "crypto"))]
|
||||||
let result = engine.accept(&sender, &receiver).await?;
|
let result = engine.accept(&sender, &receiver).await?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -333,6 +333,18 @@ impl<C: TransportConnection> GenericReceiver<C> {
|
||||||
break 'stream;
|
break 'stream;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
if frames == 0 {
|
||||||
|
tracing::warn!(
|
||||||
|
timeout = ?policy.read_timeout,
|
||||||
|
"MTP receive stream timed out before its first complete frame"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tracing::debug!(
|
||||||
|
frames,
|
||||||
|
timeout = ?policy.read_timeout,
|
||||||
|
"MTP receive stream idle timeout"
|
||||||
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -376,6 +388,9 @@ impl<C: TransportConnection> GenericReceiver<C> {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if !matches!(&body_read, Ok(Ok(()))) {
|
if !matches!(&body_read, Ok(Ok(()))) {
|
||||||
|
if matches!(&body_read, Ok(Err(CommunicationError::StreamClosed))) {
|
||||||
|
break 'stream;
|
||||||
|
}
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
pipe_chunk_len = chunk_len,
|
pipe_chunk_len = chunk_len,
|
||||||
?body_read,
|
?body_read,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,14 @@ use crate::config::ConnectionConfig;
|
||||||
use crate::error::js_error;
|
use crate::error::js_error;
|
||||||
use crate::transport::WasmTransport;
|
use crate::transport::WasmTransport;
|
||||||
|
|
||||||
|
fn server_rejection_message(outcome: &CommunicationValue) -> Option<&str> {
|
||||||
|
(outcome.get_data(DataType::Connected) == Some(&DataValue::BoolFalse)).then(|| {
|
||||||
|
outcome
|
||||||
|
.get_str(DataType::ErrorMessage)
|
||||||
|
.unwrap_or("host rejected the connection")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
impl WasmClient {
|
impl WasmClient {
|
||||||
|
|
@ -79,6 +87,18 @@ impl WasmClient {
|
||||||
.unwrap_or("host does not support this protocol version"),
|
.unwrap_or("host does not support this protocol version"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generic host rejections are IdentificationResponse frames with
|
||||||
|
// Connected=false. They intentionally do not carry a negotiated
|
||||||
|
// Version because negotiation never completed. Check this before
|
||||||
|
// reading Version, otherwise a useful server error such as an
|
||||||
|
// authentication timeout is reported as the misleading
|
||||||
|
// "host omitted a valid negotiated protocol version".
|
||||||
|
if let Some(message) = server_rejection_message(&outcome) {
|
||||||
|
self.set_state_if_current(generation, ConnectionState::Disconnected);
|
||||||
|
return Err(js_error(message));
|
||||||
|
}
|
||||||
|
|
||||||
let negotiated_version = match outcome.get_data(DataType::Version) {
|
let negotiated_version = match outcome.get_data(DataType::Version) {
|
||||||
Some(DataValue::Str(version)) => mtp_codec::Version::parse(version)
|
Some(DataValue::Str(version)) => mtp_codec::Version::parse(version)
|
||||||
.ok_or_else(|| js_error("host omitted a valid negotiated protocol version"))?,
|
.ok_or_else(|| js_error("host omitted a valid negotiated protocol version"))?,
|
||||||
|
|
@ -628,3 +648,24 @@ impl WasmClient {
|
||||||
Ok(server_challenge)
|
Ok(server_challenge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::server_rejection_message;
|
||||||
|
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reports_rejection_reason_without_a_negotiated_version() {
|
||||||
|
let response = CommunicationValue::new(CommunicationType::IdentificationResponse)
|
||||||
|
.add_typed_default(DataType::Connected, DataValue::BoolFalse)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::ErrorMessage,
|
||||||
|
DataValue::Str("authentication handshake timed out".into()),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
server_rejection_message(&response),
|
||||||
|
Some("authentication handshake timed out")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue