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}