1// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
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 crate::message::MessagePayload;
8use crate::ns;
9use crate::presence::PresencePayload;
10use crate::Element;
11use jid::Jid;
12use minidom::Node;
13use std::collections::BTreeMap;
14use std::convert::TryFrom;
15use xso::error::{Error, FromElementError};
16
17generate_attribute!(
18 /// The type of the error.
19 ErrorType, "type", {
20 /// Retry after providing credentials.
21 Auth => "auth",
22
23 /// Do not retry (the error cannot be remedied).
24 Cancel => "cancel",
25
26 /// Proceed (the condition was only a warning).
27 Continue => "continue",
28
29 /// Retry after changing the data sent.
30 Modify => "modify",
31
32 /// Retry after waiting (the error is temporary).
33 Wait => "wait",
34 }
35);
36
37generate_element_enum!(
38 /// List of valid error conditions.
39 DefinedCondition, "condition", XMPP_STANZAS, {
40 /// The sender has sent a stanza containing XML that does not conform
41 /// to the appropriate schema or that cannot be processed (e.g., an IQ
42 /// stanza that includes an unrecognized value of the 'type' attribute,
43 /// or an element that is qualified by a recognized namespace but that
44 /// violates the defined syntax for the element); the associated error
45 /// type SHOULD be "modify".
46 BadRequest => "bad-request",
47
48 /// Access cannot be granted because an existing resource exists with
49 /// the same name or address; the associated error type SHOULD be
50 /// "cancel".
51 Conflict => "conflict",
52
53 /// The feature represented in the XML stanza is not implemented by the
54 /// intended recipient or an intermediate server and therefore the
55 /// stanza cannot be processed (e.g., the entity understands the
56 /// namespace but does not recognize the element name); the associated
57 /// error type SHOULD be "cancel" or "modify".
58 FeatureNotImplemented => "feature-not-implemented",
59
60 /// The requesting entity does not possess the necessary permissions to
61 /// perform an action that only certain authorized roles or individuals
62 /// are allowed to complete (i.e., it typically relates to
63 /// authorization rather than authentication); the associated error
64 /// type SHOULD be "auth".
65 Forbidden => "forbidden",
66
67 /// The recipient or server can no longer be contacted at this address,
68 /// typically on a permanent basis (as opposed to the \<redirect/\> error
69 /// condition, which is used for temporary addressing failures); the
70 /// associated error type SHOULD be "cancel" and the error stanza
71 /// SHOULD include a new address (if available) as the XML character
72 /// data of the \<gone/\> element (which MUST be a Uniform Resource
73 /// Identifier (URI) or Internationalized Resource Identifier (IRI) at
74 /// which the entity can be contacted, typically an XMPP IRI as
75 /// specified in [XMPP‑URI](https://www.rfc-editor.org/rfc/rfc5122)).
76 Gone => "gone",
77
78 /// The server has experienced a misconfiguration or other internal
79 /// error that prevents it from processing the stanza; the associated
80 /// error type SHOULD be "cancel".
81 InternalServerError => "internal-server-error",
82
83 /// The addressed JID or item requested cannot be found; the associated
84 /// error type SHOULD be "cancel".
85 ItemNotFound => "item-not-found",
86
87 /// The sending entity has provided (e.g., during resource binding) or
88 /// communicated (e.g., in the 'to' address of a stanza) an XMPP
89 /// address or aspect thereof that violates the rules defined in
90 /// [XMPP‑ADDR]; the associated error type SHOULD be "modify".
91 JidMalformed => "jid-malformed",
92
93 /// The recipient or server understands the request but cannot process
94 /// it because the request does not meet criteria defined by the
95 /// recipient or server (e.g., a request to subscribe to information
96 /// that does not simultaneously include configuration parameters
97 /// needed by the recipient); the associated error type SHOULD be
98 /// "modify".
99 NotAcceptable => "not-acceptable",
100
101 /// The recipient or server does not allow any entity to perform the
102 /// action (e.g., sending to entities at a blacklisted domain); the
103 /// associated error type SHOULD be "cancel".
104 NotAllowed => "not-allowed",
105
106 /// The sender needs to provide credentials before being allowed to
107 /// perform the action, or has provided improper credentials (the name
108 /// "not-authorized", which was borrowed from the "401 Unauthorized"
109 /// error of HTTP, might lead the reader to think that this condition
110 /// relates to authorization, but instead it is typically used in
111 /// relation to authentication); the associated error type SHOULD be
112 /// "auth".
113 NotAuthorized => "not-authorized",
114
115 /// The entity has violated some local service policy (e.g., a message
116 /// contains words that are prohibited by the service) and the server
117 /// MAY choose to specify the policy in the \<text/\> element or in an
118 /// application-specific condition element; the associated error type
119 /// SHOULD be "modify" or "wait" depending on the policy being
120 /// violated.
121 PolicyViolation => "policy-violation",
122
123 /// The intended recipient is temporarily unavailable, undergoing
124 /// maintenance, etc.; the associated error type SHOULD be "wait".
125 RecipientUnavailable => "recipient-unavailable",
126
127 /// The recipient or server is redirecting requests for this
128 /// information to another entity, typically in a temporary fashion (as
129 /// opposed to the \<gone/\> error condition, which is used for permanent
130 /// addressing failures); the associated error type SHOULD be "modify"
131 /// and the error stanza SHOULD contain the alternate address in the
132 /// XML character data of the \<redirect/\> element (which MUST be a URI
133 /// or IRI with which the sender can communicate, typically an XMPP IRI
134 /// as specified in [XMPP‑URI](https://xmpp.org/rfcs/rfc5122.html)).
135 Redirect => "redirect",
136
137 /// The requesting entity is not authorized to access the requested
138 /// service because prior registration is necessary (examples of prior
139 /// registration include members-only rooms in XMPP multi-user chat
140 /// [XEP‑0045] and gateways to non-XMPP instant messaging services,
141 /// which traditionally required registration in order to use the
142 /// gateway [XEP‑0100]); the associated error type SHOULD be "auth".
143 RegistrationRequired => "registration-required",
144
145 /// A remote server or service specified as part or all of the JID of
146 /// the intended recipient does not exist or cannot be resolved (e.g.,
147 /// there is no _xmpp-server._tcp DNS SRV record, the A or AAAA
148 /// fallback resolution fails, or A/AAAA lookups succeed but there is
149 /// no response on the IANA-registered port 5269); the associated error
150 /// type SHOULD be "cancel".
151 RemoteServerNotFound => "remote-server-not-found",
152
153 /// A remote server or service specified as part or all of the JID of
154 /// the intended recipient (or needed to fulfill a request) was
155 /// resolved but communications could not be established within a
156 /// reasonable amount of time (e.g., an XML stream cannot be
157 /// established at the resolved IP address and port, or an XML stream
158 /// can be established but stream negotiation fails because of problems
159 /// with TLS, SASL, Server Dialback, etc.); the associated error type
160 /// SHOULD be "wait" (unless the error is of a more permanent nature,
161 /// e.g., the remote server is found but it cannot be authenticated or
162 /// it violates security policies).
163 RemoteServerTimeout => "remote-server-timeout",
164
165 /// The server or recipient is busy or lacks the system resources
166 /// necessary to service the request; the associated error type SHOULD
167 /// be "wait".
168 ResourceConstraint => "resource-constraint",
169
170 /// The server or recipient does not currently provide the requested
171 /// service; the associated error type SHOULD be "cancel".
172 ServiceUnavailable => "service-unavailable",
173
174 /// The requesting entity is not authorized to access the requested
175 /// service because a prior subscription is necessary (examples of
176 /// prior subscription include authorization to receive presence
177 /// information as defined in [XMPP‑IM] and opt-in data feeds for XMPP
178 /// publish-subscribe as defined in [XEP‑0060]); the associated error
179 /// type SHOULD be "auth".
180 SubscriptionRequired => "subscription-required",
181
182 /// The error condition is not one of those defined by the other
183 /// conditions in this list; any error type can be associated with this
184 /// condition, and it SHOULD NOT be used except in conjunction with an
185 /// application-specific condition.
186 UndefinedCondition => "undefined-condition",
187
188 /// The recipient or server understood the request but was not
189 /// expecting it at this time (e.g., the request was out of order); the
190 /// associated error type SHOULD be "wait" or "modify".
191 UnexpectedRequest => "unexpected-request",
192 }
193);
194
195type Lang = String;
196
197/// The representation of a stanza error.
198#[derive(Debug, Clone, PartialEq)]
199pub struct StanzaError {
200 /// The type of this error.
201 pub type_: ErrorType,
202
203 /// The JID of the entity who set this error.
204 pub by: Option<Jid>,
205
206 /// One of the defined conditions for this error to happen.
207 pub defined_condition: DefinedCondition,
208
209 /// Human-readable description of this error.
210 pub texts: BTreeMap<Lang, String>,
211
212 /// A protocol-specific extension for this error.
213 pub other: Option<Element>,
214
215 /// May include an alternate address if `defined_condition` is `Gone` or `Redirect`. It is
216 /// a Uniform Resource Identifier [URI] or Internationalized Resource Identifier [IRI] at
217 /// which the entity can be contacted, typically an XMPP IRI as specified in [XMPP‑URI]
218 pub alternate_address: Option<String>,
219}
220
221impl MessagePayload for StanzaError {}
222impl PresencePayload for StanzaError {}
223
224impl StanzaError {
225 /// Create a new `<error/>` with the according content.
226 pub fn new<L, T>(
227 type_: ErrorType,
228 defined_condition: DefinedCondition,
229 lang: L,
230 text: T,
231 ) -> StanzaError
232 where
233 L: Into<Lang>,
234 T: Into<String>,
235 {
236 StanzaError {
237 type_,
238 by: None,
239 defined_condition,
240 texts: {
241 let mut map = BTreeMap::new();
242 map.insert(lang.into(), text.into());
243 map
244 },
245 other: None,
246 alternate_address: None,
247 }
248 }
249}
250
251impl TryFrom<Element> for StanzaError {
252 type Error = FromElementError;
253
254 fn try_from(elem: Element) -> Result<StanzaError, FromElementError> {
255 check_self!(elem, "error", DEFAULT_NS);
256 // The code attribute has been deprecated in [XEP-0086](https://xmpp.org/extensions/xep-0086.html)
257 // which was deprecated in 2007. We don't error when it's here, but don't include it in the final struct.
258 check_no_unknown_attributes!(elem, "error", ["type", "by", "code"]);
259
260 let mut stanza_error = StanzaError {
261 type_: get_attr!(elem, "type", Required),
262 by: get_attr!(elem, "by", Option),
263 defined_condition: DefinedCondition::UndefinedCondition,
264 texts: BTreeMap::new(),
265 other: None,
266 alternate_address: None,
267 };
268 let mut defined_condition = None;
269
270 for child in elem.children() {
271 if child.is("text", ns::XMPP_STANZAS) {
272 check_no_children!(child, "text");
273 check_no_unknown_attributes!(child, "text", ["xml:lang"]);
274 let lang = get_attr!(child, "xml:lang", Default);
275 if stanza_error.texts.insert(lang, child.text()).is_some() {
276 return Err(
277 Error::Other("Text element present twice for the same xml:lang.").into(),
278 );
279 }
280 } else if child.has_ns(ns::XMPP_STANZAS) {
281 if defined_condition.is_some() {
282 return Err(Error::Other(
283 "Error must not have more than one defined-condition.",
284 )
285 .into());
286 }
287 check_no_children!(child, "defined-condition");
288 check_no_attributes!(child, "defined-condition");
289 let condition = DefinedCondition::try_from(child.clone())?;
290
291 if condition == DefinedCondition::Gone || condition == DefinedCondition::Redirect {
292 stanza_error.alternate_address = child.nodes().find_map(|node| {
293 let Node::Text(text) = node else { return None };
294 return Some(text.to_string());
295 });
296 }
297
298 defined_condition = Some(condition);
299 } else {
300 if stanza_error.other.is_some() {
301 return Err(
302 Error::Other("Error must not have more than one other element.").into(),
303 );
304 }
305 stanza_error.other = Some(child.clone());
306 }
307 }
308 stanza_error.defined_condition =
309 defined_condition.ok_or(Error::Other("Error must have a defined-condition."))?;
310
311 Ok(stanza_error)
312 }
313}
314
315impl From<StanzaError> for Element {
316 fn from(err: StanzaError) -> Element {
317 Element::builder("error", ns::DEFAULT_NS)
318 .attr("type", err.type_)
319 .attr("by", err.by)
320 .append(err.defined_condition)
321 .append_all(err.texts.into_iter().map(|(lang, text)| {
322 Element::builder("text", ns::XMPP_STANZAS)
323 .attr("xml:lang", lang)
324 .append(text)
325 }))
326 .append_all(err.other)
327 .build()
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 #[cfg(target_pointer_width = "32")]
336 #[test]
337 fn test_size() {
338 assert_size!(ErrorType, 1);
339 assert_size!(DefinedCondition, 1);
340 assert_size!(StanzaError, 104);
341 }
342
343 #[cfg(target_pointer_width = "64")]
344 #[test]
345 fn test_size() {
346 assert_size!(ErrorType, 1);
347 assert_size!(DefinedCondition, 1);
348 assert_size!(StanzaError, 208);
349 }
350
351 #[test]
352 fn test_simple() {
353 #[cfg(not(feature = "component"))]
354 let elem: Element = "<error xmlns='jabber:client' type='cancel'><undefined-condition xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error>".parse().unwrap();
355 #[cfg(feature = "component")]
356 let elem: Element = "<error xmlns='jabber:component:accept' type='cancel'><undefined-condition xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error>".parse().unwrap();
357 let error = StanzaError::try_from(elem).unwrap();
358 assert_eq!(error.type_, ErrorType::Cancel);
359 assert_eq!(
360 error.defined_condition,
361 DefinedCondition::UndefinedCondition
362 );
363 }
364
365 #[test]
366 fn test_invalid_type() {
367 #[cfg(not(feature = "component"))]
368 let elem: Element = "<error xmlns='jabber:client'/>".parse().unwrap();
369 #[cfg(feature = "component")]
370 let elem: Element = "<error xmlns='jabber:component:accept'/>".parse().unwrap();
371 let error = StanzaError::try_from(elem).unwrap_err();
372 let message = match error {
373 FromElementError::Invalid(Error::Other(string)) => string,
374 _ => panic!(),
375 };
376 assert_eq!(message, "Required attribute 'type' missing.");
377
378 #[cfg(not(feature = "component"))]
379 let elem: Element = "<error xmlns='jabber:client' type='coucou'/>"
380 .parse()
381 .unwrap();
382 #[cfg(feature = "component")]
383 let elem: Element = "<error xmlns='jabber:component:accept' type='coucou'/>"
384 .parse()
385 .unwrap();
386 let error = StanzaError::try_from(elem).unwrap_err();
387 let message = match error {
388 FromElementError::Invalid(Error::TextParseError(string)) => string,
389 _ => panic!(),
390 };
391 assert_eq!(message.to_string(), "Unknown value for 'type' attribute.");
392 }
393
394 #[test]
395 fn test_invalid_condition() {
396 #[cfg(not(feature = "component"))]
397 let elem: Element = "<error xmlns='jabber:client' type='cancel'/>"
398 .parse()
399 .unwrap();
400 #[cfg(feature = "component")]
401 let elem: Element = "<error xmlns='jabber:component:accept' type='cancel'/>"
402 .parse()
403 .unwrap();
404 let error = StanzaError::try_from(elem).unwrap_err();
405 let message = match error {
406 FromElementError::Invalid(Error::Other(string)) => string,
407 _ => panic!(),
408 };
409 assert_eq!(message, "Error must have a defined-condition.");
410 }
411
412 #[test]
413 fn test_error_code() {
414 let elem: Element = r#"<error code="501" type="cancel" xmlns='jabber:client'>
415 <feature-not-implemented xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
416 <text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>The feature requested is not implemented by the recipient or server and therefore cannot be processed.</text>
417</error>"#
418 .parse()
419 .unwrap();
420 let stanza_error = StanzaError::try_from(elem).unwrap();
421 assert_eq!(stanza_error.type_, ErrorType::Cancel);
422 }
423
424 #[test]
425 fn test_error_multiple_text() {
426 let elem: Element = r#"<error type="cancel" xmlns='jabber:client'>
427 <item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
428 <text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' xml:lang="fr">Nœud non trouvé</text>
429 <text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' xml:lang="en">Node not found</text>
430</error>"#
431 .parse()
432 .unwrap();
433 let stanza_error = StanzaError::try_from(elem).unwrap();
434 assert_eq!(stanza_error.type_, ErrorType::Cancel);
435 }
436
437 #[test]
438 fn test_gone_with_new_address() {
439 #[cfg(not(feature = "component"))]
440 let elem: Element = "<error xmlns='jabber:client' type='cancel'><gone xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>xmpp:room@muc.example.org?join</gone></error>"
441 .parse()
442 .unwrap();
443 #[cfg(feature = "component")]
444 let elem: Element = "<error xmlns='jabber:component:accept' type='cancel'><gone xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>xmpp:room@muc.example.org?join</gone></error>"
445 .parse()
446 .unwrap();
447 let error = StanzaError::try_from(elem).unwrap();
448 assert_eq!(error.type_, ErrorType::Cancel);
449 assert_eq!(error.defined_condition, DefinedCondition::Gone);
450 assert_eq!(
451 error.alternate_address,
452 Some("xmpp:room@muc.example.org?join".to_string())
453 );
454 }
455
456 #[test]
457 fn test_gone_without_new_address() {
458 #[cfg(not(feature = "component"))]
459 let elem: Element = "<error xmlns='jabber:client' type='cancel'><gone xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' /></error>"
460 .parse()
461 .unwrap();
462 #[cfg(feature = "component")]
463 let elem: Element = "<error xmlns='jabber:component:accept' type='cancel'><gone xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' /></error>"
464 .parse()
465 .unwrap();
466 let error = StanzaError::try_from(elem).unwrap();
467 assert_eq!(error.type_, ErrorType::Cancel);
468 assert_eq!(error.defined_condition, DefinedCondition::Gone);
469 assert_eq!(error.alternate_address, None);
470 }
471
472 #[test]
473 fn test_redirect_with_alternate_address() {
474 #[cfg(not(feature = "component"))]
475 let elem: Element = "<error xmlns='jabber:client' type='modify'><redirect xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>xmpp:characters@conference.example.org</redirect></error>"
476 .parse()
477 .unwrap();
478 #[cfg(feature = "component")]
479 let elem: Element = "<error xmlns='jabber:component:accept' type='modify'><redirect xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>xmpp:characters@conference.example.org</redirect></error>"
480 .parse()
481 .unwrap();
482 let error = StanzaError::try_from(elem).unwrap();
483 assert_eq!(error.type_, ErrorType::Modify);
484 assert_eq!(error.defined_condition, DefinedCondition::Redirect);
485 assert_eq!(
486 error.alternate_address,
487 Some("xmpp:characters@conference.example.org".to_string())
488 );
489 }
490
491 #[test]
492 fn test_redirect_without_alternate_address() {
493 #[cfg(not(feature = "component"))]
494 let elem: Element = "<error xmlns='jabber:client' type='modify'><redirect xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' /></error>"
495 .parse()
496 .unwrap();
497 #[cfg(feature = "component")]
498 let elem: Element = "<error xmlns='jabber:component:accept' type='modify'><redirect xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' /></error>"
499 .parse()
500 .unwrap();
501 let error = StanzaError::try_from(elem).unwrap();
502 assert_eq!(error.type_, ErrorType::Modify);
503 assert_eq!(error.defined_condition, DefinedCondition::Redirect);
504 assert_eq!(error.alternate_address, None);
505 }
506}