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 /// Stream management feature
47 #[xml(child(default))]
48 pub stream_management: Option<crate::sm::StreamManagement>,
49
50 /// Stream management feature
51 #[xml(flag(namespace = ns::REGISTER_FEATURE, name = "register"))]
52 pub in_band_registration: bool,
53
54 /// Other stream features advertised
55 ///
56 /// If some features you use end up here, you may want to contribute
57 /// a typed equivalent to the xmpp-parsers project!
58 #[xml(element(n = ..))]
59 pub others: Vec<Element>,
60}
61
62/// List of supported SASL mechanisms
63#[derive(FromXml, AsXml, PartialEq, Debug, Clone, Default)]
64#[xml(namespace = ns::SASL, name = "mechanisms")]
65pub struct SaslMechanisms {
66 /// List of information elements describing this SASL mechanism.
67 #[xml(extract(n = .., name = "mechanism", fields(text(type_ = String))))]
68 pub mechanisms: Vec<String>,
69}
70
71impl StreamFeatures {
72 /// Can initiate TLS session with this server?
73 pub fn can_starttls(&self) -> bool {
74 self.starttls.is_some()
75 }
76
77 /// Does server support user resource binding?
78 pub fn can_bind(&self) -> bool {
79 self.bind.is_some()
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use minidom::Element;
87
88 #[cfg(target_pointer_width = "32")]
89 #[test]
90 fn test_size() {
91 assert_size!(SaslMechanisms, 12);
92 assert_size!(StreamFeatures, 92);
93 }
94
95 #[cfg(target_pointer_width = "64")]
96 #[test]
97 fn test_size() {
98 assert_size!(SaslMechanisms, 24);
99 assert_size!(StreamFeatures, 168);
100 }
101
102 #[test]
103 fn test_sasl_mechanisms() {
104 let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
105 <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
106 <mechanism>PLAIN</mechanism>
107 <mechanism>SCRAM-SHA-1</mechanism>
108 <mechanism>SCRAM-SHA-1-PLUS</mechanism>
109 </mechanisms>
110 </stream:features>"
111 .parse()
112 .unwrap();
113
114 let features = StreamFeatures::try_from(elem).unwrap();
115 assert_eq!(
116 features.sasl_mechanisms.mechanisms,
117 ["PLAIN", "SCRAM-SHA-1", "SCRAM-SHA-1-PLUS"]
118 );
119 }
120
121 #[test]
122 fn test_required_starttls() {
123 let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
124 <starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'>
125 <required/>
126 </starttls>
127 </stream:features>"
128 .parse()
129 .unwrap();
130
131 let features = StreamFeatures::try_from(elem).unwrap();
132
133 assert_eq!(features.can_bind(), false);
134 assert_eq!(features.sasl_mechanisms.mechanisms.len(), 0);
135 assert_eq!(features.can_starttls(), true);
136 assert_eq!(features.starttls.unwrap().required, true);
137 }
138
139 #[test]
140 fn test_deprecated_compression() {
141 let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
142 <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
143 <compression xmlns='http://jabber.org/features/compress'>
144 <method>zlib</method>
145 <method>lzw</method>
146 </compression>
147 </stream:features>"
148 .parse()
149 .unwrap();
150
151 let features = StreamFeatures::try_from(elem).unwrap();
152
153 assert_eq!(features.can_bind(), true);
154 assert_eq!(features.sasl_mechanisms.mechanisms.len(), 0);
155 assert_eq!(features.can_starttls(), false);
156 assert_eq!(features.others.len(), 1);
157
158 let compression = &features.others[0];
159 assert!(compression.is("compression", "http://jabber.org/features/compress"));
160 let mut children = compression.children();
161
162 let child = children.next().expect("zlib not found");
163 assert_eq!(child.name(), "method");
164 let mut texts = child.texts();
165 assert_eq!(texts.next().unwrap(), "zlib");
166 assert_eq!(texts.next(), None);
167
168 let child = children.next().expect("lzw not found");
169 assert_eq!(child.name(), "method");
170 let mut texts = child.texts();
171 assert_eq!(texts.next().unwrap(), "lzw");
172 assert_eq!(texts.next(), None);
173 }
174
175 #[test]
176 fn test_empty_features() {
177 let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams'/>"
178 .parse()
179 .unwrap();
180
181 let features = StreamFeatures::try_from(elem).unwrap();
182
183 assert_eq!(features.can_bind(), false);
184 assert_eq!(features.sasl_mechanisms.mechanisms.len(), 0);
185 assert_eq!(features.can_starttls(), false);
186 }
187}