// Copyright (c) 2024 Jonas Schäfer // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. //! # RFC 6120 XML Streams //! //! **Note:** The XML stream is a low-level API which you should probably not //! use directly. //! //! Establishing an XML stream is always a multi-stage process due to how //! stream negotiation works. Based on the values sent by the initiator in the //! stream header, the responder may choose to offer different features. //! //! In order to allow this, the following multi-step processes are defined. //! //! ## Initiating an XML stream //! //! To initiate an XML stream, you need to: //! //! 1. Call [`initiate_stream`] to obtain the [`PendingFeaturesRecv`] object. //! That object holds the stream header sent by the peer for inspection. //! 2. Call [`PendingFeaturesRecv::recv_features`] if you are content with //! the content of the stream header to obtain the [`XmlStream`] object and //! the features sent by the peer. //! //! ## Accepting an XML stream connection //! //! To accept an XML stream, you need to: //! //! 1. Call [`accept_stream`] to obtain the [`AcceptedStream`] object. //! That object holds the stream header sent by the peer for inspection. //! 2. Call [`AcceptedStream::send_header`] if you are content with //! the content of the stream header to obtain the [`PendingFeaturesSend`] //! object. //! 3. Call [`PendingFeaturesSend::send_features`] to send the stream features //! to the peer and obtain the [`XmlStream`] object. //! //! ## Mid-stream resets //! //! RFC 6120 describes a couple of situations where stream resets are executed //! during stream negotiation. During a stream reset, both parties drop their //! parser state and the stream is started from the beginning, with a new //! stream header sent by the initiator and received by the responder. //! //! Stream resets are inherently prone to race conditions. If the responder //! executes a read from the underlying transport between sending the element //! which triggers the stream reset and discarding its parser state, it may //! accidentally read the initiator's stream header into the *old* parser //! state instead of the post-reset parser state. //! //! Stream resets are executed with the [`XmlStream::initiate_reset`] and //! [`XmlStream::accept_reset`] functions, for initiator and responder, //! respectively. In order to avoid the race condition, //! [`XmlStream::accept_reset`] handles sending the last pre-reset element and //! resetting the stream in a single step. use core::fmt; use core::pin::Pin; use core::task::{Context, Poll}; use std::io; #[cfg(feature = "syntax-highlighting")] use std::sync::OnceLock; use futures::{ready, Sink, SinkExt, Stream}; use tokio::io::{AsyncBufRead, AsyncWrite}; use xso::{AsXml, FromXml, Item}; mod capture; mod common; mod initiator; mod responder; #[cfg(test)] mod tests; pub(crate) mod xmpp; pub use self::common::StreamHeader; use self::common::{RawXmlStream, ReadXsoError, ReadXsoState}; pub use self::initiator::{InitiatingStream, PendingFeaturesRecv}; pub use self::responder::{AcceptedStream, PendingFeaturesSend}; pub use self::xmpp::XmppStreamElement; #[cfg(feature = "syntax-highlighting")] static PS: OnceLock = OnceLock::new(); #[cfg(feature = "syntax-highlighting")] static SYNTAX: OnceLock = OnceLock::new(); #[cfg(feature = "syntax-highlighting")] static THEME: OnceLock = OnceLock::new(); #[cfg(feature = "syntax-highlighting")] fn highlight_xml(xml: &str) -> String { let ps = PS.get_or_init(syntect::parsing::SyntaxSet::load_defaults_newlines); let mut h = syntect::easy::HighlightLines::new( SYNTAX.get_or_init(|| ps.find_syntax_by_extension("xml").unwrap().clone()), THEME.get_or_init(|| { syntect::highlighting::ThemeSet::load_defaults().themes["Solarized (dark)"].clone() }), ); let ranges: Vec<_> = h.highlight_line(&xml, ps).unwrap(); let escaped = syntect::util::as_24_bit_terminal_escaped(&ranges[..], false); format!("{}\x1b[0m", escaped) } struct LogXsoBuf<'x>(&'x [u8]); impl<'x> fmt::Display for LogXsoBuf<'x> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // We always generate UTF-8, so this should be good... I think. let text = std::str::from_utf8(&self.0).unwrap(); #[cfg(feature = "syntax-highlighting")] let text = highlight_xml(text); f.write_str(&text) } } /// Initiate a new stream /// /// Initiate a new stream using the given I/O object `io`. The default /// XML namespace will be set to `stream_ns` and the stream header will use /// the attributes as set in `stream_header`, along with version `1.0`. /// /// The returned object contains the stream header sent by the remote side /// as well as the internal parser state to continue the negotiation. pub async fn initiate_stream( io: Io, stream_ns: &'static str, stream_header: StreamHeader<'_>, ) -> Result, io::Error> { let stream = InitiatingStream(RawXmlStream::new(io, stream_ns)); stream.send_header(stream_header).await } /// Accept a new XML stream as responder /// /// Prepares the responer side of an XML stream using the given I/O object /// `io`. The default XML namespace will be set to `stream_ns`. /// /// The returned object contains the stream header sent by the remote side /// as well as the internal parser state to continue the negotiation. pub async fn accept_stream( io: Io, stream_ns: &'static str, ) -> Result, io::Error> { let mut stream = RawXmlStream::new(io, stream_ns); let header = StreamHeader::recv(Pin::new(&mut stream)).await?; Ok(AcceptedStream { stream, header }) } /// A non-success state which may occur while reading an XSO from a /// [`XmlStream`] #[derive(Debug)] pub enum ReadError { /// The soft timeout of the stream triggered. /// /// User code should handle this by sending something into the stream /// which causes the peer to send data before the hard timeout triggers. SoftTimeout, /// An I/O error occurred in the underlying I/O object. /// /// This is generally fatal. HardError(io::Error), /// A parse error occurred while processing the XSO. /// /// This is non-fatal and more XSOs may be read from the stream. ParseError(xso::error::Error), /// The stream footer was received. /// /// Any future read attempts will again return this error. The stream has /// been closed by the peer and you should probably close it, too. StreamFooterReceived, } enum WriteState { Open, SendElementFoot, FooterSent, Failed, } impl WriteState { fn check_ok(&self) -> io::Result<()> { match self { WriteState::Failed => Err(io::Error::new( io::ErrorKind::NotConnected, "XML stream sink unusable because of previous write error", )), WriteState::Open | WriteState::SendElementFoot | WriteState::FooterSent => Ok(()), } } fn check_writable(&self) -> io::Result<()> { match self { WriteState::SendElementFoot | WriteState::FooterSent => Err(io::Error::new( io::ErrorKind::NotConnected, "stream footer already sent", )), WriteState::Failed | WriteState::Open => self.check_ok(), } } } pin_project_lite::pin_project! { /// XML stream /// /// This struct represents an /// [RFC 6120](https://tools.ietf.org/html/rfc6120) XML stream, where the /// payload consists of items of type `T` implementing [`FromXml`] and /// [`AsXml`]. pub struct XmlStream { #[pin] inner: RawXmlStream, read_state: Option>, write_state: WriteState, } } impl XmlStream { /// Obtain a reference to the `Io` stream. pub fn get_stream(&self) -> &Io { self.inner.get_stream() } } impl XmlStream { fn wrap(inner: RawXmlStream) -> Self { Self { inner, read_state: Some(ReadXsoState::default()), write_state: WriteState::Open, } } fn assert_retypable(&self) { match self.read_state { Some(ReadXsoState::PreData) => (), Some(_) => panic!("cannot reset stream: XSO parsing in progress!"), None => panic!("cannot reset stream: stream footer received!"), } match self.write_state.check_writable() { Ok(()) => (), Err(e) => panic!("cannot reset stream: {}", e), } } } impl XmlStream { /// Initiate a stream reset /// /// To actually send the stream header, call /// [`send_header`][`InitiatingStream::send_header`] on the result. /// /// # Panics /// /// Attempting to reset the stream while an object is being received will /// panic. This can generally only happen if you call `poll_next` /// directly, as doing that is otherwise prevented by the borrowchecker. /// /// In addition, attempting to reset a stream which has been closed by /// either side or which has had an I/O error will also cause a panic. pub fn initiate_reset(self) -> InitiatingStream { self.assert_retypable(); let mut stream = self.inner; Pin::new(&mut stream).reset_state(); InitiatingStream(stream) } /// Trigger a stream reset on the initiator side and await the new stream /// header. /// /// This is the responder-side counterpart to /// [`initiate_reset`][`Self::initiate_reset`]. The element which causes /// the stream reset must be passed as `barrier` and it will be sent /// right before resetting the parser state. This way, the race condition /// outlined in the [`xmlstream`][`self`] module's documentation is /// guaranteed to be avoided. /// /// Note that you should not send the element passed as `barrier` down the /// stream yourself, as this function takes care of it. /// /// # Stream resets without a triggering element /// /// These are not possible to do safely and not specified in RFC 6120, /// hence they cannot be done in [`XmlStream`]. /// /// # Panics /// /// Attempting to reset the stream while an object is being received will /// panic. This can generally only happen if you call `poll_next` /// directly, as doing that is otherwise prevented by the borrowchecker. /// /// In addition, attempting to reset a stream which has been closed by /// either side or which has had an I/O error will also cause a panic. pub async fn accept_reset(mut self, barrier: &T) -> io::Result> { self.assert_retypable(); self.send(barrier).await?; let mut stream = self.inner; Pin::new(&mut stream).reset_state(); let header = StreamHeader::recv(Pin::new(&mut stream)).await?; Ok(AcceptedStream { stream, header }) } /// Discard all XML state and return the inner I/O object. pub fn into_inner(self) -> Io { self.assert_retypable(); self.inner.into_inner() } } impl Stream for XmlStream { type Item = Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.project(); let result = match this.read_state.as_mut() { None => return Poll::Ready(Some(Err(ReadError::StreamFooterReceived))), Some(read_state) => ready!(read_state.poll_advance(this.inner, cx)), }; let result = match result { Ok(v) => Poll::Ready(Some(Ok(v))), Err(ReadXsoError::Hard(e)) => Poll::Ready(Some(Err(ReadError::HardError(e)))), Err(ReadXsoError::Parse(e)) => Poll::Ready(Some(Err(ReadError::ParseError(e)))), Err(ReadXsoError::Footer) => { *this.read_state = None; Poll::Ready(Some(Err(ReadError::StreamFooterReceived))) } }; *this.read_state = Some(ReadXsoState::default()); result } } impl<'x, Io: AsyncWrite, T: FromXml + AsXml + fmt::Debug> Sink<&'x T> for XmlStream { type Error = io::Error; fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.project(); this.write_state.check_writable()?; this.inner.poll_ready(cx) } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.project(); this.write_state.check_writable()?; this.inner.poll_flush(cx) } fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let mut this = self.project(); this.write_state.check_ok()?; loop { match this.write_state { // Open => initiate closing. WriteState::Open => { *this.write_state = WriteState::SendElementFoot; } // Sending => wait for readiness, then send. WriteState::SendElementFoot => { match ready!(this.inner.as_mut().poll_ready(cx)) .and_then(|_| this.inner.as_mut().start_send(Item::ElementFoot)) { Ok(()) => (), // If it fails, we fail the sink immediately. Err(e) => { *this.write_state = WriteState::Failed; return Poll::Ready(Err(e)); } } *this.write_state = WriteState::FooterSent; } // Footer sent => just poll the inner sink for closure. WriteState::FooterSent => break, WriteState::Failed => unreachable!(), // caught by check_ok() } } this.inner.poll_close(cx) } fn start_send(self: Pin<&mut Self>, item: &'x T) -> Result<(), Self::Error> { let this = self.project(); this.write_state.check_writable()?; this.inner.start_send_xso(item) } } /// Convenience alias for an XML stream using [`XmppStreamElement`]. pub type XmppStream = XmlStream;