From 24658859753cbd094f9d585bc2f3fc04455d5396 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Mon, 1 May 2017 01:24:45 +0100 Subject: [PATCH] Add a stanza error parser and serialiser. --- src/lib.rs | 2 + src/ns.rs | 2 + src/stanza_error.rs | 275 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 279 insertions(+) create mode 100644 src/stanza_error.rs diff --git a/src/lib.rs b/src/lib.rs index 0a5eac0a..67a335fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,8 @@ pub mod message; pub mod presence; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub mod iq; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub mod stanza_error; /// RFC 6121: Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence pub mod body; diff --git a/src/ns.rs b/src/ns.rs index 495c2856..8e7c08b1 100644 --- a/src/ns.rs +++ b/src/ns.rs @@ -6,6 +6,8 @@ /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub const JABBER_CLIENT: &'static str = "jabber:client"; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub const XMPP_STANZAS: &'static str = "urn:ietf:params:xml:ns:xmpp-stanzas"; /// XEP-0004: Data Forms pub const DATA_FORMS: &'static str = "jabber:x:data"; diff --git a/src/stanza_error.rs b/src/stanza_error.rs new file mode 100644 index 00000000..0a1eb425 --- /dev/null +++ b/src/stanza_error.rs @@ -0,0 +1,275 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use std::str::FromStr; +use std::collections::BTreeMap; + +use minidom::Element; + +use error::Error; +use jid::Jid; +use ns; + +#[derive(Debug, Clone, PartialEq)] +pub enum ErrorType { + Auth, + Cancel, + Continue, + Modify, + Wait, +} + +impl FromStr for ErrorType { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "auth" => ErrorType::Auth, + "cancel" => ErrorType::Cancel, + "continue" => ErrorType::Continue, + "modify" => ErrorType::Modify, + "wait" => ErrorType::Wait, + + _ => return Err(Error::ParseError("Unknown error type.")), + }) + } +} + +impl From for String { + fn from(type_: ErrorType) -> String { + String::from(match type_ { + ErrorType::Auth => "auth", + ErrorType::Cancel => "cancel", + ErrorType::Continue => "continue", + ErrorType::Modify => "modify", + ErrorType::Wait => "wait", + }) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum DefinedCondition { + BadRequest, + Conflict, + FeatureNotImplemented, + Forbidden, + Gone, + InternalServerError, + ItemNotFound, + JidMalformed, + NotAcceptable, + NotAllowed, + NotAuthorized, + PolicyViolation, + RecipientUnavailable, + Redirect, + RegistrationRequired, + RemoteServerNotFound, + RemoteServerTimeout, + ResourceConstraint, + ServiceUnavailable, + SubscriptionRequired, + UndefinedCondition, + UnexpectedRequest, +} + +impl FromStr for DefinedCondition { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "bad-request" => DefinedCondition::BadRequest, + "conflict" => DefinedCondition::Conflict, + "feature-not-implemented" => DefinedCondition::FeatureNotImplemented, + "forbidden" => DefinedCondition::Forbidden, + "gone" => DefinedCondition::Gone, + "internal-server-error" => DefinedCondition::InternalServerError, + "item-not-found" => DefinedCondition::ItemNotFound, + "jid-malformed" => DefinedCondition::JidMalformed, + "not-acceptable" => DefinedCondition::NotAcceptable, + "not-allowed" => DefinedCondition::NotAllowed, + "not-authorized" => DefinedCondition::NotAuthorized, + "policy-violation" => DefinedCondition::PolicyViolation, + "recipient-unavailable" => DefinedCondition::RecipientUnavailable, + "redirect" => DefinedCondition::Redirect, + "registration-required" => DefinedCondition::RegistrationRequired, + "remote-server-not-found" => DefinedCondition::RemoteServerNotFound, + "remote-server-timeout" => DefinedCondition::RemoteServerTimeout, + "resource-constraint" => DefinedCondition::ResourceConstraint, + "service-unavailable" => DefinedCondition::ServiceUnavailable, + "subscription-required" => DefinedCondition::SubscriptionRequired, + "undefined-condition" => DefinedCondition::UndefinedCondition, + "unexpected-request" => DefinedCondition::UnexpectedRequest, + + _ => return Err(Error::ParseError("Unknown defined-condition.")), + }) + } +} + +impl From for String { + fn from(defined_condition: DefinedCondition) -> String { + String::from(match defined_condition { + DefinedCondition::BadRequest => "bad-request", + DefinedCondition::Conflict => "conflict", + DefinedCondition::FeatureNotImplemented => "feature-not-implemented", + DefinedCondition::Forbidden => "forbidden", + DefinedCondition::Gone => "gone", + DefinedCondition::InternalServerError => "internal-server-error", + DefinedCondition::ItemNotFound => "item-not-found", + DefinedCondition::JidMalformed => "jid-malformed", + DefinedCondition::NotAcceptable => "not-acceptable", + DefinedCondition::NotAllowed => "not-allowed", + DefinedCondition::NotAuthorized => "not-authorized", + DefinedCondition::PolicyViolation => "policy-violation", + DefinedCondition::RecipientUnavailable => "recipient-unavailable", + DefinedCondition::Redirect => "redirect", + DefinedCondition::RegistrationRequired => "registration-required", + DefinedCondition::RemoteServerNotFound => "remote-server-not-found", + DefinedCondition::RemoteServerTimeout => "remote-server-timeout", + DefinedCondition::ResourceConstraint => "resource-constraint", + DefinedCondition::ServiceUnavailable => "service-unavailable", + DefinedCondition::SubscriptionRequired => "subscription-required", + DefinedCondition::UndefinedCondition => "undefined-condition", + DefinedCondition::UnexpectedRequest => "unexpected-request", + }) + } +} + +pub type Lang = String; + +#[derive(Debug, Clone)] +pub struct StanzaError { + pub type_: ErrorType, + pub by: Option, + pub defined_condition: DefinedCondition, + pub texts: BTreeMap, + pub other: Option, +} + +pub fn parse_stanza_error(root: &Element) -> Result { + if !root.is("error", ns::JABBER_CLIENT) { + return Err(Error::ParseError("This is not an error element.")); + } + + let type_ = root.attr("type") + .ok_or(Error::ParseError("Error must have a 'type' attribute."))? + .parse()?; + let by = root.attr("by") + .and_then(|by| by.parse().ok()); + let mut defined_condition = None; + let mut texts = BTreeMap::new(); + let mut other = None; + + for child in root.children() { + if child.is("text", ns::XMPP_STANZAS) { + for _ in child.children() { + return Err(Error::ParseError("Unknown element in error text.")); + } + let lang = child.attr("xml:lang").unwrap_or("").to_owned(); + if let Some(_) = texts.insert(lang, child.text()) { + return Err(Error::ParseError("Text element present twice for the same xml:lang.")); + } + } else if child.ns() == Some(ns::XMPP_STANZAS) { + if defined_condition.is_some() { + return Err(Error::ParseError("Error must not have more than one defined-condition.")); + } + for _ in child.children() { + return Err(Error::ParseError("Unknown element in defined-condition.")); + } + let condition = DefinedCondition::from_str(child.name())?; + defined_condition = Some(condition); + } else { + if other.is_some() { + return Err(Error::ParseError("Error must not have more than one other element.")); + } + other = Some(child.clone()); + } + } + + if defined_condition.is_none() { + return Err(Error::ParseError("Error must have a defined-condition.")); + } + let defined_condition = defined_condition.unwrap(); + + Ok(StanzaError { + type_: type_, + by: by, + defined_condition: defined_condition, + texts: texts, + other: other, + }) +} + +pub fn serialise(error: &StanzaError) -> Element { + let mut root = Element::builder("error") + .ns(ns::JABBER_CLIENT) + .attr("type", String::from(error.type_.clone())) + .attr("by", match error.by { + Some(ref by) => Some(String::from(by.clone())), + None => None, + }) + .append(Element::builder(error.defined_condition.clone()) + .ns(ns::XMPP_STANZAS) + .build()) + .build(); + for (lang, text) in error.texts.clone() { + let elem = Element::builder("text") + .ns(ns::XMPP_STANZAS) + .attr("xml:lang", lang) + .append(text) + .build(); + root.append_child(elem); + } + if let Some(ref other) = error.other { + root.append_child(other.clone()); + } + root +} + +#[cfg(test)] +mod tests { + use minidom::Element; + use error::Error; + use stanza_error; + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + let error = stanza_error::parse_stanza_error(&elem).unwrap(); + assert_eq!(error.type_, stanza_error::ErrorType::Cancel); + assert_eq!(error.defined_condition, stanza_error::DefinedCondition::UndefinedCondition); + } + + #[test] + fn test_invalid_type() { + let elem: Element = "".parse().unwrap(); + let error = stanza_error::parse_stanza_error(&elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Error must have a 'type' attribute."); + + let elem: Element = "".parse().unwrap(); + let error = stanza_error::parse_stanza_error(&elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown error type."); + } + + #[test] + fn test_invalid_condition() { + let elem: Element = "".parse().unwrap(); + let error = stanza_error::parse_stanza_error(&elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Error must have a defined-condition."); + } +}