stanza_error.rs

  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}