xmpp_parsers/
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 alloc::collections::BTreeSet;
8use minidom::Element;
9use xso::{AsXml, FromXml};
10
11use crate::bind::BindFeature;
12use crate::ns;
13use crate::sasl2::Authentication;
14use crate::sasl_cb::SaslChannelBinding;
15use crate::starttls::StartTls;
16use crate::stream_limits::Limits;
17
18/// Wraps `<stream:features/>`, usually the very first nonza of a
19/// XMPP stream. Indicates which features are supported.
20#[derive(FromXml, AsXml, PartialEq, Debug, Default, Clone)]
21#[xml(namespace = ns::STREAM, name = "features")]
22pub struct StreamFeatures {
23    /// StartTLS is supported, and may be mandatory.
24    #[xml(child(default))]
25    pub starttls: Option<StartTls>,
26
27    /// Bind is supported.
28    #[xml(child(default))]
29    pub bind: Option<BindFeature>,
30
31    /// List of supported SASL mechanisms
32    #[xml(extract(default, namespace = ns::SASL, name = "mechanisms", fields(
33        extract(n = .., name = "mechanism", fields(text(type_ = String)))
34    )))]
35    pub sasl_mechanisms: BTreeSet<String>,
36
37    /// Limits advertised by the server.
38    #[xml(child(default))]
39    pub limits: Option<Limits>,
40
41    /// Extensible SASL Profile, a newer authentication method than the one from the RFC.
42    #[xml(child(default))]
43    pub sasl2: Option<Authentication>,
44
45    /// SASL Channel-Binding Type Capability.
46    #[xml(child(default))]
47    pub sasl_cb: Option<SaslChannelBinding>,
48
49    /// Stream management feature
50    #[xml(child(default))]
51    pub stream_management: Option<crate::sm::StreamManagement>,
52
53    /// Stream management feature
54    #[xml(flag(namespace = ns::REGISTER_FEATURE, name = "register"))]
55    pub in_band_registration: bool,
56
57    /// Other stream features advertised
58    ///
59    /// If some features you use end up here, you may want to contribute
60    /// a typed equivalent to the xmpp-parsers project!
61    #[xml(element(n = ..))]
62    pub others: Vec<Element>,
63}
64
65impl StreamFeatures {
66    /// Can initiate TLS session with this server?
67    pub fn can_starttls(&self) -> bool {
68        self.starttls.is_some()
69    }
70
71    /// Does server support user resource binding?
72    pub fn can_bind(&self) -> bool {
73        self.bind.is_some()
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use minidom::Element;
81
82    #[cfg(target_pointer_width = "32")]
83    #[test]
84    fn test_size() {
85        assert_size!(StreamFeatures, 92);
86    }
87
88    #[cfg(target_pointer_width = "64")]
89    #[test]
90    fn test_size() {
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!(features.sasl_mechanisms.contains("PLAIN"));
108        assert!(features.sasl_mechanisms.contains("SCRAM-SHA-1"));
109        assert!(features.sasl_mechanisms.contains("SCRAM-SHA-1-PLUS"));
110        assert!(!features.sasl_mechanisms.contains("CRAM-MD5"));
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.len(), 0);
127        assert_eq!(features.can_starttls(), true);
128        assert_eq!(features.starttls.unwrap().required, 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.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.len(), 0);
177        assert_eq!(features.can_starttls(), false);
178    }
179}