sasl: Actually implement <failure/> properly, with tests this time.

Emmanuel Gil Peyrot created

Change summary

src/sasl.rs | 220 ++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 201 insertions(+), 19 deletions(-)

Detailed changes

src/sasl.rs 🔗

@@ -4,50 +4,213 @@
 // 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 helpers::{Base64, TrimmedPlainText};
-
-generate_attribute!(Mechanism, "mechanism", {
-    Plain => "PLAIN",
-    ScramSha1 => "SCRAM-SHA-1",
-    ScramSha1Plus => "SCRAM-SHA-1-PLUS",
-    ScramSha256 => "SCRAM-SHA-256",
-    ScramSha256Plus => "SCRAM-SHA-256-PLUS",
-    Anonymous => "ANONYMOUS",
-});
-
-generate_element!(Auth, "auth", SASL,
+#![deny(missing_docs)]
+
+use std::collections::BTreeMap;
+
+use try_from::TryFrom;
+use minidom::Element;
+use error::Error;
+use ns;
+
+use helpers::Base64;
+
+generate_attribute!(
+    /// The list of available SASL mechanisms.
+    Mechanism, "mechanism", {
+        /// Uses no hashing mechanism and transmit the password in clear to the
+        /// server, using a single step.
+        Plain => "PLAIN",
+
+        /// Challenge-based mechanism using HMAC and SHA-1, allows both the
+        /// client and the server to avoid having to store the password in
+        /// clear.
+        ///
+        /// See https://tools.ietf.org/html/rfc5802
+        ScramSha1 => "SCRAM-SHA-1",
+
+        /// Same as [ScramSha1](#structfield.ScramSha1), with the addition of
+        /// channel binding.
+        ScramSha1Plus => "SCRAM-SHA-1-PLUS",
+
+        /// Same as [ScramSha1](#structfield.ScramSha1), but using SHA-256
+        /// instead of SHA-1 as the hash function.
+        ScramSha256 => "SCRAM-SHA-256",
+
+        /// Same as [ScramSha256](#structfield.ScramSha256), with the addition
+        /// of channel binding.
+        ScramSha256Plus => "SCRAM-SHA-256-PLUS",
+
+        /// Creates a temporary JID on login, which will be destroyed on
+        /// disconnect.
+        Anonymous => "ANONYMOUS",
+    }
+);
+
+generate_element!(
+    /// The first step of the SASL process, selecting the mechanism and sending
+    /// the first part of the handshake.
+    Auth, "auth", SASL,
     attributes: [
+        /// The mechanism used.
         mechanism: Mechanism = "mechanism" => required
     ],
     text: (
+        /// The content of the handshake.
         data: Base64<Vec<u8>>
     )
 );
 
-generate_element!(Challenge, "challenge", SASL,
+generate_element!(
+    /// In case the mechanism selected at the [auth](struct.Auth.html) step
+    /// requires a second step, the server sends this element with additional
+    /// data.
+    Challenge, "challenge", SASL,
     text: (
+        /// The challenge data.
         data: Base64<Vec<u8>>
     )
 );
 
-generate_element!(Response, "response", SASL,
+generate_element!(
+    /// In case the mechanism selected at the [auth](struct.Auth.html) step
+    /// requires a second step, this contains the client’s response to the
+    /// server’s [challenge](struct.Challenge.html).
+    Response, "response", SASL,
     text: (
+        /// The response data.
         data: Base64<Vec<u8>>
     )
 );
 
-generate_element!(Success, "success", SASL,
+generate_empty_element!(
+    /// Sent by the client at any point after [auth](struct.Auth.html) if it
+    /// wants to cancel the current authentication process.
+    Abort, "abort", SASL
+);
+
+generate_element!(
+    /// Sent by the server on SASL success.
+    Success, "success", SASL,
     text: (
+        /// Possible data sent on success.
         data: Base64<Vec<u8>>
     )
 );
 
-generate_element!(Failure, "failure", SASL,
-    text: (
-        data: TrimmedPlainText<String>
-    )
+generate_element_enum!(
+    /// List of possible failure conditions for SASL.
+    DefinedCondition, "defined-condition", SASL, {
+        /// The client aborted the authentication with
+        /// [abort](struct.Abort.html).
+        Aborted => "aborted",
+
+        /// The account the client is trying to authenticate against has been
+        /// disabled.
+        AccountDisabled => "account-disabled",
+
+        /// The credentials for this account have expired.
+        CredentialsExpired => "credentials-expired",
+
+        /// You must enable StartTLS or use direct TLS before using this
+        /// authentication mechanism.
+        EncryptionRequired => "encryption-required",
+
+        /// The base64 data sent by the client is invalid.
+        IncorrectEncoding => "incorrect-encoding",
+
+        /// The authzid provided by the client is invalid.
+        InvalidAuthzid => "invalid-authzid",
+
+        /// The client tried to use an invalid mechanism, or none.
+        InvalidMechanism => "invalid-mechanism",
+
+        /// The client sent a bad request.
+        MalformedRequest => "malformed-request",
+
+        /// The mechanism selected is weaker than what the server allows.
+        MechanismTooWeak => "mechanism-too-weak",
+
+        /// The credentials provided are invalid.
+        NotAuthorized => "not-authorized",
+
+        /// The server encountered an issue which may be fixed later, the
+        /// client should retry at some point.
+        TemporaryAuthFailure => "temporary-auth-failure",
+    }
 );
 
+type Lang = String;
+
+/// Sent by the server on SASL failure.
+#[derive(Debug, Clone)]
+pub struct Failure {
+    /// One of the allowed defined-conditions for SASL.
+    pub defined_condition: DefinedCondition,
+
+    /// A human-readable explanation for the failure.
+    pub texts: BTreeMap<Lang, String>,
+}
+
+impl TryFrom<Element> for Failure {
+    type Err = Error;
+
+    fn try_from(root: Element) -> Result<Failure, Error> {
+        check_self!(root, "failure", SASL);
+        check_no_attributes!(root, "failure");
+
+        let mut defined_condition = None;
+        let mut texts = BTreeMap::new();
+
+        for child in root.children() {
+            if child.is("text", ns::SASL) {
+                check_no_unknown_attributes!(child, "text", ["xml:lang"]);
+                check_no_children!(child, "text");
+                let lang = get_attr!(child, "xml:lang", default);
+                if texts.insert(lang, child.text()).is_some() {
+                    return Err(Error::ParseError("Text element present twice for the same xml:lang in failure element."));
+                }
+            } else if child.has_ns(ns::SASL) {
+                if defined_condition.is_some() {
+                    return Err(Error::ParseError("Failure must not have more than one defined-condition."));
+                }
+                check_no_attributes!(child, "defined-condition");
+                check_no_children!(child, "defined-condition");
+                let condition = match DefinedCondition::try_from(child.clone()) {
+                    Ok(condition) => condition,
+                    // TODO: do we really want to eat this error?
+                    Err(_) => DefinedCondition::NotAuthorized,
+                };
+                defined_condition = Some(condition);
+            } else {
+                return Err(Error::ParseError("Unknown element in Failure."));
+            }
+        }
+        let defined_condition = defined_condition.ok_or(Error::ParseError("Failure must have a defined-condition."))?;
+
+        Ok(Failure {
+            defined_condition: defined_condition,
+            texts: texts,
+        })
+    }
+}
+
+impl From<Failure> for Element {
+    fn from(failure: Failure) -> Element {
+        Element::builder("failure")
+                .ns(ns::SASL)
+                .append(failure.defined_condition)
+                .append(failure.texts.into_iter().map(|(lang, text)| {
+                     Element::builder("text")
+                             .ns(ns::SASL)
+                             .attr("xml:lang", lang)
+                             .append(text)
+                             .build()
+                 }).collect::<Vec<_>>())
+                .build()
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -61,4 +224,23 @@ mod tests {
         assert_eq!(auth.mechanism, Mechanism::Plain);
         assert!(auth.data.is_empty());
     }
+
+    #[test]
+    fn section_6_5_1() {
+        let elem: Element = "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><aborted/></failure>".parse().unwrap();
+        let failure = Failure::try_from(elem).unwrap();
+        assert_eq!(failure.defined_condition, DefinedCondition::Aborted);
+        assert!(failure.texts.is_empty());
+    }
+
+    #[test]
+    fn section_6_5_2() {
+        let elem: Element = "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
+            <account-disabled/>
+            <text xml:lang='en'>Call 212-555-1212 for assistance.</text>
+        </failure>".parse().unwrap();
+        let failure = Failure::try_from(elem).unwrap();
+        assert_eq!(failure.defined_condition, DefinedCondition::AccountDisabled);
+        assert_eq!(failure.texts["en"], String::from("Call 212-555-1212 for assistance."));
+    }
 }