stream_features.rs

  1// Copyright (c) 2024 xmpp-rs contributors
  2//
  3// This Source Code Form is subject to the terms of the Mozilla Public
  4// License, v. 2.0. If a copy of the MPL was not distributed with this
  5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6
  7use minidom::Element;
  8use xso::{AsXml, FromXml};
  9
 10use crate::bind::BindFeature;
 11use crate::ns;
 12use crate::sasl2::Authentication;
 13use crate::sasl_cb::SaslChannelBinding;
 14use crate::starttls::StartTls;
 15use crate::stream_limits::Limits;
 16
 17/// Wraps `<stream:features/>`, usually the very first nonza of a
 18/// XMPP stream. Indicates which features are supported.
 19#[derive(FromXml, AsXml, PartialEq, Debug, Default, Clone)]
 20#[xml(namespace = ns::STREAM, name = "features")]
 21pub struct StreamFeatures {
 22    /// StartTLS is supported, and may be mandatory.
 23    #[xml(child(default))]
 24    pub starttls: Option<StartTls>,
 25
 26    /// Bind is supported.
 27    #[xml(child(default))]
 28    pub bind: Option<BindFeature>,
 29
 30    /// List of supported SASL mechanisms
 31    #[xml(child(default))]
 32    pub sasl_mechanisms: SaslMechanisms,
 33
 34    /// Limits advertised by the server.
 35    #[xml(child(default))]
 36    pub limits: Option<Limits>,
 37
 38    /// Extensible SASL Profile, a newer authentication method than the one from the RFC.
 39    #[xml(child(default))]
 40    pub sasl2: Option<Authentication>,
 41
 42    /// SASL Channel-Binding Type Capability.
 43    #[xml(child(default))]
 44    pub sasl_cb: Option<SaslChannelBinding>,
 45
 46    /// Other stream features advertised
 47    ///
 48    /// If some features you use end up here, you may want to contribute
 49    /// a typed equivalent to the xmpp-parsers project!
 50    #[xml(element(n = ..))]
 51    pub others: Vec<Element>,
 52}
 53
 54/// List of supported SASL mechanisms
 55#[derive(FromXml, AsXml, PartialEq, Debug, Clone, Default)]
 56#[xml(namespace = ns::SASL, name = "mechanisms")]
 57pub struct SaslMechanisms {
 58    /// List of information elements describing this SASL mechanism.
 59    #[xml(extract(n = .., name = "mechanism", fields(text(type_ = String))))]
 60    pub mechanisms: Vec<String>,
 61}
 62
 63impl StreamFeatures {
 64    /// Can initiate TLS session with this server?
 65    pub fn can_starttls(&self) -> bool {
 66        self.starttls.is_some()
 67    }
 68
 69    /// Does server support user resource binding?
 70    pub fn can_bind(&self) -> bool {
 71        self.bind.is_some()
 72    }
 73}
 74
 75#[cfg(test)]
 76mod tests {
 77    use super::*;
 78    use minidom::Element;
 79
 80    #[cfg(target_pointer_width = "32")]
 81    #[test]
 82    fn test_size() {
 83        assert_size!(SaslMechanisms, 12);
 84        assert_size!(StreamFeatures, 92);
 85    }
 86
 87    #[cfg(target_pointer_width = "64")]
 88    #[test]
 89    fn test_size() {
 90        assert_size!(SaslMechanisms, 24);
 91        assert_size!(StreamFeatures, 168);
 92    }
 93
 94    #[test]
 95    fn test_sasl_mechanisms() {
 96        let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
 97            <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
 98                <mechanism>PLAIN</mechanism>
 99                <mechanism>SCRAM-SHA-1</mechanism>
100                <mechanism>SCRAM-SHA-1-PLUS</mechanism>
101            </mechanisms>
102        </stream:features>"
103            .parse()
104            .unwrap();
105
106        let features = StreamFeatures::try_from(elem).unwrap();
107        assert_eq!(
108            features.sasl_mechanisms.mechanisms,
109            ["PLAIN", "SCRAM-SHA-1", "SCRAM-SHA-1-PLUS"]
110        );
111    }
112
113    #[test]
114    fn test_required_starttls() {
115        let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
116                                 <starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'>
117                                     <required/>
118                                 </starttls>
119                             </stream:features>"
120            .parse()
121            .unwrap();
122
123        let features = StreamFeatures::try_from(elem).unwrap();
124
125        assert_eq!(features.can_bind(), false);
126        assert_eq!(features.sasl_mechanisms.mechanisms.len(), 0);
127        assert_eq!(features.can_starttls(), true);
128        assert_eq!(features.starttls.unwrap().required.is_some(), true);
129    }
130
131    #[test]
132    fn test_deprecated_compression() {
133        let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
134                                 <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
135                                 <compression xmlns='http://jabber.org/features/compress'>
136                                     <method>zlib</method>
137                                     <method>lzw</method>
138                                 </compression>
139                             </stream:features>"
140            .parse()
141            .unwrap();
142
143        let features = StreamFeatures::try_from(elem).unwrap();
144
145        assert_eq!(features.can_bind(), true);
146        assert_eq!(features.sasl_mechanisms.mechanisms.len(), 0);
147        assert_eq!(features.can_starttls(), false);
148        assert_eq!(features.others.len(), 1);
149
150        let compression = &features.others[0];
151        assert!(compression.is("compression", "http://jabber.org/features/compress"));
152        let mut children = compression.children();
153
154        let child = children.next().expect("zlib not found");
155        assert_eq!(child.name(), "method");
156        let mut texts = child.texts();
157        assert_eq!(texts.next().unwrap(), "zlib");
158        assert_eq!(texts.next(), None);
159
160        let child = children.next().expect("lzw not found");
161        assert_eq!(child.name(), "method");
162        let mut texts = child.texts();
163        assert_eq!(texts.next().unwrap(), "lzw");
164        assert_eq!(texts.next(), None);
165    }
166
167    #[test]
168    fn test_empty_features() {
169        let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams'/>"
170            .parse()
171            .unwrap();
172
173        let features = StreamFeatures::try_from(elem).unwrap();
174
175        assert_eq!(features.can_bind(), false);
176        assert_eq!(features.sasl_mechanisms.mechanisms.len(), 0);
177        assert_eq!(features.can_starttls(), false);
178    }
179}