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