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 xso::{AsXml, FromXml};
8
9use crate::ns;
10
11/// Wraps `<stream:features/>`, usually the very first nonza of a
12/// XMPP stream. Indicates which features are supported.
13#[derive(FromXml, AsXml, PartialEq, Debug, Default, Clone)]
14#[xml(namespace = ns::STREAM, name = "features")]
15pub struct StreamFeatures {
16 /// StartTLS is supported, and may be mandatory.
17 #[xml(child(default))]
18 pub starttls: Option<StartTls>,
19
20 /// Bind is supported.
21 #[xml(child(default))]
22 pub bind: Option<Bind>,
23
24 /// List of supported SASL mechanisms
25 #[xml(child(default))]
26 pub sasl_mechanisms: SaslMechanisms,
27}
28
29/// StartTLS is supported, and may be mandatory.
30#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
31#[xml(namespace = ns::TLS, name = "starttls")]
32pub struct StartTls {
33 /// Marker for mandatory StartTLS.
34 #[xml(child(default))]
35 pub required: Option<RequiredStartTls>,
36}
37
38/// Marker for mandatory StartTLS.
39#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
40#[xml(namespace = ns::TLS, name = "required")]
41pub struct RequiredStartTls;
42
43/// Bind is supported.
44#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
45#[xml(namespace = ns::BIND, name = "bind")]
46pub struct Bind;
47
48generate_element!(
49 /// List of supported SASL mechanisms
50 #[derive(Default)]
51 SaslMechanisms, "mechanisms", SASL,
52 children: [
53 /// List of information elements describing this avatar.
54 mechanisms: Vec<SaslMechanism> = ("mechanism", SASL) => SaslMechanism,
55 ]
56);
57
58// TODO: Uncomment me when xso supports collections, see
59// https://gitlab.com/xmpp-rs/xmpp-rs/-/issues/136
60// #[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
61// #[xml(namespace = ns::SASL, name = "mechanisms")]
62// pub struct SaslMechanisms {
63// #[xml(child(default))]
64// mechanisms: Vec<SaslMechanism>,
65// }
66
67/// The name of a SASL mechanism.
68#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
69#[xml(namespace = ns::SASL, name = "mechanism")]
70pub struct SaslMechanism {
71 /// The stringy name of the mechanism.
72 #[xml(text)]
73 pub mechanism: String,
74}
75
76impl StreamFeatures {
77 /// Can initiate TLS session with this server?
78 pub fn can_starttls(&self) -> bool {
79 self.starttls.is_some()
80 }
81
82 /// Does server support user resource binding?
83 pub fn can_bind(&self) -> bool {
84 self.bind.is_some()
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use minidom::Element;
92
93 #[cfg(target_pointer_width = "32")]
94 #[test]
95 fn test_size() {
96 assert_size!(SaslMechanism, 12);
97 assert_size!(SaslMechanisms, 12);
98 assert_size!(Bind, 0);
99 assert_size!(RequiredStartTls, 0);
100 assert_size!(StartTls, 1);
101 assert_size!(StreamFeatures, 16);
102 }
103
104 #[cfg(target_pointer_width = "64")]
105 #[test]
106 fn test_size() {
107 assert_size!(SaslMechanism, 24);
108 assert_size!(SaslMechanisms, 24);
109 assert_size!(Bind, 0);
110 assert_size!(RequiredStartTls, 0);
111 assert_size!(StartTls, 1);
112 assert_size!(StreamFeatures, 32);
113 }
114
115 #[test]
116 fn test_required_starttls() {
117 let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
118 <starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'>
119 <required/>
120 </starttls>
121 </stream:features>"
122 .parse()
123 .unwrap();
124
125 let features = StreamFeatures::try_from(elem).unwrap();
126
127 assert_eq!(features.can_bind(), false);
128 assert_eq!(features.sasl_mechanisms.mechanisms.len(), 0);
129 assert_eq!(features.can_starttls(), true);
130 assert_eq!(features.starttls.unwrap().required.is_some(), true);
131 }
132
133 #[test]
134 // TODO: Unignore me when xso supports collections of unknown children, see
135 // https://gitlab.com/xmpp-rs/xmpp-rs/-/issues/136
136 #[ignore]
137 fn test_deprecated_compression() {
138 let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
139 <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
140 <compression xmlns='http://jabber.org/features/compress'>
141 <method>zlib</method>
142 <method>lzw</method>
143 </compression>
144 </stream:features>"
145 .parse()
146 .unwrap();
147
148 let features = StreamFeatures::try_from(elem).unwrap();
149
150 assert_eq!(features.can_bind(), true);
151 assert_eq!(features.sasl_mechanisms.mechanisms.len(), 0);
152 assert_eq!(features.can_starttls(), false);
153 }
154
155 #[test]
156 fn test_empty_features() {
157 let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams'/>"
158 .parse()
159 .unwrap();
160
161 let features = StreamFeatures::try_from(elem).unwrap();
162
163 assert_eq!(features.can_bind(), false);
164 assert_eq!(features.sasl_mechanisms.mechanisms.len(), 0);
165 assert_eq!(features.can_starttls(), false);
166 }
167}