Support <stream:features> in parsers

xmppftw created

Change summary

parsers/ChangeLog              |   5 +
parsers/src/lib.rs             |   2 
parsers/src/stream_features.rs | 167 ++++++++++++++++++++++++++++++++++++
3 files changed, 174 insertions(+)

Detailed changes

parsers/ChangeLog 🔗

@@ -1,3 +1,8 @@
+Version NEXT:
+XXXX-YY-ZZ RELEASER <admin@example.com>
+    * New parsers/serialisers:
+        - Stream Features (RFC 6120) (!400)
+
 Version 0.21.0:
 2024-07-25 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
     * New parsers/serialisers:

parsers/src/lib.rs 🔗

@@ -55,6 +55,8 @@ pub mod sasl;
 pub mod stanza_error;
 /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
 pub mod stream;
+/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
+pub mod stream_features;
 
 /// RFC 6121: Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence
 pub mod roster;

parsers/src/stream_features.rs 🔗

@@ -0,0 +1,167 @@
+// Copyright (c) 2024 xmpp-rs contributors
+//
+// 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/.
+
+use xso::{AsXml, FromXml};
+
+use crate::ns;
+
+/// Wraps `<stream:features/>`, usually the very first nonza of a
+/// XMPP stream. Indicates which features are supported.
+#[derive(FromXml, AsXml, PartialEq, Debug, Default, Clone)]
+#[xml(namespace = ns::STREAM, name = "features")]
+pub struct StreamFeatures {
+    /// StartTLS is supported, and may be mandatory.
+    #[xml(child(default))]
+    pub starttls: Option<StartTls>,
+
+    /// Bind is supported.
+    #[xml(child(default))]
+    pub bind: Option<Bind>,
+
+    /// List of supported SASL mechanisms
+    #[xml(child(default))]
+    pub sasl_mechanisms: SaslMechanisms,
+}
+
+/// StartTLS is supported, and may be mandatory.
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = ns::TLS, name = "starttls")]
+pub struct StartTls {
+    /// Marker for mandatory StartTLS.
+    #[xml(child(default))]
+    pub required: Option<RequiredStartTls>,
+}
+
+/// Marker for mandatory StartTLS.
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = ns::TLS, name = "required")]
+pub struct RequiredStartTls;
+
+/// Bind is supported.
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = ns::BIND, name = "bind")]
+pub struct Bind;
+
+generate_element!(
+    /// List of supported SASL mechanisms
+    #[derive(Default)]
+    SaslMechanisms, "mechanisms", SASL,
+    children: [
+        /// List of information elements describing this avatar.
+        mechanisms: Vec<SaslMechanism> = ("mechanism", SASL) => SaslMechanism,
+    ]
+);
+
+// TODO: Uncomment me when xso supports collections, see
+// https://gitlab.com/xmpp-rs/xmpp-rs/-/issues/136
+// #[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+// #[xml(namespace = ns::SASL, name = "mechanisms")]
+// pub struct SaslMechanisms {
+//     #[xml(child(default))]
+//     mechanisms: Vec<SaslMechanism>,
+// }
+
+/// The name of a SASL mechanism.
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = ns::SASL, name = "mechanism")]
+pub struct SaslMechanism {
+    /// The stringy name of the mechanism.
+    #[xml(text)]
+    pub mechanism: String,
+}
+
+impl StreamFeatures {
+    /// Can initiate TLS session with this server?
+    pub fn can_starttls(&self) -> bool {
+        self.starttls.is_some()
+    }
+
+    /// Does server support user resource binding?
+    pub fn can_bind(&self) -> bool {
+        self.bind.is_some()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use minidom::Element;
+
+    #[cfg(target_pointer_width = "32")]
+    #[test]
+    fn test_size() {
+        assert_size!(SaslMechanism, 12);
+        assert_size!(SaslMechanisms, 12);
+        assert_size!(Bind, 0);
+        assert_size!(RequiredStartTls, 0);
+        assert_size!(StartTls, 1);
+        assert_size!(StreamFeatures, 16);
+    }
+
+    #[cfg(target_pointer_width = "64")]
+    #[test]
+    fn test_size() {
+        assert_size!(SaslMechanism, 24);
+        assert_size!(SaslMechanisms, 24);
+        assert_size!(Bind, 0);
+        assert_size!(RequiredStartTls, 0);
+        assert_size!(StartTls, 1);
+        assert_size!(StreamFeatures, 32);
+    }
+
+    #[test]
+    fn test_required_starttls() {
+        let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
+                                 <starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'>
+                                     <required/>
+                                 </starttls>
+                             </stream:features>"
+            .parse()
+            .unwrap();
+
+        let features = StreamFeatures::try_from(elem).unwrap();
+
+        assert_eq!(features.can_bind(), false);
+        assert_eq!(features.sasl_mechanisms.mechanisms.len(), 0);
+        assert_eq!(features.can_starttls(), true);
+        assert_eq!(features.starttls.unwrap().required.is_some(), true);
+    }
+
+    #[test]
+    // TODO: Unignore me when xso supports collections of unknown children, see
+    // https://gitlab.com/xmpp-rs/xmpp-rs/-/issues/136
+    #[ignore]
+    fn test_deprecated_compression() {
+        let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
+                                 <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
+                                 <compression xmlns='http://jabber.org/features/compress'>
+                                     <method>zlib</method>
+                                     <method>lzw</method>
+                                 </compression>
+                             </stream:features>"
+            .parse()
+            .unwrap();
+
+        let features = StreamFeatures::try_from(elem).unwrap();
+
+        assert_eq!(features.can_bind(), true);
+        assert_eq!(features.sasl_mechanisms.mechanisms.len(), 0);
+        assert_eq!(features.can_starttls(), false);
+    }
+
+    #[test]
+    fn test_empty_features() {
+        let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams'/>"
+            .parse()
+            .unwrap();
+
+        let features = StreamFeatures::try_from(elem).unwrap();
+
+        assert_eq!(features.can_bind(), false);
+        assert_eq!(features.sasl_mechanisms.mechanisms.len(), 0);
+        assert_eq!(features.can_starttls(), false);
+    }
+}