xmpp_parsers/stream_error.rs
1// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
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 alloc::collections::BTreeMap;
8use core::{error::Error, fmt};
9
10use minidom::Element;
11use xso::{AsXml, FromXml};
12
13use crate::{message::Lang, ns};
14
15/// Enumeration of all stream error conditions as defined in [RFC 6120].
16///
17/// All variant documentation is directly quoted from [RFC 6120].
18///
19/// [RFC 6120]: https://datatracker.ietf.org/doc/html/rfc6120#section-4.9.3
20#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
21#[xml(namespace = ns::XMPP_STREAMS)]
22pub enum DefinedCondition {
23 /// The entity has sent XML that cannot be processed.
24 ///
25 /// This error can be used instead of the more specific XML-related
26 /// errors, such as `<bad-namespace-prefix/>`, `<invalid-xml/>`,
27 /// `<not-well-formed/>`, `<restricted-xml/>`, and
28 /// `<unsupported-encoding/>`. However, the more specific errors are
29 /// RECOMMENDED.
30 #[xml(name = "bad-format")]
31 BadFormat,
32
33 /// The entity has sent a namespace prefix that is unsupported, or has
34 /// sent no namespace prefix on an element that needs such a prefix (see
35 /// [Section 11.2](https://datatracker.ietf.org/doc/html/rfc6120#section-11.2)).
36 #[xml(name = "bad-namespace-prefix")]
37 BadNamespacePrefix,
38
39 /// The server either (1) is closing the existing stream for this entity
40 /// because a new stream has been initiated that conflicts with the
41 /// existing stream, or (2) is refusing a new stream for this entity
42 /// because allowing the new stream would conflict with an existing
43 /// stream (e.g., because the server allows only a certain number of
44 /// connections from the same IP address or allows only one server-to-
45 /// server stream for a given domain pair as a way of helping to ensure
46 /// in-order processing as described under
47 /// [Section 10.1](https://datatracker.ietf.org/doc/html/rfc6120#section-10.1)).
48 ///
49 /// If a client receives a `<conflict/>` stream error, during the resource
50 /// binding aspect of its reconnection attempt it MUST NOT blindly request
51 /// the resourcepart it used during the former session but instead MUST
52 /// choose a different resourcepart; details are provided under
53 /// [Section 7](https://datatracker.ietf.org/doc/html/rfc6120#section-7).
54 #[xml(name = "conflict")]
55 Conflict,
56
57 /// One party is closing the stream because it has reason to believe that
58 /// the other party has permanently lost the ability to communicate over
59 /// the stream. The lack of ability to communicate can be discovered
60 /// using various methods, such as whitespace keepalives as specified
61 /// under
62 /// [Section 4.4](https://datatracker.ietf.org/doc/html/rfc6120#section-4.4),
63 /// XMPP-level pings as defined in
64 /// [XEP-0199](https://xmpp.org/extensions/xep-0199.html), and
65 /// XMPP Stream Management as defined in
66 /// [XEP-0198](https://xmpp.org/extensions/xep-0198.html).
67 ///
68 /// Interoperability Note: RFC 3920 specified that the
69 /// `<connection-timeout/>` stream error is to be used if the peer has not
70 /// generated any traffic over the stream for some period of time.
71 /// That behavior is no longer recommended; instead, the error SHOULD be
72 /// used only if the connected client or peer server has not responded to
73 /// data sent over the stream.
74 #[xml(name = "connection-timeout")]
75 ConnectionTimeout,
76
77 /// The value of the 'to' attribute provided in the initial stream header
78 /// corresponds to an FQDN that is no longer serviced by the receiving
79 /// entity.
80 #[xml(name = "host-gone")]
81 HostGone,
82
83 /// The value of the 'to' attribute provided in the initial stream header
84 /// does not correspond to an FQDN that is serviced by the receiving
85 /// entity.
86 #[xml(name = "host-unknown")]
87 HostUnknown,
88
89 /// A stanza sent between two servers lacks a 'to' or 'from' attribute,
90 /// the 'from' or 'to' attribute has no value, or the value violates the
91 /// rules for XMPP addresses
92 /// (see [RFC 6122](https://datatracker.ietf.org/doc/html/rfc6122)).
93 #[xml(name = "improper-addressing")]
94 ImproperAddressing,
95
96 /// The server has experienced a misconfiguration or other internal error
97 /// that prevents it from servicing the stream.
98 #[xml(name = "internal-server-error")]
99 InternalServerError,
100
101 /// The data provided in a 'from' attribute does not match an authorized
102 /// JID or validated domain as negotiated (1) between two servers using
103 /// SASL or Server Dialback, or (2) between a client and a server via
104 /// SASL authentication and resource binding.
105 #[xml(name = "invalid-from")]
106 InvalidFrom,
107
108 /// The stream namespace name is something other than
109 /// `http://etherx.jabber.org/streams` (see
110 /// [Section 11.2](https://datatracker.ietf.org/doc/html/rfc6120#section-11.2))
111 /// or the content namespace declared as the default namespace is not
112 /// supported (e.g., something other than `jabber:client` or
113 /// `jabber:server`).
114 #[xml(name = "invalid-namespace")]
115 InvalidNamespace,
116
117 /// The entity has sent invalid XML over the stream to a server that
118 /// performs validation (see
119 /// [Section 11.4](https://datatracker.ietf.org/doc/html/rfc6120#section-11.4)).
120 #[xml(name = "invalid-xml")]
121 InvalidXml,
122
123 /// The entity has attempted to send XML stanzas or other outbound data
124 /// before the stream has been authenticated, or otherwise is not
125 /// authorized to perform an action related to stream negotiation; the
126 /// receiving entity MUST NOT process the offending data before sending
127 /// the stream error.
128 #[xml(name = "not-authorized")]
129 NotAuthorized,
130
131 /// The initiating entity has sent XML that violates the well-formedness
132 /// rules of [XML](https://www.w3.org/TR/REC-xml/) or
133 /// [XML-NAMES](https://www.w3.org/TR/REC-xml-names/).
134 #[xml(name = "not-well-formed")]
135 NotWellFormed,
136
137 /// The entity has violated some local service policy (e.g., a stanza
138 /// exceeds a configured size limit); the server MAY choose to specify
139 /// the policy in the `<text/>` element or in an application-specific
140 /// condition element.
141 #[xml(name = "policy-violation")]
142 PolicyViolation,
143
144 /// The server is unable to properly connect to a remote entity that is
145 /// needed for authentication or authorization (e.g., in certain
146 /// scenarios related to Server Dialback
147 /// [XEP-0220](https://xmpp.org/extensions/xep-0220.html)); this condition
148 /// is not to be used when the cause of the error is within the
149 /// administrative domain of the XMPP service provider, in which case the
150 /// `<internal-server-error/>` condition is more appropriate.
151 #[xml(name = "remote-connection-failed")]
152 RemoteConnectionFailed,
153
154 /// The server is closing the stream because it has new (typically
155 /// security-critical) features to offer, because the keys or
156 /// certificates used to establish a secure context for the stream have
157 /// expired or have been revoked during the life of the stream
158 /// ([Section 13.7.2.3](https://datatracker.ietf.org/doc/html/rfc6120#section-13.7.2.3)),
159 /// because the TLS sequence number has wrapped
160 /// ([Section 5.3.5](https://datatracker.ietf.org/doc/html/rfc6120#section-5.3.5)),
161 /// etc. The reset applies to the stream and to any security context
162 /// established for that stream (e.g., via TLS and SASL), which means that
163 /// encryption and authentication need to be negotiated again for the new
164 /// stream (e.g., TLS session resumption cannot be used).
165 #[xml(name = "reset")]
166 Reset,
167
168 /// The server lacks the system resources necessary to service the stream.
169 #[xml(name = "resource-constraint")]
170 ResourceConstraint,
171
172 /// The entity has attempted to send restricted XML features such as a
173 /// comment, processing instruction, DTD subset, or XML entity reference
174 /// (see
175 /// [Section 11.1](https://datatracker.ietf.org/doc/html/rfc6120#section-11.1)).
176 #[xml(name = "restricted-xml")]
177 RestrictedXml,
178
179 /// The server will not provide service to the initiating entity but is
180 /// redirecting traffic to another host under the administrative control
181 /// of the same service provider. The XML character data of the
182 /// `<see-other-host/>` element returned by the server MUST specify the
183 /// alternate FQDN or IP address at which to connect, which MUST be a
184 /// valid domainpart or a domainpart plus port number (separated by the
185 /// ':' character in the form "domainpart:port"). If the domainpart is
186 /// the same as the source domain, derived domain, or resolved IPv4 or
187 /// IPv6 address to which the initiating entity originally connected
188 /// (differing only by the port number), then the initiating entity
189 /// SHOULD simply attempt to reconnect at that address. (The format of
190 /// an IPv6 address MUST follow
191 /// [IPv6-ADDR](https://datatracker.ietf.org/doc/html/rfc6120#ref-IPv6-ADDR),
192 /// which includes the enclosing the IPv6 address in square brackets
193 /// '[' and ']' as originally defined by
194 /// [URI](https://datatracker.ietf.org/doc/html/rfc6120#ref-URI).
195 /// ) Otherwise, the initiating entity MUST resolve the FQDN
196 /// specified in the `<see-other-host/>` element as described under
197 /// [Section 3.2](https://datatracker.ietf.org/doc/html/rfc6120#section-3.2).
198 ///
199 /// When negotiating a stream with the host to which it has been
200 /// redirected, the initiating entity MUST apply the same policies it
201 /// would have applied to the original connection attempt (e.g., a policy
202 /// requiring TLS), MUST specify the same 'to' address on the initial
203 /// stream header, and MUST verify the identity of the new host using the
204 /// same reference identifier(s) it would have used for the original
205 /// connection attempt (in accordance with
206 /// [TLS-CERTS](https://datatracker.ietf.org/doc/html/rfc6120#ref-TLS-CERTS)).
207 /// Even if the receiving entity returns a `<see-other-host/>` error
208 /// before the confidentiality and integrity of the stream have been
209 /// established (thus introducing the possibility of a denial-of-service
210 /// attack), the fact that the initiating entity needs to verify the
211 /// identity of the XMPP service based on the same reference identifiers
212 /// implies that the initiating entity will not connect to a malicious
213 /// entity. To reduce the possibility of a denial-of-service attack, (a)
214 /// the receiving entity SHOULD NOT close the stream with a
215 /// `<see-other-host/>` stream error until after the confidentiality and
216 /// integrity of the stream have been protected via TLS or an equivalent
217 /// security layer (such as the SASL GSSAPI mechanism), and (b) the
218 /// receiving entity MAY have a policy of following redirects only if it
219 /// has authenticated the receiving entity. In addition, the initiating
220 /// entity SHOULD abort the connection attempt after a certain number of
221 /// successive redirects (e.g., at least 2 but no more than 5).
222 #[xml(name = "see-other-host")]
223 SeeOtherHost(#[xml(text)] String),
224
225 /// The server is being shut down and all active streams are being closed.
226 #[xml(name = "system-shutdown")]
227 SystemShutdown,
228
229 /// The error condition is not one of those defined by the other
230 /// conditions in this list; this error condition SHOULD NOT be used
231 /// except in conjunction with an application-specific condition.
232 #[xml(name = "undefined-condition")]
233 UndefinedCondition,
234
235 /// The initiating entity has encoded the stream in an encoding that is
236 /// not supported by the server (see
237 /// [Section 11.6](https://datatracker.ietf.org/doc/html/rfc6120#section-11.6))
238 /// or has otherwise improperly encoded the stream (e.g., by violating the
239 /// rules of the
240 /// [UTF-8](https://datatracker.ietf.org/doc/html/rfc6120#ref-UTF-8)
241 /// encoding).
242 #[xml(name = "unsupported-encoding")]
243 UnsupportedEncoding,
244
245 /// The receiving entity has advertised a mandatory-to-negotiate stream
246 /// feature that the initiating entity does not support, and has offered
247 /// no other mandatory-to-negotiate feature alongside the unsupported
248 /// feature.
249 #[xml(name = "unsupported-feature")]
250 UnsupportedFeature,
251
252 /// The initiating entity has sent a first-level child of the stream that
253 /// is not supported by the server, either because the receiving entity
254 /// does not understand the namespace or because the receiving entity
255 /// does not understand the element name for the applicable namespace
256 /// (which might be the content namespace declared as the default
257 /// namespace).
258 #[xml(name = "unsupported-stanza-type")]
259 UnsupportedStanzaType,
260
261 /// The 'version' attribute provided by the initiating entity in the
262 /// stream header specifies a version of XMPP that is not supported by
263 /// the server.
264 #[xml(name = "unsupported-version")]
265 UnsupportedVersion,
266}
267
268impl fmt::Display for DefinedCondition {
269 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
270 let s = match self {
271 Self::BadFormat => "bad-format",
272 Self::BadNamespacePrefix => "bad-namespace-prefix",
273 Self::Conflict => "conflict",
274 Self::ConnectionTimeout => "connection-timeout",
275 Self::HostGone => "host-gone",
276 Self::HostUnknown => "host-unknown",
277 Self::ImproperAddressing => "improper-addressing",
278 Self::InternalServerError => "internal-server-error",
279 Self::InvalidFrom => "invalid-from",
280 Self::InvalidNamespace => "invalid-namespace",
281 Self::InvalidXml => "invalid-xml",
282 Self::NotAuthorized => "not-authorized",
283 Self::NotWellFormed => "not-well-formed",
284 Self::PolicyViolation => "policy-violation",
285 Self::RemoteConnectionFailed => "remote-connection-failed",
286 Self::Reset => "reset",
287 Self::ResourceConstraint => "resource-constraint",
288 Self::RestrictedXml => "restricted-xml",
289 Self::SeeOtherHost(ref host) => return write!(f, "see-other-host: {}", host),
290 Self::SystemShutdown => "system-shutdown",
291 Self::UndefinedCondition => "undefined-condition",
292 Self::UnsupportedEncoding => "unsupported-encoding",
293 Self::UnsupportedFeature => "unsupported-feature",
294 Self::UnsupportedStanzaType => "unsupported-stanza-type",
295 Self::UnsupportedVersion => "unsupported-version",
296 };
297 f.write_str(s)
298 }
299}
300
301/// Stream error as specified in RFC 6120.
302#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
303#[xml(namespace = ns::STREAM, name = "error")]
304pub struct StreamError {
305 /// The enumerated error condition which triggered this stream error.
306 #[xml(child)]
307 pub condition: DefinedCondition,
308
309 /// Optional error text
310 #[xml(extract(n = .., name = "text", namespace = ns::XMPP_STREAMS, fields(
311 lang(type_ = Lang, default),
312 text(type_ = String),
313 )))]
314 pub texts: BTreeMap<Lang, String>,
315
316 /// Optional application-defined element which refines the specified
317 /// [`Self::condition`].
318 // TODO: use n = 1 once we have it.
319 #[xml(element(n = ..))]
320 pub application_specific: Vec<Element>,
321}
322
323impl fmt::Display for StreamError {
324 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
325 <DefinedCondition as fmt::Display>::fmt(&self.condition, f)?;
326 if let Some((_, text)) = self.get_best_text(vec!["en"]) {
327 write!(f, " ({:?})", text)?
328 }
329 if let Some(cond) = self.application_specific.first() {
330 f.write_str(&String::from(cond))?;
331 }
332 Ok(())
333 }
334}
335
336impl StreamError {
337 /// Create a new StreamError with condition, text, and language
338 pub fn new<S: Into<String>, L: Into<Lang>>(
339 condition: DefinedCondition,
340 lang: L,
341 text: S,
342 ) -> Self {
343 let mut texts = BTreeMap::new();
344 texts.insert(lang.into(), text.into());
345 Self {
346 condition,
347 texts,
348 application_specific: Vec::new(),
349 }
350 }
351
352 /// Add a text element with the specified language
353 pub fn add_text<L: Into<Lang>, S: Into<String>>(mut self, lang: L, text: S) -> Self {
354 self.texts.insert(lang.into(), text.into());
355 self
356 }
357
358 /// Append application specific element(s)
359 pub fn with_application_specific(mut self, application_specific: Vec<Element>) -> Self {
360 self.application_specific = application_specific;
361 self
362 }
363
364 /// Get the best matching text from a list of preferred languages.
365 ///
366 /// This follows the same logic as Message::get_best_body:
367 /// 1. First tries to find a match from the preferred languages list
368 /// 2. Falls back to empty language ("") if available
369 /// 3. Returns the first entry if no matches found
370 ///
371 /// Returns None if no text elements exist.
372 pub fn get_best_text(&self, preferred_langs: Vec<&str>) -> Option<(Lang, &String)> {
373 Self::get_best(&self.texts, preferred_langs)
374 }
375
376 /// Cloned variant of [`StreamError::get_best_text`]
377 pub fn get_best_text_cloned(&self, preferred_langs: Vec<&str>) -> Option<(Lang, String)> {
378 Self::get_best_cloned(&self.texts, preferred_langs)
379 }
380
381 // Private helper methods matching Message's pattern
382 fn get_best<'a, T>(
383 map: &'a BTreeMap<Lang, T>,
384 preferred_langs: Vec<&str>,
385 ) -> Option<(Lang, &'a T)> {
386 if map.is_empty() {
387 return None;
388 }
389 for lang in preferred_langs {
390 if let Some(value) = map.get(lang) {
391 return Some((Lang::from(lang), value));
392 }
393 }
394 if let Some(value) = map.get("") {
395 return Some((Lang::new(), value));
396 }
397 map.iter().map(|(lang, value)| (lang.clone(), value)).next()
398 }
399
400 fn get_best_cloned<T: ToOwned<Owned = T>>(
401 map: &BTreeMap<Lang, T>,
402 preferred_langs: Vec<&str>,
403 ) -> Option<(Lang, T)> {
404 if let Some((lang, item)) = Self::get_best::<T>(map, preferred_langs) {
405 Some((lang, item.to_owned()))
406 } else {
407 None
408 }
409 }
410
411 /// Check if the error has any text elements
412 pub fn has_text(&self) -> bool {
413 !self.texts.is_empty()
414 }
415}
416
417/// Wrapper around [`StreamError`] which implements [`core::error::Error`]
418/// with an appropriate error message.
419#[derive(FromXml, AsXml, Debug)]
420#[xml(transparent)]
421pub struct ReceivedStreamError(pub StreamError);
422
423impl fmt::Display for ReceivedStreamError {
424 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
425 write!(f, "received stream error: {}", self.0)
426 }
427}
428
429impl Error for ReceivedStreamError {}
430
431/// Wrapper around [`StreamError`] which implements [`core::error::Error`]
432/// with an appropriate error message.
433#[derive(FromXml, AsXml, Debug)]
434#[xml(transparent)]
435pub struct SentStreamError(pub StreamError);
436
437impl fmt::Display for SentStreamError {
438 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
439 write!(f, "sent stream error: {}", self.0)
440 }
441}
442
443impl Error for SentStreamError {}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448
449 #[test]
450 fn parses_condition_from_prosody() {
451 let doc = "<undefined-condition xmlns='urn:ietf:params:xml:ns:xmpp-streams'/>";
452 let err: DefinedCondition = xso::from_bytes(doc.as_bytes()).unwrap();
453 assert_eq!(err, DefinedCondition::UndefinedCondition);
454 }
455
456 #[test]
457 fn parses_stream_error_from_prosody() {
458 let doc = "<stream:error xmlns:stream='http://etherx.jabber.org/streams'><undefined-condition xmlns='urn:ietf:params:xml:ns:xmpp-streams'/><text xmlns='urn:ietf:params:xml:ns:xmpp-streams'>No stream features to proceed with</text></stream:error>";
459 let err: StreamError = xso::from_bytes(doc.as_bytes()).unwrap();
460 assert_eq!(err.condition, DefinedCondition::UndefinedCondition);
461 }
462
463 #[test]
464 fn test_stream_error_with_text() {
465 let doc = br#"<stream:error xmlns:stream='http://etherx.jabber.org/streams'>
466 <system-shutdown xmlns='urn:ietf:params:xml:ns:xmpp-streams'/>
467 <text xmlns='urn:ietf:params:xml:ns:xmpp-streams'>Server is shutting down for maintenance.</text>
468 </stream:error>"#;
469
470 let err: StreamError = xso::from_bytes(doc).unwrap();
471 assert_eq!(err.condition, DefinedCondition::SystemShutdown);
472 assert!(err.has_text());
473
474 let (lang, text) = err.get_best_text(vec![]).unwrap();
475 assert_eq!(text, "Server is shutting down for maintenance.");
476 assert_eq!(lang, "");
477 }
478
479 #[test]
480 fn test_stream_error_with_multiple_languages() {
481 let doc = br#"<stream:error xmlns:stream='http://etherx.jabber.org/streams'>
482 <policy-violation xmlns='urn:ietf:params:xml:ns:xmpp-streams'/>
483 <text xmlns='urn:ietf:params:xml:ns:xmpp-streams' xml:lang='en'>Message too large</text>
484 <text xmlns='urn:ietf:params:xml:ns:xmpp-streams' xml:lang='de'>Nachricht zu lang</text>
485 </stream:error>"#;
486
487 let err: StreamError = xso::from_bytes(doc).unwrap();
488 assert_eq!(err.condition, DefinedCondition::PolicyViolation);
489
490 // Test German preference
491 let (lang, text) = err.get_best_text(vec!["de"]).unwrap();
492 assert_eq!(lang, "de");
493 assert_eq!(text, "Nachricht zu lang");
494
495 // Test English preference
496 let (lang, text) = err.get_best_text(vec!["en"]).unwrap();
497 assert_eq!(lang, "en");
498 assert_eq!(text, "Message too large");
499
500 // Test cloned variant
501 let (lang, text) = err.get_best_text_cloned(vec!["en"]).unwrap();
502 assert_eq!(lang, "en");
503 assert_eq!(text, "Message too large");
504 }
505
506 #[test]
507 fn test_stream_error_constructors() {
508 let err = StreamError::new(DefinedCondition::Reset, "en", "Connection reset");
509 let (lang, text) = err.get_best_text(vec!["en"]).unwrap();
510 assert_eq!(lang, "en");
511 assert_eq!(text, "Connection reset");
512
513 let err = err.add_text("de", "Verbindung zurückgesetzt");
514 assert_eq!(err.texts.len(), 2);
515 }
516}