1use xso::{
8 error::{Error, FromElementError},
9 text::Base64,
10 AsXml, FromXml,
11};
12
13use crate::ns;
14use minidom::Element;
15use std::collections::BTreeMap;
16
17generate_attribute!(
18 Mechanism, "mechanism", {
20 Plain => "PLAIN",
23
24 ScramSha1 => "SCRAM-SHA-1",
30
31 ScramSha1Plus => "SCRAM-SHA-1-PLUS",
34
35 ScramSha256 => "SCRAM-SHA-256",
38
39 ScramSha256Plus => "SCRAM-SHA-256-PLUS",
42
43 Anonymous => "ANONYMOUS",
46 }
47);
48
49#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
52#[xml(namespace = ns::SASL, name = "auth")]
53pub struct Auth {
54 #[xml(attribute)]
56 pub mechanism: Mechanism,
57
58 #[xml(text = Base64)]
60 pub data: Vec<u8>,
61}
62
63#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
67#[xml(namespace = ns::SASL, name = "challenge")]
68pub struct Challenge {
69 #[xml(text = Base64)]
71 pub data: Vec<u8>,
72}
73
74#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
78#[xml(namespace = ns::SASL, name = "response")]
79pub struct Response {
80 #[xml(text = Base64)]
82 pub data: Vec<u8>,
83}
84
85#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
88#[xml(namespace = ns::SASL, name = "abort")]
89pub struct Abort;
90
91#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
93#[xml(namespace = ns::SASL, name = "success")]
94pub struct Success {
95 #[xml(text = Base64)]
97 pub data: Vec<u8>,
98}
99
100generate_element_enum!(
101 DefinedCondition, "defined-condition", SASL, {
103 Aborted => "aborted",
106
107 AccountDisabled => "account-disabled",
110
111 CredentialsExpired => "credentials-expired",
113
114 EncryptionRequired => "encryption-required",
117
118 IncorrectEncoding => "incorrect-encoding",
120
121 InvalidAuthzid => "invalid-authzid",
123
124 InvalidMechanism => "invalid-mechanism",
126
127 MalformedRequest => "malformed-request",
129
130 MechanismTooWeak => "mechanism-too-weak",
132
133 NotAuthorized => "not-authorized",
135
136 TemporaryAuthFailure => "temporary-auth-failure",
139 }
140);
141
142type Lang = String;
143
144#[derive(Debug, Clone)]
146pub struct Failure {
147 pub defined_condition: DefinedCondition,
149
150 pub texts: BTreeMap<Lang, String>,
152}
153
154impl TryFrom<Element> for Failure {
155 type Error = FromElementError;
156
157 fn try_from(root: Element) -> Result<Failure, FromElementError> {
158 check_self!(root, "failure", SASL);
159 check_no_attributes!(root, "failure");
160
161 let mut defined_condition = None;
162 let mut texts = BTreeMap::new();
163
164 for child in root.children() {
165 if child.is("text", ns::SASL) {
166 check_no_unknown_attributes!(child, "text", ["xml:lang"]);
167 check_no_children!(child, "text");
168 let lang = get_attr!(child, "xml:lang", Default);
169 if texts.insert(lang, child.text()).is_some() {
170 return Err(Error::Other(
171 "Text element present twice for the same xml:lang in failure element.",
172 )
173 .into());
174 }
175 } else if child.has_ns(ns::SASL) {
176 if defined_condition.is_some() {
177 return Err(Error::Other(
178 "Failure must not have more than one defined-condition.",
179 )
180 .into());
181 }
182 check_no_attributes!(child, "defined-condition");
183 check_no_children!(child, "defined-condition");
184 let condition = match DefinedCondition::try_from(child.clone()) {
185 Ok(condition) => condition,
186 Err(_) => DefinedCondition::NotAuthorized,
188 };
189 defined_condition = Some(condition);
190 } else {
191 return Err(Error::Other("Unknown element in Failure.").into());
192 }
193 }
194 let defined_condition =
195 defined_condition.ok_or(Error::Other("Failure must have a defined-condition."))?;
196
197 Ok(Failure {
198 defined_condition,
199 texts,
200 })
201 }
202}
203
204impl From<Failure> for Element {
205 fn from(failure: Failure) -> Element {
206 Element::builder("failure", ns::SASL)
207 .append(failure.defined_condition)
208 .append_all(failure.texts.into_iter().map(|(lang, text)| {
209 Element::builder("text", ns::SASL)
210 .attr("xml:lang", lang)
211 .append(text)
212 }))
213 .build()
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[cfg(target_pointer_width = "32")]
222 #[test]
223 fn test_size() {
224 assert_size!(Mechanism, 1);
225 assert_size!(Auth, 16);
226 assert_size!(Challenge, 12);
227 assert_size!(Response, 12);
228 assert_size!(Abort, 0);
229 assert_size!(Success, 12);
230 assert_size!(DefinedCondition, 1);
231 assert_size!(Failure, 16);
232 }
233
234 #[cfg(target_pointer_width = "64")]
235 #[test]
236 fn test_size() {
237 assert_size!(Mechanism, 1);
238 assert_size!(Auth, 32);
239 assert_size!(Challenge, 24);
240 assert_size!(Response, 24);
241 assert_size!(Abort, 0);
242 assert_size!(Success, 24);
243 assert_size!(DefinedCondition, 1);
244 assert_size!(Failure, 32);
245 }
246
247 #[test]
248 fn test_simple() {
249 let elem: Element = "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'/>"
250 .parse()
251 .unwrap();
252 let auth = Auth::try_from(elem).unwrap();
253 assert_eq!(auth.mechanism, Mechanism::Plain);
254 assert!(auth.data.is_empty());
255 }
256
257 #[test]
258 fn section_6_5_1() {
259 let elem: Element =
260 "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><aborted/></failure>"
261 .parse()
262 .unwrap();
263 let failure = Failure::try_from(elem).unwrap();
264 assert_eq!(failure.defined_condition, DefinedCondition::Aborted);
265 assert!(failure.texts.is_empty());
266 }
267
268 #[test]
269 fn section_6_5_2() {
270 let elem: Element = "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
271 <account-disabled/>
272 <text xml:lang='en'>Call 212-555-1212 for assistance.</text>
273 </failure>"
274 .parse()
275 .unwrap();
276 let failure = Failure::try_from(elem).unwrap();
277 assert_eq!(failure.defined_condition, DefinedCondition::AccountDisabled);
278 assert_eq!(
279 failure.texts["en"],
280 String::from("Call 212-555-1212 for assistance.")
281 );
282 }
283
284 #[cfg(feature = "disable-validation")]
287 #[test]
288 fn invalid_failure_with_non_prefixed_text_lang() {
289 let elem: Element = "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
290 <not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
291 <text xmlns='urn:ietf:params:xml:ns:xmpp-sasl' lang='en'>Invalid username or password</text>
292 </failure>"
293 .parse()
294 .unwrap();
295 let failure = Failure::try_from(elem).unwrap();
296 assert_eq!(failure.defined_condition, DefinedCondition::NotAuthorized);
297 assert_eq!(
298 failure.texts[""],
299 String::from("Invalid username or password")
300 );
301 }
302}