diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000000000000000000000000000000000000..a9d37c560c6ab8d4afbf47eda643e8c42e857716 --- /dev/null +++ b/.hgignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index 27622a8347733ca847be5a8b219f3a19a966b587..120ee5ce4432a1f2a3c5c50c2b8299b6500f0f32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,21 +1,15 @@ -[package] -name = "xmpp" -version = "0.3.0" -authors = [ - "Emmanuel Gil Peyrot ", - "Maxime “pep” Buquet ", +[workspace] +members = [ # alphabetically sorted + "jid-rs", + "minidom-rs", + "tokio-xmpp", + "xmpp-parsers", + "xmpp-rs", ] -description = "High-level XMPP library" -homepage = "https://gitlab.com/linkmauve/xmpp-rs" -repository = "https://gitlab.com/linkmauve/xmpp-rs" -keywords = ["xmpp", "jabber", "chat", "messaging", "bot"] -categories = ["network-programming"] -license = "MPL-2.0" -edition = "2018" -[dependencies] -tokio-xmpp = "1.0.1" -xmpp-parsers = "0.15" -futures = "0.1" -tokio = "0.1" -log = "0.4" +[patch.crates-io] +jid = { path = "jid-rs" } +minidom = { path = "minidom-rs" } +tokio-xmpp = { path = "tokio-xmpp" } +xmpp-parsers = { path = "xmpp-parsers" } +xmpp = { path = "xmpp-rs" } diff --git a/jid-rs/.gitlab-ci.yml b/jid-rs/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..6996a5520f6bae6c976cf77023259e37e247f9c8 --- /dev/null +++ b/jid-rs/.gitlab-ci.yml @@ -0,0 +1,62 @@ +stages: + - build + - test + +variables: + FEATURES: "" + RUST_BACKTRACE: "full" + +.stable: + image: rust:latest + cache: + key: stable + paths: + - target/ + +.nightly: + image: rustlang/rust:nightly + cache: + key: nightly + paths: + - target/ + +.build: + stage: build + script: + - cargo build --verbose --no-default-features --features=$FEATURES + +.test: + stage: test + script: + - cargo test --lib --verbose --no-default-features --features=$FEATURES + +rust-latest-build: + extends: + - .build + - .stable + +rust-nightly-build: + extends: + - .build + - .nightly + + +rust-latest-test: + extends: + - .test + - .stable + +rust-nightly-test: + extends: + - .test + - .nightly + +rust-latest-build with features=minidom: + extends: rust-latest-build + variables: + FEATURES: "minidom" + +rust-latest-test with features=minidom: + extends: rust-latest-test + variables: + FEATURES: "minidom" diff --git a/jid-rs/CHANGELOG.md b/jid-rs/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..7d82b5a77f6979243980efca911330da4f1958a5 --- /dev/null +++ b/jid-rs/CHANGELOG.md @@ -0,0 +1,62 @@ +Version 0.8, released 2019-10-15: + * Updates + - CI: Split jobs, add tests, and caching + * Breaking + - 0.7.1 was actually a breaking release + +Version 0.7.2, released 2019-09-13: + * Updates + - Impl Error for JidParseError again, it got removed due to the failure removal but is still wanted. + +Version 0.7.1, released 2019-09-06: + * Updates + - Remove failure dependency, to keep compilation times in check + - Impl Display for Jid + +Version 0.7.0, released 2019-07-26: + * Breaking + - Update minidom dependency to 0.11 + +Version 0.6.2, released 2019-07-20: + * Updates + - Implement From and From for Jid + - Add node and domain getters on Jid + +Version 0.6.1, released 2019-06-10: + * Updates + - Change the license from LGPLv3 to MPL-2.0. + +Version 0.6.0, released 2019-06-10: + * Updates + - Jid is now an enum, with two variants, Bare(BareJid) and Full(FullJid). + - BareJid and FullJid are two specialised variants of a JID. + +Version 0.5.3, released 2019-01-16: + * Updates + - Link Mauve bumped the minidom dependency version. + - Use Edition 2018, putting the baseline rustc version to 1.31. + - Run cargo-fmt on the code, to lower the barrier of entry. + +Version 0.5.2, released 2018-07-31: + * Updates + - Astro bumped the minidom dependency version. + - Updated the changelog to reflect that 0.5.1 was never actually released. + +Version 0.5.1, "released" 2018-03-01: + * Updates + - Link Mauve implemented failure::Fail on JidParseError. + - Link Mauve simplified the code a bit. + +Version 0.5.0, released 2018-02-18: + * Updates + - Link Mauve has updated the optional `minidom` dependency. + - Link Mauve has added tests for invalid JIDs, which adds more error cases. + +Version 0.4.0, released 2017-12-27: + * Updates + - Maxime Buquet has updated the optional `minidom` dependency. + - The repository has been transferred to xmpp-rs/jid-rs. + +Version 0.3.1, released 2017-10-31: + * Additions + - Link Mauve added a minidom::IntoElements implementation on Jid behind the "minidom" feature. ( https://gitlab.com/lumi/jid-rs/merge_requests/9 ) diff --git a/jid-rs/Cargo.toml b/jid-rs/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..4f4894d07d13c7bd39a35e1f58b69cc57160f0d6 --- /dev/null +++ b/jid-rs/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "jid" +version = "0.8.0" +authors = [ + "lumi ", + "Emmanuel Gil Peyrot ", + "Maxime “pep” Buquet ", +] +description = "A crate which provides a Jid struct for Jabber IDs." +homepage = "https://gitlab.com/xmpp-rs/jid-rs" +repository = "https://gitlab.com/xmpp-rs/jid-rs" +documentation = "https://docs.rs/jid" +readme = "README.md" +keywords = ["xmpp", "jid"] +license = "MPL-2.0" +edition = "2018" + +[badges] +gitlab = { repository = "xmpp-rs/jid-rs" } + +[dependencies] +minidom = { version = "0.11", optional = true } diff --git a/LICENSE b/jid-rs/LICENSE similarity index 100% rename from LICENSE rename to jid-rs/LICENSE diff --git a/jid-rs/README.md b/jid-rs/README.md new file mode 100644 index 0000000000000000000000000000000000000000..905c3c3a70d45598297eb2bac8450c074ce78c1d --- /dev/null +++ b/jid-rs/README.md @@ -0,0 +1,18 @@ +jid-rs +====== + +What's this? +------------ + +A crate which provides a struct Jid for Jabber IDs. It's used in xmpp-rs but other XMPP libraries +can of course use this. + +What license is it under? +------------------------- + +MPL-2.0 or later, see the `LICENSE` file. + +Notes +----- + +This library does not yet implement RFC7622. diff --git a/jid-rs/src/lib.rs b/jid-rs/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..ec56a71ecd626d839c8b5efb72cdfbe1c022355c --- /dev/null +++ b/jid-rs/src/lib.rs @@ -0,0 +1,813 @@ +// Copyright (c) 2017, 2018 lumi +// Copyright (c) 2017, 2018, 2019 Emmanuel Gil Peyrot +// Copyright (c) 2017, 2018, 2019 Maxime “pep” Buquet +// Copyright (c) 2017, 2018 Astro +// Copyright (c) 2017 Bastien Orivel +// +// 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/. + +#![deny(missing_docs)] + +//! Provides a type for Jabber IDs. +//! +//! For usage, check the documentation on the `Jid` struct. + +use std::convert::{Into, TryFrom}; +use std::error::Error as StdError; +use std::fmt; +use std::str::FromStr; + +/// An error that signifies that a `Jid` cannot be parsed from a string. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum JidParseError { + /// Happens when there is no domain, that is either the string is empty, + /// starts with a /, or contains the @/ sequence. + NoDomain, + + /// Happens when there is no resource, that is string contains no /. + NoResource, + + /// Happens when the node is empty, that is the string starts with a @. + EmptyNode, + + /// Happens when the resource is empty, that is the string ends with a /. + EmptyResource, +} + +impl StdError for JidParseError {} + +impl fmt::Display for JidParseError { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "{}", match self { + JidParseError::NoDomain => "no domain found in this JID", + JidParseError::NoResource => "no resource found in this full JID", + JidParseError::EmptyNode => "nodepart empty despite the presence of a @", + JidParseError::EmptyResource => "resource empty despite the presence of a /", + }) + } +} + +/// An enum representing a Jabber ID. It can be either a `FullJid` or a `BareJid`. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Jid { + /// Bare Jid + Bare(BareJid), + + /// Full Jid + Full(FullJid), +} + +impl FromStr for Jid { + type Err = JidParseError; + + fn from_str(s: &str) -> Result { + let (ns, ds, rs): StringJid = _from_str(s)?; + Ok(match rs { + Some(rs) => Jid::Full(FullJid { + node: ns, + domain: ds, + resource: rs, + }), + None => Jid::Bare(BareJid { + node: ns, + domain: ds, + }), + }) + } +} + +impl From for String { + fn from(jid: Jid) -> String { + match jid { + Jid::Bare(bare) => String::from(bare), + Jid::Full(full) => String::from(full), + } + } +} + +impl From for Jid { + fn from(bare_jid: BareJid) -> Jid { + Jid::Bare(bare_jid) + } +} + +impl From for Jid { + fn from(full_jid: FullJid) -> Jid { + Jid::Full(full_jid) + } +} + +impl fmt::Display for Jid { + fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { + fmt.write_str(String::from(self.clone()).as_ref()) + } +} + +impl Jid { + /// The node part of the Jabber ID, if it exists, else None. + pub fn node(self) -> Option { + match self { + Jid::Bare(BareJid { node, .. }) | Jid::Full(FullJid { node, .. }) => node, + } + } + + /// The domain of the Jabber ID. + pub fn domain(self) -> String { + match self { + Jid::Bare(BareJid { domain, .. }) | Jid::Full(FullJid { domain, .. }) => domain, + } + } +} + +impl From for BareJid { + fn from(jid: Jid) -> BareJid { + match jid { + Jid::Full(full) => full.into(), + Jid::Bare(bare) => bare, + } + } +} + +impl TryFrom for FullJid { + type Error = JidParseError; + + fn try_from(jid: Jid) -> Result { + match jid { + Jid::Full(full) => Ok(full), + Jid::Bare(_) => Err(JidParseError::NoResource), + } + } +} + +/// A struct representing a full Jabber ID. +/// +/// A full Jabber ID is composed of 3 components, of which one is optional: +/// +/// - A node/name, `node`, which is the optional part before the @. +/// - A domain, `domain`, which is the mandatory part after the @ but before the /. +/// - A resource, `resource`, which is the part after the /. +/// +/// Unlike a `BareJid`, it always contains a resource, and should only be used when you are certain +/// there is no case where a resource can be missing. Otherwise, use a `Jid` enum. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct FullJid { + /// The node part of the Jabber ID, if it exists, else None. + pub node: Option, + /// The domain of the Jabber ID. + pub domain: String, + /// The resource of the Jabber ID. + pub resource: String, +} + +/// A struct representing a bare Jabber ID. +/// +/// A bare Jabber ID is composed of 2 components, of which one is optional: +/// +/// - A node/name, `node`, which is the optional part before the @. +/// - A domain, `domain`, which is the mandatory part after the @. +/// +/// Unlike a `FullJid`, it can’t contain a resource, and should only be used when you are certain +/// there is no case where a resource can be set. Otherwise, use a `Jid` enum. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct BareJid { + /// The node part of the Jabber ID, if it exists, else None. + pub node: Option, + /// The domain of the Jabber ID. + pub domain: String, +} + +impl From for String { + fn from(jid: FullJid) -> String { + let mut string = String::new(); + if let Some(ref node) = jid.node { + string.push_str(node); + string.push('@'); + } + string.push_str(&jid.domain); + string.push('/'); + string.push_str(&jid.resource); + string + } +} + +impl From for String { + fn from(jid: BareJid) -> String { + let mut string = String::new(); + if let Some(ref node) = jid.node { + string.push_str(node); + string.push('@'); + } + string.push_str(&jid.domain); + string + } +} + +impl Into for FullJid { + fn into(self) -> BareJid { + BareJid { + node: self.node, + domain: self.domain, + } + } +} + +impl fmt::Debug for FullJid { + fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(fmt, "FullJID({})", self) + } +} + +impl fmt::Debug for BareJid { + fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(fmt, "BareJID({})", self) + } +} + +impl fmt::Display for FullJid { + fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { + fmt.write_str(String::from(self.clone()).as_ref()) + } +} + +impl fmt::Display for BareJid { + fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { + fmt.write_str(String::from(self.clone()).as_ref()) + } +} + +enum ParserState { + Node, + Domain, + Resource, +} + +type StringJid = (Option, String, Option); +fn _from_str(s: &str) -> Result { + // TODO: very naive, may need to do it differently + let iter = s.chars(); + let mut buf = String::with_capacity(s.len()); + let mut state = ParserState::Node; + let mut node = None; + let mut domain = None; + let mut resource = None; + for c in iter { + match state { + ParserState::Node => { + match c { + '@' => { + if buf == "" { + return Err(JidParseError::EmptyNode); + } + state = ParserState::Domain; + node = Some(buf.clone()); // TODO: performance tweaks, do not need to copy it + buf.clear(); + } + '/' => { + if buf == "" { + return Err(JidParseError::NoDomain); + } + state = ParserState::Resource; + domain = Some(buf.clone()); // TODO: performance tweaks + buf.clear(); + } + c => { + buf.push(c); + } + } + } + ParserState::Domain => { + match c { + '/' => { + if buf == "" { + return Err(JidParseError::NoDomain); + } + state = ParserState::Resource; + domain = Some(buf.clone()); // TODO: performance tweaks + buf.clear(); + } + c => { + buf.push(c); + } + } + } + ParserState::Resource => { + buf.push(c); + } + } + } + if !buf.is_empty() { + match state { + ParserState::Node => { + domain = Some(buf); + } + ParserState::Domain => { + domain = Some(buf); + } + ParserState::Resource => { + resource = Some(buf); + } + } + } else if let ParserState::Resource = state { + return Err(JidParseError::EmptyResource); + } + Ok((node, domain.ok_or(JidParseError::NoDomain)?, resource)) +} + +impl FromStr for FullJid { + type Err = JidParseError; + + fn from_str(s: &str) -> Result { + let (ns, ds, rs): StringJid = _from_str(s)?; + Ok(FullJid { + node: ns, + domain: ds, + resource: rs.ok_or(JidParseError::NoResource)?, + }) + } +} + +impl FullJid { + /// Constructs a full Jabber ID containing all three components. + /// + /// This is of the form `node`@`domain`/`resource`. + /// + /// # Examples + /// + /// ``` + /// use jid::FullJid; + /// + /// let jid = FullJid::new("node", "domain", "resource"); + /// + /// assert_eq!(jid.node, Some("node".to_owned())); + /// assert_eq!(jid.domain, "domain".to_owned()); + /// assert_eq!(jid.resource, "resource".to_owned()); + /// ``` + pub fn new(node: NS, domain: DS, resource: RS) -> FullJid + where + NS: Into, + DS: Into, + RS: Into, + { + FullJid { + node: Some(node.into()), + domain: domain.into(), + resource: resource.into(), + } + } + + /// Constructs a new Jabber ID from an existing one, with the node swapped out with a new one. + /// + /// # Examples + /// + /// ``` + /// use jid::FullJid; + /// + /// let jid = FullJid::new("node", "domain", "resource"); + /// + /// assert_eq!(jid.node, Some("node".to_owned())); + /// + /// let new_jid = jid.with_node("new_node"); + /// + /// assert_eq!(new_jid.node, Some("new_node".to_owned())); + /// ``` + pub fn with_node(&self, node: NS) -> FullJid + where + NS: Into, + { + FullJid { + node: Some(node.into()), + domain: self.domain.clone(), + resource: self.resource.clone(), + } + } + + /// Constructs a new Jabber ID from an existing one, with the domain swapped out with a new one. + /// + /// # Examples + /// + /// ``` + /// use jid::FullJid; + /// + /// let jid = FullJid::new("node", "domain", "resource"); + /// + /// assert_eq!(jid.domain, "domain".to_owned()); + /// + /// let new_jid = jid.with_domain("new_domain"); + /// + /// assert_eq!(new_jid.domain, "new_domain"); + /// ``` + pub fn with_domain(&self, domain: DS) -> FullJid + where + DS: Into, + { + FullJid { + node: self.node.clone(), + domain: domain.into(), + resource: self.resource.clone(), + } + } + + /// Constructs a full Jabber ID from a bare Jabber ID, specifying a `resource`. + /// + /// # Examples + /// + /// ``` + /// use jid::FullJid; + /// + /// let jid = FullJid::new("node", "domain", "resource"); + /// + /// assert_eq!(jid.resource, "resource".to_owned()); + /// + /// let new_jid = jid.with_resource("new_resource"); + /// + /// assert_eq!(new_jid.resource, "new_resource"); + /// ``` + pub fn with_resource(&self, resource: RS) -> FullJid + where + RS: Into, + { + FullJid { + node: self.node.clone(), + domain: self.domain.clone(), + resource: resource.into(), + } + } +} + +impl FromStr for BareJid { + type Err = JidParseError; + + fn from_str(s: &str) -> Result { + let (ns, ds, _rs): StringJid = _from_str(s)?; + Ok(BareJid { + node: ns, + domain: ds, + }) + } +} + +impl BareJid { + /// Constructs a bare Jabber ID, containing two components. + /// + /// This is of the form `node`@`domain`. + /// + /// # Examples + /// + /// ``` + /// use jid::BareJid; + /// + /// let jid = BareJid::new("node", "domain"); + /// + /// assert_eq!(jid.node, Some("node".to_owned())); + /// assert_eq!(jid.domain, "domain".to_owned()); + /// ``` + pub fn new(node: NS, domain: DS) -> BareJid + where + NS: Into, + DS: Into, + { + BareJid { + node: Some(node.into()), + domain: domain.into(), + } + } + + /// Constructs a bare Jabber ID containing only a `domain`. + /// + /// This is of the form `domain`. + /// + /// # Examples + /// + /// ``` + /// use jid::BareJid; + /// + /// let jid = BareJid::domain("domain"); + /// + /// assert_eq!(jid.node, None); + /// assert_eq!(jid.domain, "domain".to_owned()); + /// ``` + pub fn domain(domain: DS) -> BareJid + where + DS: Into, + { + BareJid { + node: None, + domain: domain.into(), + } + } + + /// Constructs a new Jabber ID from an existing one, with the node swapped out with a new one. + /// + /// # Examples + /// + /// ``` + /// use jid::BareJid; + /// + /// let jid = BareJid::domain("domain"); + /// + /// assert_eq!(jid.node, None); + /// + /// let new_jid = jid.with_node("node"); + /// + /// assert_eq!(new_jid.node, Some("node".to_owned())); + /// ``` + pub fn with_node(&self, node: NS) -> BareJid + where + NS: Into, + { + BareJid { + node: Some(node.into()), + domain: self.domain.clone(), + } + } + + /// Constructs a new Jabber ID from an existing one, with the domain swapped out with a new one. + /// + /// # Examples + /// + /// ``` + /// use jid::BareJid; + /// + /// let jid = BareJid::domain("domain"); + /// + /// assert_eq!(jid.domain, "domain"); + /// + /// let new_jid = jid.with_domain("new_domain"); + /// + /// assert_eq!(new_jid.domain, "new_domain"); + /// ``` + pub fn with_domain(&self, domain: DS) -> BareJid + where + DS: Into, + { + BareJid { + node: self.node.clone(), + domain: domain.into(), + } + } + + /// Constructs a full Jabber ID from a bare Jabber ID, specifying a `resource`. + /// + /// # Examples + /// + /// ``` + /// use jid::BareJid; + /// + /// let bare = BareJid::new("node", "domain"); + /// let full = bare.with_resource("resource"); + /// + /// assert_eq!(full.node, Some("node".to_owned())); + /// assert_eq!(full.domain, "domain".to_owned()); + /// assert_eq!(full.resource, "resource".to_owned()); + /// ``` + pub fn with_resource(self, resource: RS) -> FullJid + where + RS: Into, + { + FullJid { + node: self.node, + domain: self.domain, + resource: resource.into(), + } + } +} + +#[cfg(feature = "minidom")] +use minidom::{IntoAttributeValue, Node}; + +#[cfg(feature = "minidom")] +impl IntoAttributeValue for Jid { + fn into_attribute_value(self) -> Option { + Some(String::from(self)) + } +} + +#[cfg(feature = "minidom")] +impl Into for Jid { + fn into(self) -> Node { + Node::Text(String::from(self)) + } +} + +#[cfg(feature = "minidom")] +impl IntoAttributeValue for FullJid { + fn into_attribute_value(self) -> Option { + Some(String::from(self)) + } +} + +#[cfg(feature = "minidom")] +impl Into for FullJid { + fn into(self) -> Node { + Node::Text(String::from(self)) + } +} + +#[cfg(feature = "minidom")] +impl IntoAttributeValue for BareJid { + fn into_attribute_value(self) -> Option { + Some(String::from(self)) + } +} + +#[cfg(feature = "minidom")] +impl Into for BareJid { + fn into(self) -> Node { + Node::Text(String::from(self)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::str::FromStr; + use std::collections::HashMap; + + #[test] + fn can_parse_full_jids() { + assert_eq!( + FullJid::from_str("a@b.c/d"), + Ok(FullJid::new("a", "b.c", "d")) + ); + assert_eq!( + FullJid::from_str("b.c/d"), + Ok(FullJid { + node: None, + domain: "b.c".to_owned(), + resource: "d".to_owned(), + }) + ); + + assert_eq!(FullJid::from_str("a@b.c"), Err(JidParseError::NoResource)); + assert_eq!(FullJid::from_str("b.c"), Err(JidParseError::NoResource)); + } + + #[test] + fn can_parse_bare_jids() { + assert_eq!(BareJid::from_str("a@b.c/d"), Ok(BareJid::new("a", "b.c"))); + assert_eq!( + BareJid::from_str("b.c/d"), + Ok(BareJid { + node: None, + domain: "b.c".to_owned(), + }) + ); + + assert_eq!(BareJid::from_str("a@b.c"), Ok(BareJid::new("a", "b.c"))); + assert_eq!( + BareJid::from_str("b.c"), + Ok(BareJid { + node: None, + domain: "b.c".to_owned(), + }) + ); + } + + #[test] + fn can_parse_jids() { + let full = FullJid::from_str("a@b.c/d").unwrap(); + let bare = BareJid::from_str("e@f.g").unwrap(); + + assert_eq!(Jid::from_str("a@b.c/d"), Ok(Jid::Full(full))); + assert_eq!(Jid::from_str("e@f.g"), Ok(Jid::Bare(bare))); + } + + #[test] + fn full_to_bare_jid() { + let bare: BareJid = FullJid::new("a", "b.c", "d").into(); + assert_eq!(bare, BareJid::new("a", "b.c")); + } + + #[test] + fn bare_to_full_jid() { + assert_eq!( + BareJid::new("a", "b.c").with_resource("d"), + FullJid::new("a", "b.c", "d") + ); + } + + #[test] + fn node_from_jid() { + assert_eq!( + Jid::Full(FullJid::new("a", "b.c", "d")).node(), + Some(String::from("a")), + ); + } + + #[test] + fn domain_from_jid() { + assert_eq!( + Jid::Bare(BareJid::new("a", "b.c")).domain(), + String::from("b.c"), + ); + } + + #[test] + fn jid_to_full_bare() { + let full = FullJid::new("a", "b.c", "d"); + let bare = BareJid::new("a", "b.c"); + + assert_eq!( + FullJid::try_from(Jid::Full(full.clone())), + Ok(full.clone()), + ); + assert_eq!( + FullJid::try_from(Jid::Bare(bare.clone())), + Err(JidParseError::NoResource), + ); + assert_eq!( + BareJid::from(Jid::Full(full.clone())), + bare.clone(), + ); + assert_eq!( + BareJid::from(Jid::Bare(bare.clone())), + bare, + ); + } + + #[test] + fn serialise() { + assert_eq!( + String::from(FullJid::new("a", "b", "c")), + String::from("a@b/c") + ); + assert_eq!(String::from(BareJid::new("a", "b")), String::from("a@b")); + } + + #[test] + fn hash() { + let _map: HashMap = HashMap::new(); + } + + #[test] + fn invalid_jids() { + assert_eq!(BareJid::from_str(""), Err(JidParseError::NoDomain)); + assert_eq!(BareJid::from_str("/c"), Err(JidParseError::NoDomain)); + assert_eq!(BareJid::from_str("a@/c"), Err(JidParseError::NoDomain)); + assert_eq!(BareJid::from_str("@b"), Err(JidParseError::EmptyNode)); + assert_eq!(BareJid::from_str("b/"), Err(JidParseError::EmptyResource)); + + assert_eq!(FullJid::from_str(""), Err(JidParseError::NoDomain)); + assert_eq!(FullJid::from_str("/c"), Err(JidParseError::NoDomain)); + assert_eq!(FullJid::from_str("a@/c"), Err(JidParseError::NoDomain)); + assert_eq!(FullJid::from_str("@b"), Err(JidParseError::EmptyNode)); + assert_eq!(FullJid::from_str("b/"), Err(JidParseError::EmptyResource)); + assert_eq!(FullJid::from_str("a@b"), Err(JidParseError::NoResource)); + } + + #[test] + fn display_jids() { + assert_eq!(format!("{}", FullJid::new("a", "b", "c")), String::from("a@b/c")); + assert_eq!(format!("{}", BareJid::new("a", "b")), String::from("a@b")); + assert_eq!(format!("{}", Jid::Full(FullJid::new("a", "b", "c"))), String::from("a@b/c")); + assert_eq!(format!("{}", Jid::Bare(BareJid::new("a", "b"))), String::from("a@b")); + } + + #[cfg(feature = "minidom")] + #[test] + fn minidom() { + let elem: minidom::Element = "".parse().unwrap(); + let to: Jid = elem.attr("from").unwrap().parse().unwrap(); + assert_eq!(to, Jid::Full(FullJid::new("a", "b", "c"))); + + let elem: minidom::Element = "".parse().unwrap(); + let to: Jid = elem.attr("from").unwrap().parse().unwrap(); + assert_eq!(to, Jid::Bare(BareJid::new("a", "b"))); + + let elem: minidom::Element = "".parse().unwrap(); + let to: FullJid = elem.attr("from").unwrap().parse().unwrap(); + assert_eq!(to, FullJid::new("a", "b", "c")); + + let elem: minidom::Element = "".parse().unwrap(); + let to: BareJid = elem.attr("from").unwrap().parse().unwrap(); + assert_eq!(to, BareJid::new("a", "b")); + } + + #[cfg(feature = "minidom")] + #[test] + fn minidom_into_attr() { + let full = FullJid::new("a", "b", "c"); + let elem = minidom::Element::builder("message") + .ns("jabber:client") + .attr("from", full.clone()) + .build(); + assert_eq!(elem.attr("from"), Some(String::from(full).as_ref())); + + let bare = BareJid::new("a", "b"); + let elem = minidom::Element::builder("message") + .ns("jabber:client") + .attr("from", bare.clone()) + .build(); + assert_eq!(elem.attr("from"), Some(String::from(bare.clone()).as_ref())); + + let jid = Jid::Bare(bare.clone()); + let _elem = minidom::Element::builder("message") + .ns("jabber:client") + .attr("from", jid) + .build(); + assert_eq!(elem.attr("from"), Some(String::from(bare).as_ref())); + } +} diff --git a/minidom-rs/.gitlab-ci.yml b/minidom-rs/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..efd57f1e923b2926fbf8cf39bbd5ef0727d7a1e9 --- /dev/null +++ b/minidom-rs/.gitlab-ci.yml @@ -0,0 +1,62 @@ +stages: + - build + - test + +variables: + FEATURES: "" + RUST_BACKTRACE: "full" + +.stable: + image: rust:latest + cache: + key: stable + paths: + - target/ + +.nightly: + image: rustlang/rust:nightly + cache: + key: nightly + paths: + - target/ + +.build: + stage: build + script: + - cargo build --verbose --no-default-features --features=$FEATURES + +.test: + stage: test + script: + - cargo test --verbose --no-default-features --features=$FEATURES + +rust-latest-build: + extends: + - .build + - .stable + +rust-nightly-build: + extends: + - .build + - .nightly + + +rust-latest-test: + extends: + - .test + - .stable + +rust-nightly-test: + extends: + - .test + - .nightly + +rust-latest-build with features=comments: + extends: rust-latest-build + variables: + FEATURES: "comments" + +rust-latest-test with features=comments: + extends: rust-latest-test + variables: + FEATURES: "comments" diff --git a/minidom-rs/CHANGELOG.md b/minidom-rs/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..275c0e69884a0b3638462e6353d571fe7390aad0 --- /dev/null +++ b/minidom-rs/CHANGELOG.md @@ -0,0 +1,51 @@ +Version XXX, released YYY: + * Changes + * Update edition to 2018 + * Fixes + * Update old CI configuration with newer Rust images +Version 0.11.1, released 2019-09-06: + * Changes + * Update to quick-xml 0.16 + * Add a default "comments" feature to transform comments into errors when unset. +Version 0.11.0, released 2019-06-14: + * Breaking + * Get rid of IntoElements, replace with `Into` and ` IntoIterator>` + * Fixes + * Remote unused `mut` attribute on variable + * Changes + * Update quick-xml to 0.14 + * Split Node into its own module + * Nicer Debug implementation for NamespaceSet +Version 0.10.0, released 2018-10-21: + * Changes + * Update quick-xml to 0.13 + * Update doc to reflect switch from xml-rs to quick-xml. +Version 0.9.1, released 2018-05-29: + * Fixes + * Lumi fixed CDATA handling, minidom will not unescape CDATA bodies anymore. + * Small changes + - Link Mauve implemented IntoAttributeValue on std::net::IpAddr. +Version 0.9.0, released 2018-04-10: + * Small changes + - Upgrade quick_xml to 0.12.1 +Version 0.8.0, released 2018-02-18: + * Additions + - Link Mauve replaced error\_chain with failure ( https://gitlab.com/lumi/minidom-rs/merge_requests/27 ) + - Yue Liu added support for writing comments and made the writing methods use quick-xml's EventWriter ( https://gitlab.com/lumi/minidom-rs/merge_requests/26 ) +Version 0.6.2, released 2017-08-27: + * Additions + - Link Mauve added an implementation of IntoElements for all Into ( https://gitlab.com/lumi/minidom-rs/merge_requests/19 ) +Version 0.6.1, released 2017-08-20: + * Additions + - Astro added Element::has_ns, which checks whether an element's namespace matches the passed argument. ( https://gitlab.com/lumi/minidom-rs/merge_requests/16 ) + - Link Mauve updated the quick-xml dependency to the latest version. + * Fixes + - Because break value is now stable, Link Mauve rewrote some code marked FIXME to use it. +Version 0.6.0, released 2017-08-13: + * Big changes + - Astro added proper support for namespace prefixes. ( https://gitlab.com/lumi/minidom-rs/merge_requests/14 ) + * Fixes + - Astro fixed a regression that caused the writer not to escape its xml output properly. ( https://gitlab.com/lumi/minidom-rs/merge_requests/15 ) +Version 0.5.0, released 2017-06-10: + * Big changes + - Eijebong made parsing a lot faster by switching the crate from xml-rs to quick_xml. ( https://gitlab.com/lumi/minidom-rs/merge_requests/11 ) diff --git a/minidom-rs/Cargo.toml b/minidom-rs/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..e46db5a346cca27dacd00c280ea00690bdc7016a --- /dev/null +++ b/minidom-rs/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "minidom" +version = "0.11.1" +authors = [ + "lumi ", + "Emmanuel Gil Peyrot ", + "Bastien Orivel ", + "Astro ", + "Maxime “pep” Buquet ", +] +description = "A small, simple DOM implementation on top of quick-xml" +homepage = "https://gitlab.com/lumi/minidom-rs" +repository = "https://gitlab.com/lumi/minidom-rs" +documentation = "https://docs.rs/minidom" +readme = "README.md" +keywords = ["xml"] +license = "MIT" +edition = "2018" + +[badges] +gitlab = { repository = "lumi/minidom-rs" } + +[dependencies] +quick-xml = "0.17" + +[features] +default = ["comments"] +comments = [] diff --git a/minidom-rs/LICENSE b/minidom-rs/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..e3f453dd6acacbda82e62c5535f8055520dcad5e --- /dev/null +++ b/minidom-rs/LICENSE @@ -0,0 +1,15 @@ + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/minidom-rs/README.md b/minidom-rs/README.md new file mode 100644 index 0000000000000000000000000000000000000000..93582491b6513fc889957fd7c8b2a7d331329e36 --- /dev/null +++ b/minidom-rs/README.md @@ -0,0 +1,32 @@ +minidom-rs +========== + +What's this? +------------ + +A minimal DOM library on top of quick-xml. + +What license is it under? +------------------------- + +MIT. See `LICENSE`. + +License yadda yadda. +-------------------- + +Copyright 2017 minidom-rs contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/minidom-rs/examples/articles.rs b/minidom-rs/examples/articles.rs new file mode 100644 index 0000000000000000000000000000000000000000..29b1e513464b36a0b094d0babd282c151ab24316 --- /dev/null +++ b/minidom-rs/examples/articles.rs @@ -0,0 +1,45 @@ +extern crate minidom; + +use minidom::Element; + +const DATA: &str = r#" +
+ 10 Terrible Bugs You Would NEVER Believe Happened + + Rust fixed them all. <3 + +
+
+ BREAKING NEWS: Physical Bug Jumps Out Of Programmer's Screen + + Just kidding! + +
+
"#; + +const ARTICLE_NS: &str = "article"; + +#[derive(Debug)] +pub struct Article { + title: String, + body: String, +} + +fn main() { + let root: Element = DATA.parse().unwrap(); + + let mut articles: Vec
= Vec::new(); + + for child in root.children() { + if child.is("article", ARTICLE_NS) { + let title = child.get_child("title", ARTICLE_NS).unwrap().text(); + let body = child.get_child("body", ARTICLE_NS).unwrap().text(); + articles.push(Article { + title: title, + body: body.trim().to_owned(), + }); + } + } + + println!("{:?}", articles); +} diff --git a/minidom-rs/src/convert.rs b/minidom-rs/src/convert.rs new file mode 100644 index 0000000000000000000000000000000000000000..7c6d314e56df62c13a3c6c9e62c6491859a14c54 --- /dev/null +++ b/minidom-rs/src/convert.rs @@ -0,0 +1,69 @@ +//! A module which exports a few traits for converting types to elements and attributes. + +/// A trait for types which can be converted to an attribute value. +pub trait IntoAttributeValue { + /// Turns this into an attribute string, or None if it shouldn't be added. + fn into_attribute_value(self) -> Option; +} + +macro_rules! impl_into_attribute_value { + ($t:ty) => { + impl IntoAttributeValue for $t { + fn into_attribute_value(self) -> Option { + Some(format!("{}", self)) + } + } + } +} + +macro_rules! impl_into_attribute_values { + ($($t:ty),*) => { + $(impl_into_attribute_value!($t);)* + } +} + +impl_into_attribute_values!(usize, u64, u32, u16, u8, isize, i64, i32, i16, i8, ::std::net::IpAddr); + +impl IntoAttributeValue for String { + fn into_attribute_value(self) -> Option { + Some(self) + } +} + +impl<'a> IntoAttributeValue for &'a String { + fn into_attribute_value(self) -> Option { + Some(self.to_owned()) + } +} + +impl<'a> IntoAttributeValue for &'a str { + fn into_attribute_value(self) -> Option { + Some(self.to_owned()) + } +} + +impl IntoAttributeValue for Option { + fn into_attribute_value(self) -> Option { + self.and_then(IntoAttributeValue::into_attribute_value) + } +} + +#[cfg(test)] +mod tests { + use super::IntoAttributeValue; + use std::net::IpAddr; + use std::str::FromStr; + + #[test] + fn test_into_attribute_value_on_ints() { + assert_eq!(16u8.into_attribute_value().unwrap() , "16"); + assert_eq!(17u16.into_attribute_value().unwrap() , "17"); + assert_eq!(18u32.into_attribute_value().unwrap() , "18"); + assert_eq!(19u64.into_attribute_value().unwrap() , "19"); + assert_eq!( 16i8.into_attribute_value().unwrap() , "16"); + assert_eq!((-17i16).into_attribute_value().unwrap(), "-17"); + assert_eq!( 18i32.into_attribute_value().unwrap(), "18"); + assert_eq!((-19i64).into_attribute_value().unwrap(), "-19"); + assert_eq!(IpAddr::from_str("127.000.0.1").unwrap().into_attribute_value().unwrap(), "127.0.0.1"); + } +} diff --git a/minidom-rs/src/element.rs b/minidom-rs/src/element.rs new file mode 100644 index 0000000000000000000000000000000000000000..08ccc709ecad30354909a673d5eefd9321c349a8 --- /dev/null +++ b/minidom-rs/src/element.rs @@ -0,0 +1,979 @@ +//! Provides an `Element` type, which represents DOM nodes, and a builder to create them with. + +use crate::convert::IntoAttributeValue; +use crate::error::{Error, Result}; +use crate::namespace_set::NamespaceSet; +use crate::node::Node; + +use std::io:: Write; +use std::collections::{btree_map, BTreeMap}; + +use std::str; +use std::rc::Rc; +use std::borrow::Cow; + +use quick_xml::Reader as EventReader; +use quick_xml::Writer as EventWriter; +use quick_xml::events::{Event, BytesStart, BytesEnd, BytesDecl}; + +use std::io::BufRead; + +use std::str::FromStr; + +use std::slice; + +/// helper function to escape a `&[u8]` and replace all +/// xml special characters (<, >, &, ', ") with their corresponding +/// xml escaped value. +pub fn escape(raw: &[u8]) -> Cow<[u8]> { + let mut escapes: Vec<(usize, &'static [u8])> = Vec::new(); + let mut bytes = raw.iter(); + fn to_escape(b: u8) -> bool { + match b { + b'<' | b'>' | b'\'' | b'&' | b'"' => true, + _ => false, + } + } + + let mut loc = 0; + while let Some(i) = bytes.position(|&b| to_escape(b)) { + loc += i; + match raw[loc] { + b'<' => escapes.push((loc, b"<")), + b'>' => escapes.push((loc, b">")), + b'\'' => escapes.push((loc, b"'")), + b'&' => escapes.push((loc, b"&")), + b'"' => escapes.push((loc, b""")), + _ => unreachable!("Only '<', '>','\', '&' and '\"' are escaped"), + } + loc += 1; + } + + if escapes.is_empty() { + Cow::Borrowed(raw) + } else { + let len = raw.len(); + let mut v = Vec::with_capacity(len); + let mut start = 0; + for (i, r) in escapes { + v.extend_from_slice(&raw[start..i]); + v.extend_from_slice(r); + start = i + 1; + } + + if start < len { + v.extend_from_slice(&raw[start..]); + } + Cow::Owned(v) + } +} + + +#[derive(Clone, PartialEq, Eq, Debug)] +/// A struct representing a DOM Element. +pub struct Element { + prefix: Option, + name: String, + namespaces: Rc, + attributes: BTreeMap, + children: Vec, +} + +impl<'a> From<&'a Element> for String { + fn from(elem: &'a Element) -> String { + let mut writer = Vec::new(); + elem.write_to(&mut writer).unwrap(); + String::from_utf8(writer).unwrap() + } +} + +impl FromStr for Element { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut reader = EventReader::from_str(s); + Element::from_reader(&mut reader) + } +} + +impl Element { + fn new>(name: String, prefix: Option, namespaces: NS, attributes: BTreeMap, children: Vec) -> Element { + Element { + prefix, name, + namespaces: Rc::new(namespaces.into()), + attributes, + children, + } + } + + /// Return a builder for an `Element` with the given `name`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem = Element::builder("name") + /// .ns("namespace") + /// .attr("name", "value") + /// .append("inner") + /// .build(); + /// + /// assert_eq!(elem.name(), "name"); + /// assert_eq!(elem.ns(), Some("namespace".to_owned())); + /// assert_eq!(elem.attr("name"), Some("value")); + /// assert_eq!(elem.attr("inexistent"), None); + /// assert_eq!(elem.text(), "inner"); + /// ``` + pub fn builder>(name: S) -> ElementBuilder { + let (prefix, name) = split_element_name(name).unwrap(); + ElementBuilder { + root: Element::new(name, prefix, None, BTreeMap::new(), Vec::new()), + namespaces: Default::default(), + } + } + + /// Returns a bare minimum `Element` with this name. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let bare = Element::bare("name"); + /// + /// assert_eq!(bare.name(), "name"); + /// assert_eq!(bare.ns(), None); + /// assert_eq!(bare.attr("name"), None); + /// assert_eq!(bare.text(), ""); + /// ``` + pub fn bare>(name: S) -> Element { + Element { + prefix: None, + name: name.into(), + namespaces: Rc::new(NamespaceSet::default()), + attributes: BTreeMap::new(), + children: Vec::new(), + } + } + + /// Returns a reference to the name of this element. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns a reference to the prefix of this element. + /// + /// # Examples + /// ```rust + /// use minidom::Element; + /// + /// let elem = Element::builder("prefix:name") + /// .build(); + /// + /// assert_eq!(elem.name(), "name"); + /// assert_eq!(elem.prefix(), Some("prefix")); + /// ``` + pub fn prefix(&self) -> Option<&str> { + self.prefix.as_ref().map(String::as_ref) + } + + /// Returns a reference to the namespace of this element, if it has one, else `None`. + pub fn ns(&self) -> Option { + self.namespaces.get(&self.prefix) + } + + /// Returns a reference to the value of the given attribute, if it exists, else `None`. + pub fn attr(&self, name: &str) -> Option<&str> { + if let Some(value) = self.attributes.get(name) { + return Some(value) + } + None + } + + /// Returns an iterator over the attributes of this element. + /// + /// # Example + /// + /// ```rust + /// use minidom::Element; + /// + /// let elm: Element = "".parse().unwrap(); + /// + /// let mut iter = elm.attrs(); + /// + /// assert_eq!(iter.next().unwrap(), ("a", "b")); + /// assert_eq!(iter.next(), None); + /// ``` + pub fn attrs(&self) -> Attrs { + Attrs { + iter: self.attributes.iter(), + } + } + + /// Returns an iterator over the attributes of this element, with the value being a mutable + /// reference. + pub fn attrs_mut(&mut self) -> AttrsMut { + AttrsMut { + iter: self.attributes.iter_mut(), + } + } + + /// Modifies the value of an attribute. + pub fn set_attr, V: IntoAttributeValue>(&mut self, name: S, val: V) { + let name = name.into(); + let val = val.into_attribute_value(); + + if let Some(value) = self.attributes.get_mut(&name) { + *value = val.expect("removing existing value via set_attr, this is not yet supported (TODO)"); // TODO + return; + } + + if let Some(val) = val { + self.attributes.insert(name, val); + } + } + + /// Returns whether the element has the given name and namespace. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem = Element::builder("name").ns("namespace").build(); + /// + /// assert_eq!(elem.is("name", "namespace"), true); + /// assert_eq!(elem.is("name", "wrong"), false); + /// assert_eq!(elem.is("wrong", "namespace"), false); + /// assert_eq!(elem.is("wrong", "wrong"), false); + /// ``` + pub fn is, NS: AsRef>(&self, name: N, namespace: NS) -> bool { + self.name == name.as_ref() && + self.has_ns(namespace) + } + + /// Returns whether the element has the given namespace. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem = Element::builder("name").ns("namespace").build(); + /// + /// assert_eq!(elem.has_ns("namespace"), true); + /// assert_eq!(elem.has_ns("wrong"), false); + /// ``` + pub fn has_ns>(&self, namespace: NS) -> bool { + self.namespaces.has(&self.prefix, namespace) + } + + /// Parse a document from an `EventReader`. + pub fn from_reader(reader: &mut EventReader) -> Result { + let mut buf = Vec::new(); + + let root: Element = loop { + let e = reader.read_event(&mut buf)?; + match e { + Event::Empty(ref e) | Event::Start(ref e) => { + break build_element(reader, e)?; + }, + Event::Eof => { + return Err(Error::EndOfDocument); + }, + #[cfg(not(feature = "comments"))] + Event::Comment { .. } => { + return Err(Error::CommentsDisabled); + } + #[cfg(feature = "comments")] + Event::Comment { .. } => (), + Event::Text { .. } | + Event::End { .. } | + Event::CData { .. } | + Event::Decl { .. } | + Event::PI { .. } | + Event::DocType { .. } => (), // TODO: may need more errors + } + }; + + let mut stack = vec![root]; + + loop { + match reader.read_event(&mut buf)? { + Event::Empty(ref e) => { + let elem = build_element(reader, e)?; + // Since there is no Event::End after, directly append it to the current node + stack.last_mut().unwrap().append_child(elem); + }, + Event::Start(ref e) => { + let elem = build_element(reader, e)?; + stack.push(elem); + }, + Event::End(ref e) => { + if stack.len() <= 1 { + break; + } + let elem = stack.pop().unwrap(); + if let Some(to) = stack.last_mut() { + // TODO: check whether this is correct, we are comparing &[u8]s, not &strs + let elem_name = e.name(); + let mut split_iter = elem_name.splitn(2, |u| *u == 0x3A); + let possible_prefix = split_iter.next().unwrap(); // Can't be empty. + match split_iter.next() { + Some(name) => { + match elem.prefix() { + Some(prefix) => { + if possible_prefix != prefix.as_bytes() { + return Err(Error::InvalidElementClosed); + } + }, + None => { + return Err(Error::InvalidElementClosed); + }, + } + if name != elem.name().as_bytes() { + return Err(Error::InvalidElementClosed); + } + }, + None => { + if elem.prefix().is_some() { + return Err(Error::InvalidElementClosed); + } + if possible_prefix != elem.name().as_bytes() { + return Err(Error::InvalidElementClosed); + } + }, + } + to.append_child(elem); + } + }, + Event::Text(s) => { + let text = s.unescape_and_decode(reader)?; + if text != "" { + let current_elem = stack.last_mut().unwrap(); + current_elem.append_text_node(text); + } + }, + Event::CData(s) => { + let text = reader.decode(&s)?.to_owned(); + if text != "" { + let current_elem = stack.last_mut().unwrap(); + current_elem.append_text_node(text); + } + }, + Event::Eof => { + break; + }, + #[cfg(not(feature = "comments"))] + Event::Comment(_) => return Err(Error::CommentsDisabled), + #[cfg(feature = "comments")] + Event::Comment(s) => { + let comment = reader.decode(&s)?.to_owned(); + if comment != "" { + let current_elem = stack.last_mut().unwrap(); + current_elem.append_comment_node(comment); + } + }, + Event::Decl { .. } | + Event::PI { .. } | + Event::DocType { .. } => (), + } + } + Ok(stack.pop().unwrap()) + } + + /// Output a document to a `Writer`. + pub fn write_to(&self, writer: &mut W) -> Result<()> { + self.to_writer(&mut EventWriter::new(writer)) + } + + /// Output the document to quick-xml `Writer` + pub fn to_writer(&self, writer: &mut EventWriter) -> Result<()> { + writer.write_event(Event::Decl(BytesDecl::new(b"1.0", Some(b"utf-8"), None)))?; + self.write_to_inner(writer) + } + + /// Like `write_to()` but without the `` prelude + pub fn write_to_inner(&self, writer: &mut EventWriter) -> Result<()> { + let name = match self.prefix { + None => Cow::Borrowed(&self.name), + Some(ref prefix) => Cow::Owned(format!("{}:{}", prefix, self.name)), + }; + + let mut start = BytesStart::borrowed(name.as_bytes(), name.len()); + for (prefix, ns) in self.namespaces.declared_ns() { + match *prefix { + None => start.push_attribute(("xmlns", ns.as_ref())), + Some(ref prefix) => { + let key = format!("xmlns:{}", prefix); + start.push_attribute((key.as_bytes(), ns.as_bytes())) + }, + } + } + for (key, value) in &self.attributes { + start.push_attribute((key.as_bytes(), escape(value.as_bytes()).as_ref())); + } + + if self.children.is_empty() { + writer.write_event(Event::Empty(start))?; + return Ok(()) + } + + writer.write_event(Event::Start(start))?; + + for child in &self.children { + child.write_to_inner(writer)?; + } + + writer.write_event(Event::End(BytesEnd::borrowed(name.as_bytes())))?; + Ok(()) + } + + /// Returns an iterator over references to every child node of this element. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem: Element = "abc".parse().unwrap(); + /// + /// let mut iter = elem.nodes(); + /// + /// assert_eq!(iter.next().unwrap().as_text().unwrap(), "a"); + /// assert_eq!(iter.next().unwrap().as_element().unwrap().name(), "c1"); + /// assert_eq!(iter.next().unwrap().as_text().unwrap(), "b"); + /// assert_eq!(iter.next().unwrap().as_element().unwrap().name(), "c2"); + /// assert_eq!(iter.next().unwrap().as_text().unwrap(), "c"); + /// assert_eq!(iter.next(), None); + /// ``` + #[inline] pub fn nodes(&self) -> Nodes { + self.children.iter() + } + + /// Returns an iterator over mutable references to every child node of this element. + #[inline] pub fn nodes_mut(&mut self) -> NodesMut { + self.children.iter_mut() + } + + /// Returns an iterator over references to every child element of this element. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem: Element = "hellothisisignored".parse().unwrap(); + /// + /// let mut iter = elem.children(); + /// assert_eq!(iter.next().unwrap().name(), "child1"); + /// assert_eq!(iter.next().unwrap().name(), "child2"); + /// assert_eq!(iter.next().unwrap().name(), "child3"); + /// assert_eq!(iter.next(), None); + /// ``` + #[inline] pub fn children(&self) -> Children { + Children { + iter: self.children.iter(), + } + } + + /// Returns an iterator over mutable references to every child element of this element. + #[inline] pub fn children_mut(&mut self) -> ChildrenMut { + ChildrenMut { + iter: self.children.iter_mut(), + } + } + + /// Returns an iterator over references to every text node of this element. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem: Element = "hello world!".parse().unwrap(); + /// + /// let mut iter = elem.texts(); + /// assert_eq!(iter.next().unwrap(), "hello"); + /// assert_eq!(iter.next().unwrap(), " world!"); + /// assert_eq!(iter.next(), None); + /// ``` + #[inline] pub fn texts(&self) -> Texts { + Texts { + iter: self.children.iter(), + } + } + + /// Returns an iterator over mutable references to every text node of this element. + #[inline] pub fn texts_mut(&mut self) -> TextsMut { + TextsMut { + iter: self.children.iter_mut(), + } + } + + /// Appends a child node to the `Element`, returning the appended node. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let mut elem = Element::bare("root"); + /// + /// assert_eq!(elem.children().count(), 0); + /// + /// elem.append_child(Element::bare("child")); + /// + /// { + /// let mut iter = elem.children(); + /// assert_eq!(iter.next().unwrap().name(), "child"); + /// assert_eq!(iter.next(), None); + /// } + /// + /// let child = elem.append_child(Element::bare("new")); + /// + /// assert_eq!(child.name(), "new"); + /// ``` + pub fn append_child(&mut self, child: Element) -> &mut Element { + child.namespaces.set_parent(Rc::clone(&self.namespaces)); + + self.children.push(Node::Element(child)); + if let Node::Element(ref mut cld) = *self.children.last_mut().unwrap() { + cld + } else { + unreachable!() + } + } + + /// Appends a text node to an `Element`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let mut elem = Element::bare("node"); + /// + /// assert_eq!(elem.text(), ""); + /// + /// elem.append_text_node("text"); + /// + /// assert_eq!(elem.text(), "text"); + /// ``` + pub fn append_text_node>(&mut self, child: S) { + self.children.push(Node::Text(child.into())); + } + + /// Appends a comment node to an `Element`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let mut elem = Element::bare("node"); + /// + /// elem.append_comment_node("comment"); + /// ``` + #[cfg(feature = "comments")] + pub fn append_comment_node>(&mut self, child: S) { + self.children.push(Node::Comment(child.into())); + } + + /// Appends a node to an `Element`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::{Element, Node}; + /// + /// let mut elem = Element::bare("node"); + /// + /// elem.append_node(Node::Text("hello".to_owned())); + /// + /// assert_eq!(elem.text(), "hello"); + /// ``` + pub fn append_node(&mut self, node: Node) { + self.children.push(node); + } + + /// Returns the concatenation of all text nodes in the `Element`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem: Element = "hello, world!".parse().unwrap(); + /// + /// assert_eq!(elem.text(), "hello, world!"); + /// ``` + pub fn text(&self) -> String { + self.texts().fold(String::new(), |ret, new| ret + new) + } + + /// Returns a reference to the first child element with the specific name and namespace, if it + /// exists in the direct descendants of this `Element`, else returns `None`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem: Element = r#""#.parse().unwrap(); + /// + /// assert!(elem.get_child("a", "ns").unwrap().is("a", "ns")); + /// assert!(elem.get_child("a", "other_ns").unwrap().is("a", "other_ns")); + /// assert!(elem.get_child("b", "ns").unwrap().is("b", "ns")); + /// assert_eq!(elem.get_child("c", "ns"), None); + /// assert_eq!(elem.get_child("b", "other_ns"), None); + /// assert_eq!(elem.get_child("a", "inexistent_ns"), None); + /// ``` + pub fn get_child, NS: AsRef>(&self, name: N, namespace: NS) -> Option<&Element> { + for fork in &self.children { + if let Node::Element(ref e) = *fork { + if e.is(name.as_ref(), namespace.as_ref()) { + return Some(e); + } + } + } + None + } + + /// Returns a mutable reference to the first child element with the specific name and namespace, + /// if it exists in the direct descendants of this `Element`, else returns `None`. + pub fn get_child_mut, NS: AsRef>(&mut self, name: N, namespace: NS) -> Option<&mut Element> { + for fork in &mut self.children { + if let Node::Element(ref mut e) = *fork { + if e.is(name.as_ref(), namespace.as_ref()) { + return Some(e); + } + } + } + None + } + + /// Returns whether a specific child with this name and namespace exists in the direct + /// descendants of the `Element`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let elem: Element = r#""#.parse().unwrap(); + /// + /// assert_eq!(elem.has_child("a", "other_ns"), true); + /// assert_eq!(elem.has_child("a", "ns"), true); + /// assert_eq!(elem.has_child("a", "inexistent_ns"), false); + /// assert_eq!(elem.has_child("b", "ns"), true); + /// assert_eq!(elem.has_child("b", "other_ns"), false); + /// assert_eq!(elem.has_child("b", "inexistent_ns"), false); + /// ``` + pub fn has_child, NS: AsRef>(&self, name: N, namespace: NS) -> bool { + self.get_child(name, namespace).is_some() + } + + /// Removes the first child with this name and namespace, if it exists, and returns an + /// `Option` containing this child if it succeeds. + /// Returns `None` if no child matches this name and namespace. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Element; + /// + /// let mut elem: Element = r#""#.parse().unwrap(); + /// + /// assert!(elem.remove_child("a", "ns").unwrap().is("a", "ns")); + /// assert!(elem.remove_child("a", "ns").is_none()); + /// assert!(elem.remove_child("inexistent", "inexistent").is_none()); + /// ``` + pub fn remove_child, NS: AsRef>(&mut self, name: N, namespace: NS) -> Option { + let name = name.as_ref(); + let namespace = namespace.as_ref(); + let idx = self.children.iter().position(|x| { + if let Node::Element(ref elm) = x { + elm.is(name, namespace) + } else { + false + } + })?; + self.children.remove(idx).into_element() + } +} + +fn split_element_name>(s: S) -> Result<(Option, String)> { + let name_parts = s.as_ref().split(':').collect::>(); + match name_parts.len() { + 2 => Ok((Some(name_parts[0].to_owned()), name_parts[1].to_owned())), + 1 => Ok((None, name_parts[0].to_owned())), + _ => Err(Error::InvalidElement), + } +} + +fn build_element(reader: &EventReader, event: &BytesStart) -> Result { + let mut namespaces = BTreeMap::new(); + let attributes = event.attributes() + .map(|o| { + let o = o?; + let key = str::from_utf8(o.key)?.to_owned(); + let value = o.unescape_and_decode_value(reader)?; + Ok((key, value)) + }) + .filter(|o| { + match *o { + Ok((ref key, ref value)) if key == "xmlns" => { + namespaces.insert(None, value.to_owned()); + false + }, + Ok((ref key, ref value)) if key.starts_with("xmlns:") => { + namespaces.insert(Some(key[6..].to_owned()), value.to_owned()); + false + }, + _ => true, + } + }) + .collect::>>()?; + + let (prefix, name) = split_element_name(str::from_utf8(event.name())?)?; + let element = Element::new(name, prefix, namespaces, attributes, Vec::new()); + Ok(element) +} + +/// An iterator over references to child elements of an `Element`. +pub struct Children<'a> { + iter: slice::Iter<'a, Node>, +} + +impl<'a> Iterator for Children<'a> { + type Item = &'a Element; + + fn next(&mut self) -> Option<&'a Element> { + for item in &mut self.iter { + if let Node::Element(ref child) = *item { + return Some(child); + } + } + None + } +} + +/// An iterator over mutable references to child elements of an `Element`. +pub struct ChildrenMut<'a> { + iter: slice::IterMut<'a, Node>, +} + +impl<'a> Iterator for ChildrenMut<'a> { + type Item = &'a mut Element; + + fn next(&mut self) -> Option<&'a mut Element> { + for item in &mut self.iter { + if let Node::Element(ref mut child) = *item { + return Some(child); + } + } + None + } +} + +/// An iterator over references to child text nodes of an `Element`. +pub struct Texts<'a> { + iter: slice::Iter<'a, Node>, +} + +impl<'a> Iterator for Texts<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option<&'a str> { + for item in &mut self.iter { + if let Node::Text(ref child) = *item { + return Some(child); + } + } + None + } +} + +/// An iterator over mutable references to child text nodes of an `Element`. +pub struct TextsMut<'a> { + iter: slice::IterMut<'a, Node>, +} + +impl<'a> Iterator for TextsMut<'a> { + type Item = &'a mut String; + + fn next(&mut self) -> Option<&'a mut String> { + for item in &mut self.iter { + if let Node::Text(ref mut child) = *item { + return Some(child); + } + } + None + } +} + +/// An iterator over references to all child nodes of an `Element`. +pub type Nodes<'a> = slice::Iter<'a, Node>; + +/// An iterator over mutable references to all child nodes of an `Element`. +pub type NodesMut<'a> = slice::IterMut<'a, Node>; + +/// An iterator over the attributes of an `Element`. +pub struct Attrs<'a> { + iter: btree_map::Iter<'a, String, String>, +} + +impl<'a> Iterator for Attrs<'a> { + type Item = (&'a str, &'a str); + + fn next(&mut self) -> Option { + self.iter.next().map(|(x, y)| (x.as_ref(), y.as_ref())) + } +} + +/// An iterator over the attributes of an `Element`, with the values mutable. +pub struct AttrsMut<'a> { + iter: btree_map::IterMut<'a, String, String>, +} + +impl<'a> Iterator for AttrsMut<'a> { + type Item = (&'a str, &'a mut String); + + fn next(&mut self) -> Option { + self.iter.next().map(|(x, y)| (x.as_ref(), y)) + } +} + +/// A builder for `Element`s. +pub struct ElementBuilder { + root: Element, + namespaces: BTreeMap, String>, +} + +impl ElementBuilder { + /// Sets the namespace. + pub fn ns>(mut self, namespace: S) -> ElementBuilder { + self.namespaces + .insert(self.root.prefix.clone(), namespace.into()); + self + } + + /// Sets an attribute. + pub fn attr, V: IntoAttributeValue>(mut self, name: S, value: V) -> ElementBuilder { + self.root.set_attr(name, value); + self + } + + /// Appends anything implementing `Into` into the tree. + pub fn append>(mut self, node: T) -> ElementBuilder { + self.root.append_node(node.into()); + self + } + + /// Appends an iterator of things implementing `Into` into the tree. + pub fn append_all, I: IntoIterator>(mut self, iter: I) -> ElementBuilder { + for node in iter { + self.root.append_node(node.into()); + } + self + } + + /// Builds the `Element`. + pub fn build(self) -> Element { + let mut element = self.root; + // Set namespaces + element.namespaces = Rc::new(NamespaceSet::from(self.namespaces)); + // Propagate namespaces + for node in &element.children { + if let Node::Element(ref e) = *node { + e.namespaces.set_parent(Rc::clone(&element.namespaces)); + } + } + element + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_element_new() { + use std::iter::FromIterator; + + let elem = Element::new( "name".to_owned() + , None + , Some("namespace".to_owned()) + , BTreeMap::from_iter(vec![ ("name".to_string(), "value".to_string()) ].into_iter() ) + , Vec::new() ); + + assert_eq!(elem.name(), "name"); + assert_eq!(elem.ns(), Some("namespace".to_owned())); + assert_eq!(elem.attr("name"), Some("value")); + assert_eq!(elem.attr("inexistent"), None); + } + + #[test] + fn test_from_reader_simple() { + let xml = ""; + let mut reader = EventReader::from_str(xml); + let elem = Element::from_reader(&mut reader); + + let elem2 = Element::builder("foo").build(); + + assert_eq!(elem.unwrap(), elem2); + } + + #[test] + fn test_from_reader_nested() { + let xml = ""; + let mut reader = EventReader::from_str(xml); + let elem = Element::from_reader(&mut reader); + + let nested = Element::builder("bar") + .attr("baz", "qxx") + .build(); + let elem2 = Element::builder("foo") + .append(nested) + .build(); + + assert_eq!(elem.unwrap(), elem2); + } + + #[test] + fn test_from_reader_with_prefix() { + let xml = ""; + let mut reader = EventReader::from_str(xml); + let elem = Element::from_reader(&mut reader); + + let nested = Element::builder("prefix:bar") + .attr("baz", "qxx") + .build(); + let elem2 = Element::builder("foo") + .append(nested) + .build(); + + assert_eq!(elem.unwrap(), elem2); + } + + #[test] + fn parses_spectest_xml() { // From: https://gitlab.com/lumi/minidom-rs/issues/8 + let xml = r#" + + + + "#; + let mut reader = EventReader::from_str(xml); + let _ = Element::from_reader(&mut reader).unwrap(); + } + + #[test] + fn does_not_unescape_cdata() { + let xml = "]]>"; + let mut reader = EventReader::from_str(xml); + let elem = Element::from_reader(&mut reader).unwrap(); + assert_eq!(elem.text(), "'>blah"); + } +} diff --git a/minidom-rs/src/error.rs b/minidom-rs/src/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..579cee405aca8c68af1aa1f97dd0ff02329774df --- /dev/null +++ b/minidom-rs/src/error.rs @@ -0,0 +1,83 @@ +//! Provides an error type for this crate. + +use std::convert::From; +use std::error::Error as StdError; + +/// Our main error type. +#[derive(Debug)] +pub enum Error { + /// An error from quick_xml. + XmlError(::quick_xml::Error), + + /// An UTF-8 conversion error. + Utf8Error(::std::str::Utf8Error), + + /// An I/O error, from std::io. + IoError(::std::io::Error), + + /// An error which is returned when the end of the document was reached prematurely. + EndOfDocument, + + /// An error which is returned when an element is closed when it shouldn't be + InvalidElementClosed, + + /// An error which is returned when an elemet's name contains more than one colon + InvalidElement, + + /// An error which is returned when a comment is to be parsed by minidom + #[cfg(not(comments))] + CommentsDisabled, +} + +impl StdError for Error { + fn cause(&self) -> Option<&dyn StdError> { + match self { + // TODO: return Some(e) for this case after the merge of + // https://github.com/tafia/quick-xml/pull/170 + Error::XmlError(_e) => None, + Error::Utf8Error(e) => Some(e), + Error::IoError(e) => Some(e), + Error::EndOfDocument => None, + Error::InvalidElementClosed => None, + Error::InvalidElement => None, + #[cfg(not(comments))] + Error::CommentsDisabled => None, + } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Error::XmlError(e) => write!(fmt, "XML error: {}", e), + Error::Utf8Error(e) => write!(fmt, "UTF-8 error: {}", e), + Error::IoError(e) => write!(fmt, "IO error: {}", e), + Error::EndOfDocument => write!(fmt, "the end of the document has been reached prematurely"), + Error::InvalidElementClosed => write!(fmt, "the XML is invalid, an element was wrongly closed"), + Error::InvalidElement => write!(fmt, "the XML element is invalid"), + #[cfg(not(comments))] + Error::CommentsDisabled => write!(fmt, "a comment has been found even though comments are disabled by feature"), + } + } +} + +impl From<::quick_xml::Error> for Error { + fn from(err: ::quick_xml::Error) -> Error { + Error::XmlError(err) + } +} + +impl From<::std::str::Utf8Error> for Error { + fn from(err: ::std::str::Utf8Error) -> Error { + Error::Utf8Error(err) + } +} + +impl From<::std::io::Error> for Error { + fn from(err: ::std::io::Error) -> Error { + Error::IoError(err) + } +} + +/// Our simplified Result type. +pub type Result = ::std::result::Result; diff --git a/minidom-rs/src/lib.rs b/minidom-rs/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..c7f43b23010d693ef3df8a92e2969cb8c608aa41 --- /dev/null +++ b/minidom-rs/src/lib.rs @@ -0,0 +1,80 @@ +#![deny(missing_docs)] + +//! A minimal DOM crate built on top of quick-xml. +//! +//! This library exports an `Element` struct which represents a DOM tree. +//! +//! # Example +//! +//! Run with `cargo run --example articles`. Located in `examples/articles.rs`. +//! +//! ```rust,ignore +//! extern crate minidom; +//! +//! use minidom::Element; +//! +//! const DATA: &'static str = r#" +//!
+//! 10 Terrible Bugs You Would NEVER Believe Happened +//! +//! Rust fixed them all. <3 +//! +//!
+//!
+//! BREAKING NEWS: Physical Bug Jumps Out Of Programmer's Screen +//! +//! Just kidding! +//! +//!
+//!
"#; +//! +//! const ARTICLE_NS: &'static str = "article"; +//! +//! #[derive(Debug)] +//! pub struct Article { +//! title: String, +//! body: String, +//! } +//! +//! fn main() { +//! let root: Element = DATA.parse().unwrap(); +//! +//! let mut articles: Vec
= Vec::new(); +//! +//! for child in root.children() { +//! if child.is("article", ARTICLE_NS) { +//! let title = child.get_child("title", ARTICLE_NS).unwrap().text(); +//! let body = child.get_child("body", ARTICLE_NS).unwrap().text(); +//! articles.push(Article { +//! title: title, +//! body: body.trim().to_owned(), +//! }); +//! } +//! } +//! +//! println!("{:?}", articles); +//! } +//! ``` +//! +//! # Usage +//! +//! To use `minidom`, add this to your `Cargo.toml` under `dependencies`: +//! +//! ```toml,ignore +//! minidom = "*" +//! ``` + +pub use quick_xml; + +pub mod error; +pub mod element; +pub mod convert; +pub mod node; +mod namespace_set; + +#[cfg(test)] mod tests; + +pub use error::{Error, Result}; +pub use element::{Element, Children, ChildrenMut, ElementBuilder}; +pub use node::Node; +pub use convert::IntoAttributeValue; diff --git a/minidom-rs/src/namespace_set.rs b/minidom-rs/src/namespace_set.rs new file mode 100644 index 0000000000000000000000000000000000000000..163a87304aadb1320e4ff4731f2a8f66614d97d6 --- /dev/null +++ b/minidom-rs/src/namespace_set.rs @@ -0,0 +1,170 @@ +use std::collections::BTreeMap; +use std::cell::RefCell; +use std::fmt; +use std::rc::Rc; + + +#[derive(Clone, PartialEq, Eq)] +pub struct NamespaceSet { + parent: RefCell>>, + namespaces: BTreeMap, String>, +} + +impl Default for NamespaceSet { + fn default() -> Self { + NamespaceSet { + parent: RefCell::new(None), + namespaces: BTreeMap::new(), + } + } +} + +impl fmt::Debug for NamespaceSet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "NamespaceSet(")?; + for (prefix, namespace) in &self.namespaces { + write!(f, "xmlns{}={:?}, ", match prefix { + None => String::new(), + Some(prefix) => format!(":{}", prefix), + }, namespace)?; + } + write!(f, "parent: {:?})", *self.parent.borrow()) + } +} + +impl NamespaceSet { + pub fn declared_ns(&self) -> &BTreeMap, String> { + &self.namespaces + } + + pub fn get(&self, prefix: &Option) -> Option { + match self.namespaces.get(prefix) { + Some(ns) => Some(ns.clone()), + None => match *self.parent.borrow() { + None => None, + Some(ref parent) => parent.get(prefix) + }, + } + } + + pub fn has>(&self, prefix: &Option, wanted_ns: NS) -> bool { + match self.namespaces.get(prefix) { + Some(ns) => + ns == wanted_ns.as_ref(), + None => match *self.parent.borrow() { + None => + false, + Some(ref parent) => + parent.has(prefix, wanted_ns), + }, + } + } + + pub fn set_parent(&self, parent: Rc) { + let mut parent_ns = self.parent.borrow_mut(); + let new_set = parent; + *parent_ns = Some(new_set); + } + +} + +impl From, String>> for NamespaceSet { + fn from(namespaces: BTreeMap, String>) -> Self { + NamespaceSet { + parent: RefCell::new(None), + namespaces, + } + } +} + +impl From> for NamespaceSet { + fn from(namespace: Option) -> Self { + match namespace { + None => Self::default(), + Some(namespace) => Self::from(namespace), + } + } +} + +impl From for NamespaceSet { + fn from(namespace: String) -> Self { + let mut namespaces = BTreeMap::new(); + namespaces.insert(None, namespace); + + NamespaceSet { + parent: RefCell::new(None), + namespaces, + } + } +} + +impl From<(Option, String)> for NamespaceSet { + fn from(prefix_namespace: (Option, String)) -> Self { + let (prefix, namespace) = prefix_namespace; + let mut namespaces = BTreeMap::new(); + namespaces.insert(prefix, namespace); + + NamespaceSet { + parent: RefCell::new(None), + namespaces, + } + } +} + +impl From<(String, String)> for NamespaceSet { + fn from(prefix_namespace: (String, String)) -> Self { + let (prefix, namespace) = prefix_namespace; + Self::from((Some(prefix), namespace)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_has() { + let namespaces = NamespaceSet::from("foo".to_owned()); + assert_eq!(namespaces.get(&None), Some("foo".to_owned())); + assert!(namespaces.has(&None, "foo")); + } + + #[test] + fn get_has_prefixed() { + let namespaces = NamespaceSet::from(("x".to_owned(), "bar".to_owned())); + assert_eq!(namespaces.get(&Some("x".to_owned())), Some("bar".to_owned())); + assert!(namespaces.has(&Some("x".to_owned()), "bar")); + } + + #[test] + fn get_has_recursive() { + let mut parent = NamespaceSet::from("foo".to_owned()); + for _ in 0..1000 { + let namespaces = NamespaceSet::default(); + namespaces.set_parent(Rc::new(parent)); + assert_eq!(namespaces.get(&None), Some("foo".to_owned())); + assert!(namespaces.has(&None, "foo")); + parent = namespaces; + } + } + + #[test] + fn get_has_prefixed_recursive() { + let mut parent = NamespaceSet::from(("x".to_owned(), "bar".to_owned())); + for _ in 0..1000 { + let namespaces = NamespaceSet::default(); + namespaces.set_parent(Rc::new(parent)); + assert_eq!(namespaces.get(&Some("x".to_owned())), Some("bar".to_owned())); + assert!(namespaces.has(&Some("x".to_owned()), "bar")); + parent = namespaces; + } + } + + #[test] + fn debug_looks_correct() { + let parent = NamespaceSet::from("http://www.w3.org/2000/svg".to_owned()); + let namespaces = NamespaceSet::from(("xhtml".to_owned(), "http://www.w3.org/1999/xhtml".to_owned())); + namespaces.set_parent(Rc::new(parent)); + assert_eq!(format!("{:?}", namespaces), "NamespaceSet(xmlns:xhtml=\"http://www.w3.org/1999/xhtml\", parent: Some(NamespaceSet(xmlns=\"http://www.w3.org/2000/svg\", parent: None)))"); + } +} diff --git a/minidom-rs/src/node.rs b/minidom-rs/src/node.rs new file mode 100644 index 0000000000000000000000000000000000000000..e0d9a9ebbcf5dd3c2d68a879664491b042c07568 --- /dev/null +++ b/minidom-rs/src/node.rs @@ -0,0 +1,207 @@ +//! Provides the `Node` struct, which represents a node in the DOM. + +use crate::element::{Element, ElementBuilder}; +use crate::error::Result; + +use std::io::Write; + +use quick_xml::Writer as EventWriter; +use quick_xml::events::{Event, BytesText}; + +/// A node in an element tree. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Node { + /// An `Element`. + Element(Element), + /// A text node. + Text(String), + #[cfg(feature = "comments")] + /// A comment node. + Comment(String), +} + +impl Node { + /// Turns this into a reference to an `Element` if this is an element node. + /// Else this returns `None`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Node; + /// + /// let elm = Node::Element("".parse().unwrap()); + /// let txt = Node::Text("meow".to_owned()); + /// + /// assert_eq!(elm.as_element().unwrap().name(), "meow"); + /// assert_eq!(txt.as_element(), None); + /// ``` + pub fn as_element(&self) -> Option<&Element> { + match *self { + Node::Element(ref e) => Some(e), + Node::Text(_) => None, + #[cfg(feature = "comments")] + Node::Comment(_) => None, + } + } + + /// Turns this into a mutable reference of an `Element` if this is an element node. + /// Else this returns `None`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Node; + /// + /// let mut elm = Node::Element("".parse().unwrap()); + /// let mut txt = Node::Text("meow".to_owned()); + /// + /// assert_eq!(elm.as_element_mut().unwrap().name(), "meow"); + /// assert_eq!(txt.as_element_mut(), None); + /// ``` + pub fn as_element_mut(&mut self) -> Option<&mut Element> { + match *self { + Node::Element(ref mut e) => Some(e), + Node::Text(_) => None, + #[cfg(feature = "comments")] + Node::Comment(_) => None, + } + } + + /// Turns this into an `Element`, consuming self, if this is an element node. + /// Else this returns `None`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Node; + /// + /// let elm = Node::Element("".parse().unwrap()); + /// let txt = Node::Text("meow".to_owned()); + /// + /// assert_eq!(elm.into_element().unwrap().name(), "meow"); + /// assert_eq!(txt.into_element(), None); + /// ``` + pub fn into_element(self) -> Option { + match self { + Node::Element(e) => Some(e), + Node::Text(_) => None, + #[cfg(feature = "comments")] + Node::Comment(_) => None, + } + } + + /// Turns this into an `&str` if this is a text node. + /// Else this returns `None`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Node; + /// + /// let elm = Node::Element("".parse().unwrap()); + /// let txt = Node::Text("meow".to_owned()); + /// + /// assert_eq!(elm.as_text(), None); + /// assert_eq!(txt.as_text().unwrap(), "meow"); + /// ``` + pub fn as_text(&self) -> Option<&str> { + match *self { + Node::Element(_) => None, + Node::Text(ref s) => Some(s), + #[cfg(feature = "comments")] + Node::Comment(_) => None, + } + } + + /// Turns this into an `&mut String` if this is a text node. + /// Else this returns `None`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Node; + /// + /// let mut elm = Node::Element("".parse().unwrap()); + /// let mut txt = Node::Text("meow".to_owned()); + /// + /// assert_eq!(elm.as_text_mut(), None); + /// { + /// let text_mut = txt.as_text_mut().unwrap(); + /// assert_eq!(text_mut, "meow"); + /// text_mut.push_str("zies"); + /// assert_eq!(text_mut, "meowzies"); + /// } + /// assert_eq!(txt.as_text().unwrap(), "meowzies"); + /// ``` + pub fn as_text_mut(&mut self) -> Option<&mut String> { + match *self { + Node::Element(_) => None, + Node::Text(ref mut s) => Some(s), + #[cfg(feature = "comments")] + Node::Comment(_) => None, + } + } + + /// Turns this into an `String`, consuming self, if this is a text node. + /// Else this returns `None`. + /// + /// # Examples + /// + /// ```rust + /// use minidom::Node; + /// + /// let elm = Node::Element("".parse().unwrap()); + /// let txt = Node::Text("meow".to_owned()); + /// + /// assert_eq!(elm.into_text(), None); + /// assert_eq!(txt.into_text().unwrap(), "meow"); + /// ``` + pub fn into_text(self) -> Option { + match self { + Node::Element(_) => None, + Node::Text(s) => Some(s), + #[cfg(feature = "comments")] + Node::Comment(_) => None, + } + } + + #[doc(hidden)] + pub(crate) fn write_to_inner(&self, writer: &mut EventWriter) -> Result<()>{ + match *self { + Node::Element(ref elmt) => elmt.write_to_inner(writer)?, + Node::Text(ref s) => { + writer.write_event(Event::Text(BytesText::from_plain_str(s)))?; + }, + #[cfg(feature = "comments")] + Node::Comment(ref s) => { + writer.write_event(Event::Comment(BytesText::from_plain_str(s)))?; + }, + } + + Ok(()) + } +} + +impl From for Node { + fn from(elm: Element) -> Node { + Node::Element(elm) + } +} + +impl From for Node { + fn from(s: String) -> Node { + Node::Text(s) + } +} + +impl<'a> From<&'a str> for Node { + fn from(s: &'a str) -> Node { + Node::Text(s.to_owned()) + } +} + +impl From for Node { + fn from(builder: ElementBuilder) -> Node { + Node::Element(builder.build()) + } +} diff --git a/minidom-rs/src/tests.rs b/minidom-rs/src/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..046d8e5c817ac79febb208709c4f96b7671f8297 --- /dev/null +++ b/minidom-rs/src/tests.rs @@ -0,0 +1,251 @@ +use crate::element::Element; + +use quick_xml::Reader; + +const TEST_STRING: &'static str = r#"meownya"#; + +fn build_test_tree() -> Element { + let mut root = Element::builder("root") + .ns("root_ns") + .attr("xml:lang", "en") + .attr("a", "b") + .build(); + root.append_text_node("meow"); + let child = Element::builder("child") + .attr("c", "d") + .build(); + root.append_child(child); + let other_child = Element::builder("child") + .ns("child_ns") + .attr("d", "e") + .attr("xml:lang", "fr") + .build(); + root.append_child(other_child); + root.append_text_node("nya"); + root +} + +#[cfg(feature = "comments")] +const COMMENT_TEST_STRING: &'static str = r#""#; + +#[cfg(feature = "comments")] +fn build_comment_test_tree() -> Element { + let mut root = Element::builder("root").build(); + root.append_comment_node("This is a child."); + let mut child = Element::builder("child").attr("attr", "val").build(); + child.append_comment_node("This is a grandchild."); + let grand_child = Element::builder("grandchild").build(); + child.append_child(grand_child); + root.append_child(child); + root +} + +#[test] +fn reader_works() { + let mut reader = Reader::from_str(TEST_STRING); + assert_eq!(Element::from_reader(&mut reader).unwrap(), build_test_tree()); +} + +#[test] +fn writer_works() { + let root = build_test_tree(); + let mut writer = Vec::new(); + { + root.write_to(&mut writer).unwrap(); + } + assert_eq!(String::from_utf8(writer).unwrap(), TEST_STRING); +} + +#[test] +fn writer_escapes_attributes() { + let root = Element::builder("root") + .attr("a", "\"Air\" quotes") + .build(); + let mut writer = Vec::new(); + { + root.write_to(&mut writer).unwrap(); + } + assert_eq!(String::from_utf8(writer).unwrap(), + r#""# + ); +} + +#[test] +fn writer_escapes_text() { + let root = Element::builder("root") + .append("<3") + .build(); + let mut writer = Vec::new(); + { + root.write_to(&mut writer).unwrap(); + } + assert_eq!(String::from_utf8(writer).unwrap(), + r#"<3"# + ); +} + +#[test] +fn builder_works() { + let elem = Element::builder("a") + .ns("b") + .attr("c", "d") + .append(Element::builder("child")) + .append("e") + .build(); + assert_eq!(elem.name(), "a"); + assert_eq!(elem.ns(), Some("b".to_owned())); + assert_eq!(elem.attr("c"), Some("d")); + assert_eq!(elem.attr("x"), None); + assert_eq!(elem.text(), "e"); + assert!(elem.has_child("child", "b")); + assert!(elem.is("a", "b")); +} + +#[test] +fn children_iter_works() { + let root = build_test_tree(); + let mut iter = root.children(); + assert!(iter.next().unwrap().is("child", "root_ns")); + assert!(iter.next().unwrap().is("child", "child_ns")); + assert_eq!(iter.next(), None); +} + +#[test] +fn get_child_works() { + let root = build_test_tree(); + assert_eq!(root.get_child("child", "inexistent_ns"), None); + assert_eq!(root.get_child("not_a_child", "root_ns"), None); + assert!(root.get_child("child", "root_ns").unwrap().is("child", "root_ns")); + assert!(root.get_child("child", "child_ns").unwrap().is("child", "child_ns")); + assert_eq!(root.get_child("child", "root_ns").unwrap().attr("c"), Some("d")); + assert_eq!(root.get_child("child", "child_ns").unwrap().attr("d"), Some("e")); +} + +#[test] +fn namespace_propagation_works() { + let mut root = Element::builder("root").ns("root_ns").build(); + let mut child = Element::bare("child"); + let grandchild = Element::bare("grandchild"); + child.append_child(grandchild); + root.append_child(child); + + assert_eq!(root.get_child("child", "root_ns").unwrap().ns(), root.ns()); + assert_eq!(root.get_child("child", "root_ns").unwrap() + .get_child("grandchild", "root_ns").unwrap() + .ns(), root.ns()); +} + +#[test] +fn two_elements_with_same_arguments_different_order_are_equal() { + let elem1: Element = "".parse().unwrap(); + let elem2: Element = "".parse().unwrap(); + assert_eq!(elem1, elem2); + + let elem1: Element = "".parse().unwrap(); + let elem2: Element = "".parse().unwrap(); + assert_ne!(elem1, elem2); +} + +#[test] +fn namespace_attributes_works() { + let mut reader = Reader::from_str(TEST_STRING); + let root = Element::from_reader(&mut reader).unwrap(); + assert_eq!("en", root.attr("xml:lang").unwrap()); + assert_eq!("fr", root.get_child("child", "child_ns").unwrap().attr("xml:lang").unwrap()); +} + +#[test] +fn wrongly_closed_elements_error() { + let elem1 = "".parse::(); + assert!(elem1.is_err()); + let elem1 = "".parse::(); + assert!(elem1.is_err()); + let elem1 = "".parse::(); + assert!(elem1.is_ok()); +} + +#[test] +fn namespace_simple() { + let elem: Element = "".parse().unwrap(); + assert_eq!(elem.name(), "message"); + assert_eq!(elem.ns(), Some("jabber:client".to_owned())); +} + +#[test] +fn namespace_prefixed() { + let elem: Element = "" + .parse().unwrap(); + assert_eq!(elem.name(), "features"); + assert_eq!(elem.ns(), Some("http://etherx.jabber.org/streams".to_owned())); +} + +#[test] +fn namespace_inherited_simple() { + let elem: Element = "".parse().unwrap(); + assert_eq!(elem.name(), "stream"); + assert_eq!(elem.ns(), Some("jabber:client".to_owned())); + let child = elem.children().next().unwrap(); + assert_eq!(child.name(), "message"); + assert_eq!(child.ns(), Some("jabber:client".to_owned())); +} + +#[test] +fn namespace_inherited_prefixed1() { + let elem: Element = "" + .parse().unwrap(); + assert_eq!(elem.name(), "features"); + assert_eq!(elem.ns(), Some("http://etherx.jabber.org/streams".to_owned())); + let child = elem.children().next().unwrap(); + assert_eq!(child.name(), "message"); + assert_eq!(child.ns(), Some("jabber:client".to_owned())); +} + +#[test] +fn namespace_inherited_prefixed2() { + let elem: Element = "" + .parse().unwrap(); + assert_eq!(elem.name(), "stream"); + assert_eq!(elem.ns(), Some("http://etherx.jabber.org/streams".to_owned())); + let child = elem.children().next().unwrap(); + assert_eq!(child.name(), "message"); + assert_eq!(child.ns(), Some("jabber:client".to_owned())); +} + +#[cfg(feature = "comments")] +#[test] +fn read_comments() { + let mut reader = Reader::from_str(COMMENT_TEST_STRING); + assert_eq!(Element::from_reader(&mut reader).unwrap(), build_comment_test_tree()); +} + +#[cfg(feature = "comments")] +#[test] +fn write_comments() { + let root = build_comment_test_tree(); + let mut writer = Vec::new(); + { + root.write_to(&mut writer).unwrap(); + } + assert_eq!(String::from_utf8(writer).unwrap(), COMMENT_TEST_STRING); +} + +#[test] +fn xml_error() { + match "".parse::() { + Err(crate::error::Error::XmlError(_)) => (), + err => panic!("No or wrong error: {:?}", err) + } + + match "() { + Err(crate::error::Error::XmlError(_)) => (), + err => panic!("No or wrong error: {:?}", err) + } +} + +#[test] +fn invalid_element_error() { + match "".parse::() { + Err(crate::error::Error::InvalidElement) => (), + err => panic!("No or wrong error: {:?}", err) + } +} diff --git a/tokio-xmpp/.gitignore b/tokio-xmpp/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..eb5a316cbd195d26e3f768c7dd8e1b47299e17f8 --- /dev/null +++ b/tokio-xmpp/.gitignore @@ -0,0 +1 @@ +target diff --git a/tokio-xmpp/.gitlab-ci.yml b/tokio-xmpp/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..ae087bde44b0e6eb8078985f13099bf76e212345 --- /dev/null +++ b/tokio-xmpp/.gitlab-ci.yml @@ -0,0 +1,14 @@ +stages: + - build +rust-latest: + stage: build + image: rust:latest + script: + - cargo build --verbose + - cargo test --verbose +rust-nightly: + stage: build + image: rustlang/rust:nightly + script: + - cargo build --verbose + - cargo test --verbose diff --git a/tokio-xmpp/Cargo.toml b/tokio-xmpp/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..0ea41d967d3a02046885d057b43822f7aeda240a --- /dev/null +++ b/tokio-xmpp/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "tokio-xmpp" +version = "1.0.1" +authors = ["Astro ", "Emmanuel Gil Peyrot ", "pep ", "O01eg "] +description = "Asynchronous XMPP for Rust with tokio" +license = "MPL-2.0" +homepage = "https://gitlab.com/xmpp-rs/tokio-xmpp" +repository = "https://gitlab.com/xmpp-rs/tokio-xmpp" +documentation = "https://docs.rs/tokio-xmpp" +categories = ["asynchronous", "network-programming"] +keywords = ["xmpp", "tokio"] +edition = "2018" + +[dependencies] +bytes = "0.4" +futures = "0.1" +idna = "0.2" +native-tls = "0.2" +sasl = "0.4" +tokio = "0.1" +tokio-codec = "0.1" +trust-dns-resolver = "0.12" +trust-dns-proto = "0.8" +tokio-io = "0.1" +tokio-tls = "0.2" +quick-xml = "0.17" +xml5ever = "0.15" +xmpp-parsers = "0.16" diff --git a/tokio-xmpp/README.md b/tokio-xmpp/README.md new file mode 100644 index 0000000000000000000000000000000000000000..eccc836f75a474b8eba96906767dfb7cdc5af3e4 --- /dev/null +++ b/tokio-xmpp/README.md @@ -0,0 +1,6 @@ +# TODO + +- [ ] minidom ns +- [ ] replace debug output with log crate +- [ ] customize tls verify? +- [ ] more tests diff --git a/tokio-xmpp/examples/contact_addr.rs b/tokio-xmpp/examples/contact_addr.rs new file mode 100644 index 0000000000000000000000000000000000000000..f60601a8cdacad09d2b0a5577e89e8d6591b0b4b --- /dev/null +++ b/tokio-xmpp/examples/contact_addr.rs @@ -0,0 +1,130 @@ +use futures::{future, Sink, Stream}; +use std::convert::TryFrom; +use std::env::args; +use std::process::exit; +use tokio::runtime::current_thread::Runtime; +use tokio_xmpp::{Client, xmpp_codec::Packet}; +use xmpp_parsers::{ + Element, + Jid, + ns, + iq::{ + Iq, + IqType, + }, + disco::{ + DiscoInfoResult, + DiscoInfoQuery, + }, + server_info::ServerInfo, +}; + +fn main() { + let args: Vec = args().collect(); + if args.len() != 4 { + println!("Usage: {} ", args[0]); + exit(1); + } + let jid = &args[1]; + let password = &args[2]; + let target = &args[3]; + + // tokio_core context + let mut rt = Runtime::new().unwrap(); + // Client instance + let client = Client::new(jid, password).unwrap(); + + // Make the two interfaces for sending and receiving independent + // of each other so we can move one into a closure. + let (mut sink, stream) = client.split(); + // Wrap sink in Option so that we can take() it for the send(self) + // to consume and return it back when ready. + let mut send = move |packet| { + sink.start_send(packet).expect("start_send"); + }; + // Main loop, processes events + let mut wait_for_stream_end = false; + let done = stream.for_each(|event| { + if wait_for_stream_end { + /* Do Nothing. */ + } else if event.is_online() { + println!("Online!"); + + let target_jid: Jid = target.clone().parse().unwrap(); + let iq = make_disco_iq(target_jid); + println!("Sending disco#info request to {}", target.clone()); + println!(">> {}", String::from(&iq)); + send(Packet::Stanza(iq)); + } else if let Some(stanza) = event.into_stanza() { + if stanza.is("iq", "jabber:client") { + let iq = Iq::try_from(stanza).unwrap(); + if let IqType::Result(Some(payload)) = iq.payload { + if payload.is("query", ns::DISCO_INFO) { + if let Ok(disco_info) = DiscoInfoResult::try_from(payload) { + for ext in disco_info.extensions { + if let Ok(server_info) = ServerInfo::try_from(ext) { + print_server_info(server_info); + wait_for_stream_end = true; + send(Packet::StreamEnd); + } + } + } + } + } + } + } + + Box::new(future::ok(())) + }); + + // Start polling `done` + match rt.block_on(done) { + Ok(_) => (), + Err(e) => { + println!("Fatal: {}", e); + () + } + } +} + +fn make_disco_iq(target: Jid) -> Element { + Iq::from_get("disco", DiscoInfoQuery { node: None }) + .with_id(String::from("contact")) + .with_to(target) + .into() +} + +fn convert_field(field: Vec) -> String { + field.iter() + .fold((field.len(), String::new()), |(l, mut acc), s| { + acc.push('<'); + acc.push_str(&s); + acc.push('>'); + if l > 1 { + acc.push(','); + acc.push(' '); + } + (0, acc) + }).1 +} + +fn print_server_info(server_info: ServerInfo) { + if server_info.abuse.len() != 0 { + println!("abuse: {}", convert_field(server_info.abuse)); + } + if server_info.admin.len() != 0 { + println!("admin: {}", convert_field(server_info.admin)); + } + if server_info.feedback.len() != 0 { + println!("feedback: {}", convert_field(server_info.feedback)); + } + if server_info.sales.len() != 0 { + println!("sales: {}", convert_field(server_info.sales)); + } + if server_info.security.len() != 0 { + println!("security: {}", convert_field(server_info.security)); + } + if server_info.support.len() != 0 { + println!("support: {}", convert_field(server_info.support)); + } +} diff --git a/tokio-xmpp/examples/download_avatars.rs b/tokio-xmpp/examples/download_avatars.rs new file mode 100644 index 0000000000000000000000000000000000000000..ec27b6e5a9ad97285735a4409179aea22d50d2a6 --- /dev/null +++ b/tokio-xmpp/examples/download_avatars.rs @@ -0,0 +1,232 @@ +use futures::{future, Future, Sink, Stream}; +use std::convert::TryFrom; +use std::env::args; +use std::fs::{create_dir_all, File}; +use std::io::{self, Write}; +use std::process::exit; +use std::str::FromStr; +use tokio::runtime::current_thread::Runtime; +use tokio_xmpp::{Client, Packet}; +use xmpp_parsers::{ + avatar::{Data as AvatarData, Metadata as AvatarMetadata}, + caps::{compute_disco, hash_caps, Caps}, + disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity}, + hashes::Algo, + iq::{Iq, IqType}, + message::Message, + ns, + presence::{Presence, Type as PresenceType}, + pubsub::{ + event::PubSubEvent, + pubsub::{Items, PubSub}, + NodeName, + }, + stanza_error::{StanzaError, ErrorType, DefinedCondition}, + Jid, +}; + +fn main() { + let args: Vec = args().collect(); + if args.len() != 3 { + println!("Usage: {} ", args[0]); + exit(1); + } + let jid = &args[1]; + let password = &args[2]; + + // tokio_core context + let mut rt = Runtime::new().unwrap(); + // Client instance + let client = Client::new(jid, password).unwrap(); + + // Make the two interfaces for sending and receiving independent + // of each other so we can move one into a closure. + let (sink, stream) = client.split(); + + // Create outgoing pipe + let (mut tx, rx) = futures::unsync::mpsc::unbounded(); + rt.spawn( + rx.forward( + sink.sink_map_err(|_| panic!("Pipe")) + ) + .map(|(rx, mut sink)| { + drop(rx); + let _ = sink.close(); + }) + .map_err(|e| { + panic!("Send error: {:?}", e); + }) + ); + + let disco_info = make_disco(); + + // Main loop, processes events + let mut wait_for_stream_end = false; + let done = stream.for_each(move |event| { + // Helper function to send an iq error. + let mut send_error = |to, id, type_, condition, text: &str| { + let error = StanzaError::new(type_, condition, "en", text); + let iq = Iq::from_error(id, error) + .with_to(to); + tx.start_send(Packet::Stanza(iq.into())).unwrap(); + }; + + if wait_for_stream_end { + /* Do nothing */ + } else if event.is_online() { + println!("Online!"); + + let caps = get_disco_caps(&disco_info, "https://gitlab.com/xmpp-rs/tokio-xmpp"); + let presence = make_presence(caps); + tx.start_send(Packet::Stanza(presence.into())).unwrap(); + } else if let Some(stanza) = event.into_stanza() { + if stanza.is("iq", "jabber:client") { + let iq = Iq::try_from(stanza).unwrap(); + if let IqType::Get(payload) = iq.payload { + if payload.is("query", ns::DISCO_INFO) { + let query = DiscoInfoQuery::try_from(payload); + match query { + Ok(query) => { + let mut disco = disco_info.clone(); + disco.node = query.node; + let iq = Iq::from_result(iq.id, Some(disco)) + .with_to(iq.from.unwrap()); + tx.start_send(Packet::Stanza(iq.into())).unwrap(); + }, + Err(err) => { + send_error(iq.from.unwrap(), iq.id, ErrorType::Modify, DefinedCondition::BadRequest, &format!("{}", err)); + }, + } + } else { + // We MUST answer unhandled get iqs with a service-unavailable error. + send_error(iq.from.unwrap(), iq.id, ErrorType::Cancel, DefinedCondition::ServiceUnavailable, "No handler defined for this kind of iq."); + } + } else if let IqType::Result(Some(payload)) = iq.payload { + if payload.is("pubsub", ns::PUBSUB) { + let pubsub = PubSub::try_from(payload).unwrap(); + let from = + iq.from.clone().unwrap_or(Jid::from_str(jid).unwrap()); + handle_iq_result(pubsub, &from); + } + } else if let IqType::Set(_) = iq.payload { + // We MUST answer unhandled set iqs with a service-unavailable error. + send_error(iq.from.unwrap(), iq.id, ErrorType::Cancel, DefinedCondition::ServiceUnavailable, "No handler defined for this kind of iq."); + } + } else if stanza.is("message", "jabber:client") { + let message = Message::try_from(stanza).unwrap(); + let from = message.from.clone().unwrap(); + if let Some(body) = message.get_best_body(vec!["en"]) { + if body.1 .0 == "die" { + println!("Secret die command triggered by {}", from); + wait_for_stream_end = true; + tx.start_send(Packet::StreamEnd).unwrap(); + } + } + for child in message.payloads { + if child.is("event", ns::PUBSUB_EVENT) { + let event = PubSubEvent::try_from(child).unwrap(); + if let PubSubEvent::PublishedItems { node, items } = event { + if node.0 == ns::AVATAR_METADATA { + for item in items.into_iter() { + let payload = item.payload.clone().unwrap(); + if payload.is("metadata", ns::AVATAR_METADATA) { + // TODO: do something with these metadata. + let _metadata = AvatarMetadata::try_from(payload).unwrap(); + println!( + "{} has published an avatar, downloading...", + from.clone() + ); + let iq = download_avatar(from.clone()); + tx.start_send(Packet::Stanza(iq.into())).unwrap(); + } + } + } + } + } + } + } else if stanza.is("presence", "jabber:client") { + // Nothing to do here. + } else { + panic!("Unknown stanza: {}", String::from(&stanza)); + } + } + + future::ok(()) + }); + + // Start polling `done` + match rt.block_on(done) { + Ok(_) => (), + Err(e) => { + println!("Fatal: {}", e); + () + } + } +} + +fn make_disco() -> DiscoInfoResult { + let identities = vec![Identity::new("client", "bot", "en", "tokio-xmpp")]; + let features = vec![ + Feature::new(ns::DISCO_INFO), + Feature::new(format!("{}+notify", ns::AVATAR_METADATA)), + ]; + DiscoInfoResult { + node: None, + identities, + features, + extensions: vec![], + } +} + +fn get_disco_caps(disco: &DiscoInfoResult, node: &str) -> Caps { + let caps_data = compute_disco(disco); + let hash = hash_caps(&caps_data, Algo::Sha_1).unwrap(); + Caps::new(node, hash) +} + +// Construct a +fn make_presence(caps: Caps) -> Presence { + let mut presence = Presence::new(PresenceType::None) + .with_priority(-1); + presence.set_status("en", "Downloading avatars."); + presence.add_payload(caps); + presence +} + +fn download_avatar(from: Jid) -> Iq { + Iq::from_get("coucou", PubSub::Items(Items { + max_items: None, + node: NodeName(String::from(ns::AVATAR_DATA)), + subid: None, + items: Vec::new(), + })) + .with_to(from) +} + +fn handle_iq_result(pubsub: PubSub, from: &Jid) { + if let PubSub::Items(items) = pubsub { + if items.node.0 == ns::AVATAR_DATA { + for item in items.items { + match (item.id.clone(), item.payload.clone()) { + (Some(id), Some(payload)) => { + let data = AvatarData::try_from(payload).unwrap(); + save_avatar(from, id.0, &data.data).unwrap(); + } + _ => {} + } + } + } + } +} + +fn save_avatar(from: &Jid, id: String, data: &[u8]) -> io::Result<()> { + let directory = format!("data/{}", from); + let filename = format!("data/{}/{}", from, id); + println!( + "Saving avatar from {} to {}.", + from, filename + ); + create_dir_all(directory)?; + let mut file = File::create(filename)?; + file.write_all(data) +} diff --git a/tokio-xmpp/examples/echo_bot.rs b/tokio-xmpp/examples/echo_bot.rs new file mode 100644 index 0000000000000000000000000000000000000000..a4144bf4f695475fc174bdc735867fd9a19a41c0 --- /dev/null +++ b/tokio-xmpp/examples/echo_bot.rs @@ -0,0 +1,106 @@ +use futures::{future, Future, Sink, Stream}; +use std::convert::TryFrom; +use std::env::args; +use std::process::exit; +use tokio::runtime::current_thread::Runtime; +use tokio_xmpp::{Client, Packet}; +use xmpp_parsers::{Jid, Element}; +use xmpp_parsers::message::{Body, Message, MessageType}; +use xmpp_parsers::presence::{Presence, Show as PresenceShow, Type as PresenceType}; + +fn main() { + let args: Vec = args().collect(); + if args.len() != 3 { + println!("Usage: {} ", args[0]); + exit(1); + } + let jid = &args[1]; + let password = &args[2]; + + // tokio_core context + let mut rt = Runtime::new().unwrap(); + // Client instance + let client = Client::new(jid, password).unwrap(); + + // Make the two interfaces for sending and receiving independent + // of each other so we can move one into a closure. + let (sink, stream) = client.split(); + + // Create outgoing pipe + let (mut tx, rx) = futures::unsync::mpsc::unbounded(); + rt.spawn( + rx.forward( + sink.sink_map_err(|_| panic!("Pipe")) + ) + .map(|(rx, mut sink)| { + drop(rx); + let _ = sink.close(); + }) + .map_err(|e| { + panic!("Send error: {:?}", e); + }) + ); + + // Main loop, processes events + let mut wait_for_stream_end = false; + let done = stream.for_each(move |event| { + if wait_for_stream_end { + /* Do nothing */ + } else if event.is_online() { + let jid = event.get_jid() + .map(|jid| format!("{}", jid)) + .unwrap_or("unknown".to_owned()); + println!("Online at {}", jid); + + let presence = make_presence(); + tx.start_send(Packet::Stanza(presence)).unwrap(); + } else if let Some(message) = event + .into_stanza() + .and_then(|stanza| Message::try_from(stanza).ok()) + { + match (message.from, message.bodies.get("")) { + (Some(ref from), Some(ref body)) if body.0 == "die" => { + println!("Secret die command triggered by {}", from); + wait_for_stream_end = true; + tx.start_send(Packet::StreamEnd).unwrap(); + } + (Some(ref from), Some(ref body)) => { + if message.type_ != MessageType::Error { + // This is a message we'll echo + let reply = make_reply(from.clone(), &body.0); + tx.start_send(Packet::Stanza(reply)).unwrap(); + } + } + _ => {} + } + } + + future::ok(()) + }); + + // Start polling `done` + match rt.block_on(done) { + Ok(_) => (), + Err(e) => { + println!("Fatal: {}", e); + () + } + } +} + +// Construct a +fn make_presence() -> Element { + let mut presence = Presence::new(PresenceType::None); + presence.show = Some(PresenceShow::Chat); + presence + .statuses + .insert(String::from("en"), String::from("Echoing messages.")); + presence.into() +} + +// Construct a chat +fn make_reply(to: Jid, body: &str) -> Element { + let mut message = Message::new(Some(to)); + message.bodies.insert(String::new(), Body(body.to_owned())); + message.into() +} diff --git a/tokio-xmpp/examples/echo_component.rs b/tokio-xmpp/examples/echo_component.rs new file mode 100644 index 0000000000000000000000000000000000000000..6a8bc33c8fbd09ca93e0bac20516de5eda72975c --- /dev/null +++ b/tokio-xmpp/examples/echo_component.rs @@ -0,0 +1,99 @@ +use futures::{future, Sink, Stream}; +use std::convert::TryFrom; +use std::env::args; +use std::process::exit; +use std::str::FromStr; +use tokio::runtime::current_thread::Runtime; +use tokio_xmpp::Component; +use xmpp_parsers::{Jid, Element}; +use xmpp_parsers::message::{Body, Message, MessageType}; +use xmpp_parsers::presence::{Presence, Show as PresenceShow, Type as PresenceType}; + +fn main() { + let args: Vec = args().collect(); + if args.len() < 3 || args.len() > 5 { + println!("Usage: {} [server] [port]", args[0]); + exit(1); + } + let jid = &args[1]; + let password = &args[2]; + let server = &args + .get(3) + .unwrap() + .parse() + .unwrap_or("127.0.0.1".to_owned()); + let port: u16 = args.get(4).unwrap().parse().unwrap_or(5347u16); + + // tokio_core context + let mut rt = Runtime::new().unwrap(); + // Component instance + println!("{} {} {} {}", jid, password, server, port); + let component = Component::new(jid, password, server, port).unwrap(); + + // Make the two interfaces for sending and receiving independent + // of each other so we can move one into a closure. + println!("Got it: {}", component.jid.clone()); + let (mut sink, stream) = component.split(); + // Wrap sink in Option so that we can take() it for the send(self) + // to consume and return it back when ready. + let mut send = move |stanza| { + sink.start_send(stanza).expect("start_send"); + }; + // Main loop, processes events + let done = stream.for_each(|event| { + if event.is_online() { + println!("Online!"); + + // TODO: replace these hardcoded JIDs + let presence = make_presence( + Jid::from_str("test@component.linkmauve.fr/coucou").unwrap(), + Jid::from_str("linkmauve@linkmauve.fr").unwrap(), + ); + send(presence); + } else if let Some(message) = event + .into_stanza() + .and_then(|stanza| Message::try_from(stanza).ok()) + { + // This is a message we'll echo + match (message.from, message.bodies.get("")) { + (Some(from), Some(body)) => { + if message.type_ != MessageType::Error { + let reply = make_reply(from, &body.0); + send(reply); + } + } + _ => (), + } + } + + Box::new(future::ok(())) + }); + + // Start polling `done` + match rt.block_on(done) { + Ok(_) => (), + Err(e) => { + println!("Fatal: {}", e); + () + } + } +} + +// Construct a +fn make_presence(from: Jid, to: Jid) -> Element { + let mut presence = Presence::new(PresenceType::None); + presence.from = Some(from); + presence.to = Some(to); + presence.show = Some(PresenceShow::Chat); + presence + .statuses + .insert(String::from("en"), String::from("Echoing messages.")); + presence.into() +} + +// Construct a chat +fn make_reply(to: Jid, body: &str) -> Element { + let mut message = Message::new(Some(to)); + message.bodies.insert(String::new(), Body(body.to_owned())); + message.into() +} diff --git a/tokio-xmpp/logo.svg b/tokio-xmpp/logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..2e15a475a809525bbe1b0492a7e17ee840587287 --- /dev/null +++ b/tokio-xmpp/logo.svg @@ -0,0 +1,172 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tokio-xmpp/src/client/auth.rs b/tokio-xmpp/src/client/auth.rs new file mode 100644 index 0000000000000000000000000000000000000000..096a0bfbca3070dea65c31942c55fc5d8dce2e99 --- /dev/null +++ b/tokio-xmpp/src/client/auth.rs @@ -0,0 +1,116 @@ +use std::str::FromStr; +use std::collections::HashSet; +use std::convert::TryFrom; +use futures::{Future, Poll, Stream, future::{ok, err, IntoFuture}}; +use sasl::client::mechanisms::{Anonymous, Plain, Scram}; +use sasl::client::Mechanism; +use sasl::common::scram::{Sha1, Sha256}; +use sasl::common::Credentials; +use tokio_io::{AsyncRead, AsyncWrite}; +use xmpp_parsers::sasl::{Auth, Challenge, Failure, Mechanism as XMPPMechanism, Response, Success}; + +use crate::xmpp_codec::Packet; +use crate::xmpp_stream::XMPPStream; +use crate::{AuthError, Error, ProtocolError}; + +const NS_XMPP_SASL: &str = "urn:ietf:params:xml:ns:xmpp-sasl"; + +pub struct ClientAuth { + future: Box, Error = Error>>, +} + +impl ClientAuth { + pub fn new(stream: XMPPStream, creds: Credentials) -> Result { + let local_mechs: Vec Box>> = vec![ + Box::new(|| Box::new(Scram::::from_credentials(creds.clone()).unwrap())), + Box::new(|| Box::new(Scram::::from_credentials(creds.clone()).unwrap())), + Box::new(|| Box::new(Plain::from_credentials(creds.clone()).unwrap())), + Box::new(|| Box::new(Anonymous::new())), + ]; + + let remote_mechs: HashSet = stream + .stream_features + .get_child("mechanisms", NS_XMPP_SASL) + .ok_or(AuthError::NoMechanism)? + .children() + .filter(|child| child.is("mechanism", NS_XMPP_SASL)) + .map(|mech_el| mech_el.text()) + .collect(); + + for local_mech in local_mechs { + let mut mechanism = local_mech(); + if remote_mechs.contains(mechanism.name()) { + let initial = mechanism.initial().map_err(AuthError::Sasl)?; + let mechanism_name = XMPPMechanism::from_str(mechanism.name()).map_err(ProtocolError::Parsers)?; + + let send_initial = Box::new(stream.send_stanza(Auth { + mechanism: mechanism_name, + data: initial, + })) + .map_err(Error::Io); + let future = Box::new(send_initial.and_then( + |stream| Self::handle_challenge(stream, mechanism) + ).and_then( + |stream| stream.restart() + )); + return Ok(ClientAuth { + future, + }); + } + } + + Err(AuthError::NoMechanism)? + } + + fn handle_challenge(stream: XMPPStream, mut mechanism: Box) -> Box, Error = Error>> { + Box::new( + stream.into_future() + .map_err(|(e, _stream)| e.into()) + .and_then(|(stanza, stream)| { + match stanza { + Some(Packet::Stanza(stanza)) => { + if let Ok(challenge) = Challenge::try_from(stanza.clone()) { + let response = mechanism + .response(&challenge.data); + Box::new( + response + .map_err(|e| AuthError::Sasl(e).into()) + .into_future() + .and_then(|response| { + // Send response and loop + stream.send_stanza(Response { data: response }) + .map_err(Error::Io) + .and_then(|stream| Self::handle_challenge(stream, mechanism)) + }) + ) + } else if let Ok(_) = Success::try_from(stanza.clone()) { + Box::new(ok(stream)) + } else if let Ok(failure) = Failure::try_from(stanza.clone()) { + Box::new(err(Error::Auth(AuthError::Fail(failure.defined_condition)))) + } else if stanza.name() == "failure" { + // Workaround for https://gitlab.com/xmpp-rs/xmpp-parsers/merge_requests/1 + Box::new(err(Error::Auth(AuthError::Sasl("failure".to_string())))) + } else { + // ignore and loop + Self::handle_challenge(stream, mechanism) + } + } + Some(_) => { + // ignore and loop + Self::handle_challenge(stream, mechanism) + } + None => Box::new(err(Error::Disconnected)) + } + }) + ) + } +} + +impl Future for ClientAuth { + type Item = XMPPStream; + type Error = Error; + + fn poll(&mut self) -> Poll { + self.future.poll() + } +} diff --git a/tokio-xmpp/src/client/bind.rs b/tokio-xmpp/src/client/bind.rs new file mode 100644 index 0000000000000000000000000000000000000000..f7d1828740dff5bfde15e22c01a4cba3ecfbdd6d --- /dev/null +++ b/tokio-xmpp/src/client/bind.rs @@ -0,0 +1,102 @@ +use futures::{sink, Async, Future, Poll, Stream}; +use std::convert::TryFrom; +use std::mem::replace; +use tokio_io::{AsyncRead, AsyncWrite}; +use xmpp_parsers::Jid; +use xmpp_parsers::bind::{BindQuery, BindResponse}; +use xmpp_parsers::iq::{Iq, IqType}; + +use crate::xmpp_codec::Packet; +use crate::xmpp_stream::XMPPStream; +use crate::{Error, ProtocolError}; + +const NS_XMPP_BIND: &str = "urn:ietf:params:xml:ns:xmpp-bind"; +const BIND_REQ_ID: &str = "resource-bind"; + +pub enum ClientBind { + Unsupported(XMPPStream), + WaitSend(sink::Send>), + WaitRecv(XMPPStream), + Invalid, +} + +impl ClientBind { + /// Consumes and returns the stream to express that you cannot use + /// the stream for anything else until the resource binding + /// req/resp are done. + pub fn new(stream: XMPPStream) -> Self { + match stream.stream_features.get_child("bind", NS_XMPP_BIND) { + None => + // No resource binding available, + // return the (probably // usable) stream immediately + { + ClientBind::Unsupported(stream) + } + Some(_) => { + let resource; + if let Jid::Full(jid) = stream.jid.clone() { + resource = Some(jid.resource); + } else { + resource = None; + } + let iq = Iq::from_set(BIND_REQ_ID, BindQuery::new(resource)); + let send = stream.send_stanza(iq); + ClientBind::WaitSend(send) + } + } + } +} + +impl Future for ClientBind { + type Item = XMPPStream; + type Error = Error; + + fn poll(&mut self) -> Poll { + let state = replace(self, ClientBind::Invalid); + + match state { + ClientBind::Unsupported(stream) => Ok(Async::Ready(stream)), + ClientBind::WaitSend(mut send) => match send.poll() { + Ok(Async::Ready(stream)) => { + replace(self, ClientBind::WaitRecv(stream)); + self.poll() + } + Ok(Async::NotReady) => { + replace(self, ClientBind::WaitSend(send)); + Ok(Async::NotReady) + } + Err(e) => Err(e)?, + }, + ClientBind::WaitRecv(mut stream) => match stream.poll() { + Ok(Async::Ready(Some(Packet::Stanza(stanza)))) => match Iq::try_from(stanza) { + Ok(iq) => { + if iq.id == BIND_REQ_ID { + match iq.payload { + IqType::Result(payload) => { + payload + .and_then(|payload| BindResponse::try_from(payload).ok()) + .map(|bind| stream.jid = bind.into()); + Ok(Async::Ready(stream)) + } + _ => Err(ProtocolError::InvalidBindResponse)?, + } + } else { + Ok(Async::NotReady) + } + } + _ => Ok(Async::NotReady), + }, + Ok(Async::Ready(_)) => { + replace(self, ClientBind::WaitRecv(stream)); + self.poll() + } + Ok(Async::NotReady) => { + replace(self, ClientBind::WaitRecv(stream)); + Ok(Async::NotReady) + } + Err(e) => Err(e)?, + }, + ClientBind::Invalid => unreachable!(), + } + } +} diff --git a/tokio-xmpp/src/client/mod.rs b/tokio-xmpp/src/client/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..bcd07c954cac2f53b4571544fcefed19b6ce1c73 --- /dev/null +++ b/tokio-xmpp/src/client/mod.rs @@ -0,0 +1,236 @@ +use futures::{done, Async, AsyncSink, Future, Poll, Sink, StartSend, Stream}; +use idna; +use xmpp_parsers::{Jid, JidParseError}; +use sasl::common::{ChannelBinding, Credentials}; +use std::mem::replace; +use std::str::FromStr; +use tokio::net::TcpStream; +use tokio_io::{AsyncRead, AsyncWrite}; +use tokio_tls::TlsStream; + +use super::event::Event; +use super::happy_eyeballs::Connecter; +use super::starttls::{StartTlsClient, NS_XMPP_TLS}; +use super::xmpp_codec::Packet; +use super::xmpp_stream; +use super::{Error, ProtocolError}; + +mod auth; +use self::auth::ClientAuth; +mod bind; +use self::bind::ClientBind; + +/// XMPP client connection and state +pub struct Client { + state: ClientState, +} + +type XMPPStream = xmpp_stream::XMPPStream>; +const NS_JABBER_CLIENT: &str = "jabber:client"; + +enum ClientState { + Invalid, + Disconnected, + Connecting(Box>), + Connected(XMPPStream), +} + +impl Client { + /// Start a new XMPP client + /// + /// Start polling the returned instance so that it will connect + /// and yield events. + pub fn new(jid: &str, password: &str) -> Result { + let jid = Jid::from_str(jid)?; + let client = Self::new_with_jid(jid, password); + Ok(client) + } + + /// Start a new client given that the JID is already parsed. + pub fn new_with_jid(jid: Jid, password: &str) -> Self { + let password = password.to_owned(); + let connect = Self::make_connect(jid, password.clone()); + let client = Client { + state: ClientState::Connecting(Box::new(connect)), + }; + client + } + + fn make_connect(jid: Jid, password: String) -> impl Future { + let username = jid.clone().node().unwrap(); + let jid1 = jid.clone(); + let jid2 = jid.clone(); + let password = password; + done(idna::domain_to_ascii(&jid.domain())) + .map_err(|_| Error::Idna) + .and_then(|domain| { + done(Connecter::from_lookup( + &domain, + Some("_xmpp-client._tcp"), + 5222, + )) + }) + .flatten() + .and_then(move |tcp_stream| { + xmpp_stream::XMPPStream::start(tcp_stream, jid1, NS_JABBER_CLIENT.to_owned()) + }) + .and_then(|xmpp_stream| { + if Self::can_starttls(&xmpp_stream) { + Ok(Self::starttls(xmpp_stream)) + } else { + Err(Error::Protocol(ProtocolError::NoTls)) + } + }) + .flatten() + .and_then(|tls_stream| XMPPStream::start(tls_stream, jid2, NS_JABBER_CLIENT.to_owned())) + .and_then( + move |xmpp_stream| done(Self::auth(xmpp_stream, username, password)), // TODO: flatten? + ) + .and_then(|auth| auth) + .and_then(|xmpp_stream| Self::bind(xmpp_stream)) + .and_then(|xmpp_stream| { + // println!("Bound to {}", xmpp_stream.jid); + Ok(xmpp_stream) + }) + } + + fn can_starttls(stream: &xmpp_stream::XMPPStream) -> bool { + stream + .stream_features + .get_child("starttls", NS_XMPP_TLS) + .is_some() + } + + fn starttls( + stream: xmpp_stream::XMPPStream, + ) -> StartTlsClient { + StartTlsClient::from_stream(stream) + } + + fn auth( + stream: xmpp_stream::XMPPStream, + username: String, + password: String, + ) -> Result, Error> { + let creds = Credentials::default() + .with_username(username) + .with_password(password) + .with_channel_binding(ChannelBinding::None); + ClientAuth::new(stream, creds) + } + + fn bind(stream: xmpp_stream::XMPPStream) -> ClientBind { + ClientBind::new(stream) + } + + /// Get the client's bound JID (the one reported by the XMPP + /// server). + pub fn bound_jid(&self) -> Option<&Jid> { + match self.state { + ClientState::Connected(ref stream) => Some(&stream.jid), + _ => None, + } + } +} + +impl Stream for Client { + type Item = Event; + type Error = Error; + + fn poll(&mut self) -> Poll, Self::Error> { + let state = replace(&mut self.state, ClientState::Invalid); + + match state { + ClientState::Invalid => Err(Error::InvalidState), + ClientState::Disconnected => Ok(Async::Ready(None)), + ClientState::Connecting(mut connect) => match connect.poll() { + Ok(Async::Ready(stream)) => { + let jid = stream.jid.clone(); + self.state = ClientState::Connected(stream); + Ok(Async::Ready(Some(Event::Online(jid)))) + } + Ok(Async::NotReady) => { + self.state = ClientState::Connecting(connect); + Ok(Async::NotReady) + } + Err(e) => Err(e), + }, + ClientState::Connected(mut stream) => { + // Poll sink + match stream.poll_complete() { + Ok(Async::NotReady) => (), + Ok(Async::Ready(())) => (), + Err(e) => return Err(e)?, + }; + + // Poll stream + match stream.poll() { + Ok(Async::Ready(None)) => { + // EOF + self.state = ClientState::Disconnected; + Ok(Async::Ready(Some(Event::Disconnected))) + } + Ok(Async::Ready(Some(Packet::Stanza(stanza)))) => { + // Receive stanza + self.state = ClientState::Connected(stream); + Ok(Async::Ready(Some(Event::Stanza(stanza)))) + } + Ok(Async::Ready(Some(Packet::Text(_)))) => { + // Ignore text between stanzas + Ok(Async::NotReady) + } + Ok(Async::Ready(Some(Packet::StreamStart(_)))) => { + // + Err(ProtocolError::InvalidStreamStart.into()) + } + Ok(Async::Ready(Some(Packet::StreamEnd))) => { + // End of stream: + Ok(Async::Ready(None)) + } + Ok(Async::NotReady) => { + // Try again later + self.state = ClientState::Connected(stream); + Ok(Async::NotReady) + } + Err(e) => Err(e)?, + } + } + } + } +} + +impl Sink for Client { + type SinkItem = Packet; + type SinkError = Error; + + fn start_send(&mut self, item: Self::SinkItem) -> StartSend { + match self.state { + ClientState::Connected(ref mut stream) => + Ok(stream.start_send(item)?), + _ => + Ok(AsyncSink::NotReady(item)), + } + } + + fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { + match self.state { + ClientState::Connected(ref mut stream) => stream.poll_complete().map_err(|e| e.into()), + _ => Ok(Async::Ready(())), + } + } + + /// This closes the inner TCP stream. + /// + /// To synchronize your shutdown with the server side, you should + /// first send `Packet::StreamEnd` and wait for the end of the + /// incoming stream before closing the connection. + fn close(&mut self) -> Poll<(), Self::SinkError> { + match self.state { + ClientState::Connected(ref mut stream) => + stream.close() + .map_err(|e| e.into()), + _ => + Ok(Async::Ready(())), + } + } +} diff --git a/tokio-xmpp/src/component/auth.rs b/tokio-xmpp/src/component/auth.rs new file mode 100644 index 0000000000000000000000000000000000000000..44104f4c19052fe1db703563fab894cf32c2d7ca --- /dev/null +++ b/tokio-xmpp/src/component/auth.rs @@ -0,0 +1,89 @@ +use futures::{sink, Async, Future, Poll, Stream}; +use std::mem::replace; +use tokio_io::{AsyncRead, AsyncWrite}; +use xmpp_parsers::component::Handshake; + +use crate::xmpp_codec::Packet; +use crate::xmpp_stream::XMPPStream; +use crate::{AuthError, Error}; + +const NS_JABBER_COMPONENT_ACCEPT: &str = "jabber:component:accept"; + +pub struct ComponentAuth { + state: ComponentAuthState, +} + +enum ComponentAuthState { + WaitSend(sink::Send>), + WaitRecv(XMPPStream), + Invalid, +} + +impl ComponentAuth { + // TODO: doesn't have to be a Result<> actually + pub fn new(stream: XMPPStream, password: String) -> Result { + // FIXME: huge hack, shouldn’t be an element! + let sid = stream.stream_features.name().to_owned(); + let mut this = ComponentAuth { + state: ComponentAuthState::Invalid, + }; + this.send( + stream, + Handshake::from_password_and_stream_id(&password, &sid), + ); + Ok(this) + } + + fn send(&mut self, stream: XMPPStream, handshake: Handshake) { + let nonza = handshake; + let send = stream.send_stanza(nonza); + + self.state = ComponentAuthState::WaitSend(send); + } +} + +impl Future for ComponentAuth { + type Item = XMPPStream; + type Error = Error; + + fn poll(&mut self) -> Poll { + let state = replace(&mut self.state, ComponentAuthState::Invalid); + + match state { + ComponentAuthState::WaitSend(mut send) => match send.poll() { + Ok(Async::Ready(stream)) => { + self.state = ComponentAuthState::WaitRecv(stream); + self.poll() + } + Ok(Async::NotReady) => { + self.state = ComponentAuthState::WaitSend(send); + Ok(Async::NotReady) + } + Err(e) => Err(e)?, + }, + ComponentAuthState::WaitRecv(mut stream) => match stream.poll() { + Ok(Async::Ready(Some(Packet::Stanza(ref stanza)))) + if stanza.is("handshake", NS_JABBER_COMPONENT_ACCEPT) => + { + self.state = ComponentAuthState::Invalid; + Ok(Async::Ready(stream)) + } + Ok(Async::Ready(Some(Packet::Stanza(ref stanza)))) + if stanza.is("error", "http://etherx.jabber.org/streams") => + { + Err(AuthError::ComponentFail.into()) + } + Ok(Async::Ready(_event)) => { + // println!("ComponentAuth ignore {:?}", _event); + Ok(Async::NotReady) + } + Ok(_) => { + self.state = ComponentAuthState::WaitRecv(stream); + Ok(Async::NotReady) + } + Err(e) => Err(e)?, + }, + ComponentAuthState::Invalid => unreachable!(), + } + } +} diff --git a/tokio-xmpp/src/component/mod.rs b/tokio-xmpp/src/component/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..d822e8839ae24993521f430191a62ac6fcd876d2 --- /dev/null +++ b/tokio-xmpp/src/component/mod.rs @@ -0,0 +1,163 @@ +//! Components in XMPP are services/gateways that are logged into an +//! XMPP server under a JID consisting of just a domain name. They are +//! allowed to use any user and resource identifiers in their stanzas. +use futures::{done, Async, AsyncSink, Future, Poll, Sink, StartSend, Stream}; +use xmpp_parsers::{Jid, JidParseError, Element}; +use std::mem::replace; +use std::str::FromStr; +use tokio::net::TcpStream; +use tokio_io::{AsyncRead, AsyncWrite}; + +use super::event::Event; +use super::happy_eyeballs::Connecter; +use super::xmpp_codec::Packet; +use super::xmpp_stream; +use super::Error; + +mod auth; +use self::auth::ComponentAuth; + +/// Component connection to an XMPP server +pub struct Component { + /// The component's Jabber-Id + pub jid: Jid, + state: ComponentState, +} + +type XMPPStream = xmpp_stream::XMPPStream; +const NS_JABBER_COMPONENT_ACCEPT: &str = "jabber:component:accept"; + +enum ComponentState { + Invalid, + Disconnected, + Connecting(Box>), + Connected(XMPPStream), +} + +impl Component { + /// Start a new XMPP component + /// + /// Start polling the returned instance so that it will connect + /// and yield events. + pub fn new(jid: &str, password: &str, server: &str, port: u16) -> Result { + let jid = Jid::from_str(jid)?; + let password = password.to_owned(); + let connect = Self::make_connect(jid.clone(), password, server, port); + Ok(Component { + jid, + state: ComponentState::Connecting(Box::new(connect)), + }) + } + + fn make_connect( + jid: Jid, + password: String, + server: &str, + port: u16, + ) -> impl Future { + let jid1 = jid.clone(); + let password = password; + done(Connecter::from_lookup(server, None, port)) + .flatten() + .and_then(move |tcp_stream| { + xmpp_stream::XMPPStream::start( + tcp_stream, + jid1, + NS_JABBER_COMPONENT_ACCEPT.to_owned(), + ) + }) + .and_then(move |xmpp_stream| Self::auth(xmpp_stream, password).expect("auth")) + } + + fn auth( + stream: xmpp_stream::XMPPStream, + password: String, + ) -> Result, Error> { + ComponentAuth::new(stream, password) + } +} + +impl Stream for Component { + type Item = Event; + type Error = Error; + + fn poll(&mut self) -> Poll, Self::Error> { + let state = replace(&mut self.state, ComponentState::Invalid); + + match state { + ComponentState::Invalid => Err(Error::InvalidState), + ComponentState::Disconnected => Ok(Async::Ready(None)), + ComponentState::Connecting(mut connect) => match connect.poll() { + Ok(Async::Ready(stream)) => { + self.state = ComponentState::Connected(stream); + Ok(Async::Ready(Some(Event::Online(self.jid.clone())))) + } + Ok(Async::NotReady) => { + self.state = ComponentState::Connecting(connect); + Ok(Async::NotReady) + } + Err(e) => Err(e), + }, + ComponentState::Connected(mut stream) => { + // Poll sink + match stream.poll_complete() { + Ok(Async::NotReady) => (), + Ok(Async::Ready(())) => (), + Err(e) => return Err(e)?, + }; + + // Poll stream + match stream.poll() { + Ok(Async::NotReady) => { + self.state = ComponentState::Connected(stream); + Ok(Async::NotReady) + } + Ok(Async::Ready(None)) => { + // EOF + self.state = ComponentState::Disconnected; + Ok(Async::Ready(Some(Event::Disconnected))) + } + Ok(Async::Ready(Some(Packet::Stanza(stanza)))) => { + self.state = ComponentState::Connected(stream); + Ok(Async::Ready(Some(Event::Stanza(stanza)))) + } + Ok(Async::Ready(_)) => { + self.state = ComponentState::Connected(stream); + Ok(Async::NotReady) + } + Err(e) => Err(e)?, + } + } + } + } +} + +impl Sink for Component { + type SinkItem = Element; + type SinkError = Error; + + fn start_send(&mut self, item: Self::SinkItem) -> StartSend { + match self.state { + ComponentState::Connected(ref mut stream) => match stream + .start_send(Packet::Stanza(item)) + { + Ok(AsyncSink::NotReady(Packet::Stanza(stanza))) => Ok(AsyncSink::NotReady(stanza)), + Ok(AsyncSink::NotReady(_)) => { + panic!("Component.start_send with stanza but got something else back") + } + Ok(AsyncSink::Ready) => Ok(AsyncSink::Ready), + Err(e) => Err(e)?, + }, + _ => Ok(AsyncSink::NotReady(item)), + } + } + + fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { + match &mut self.state { + &mut ComponentState::Connected(ref mut stream) => { + stream.poll_complete().map_err(|e| e.into()) + } + _ => Ok(Async::Ready(())), + } + } +} diff --git a/tokio-xmpp/src/error.rs b/tokio-xmpp/src/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..68a1412e9f4d2b87c91c392514e2ec5f7b1217c6 --- /dev/null +++ b/tokio-xmpp/src/error.rs @@ -0,0 +1,224 @@ +use native_tls::Error as TlsError; +use std::borrow::Cow; +use std::error::Error as StdError; +use std::fmt; +use std::io::Error as IoError; +use std::str::Utf8Error; +use trust_dns_proto::error::ProtoError; +use trust_dns_resolver::error::ResolveError; + +use xmpp_parsers::Error as ParsersError; +use xmpp_parsers::sasl::DefinedCondition as SaslDefinedCondition; + +/// Top-level error type +#[derive(Debug)] +pub enum Error { + /// I/O error + Io(IoError), + /// Error resolving DNS and establishing a connection + Connection(ConnecterError), + /// DNS label conversion error, no details available from module + /// `idna` + Idna, + /// Protocol-level error + Protocol(ProtocolError), + /// Authentication error + Auth(AuthError), + /// TLS error + Tls(TlsError), + /// Connection closed + Disconnected, + /// Shoud never happen + InvalidState, +} + +impl fmt::Display for Error { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::Io(e) => write!(fmt, "IO error: {}", e), + Error::Connection(e) => write!(fmt, "connection error: {}", e), + Error::Idna => write!(fmt, "IDNA error"), + Error::Protocol(e) => write!(fmt, "protocol error: {}", e), + Error::Auth(e) => write!(fmt, "authentication error: {}", e), + Error::Tls(e) => write!(fmt, "TLS error: {}", e), + Error::Disconnected => write!(fmt, "disconnected"), + Error::InvalidState => write!(fmt, "invalid state"), + } + } +} + +impl From for Error { + fn from(e: IoError) -> Self { + Error::Io(e) + } +} + +impl From for Error { + fn from(e: ConnecterError) -> Self { + Error::Connection(e) + } +} + +impl From for Error { + fn from(e: ProtocolError) -> Self { + Error::Protocol(e) + } +} + +impl From for Error { + fn from(e: AuthError) -> Self { + Error::Auth(e) + } +} + +impl From for Error { + fn from(e: TlsError) -> Self { + Error::Tls(e) + } +} + +/// Causes for stream parsing errors +#[derive(Debug)] +pub enum ParserError { + /// Encoding error + Utf8(Utf8Error), + /// XML parse error + Parse(ParseError), + /// Illegal `` + ShortTag, + /// Required by `impl Decoder` + Io(IoError), +} + +impl fmt::Display for ParserError { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + ParserError::Utf8(e) => write!(fmt, "UTF-8 error: {}", e), + ParserError::Parse(e) => write!(fmt, "parse error: {}", e), + ParserError::ShortTag => write!(fmt, "short tag"), + ParserError::Io(e) => write!(fmt, "IO error: {}", e), + } + } +} + +impl From for ParserError { + fn from(e: IoError) -> Self { + ParserError::Io(e) + } +} + +impl From for Error { + fn from(e: ParserError) -> Self { + ProtocolError::Parser(e).into() + } +} + +/// XML parse error wrapper type +#[derive(Debug)] +pub struct ParseError(pub Cow<'static, str>); + +impl StdError for ParseError { + fn description(&self) -> &str { + self.0.as_ref() + } + fn cause(&self) -> Option<&dyn StdError> { + None + } +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// XMPP protocol-level error +#[derive(Debug)] +pub enum ProtocolError { + /// XML parser error + Parser(ParserError), + /// Error with expected stanza schema + Parsers(ParsersError), + /// No TLS available + NoTls, + /// Invalid response to resource binding + InvalidBindResponse, + /// No xmlns attribute in + NoStreamNamespace, + /// No id attribute in + NoStreamId, + /// Encountered an unexpected XML token + InvalidToken, + /// Unexpected (shouldn't occur) + InvalidStreamStart, +} + +impl fmt::Display for ProtocolError { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + ProtocolError::Parser(e) => write!(fmt, "XML parser error: {}", e), + ProtocolError::Parsers(e) => write!(fmt, "error with expected stanza schema: {}", e), + ProtocolError::NoTls => write!(fmt, "no TLS available"), + ProtocolError::InvalidBindResponse => write!(fmt, "invalid response to resource binding"), + ProtocolError::NoStreamNamespace => write!(fmt, "no xmlns attribute in "), + ProtocolError::NoStreamId => write!(fmt, "no id attribute in "), + ProtocolError::InvalidToken => write!(fmt, "encountered an unexpected XML token"), + ProtocolError::InvalidStreamStart => write!(fmt, "unexpected "), + } + } +} + +impl From for ProtocolError { + fn from(e: ParserError) -> Self { + ProtocolError::Parser(e) + } +} + +impl From for ProtocolError { + fn from(e: ParsersError) -> Self { + ProtocolError::Parsers(e) + } +} + +/// Authentication error +#[derive(Debug)] +pub enum AuthError { + /// No matching SASL mechanism available + NoMechanism, + /// Local SASL implementation error + Sasl(String), + /// Failure from server + Fail(SaslDefinedCondition), + /// Component authentication failure + ComponentFail, +} + +impl fmt::Display for AuthError { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + AuthError::NoMechanism => write!(fmt, "no matching SASL mechanism available"), + AuthError::Sasl(s) => write!(fmt, "local SASL implementation error: {}", s), + AuthError::Fail(c) => write!(fmt, "failure from the server: {:?}", c), + AuthError::ComponentFail => write!(fmt, "component authentication failure"), + } + } +} + +/// Error establishing connection +#[derive(Debug)] +pub enum ConnecterError { + /// All attempts failed, no error available + AllFailed, + /// DNS protocol error + Dns(ProtoError), + /// DNS resolution error + Resolve(ResolveError), +} + +impl std::error::Error for ConnecterError {} + +impl std::fmt::Display for ConnecterError { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(fmt, "{:?}", self) + } +} diff --git a/tokio-xmpp/src/event.rs b/tokio-xmpp/src/event.rs new file mode 100644 index 0000000000000000000000000000000000000000..bd3dc402a2c2ad2f6b9eee29414e034db14406a2 --- /dev/null +++ b/tokio-xmpp/src/event.rs @@ -0,0 +1,54 @@ +use xmpp_parsers::{Element, Jid}; + +/// High-level event on the Stream implemented by Client and Component +#[derive(Debug)] +pub enum Event { + /// Stream is connected and initialized + Online(Jid), + /// Stream end + Disconnected, + /// Received stanza/nonza + Stanza(Element), +} + +impl Event { + /// `Online` event? + pub fn is_online(&self) -> bool { + match *self { + Event::Online(_) => true, + _ => false, + } + } + + /// Get the server-assigned JID for the `Online` event + pub fn get_jid(&self) -> Option<&Jid> { + match *self { + Event::Online(ref jid) => Some(jid), + _ => None, + } + } + + /// `Stanza` event? + pub fn is_stanza(&self, name: &str) -> bool { + match *self { + Event::Stanza(ref stanza) => stanza.name() == name, + _ => false, + } + } + + /// If this is a `Stanza` event, get its data + pub fn as_stanza(&self) -> Option<&Element> { + match *self { + Event::Stanza(ref stanza) => Some(stanza), + _ => None, + } + } + + /// If this is a `Stanza` event, unwrap into its data + pub fn into_stanza(self) -> Option { + match self { + Event::Stanza(stanza) => Some(stanza), + _ => None, + } + } +} diff --git a/tokio-xmpp/src/happy_eyeballs.rs b/tokio-xmpp/src/happy_eyeballs.rs new file mode 100644 index 0000000000000000000000000000000000000000..7696f65e2cfa42f8903259b341719f381cc3b9b3 --- /dev/null +++ b/tokio-xmpp/src/happy_eyeballs.rs @@ -0,0 +1,196 @@ +use crate::{ConnecterError, Error}; +use futures::{Async, Future, Poll}; +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::collections::VecDeque; +use std::io::Error as IoError; +use std::mem; +use std::net::SocketAddr; +use tokio::net::tcp::ConnectFuture; +use tokio::net::TcpStream; +use trust_dns_resolver::{AsyncResolver, Name, IntoName, Background, BackgroundLookup}; +use trust_dns_resolver::config::LookupIpStrategy; +use trust_dns_resolver::lookup::SrvLookupFuture; +use trust_dns_resolver::lookup_ip::LookupIpFuture; + + +enum State { + ResolveSrv(AsyncResolver, BackgroundLookup), + ResolveTarget(AsyncResolver, Background, u16), + Connecting(Option, Vec>), + Invalid, +} + +pub struct Connecter { + fallback_port: u16, + srv_domain: Option, + domain: Name, + state: State, + targets: VecDeque<(Name, u16)>, + error: Option, +} + +fn resolver() -> Result { + let (config, mut opts) = trust_dns_resolver::system_conf::read_system_conf()?; + opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6; + let (resolver, resolver_background) = AsyncResolver::new(config, opts); + tokio::runtime::current_thread::spawn(resolver_background); + Ok(resolver) +} + +impl Connecter { + pub fn from_lookup( + domain: &str, + srv: Option<&str>, + fallback_port: u16, + ) -> Result { + if let Ok(ip) = domain.parse() { + // use specified IP address, not domain name, skip the whole dns part + let connect = RefCell::new(TcpStream::connect(&SocketAddr::new(ip, fallback_port))); + return Ok(Connecter { + fallback_port, + srv_domain: None, + domain: "nohost".into_name().map_err(ConnecterError::Dns)?, + state: State::Connecting(None, vec![connect]), + targets: VecDeque::new(), + error: None, + }); + } + + let srv_domain = match srv { + Some(srv) => Some( + format!("{}.{}.", srv, domain) + .into_name() + .map_err(ConnecterError::Dns)?, + ), + None => None, + }; + + let mut self_ = Connecter { + fallback_port, + srv_domain, + domain: domain.into_name().map_err(ConnecterError::Dns)?, + state: State::Invalid, + targets: VecDeque::new(), + error: None, + }; + + let resolver = resolver()?; + // Initialize state + match &self_.srv_domain { + &Some(ref srv_domain) => { + let srv_lookup = resolver.lookup_srv(srv_domain.clone()); + self_.state = State::ResolveSrv(resolver, srv_lookup); + } + None => { + self_.targets = [(self_.domain.clone(), self_.fallback_port)] + .into_iter() + .cloned() + .collect(); + self_.state = State::Connecting(Some(resolver), vec![]); + } + } + + Ok(self_) + } +} + +impl Future for Connecter { + type Item = TcpStream; + type Error = Error; + + fn poll(&mut self) -> Poll { + let state = mem::replace(&mut self.state, State::Invalid); + match state { + State::ResolveSrv(resolver, mut srv_lookup) => { + match srv_lookup.poll() { + Ok(Async::NotReady) => { + self.state = State::ResolveSrv(resolver, srv_lookup); + Ok(Async::NotReady) + } + Ok(Async::Ready(srv_result)) => { + let srv_map: BTreeMap<_, _> = srv_result + .iter() + .map(|srv| (srv.priority(), (srv.target().clone(), srv.port()))) + .collect(); + let targets = srv_map.into_iter().map(|(_, tp)| tp).collect(); + self.targets = targets; + self.state = State::Connecting(Some(resolver), vec![]); + self.poll() + } + Err(_) => { + // ignore, fallback + self.targets = [(self.domain.clone(), self.fallback_port)] + .into_iter() + .cloned() + .collect(); + self.state = State::Connecting(Some(resolver), vec![]); + self.poll() + } + } + } + State::Connecting(resolver, mut connects) => { + if resolver.is_some() && connects.len() == 0 && self.targets.len() > 0 { + let resolver = resolver.unwrap(); + let (host, port) = self.targets.pop_front().unwrap(); + let ip_lookup = resolver.lookup_ip(host); + self.state = State::ResolveTarget(resolver, ip_lookup, port); + self.poll() + } else if connects.len() > 0 { + let mut success = None; + connects.retain(|connect| match connect.borrow_mut().poll() { + Ok(Async::NotReady) => true, + Ok(Async::Ready(connection)) => { + success = Some(connection); + false + } + Err(e) => { + if self.error.is_none() { + self.error = Some(e.into()); + } + false + } + }); + match success { + Some(connection) => Ok(Async::Ready(connection)), + None => { + self.state = State::Connecting(resolver, connects); + Ok(Async::NotReady) + } + } + } else { + // All targets tried + match self.error.take() { + None => Err(ConnecterError::AllFailed.into()), + Some(e) => Err(e), + } + } + } + State::ResolveTarget(resolver, mut ip_lookup, port) => { + match ip_lookup.poll() { + Ok(Async::NotReady) => { + self.state = State::ResolveTarget(resolver, ip_lookup, port); + Ok(Async::NotReady) + } + Ok(Async::Ready(ip_result)) => { + let connects = ip_result + .iter() + .map(|ip| RefCell::new(TcpStream::connect(&SocketAddr::new(ip, port)))) + .collect(); + self.state = State::Connecting(Some(resolver), connects); + self.poll() + } + Err(e) => { + if self.error.is_none() { + self.error = Some(ConnecterError::Resolve(e).into()); + } + // ignore, next… + self.state = State::Connecting(Some(resolver), vec![]); + self.poll() + } + } + } + _ => panic!(""), + } + } +} diff --git a/tokio-xmpp/src/lib.rs b/tokio-xmpp/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..2bd823eef822a644becfa69b81c3d3b45782317f --- /dev/null +++ b/tokio-xmpp/src/lib.rs @@ -0,0 +1,19 @@ +#![deny(unsafe_code, unused, missing_docs, bare_trait_objects)] + +//! XMPP implementation with asynchronous I/O using Tokio. + +mod starttls; +mod stream_start; +pub mod xmpp_codec; +pub use crate::xmpp_codec::Packet; +pub mod xmpp_stream; +pub use crate::starttls::StartTlsClient; +mod event; +mod happy_eyeballs; +pub use crate::event::Event; +mod client; +pub use crate::client::Client; +mod component; +pub use crate::component::Component; +mod error; +pub use crate::error::{AuthError, ConnecterError, Error, ParseError, ParserError, ProtocolError}; diff --git a/tokio-xmpp/src/starttls.rs b/tokio-xmpp/src/starttls.rs new file mode 100644 index 0000000000000000000000000000000000000000..e23402792b96ac6f858c8ead10df9a154414d966 --- /dev/null +++ b/tokio-xmpp/src/starttls.rs @@ -0,0 +1,114 @@ +use futures::sink; +use futures::stream::Stream; +use futures::{Async, Future, Poll, Sink}; +use xmpp_parsers::{Jid, Element}; +use native_tls::TlsConnector as NativeTlsConnector; +use std::mem::replace; +use tokio_io::{AsyncRead, AsyncWrite}; +use tokio_tls::{Connect, TlsConnector, TlsStream}; + +use crate::xmpp_codec::Packet; +use crate::xmpp_stream::XMPPStream; +use crate::Error; + +/// XMPP TLS XML namespace +pub const NS_XMPP_TLS: &str = "urn:ietf:params:xml:ns:xmpp-tls"; + +/// XMPP stream that switches to TLS if available in received features +pub struct StartTlsClient { + state: StartTlsClientState, + jid: Jid, +} + +enum StartTlsClientState { + Invalid, + SendStartTls(sink::Send>), + AwaitProceed(XMPPStream), + StartingTls(Connect), +} + +impl StartTlsClient { + /// Waits for + pub fn from_stream(xmpp_stream: XMPPStream) -> Self { + let jid = xmpp_stream.jid.clone(); + + let nonza = Element::builder("starttls").ns(NS_XMPP_TLS).build(); + let packet = Packet::Stanza(nonza); + let send = xmpp_stream.send(packet); + + StartTlsClient { + state: StartTlsClientState::SendStartTls(send), + jid, + } + } +} + +impl Future for StartTlsClient { + type Item = TlsStream; + type Error = Error; + + fn poll(&mut self) -> Poll { + let old_state = replace(&mut self.state, StartTlsClientState::Invalid); + let mut retry = false; + + let (new_state, result) = match old_state { + StartTlsClientState::SendStartTls(mut send) => match send.poll() { + Ok(Async::Ready(xmpp_stream)) => { + let new_state = StartTlsClientState::AwaitProceed(xmpp_stream); + retry = true; + (new_state, Ok(Async::NotReady)) + } + Ok(Async::NotReady) => { + (StartTlsClientState::SendStartTls(send), Ok(Async::NotReady)) + } + Err(e) => (StartTlsClientState::SendStartTls(send), Err(e.into())), + }, + StartTlsClientState::AwaitProceed(mut xmpp_stream) => match xmpp_stream.poll() { + Ok(Async::Ready(Some(Packet::Stanza(ref stanza)))) + if stanza.name() == "proceed" => + { + let stream = xmpp_stream.stream.into_inner(); + let connect = + TlsConnector::from(NativeTlsConnector::builder().build().unwrap()) + .connect(&self.jid.clone().domain(), stream); + let new_state = StartTlsClientState::StartingTls(connect); + retry = true; + (new_state, Ok(Async::NotReady)) + } + Ok(Async::Ready(_value)) => { + // println!("StartTlsClient ignore {:?}", _value); + ( + StartTlsClientState::AwaitProceed(xmpp_stream), + Ok(Async::NotReady), + ) + } + Ok(_) => ( + StartTlsClientState::AwaitProceed(xmpp_stream), + Ok(Async::NotReady), + ), + Err(e) => ( + StartTlsClientState::AwaitProceed(xmpp_stream), + Err(Error::Protocol(e.into())), + ), + }, + StartTlsClientState::StartingTls(mut connect) => match connect.poll() { + Ok(Async::Ready(tls_stream)) => { + (StartTlsClientState::Invalid, Ok(Async::Ready(tls_stream))) + } + Ok(Async::NotReady) => ( + StartTlsClientState::StartingTls(connect), + Ok(Async::NotReady), + ), + Err(e) => (StartTlsClientState::Invalid, Err(e.into())), + }, + StartTlsClientState::Invalid => unreachable!(), + }; + + self.state = new_state; + if retry { + self.poll() + } else { + result + } + } +} diff --git a/tokio-xmpp/src/stream_start.rs b/tokio-xmpp/src/stream_start.rs new file mode 100644 index 0000000000000000000000000000000000000000..f82c3901e0aaf8f4d58adf2fd75af38621800b95 --- /dev/null +++ b/tokio-xmpp/src/stream_start.rs @@ -0,0 +1,125 @@ +use futures::{sink, Async, Future, Poll, Sink, Stream}; +use xmpp_parsers::{Jid, Element}; +use std::mem::replace; +use tokio_codec::Framed; +use tokio_io::{AsyncRead, AsyncWrite}; + +use crate::xmpp_codec::{Packet, XMPPCodec}; +use crate::xmpp_stream::XMPPStream; +use crate::{Error, ProtocolError}; + +const NS_XMPP_STREAM: &str = "http://etherx.jabber.org/streams"; + +pub struct StreamStart { + state: StreamStartState, + jid: Jid, + ns: String, +} + +enum StreamStartState { + SendStart(sink::Send>), + RecvStart(Framed), + RecvFeatures(Framed, String), + Invalid, +} + +impl StreamStart { + pub fn from_stream(stream: Framed, jid: Jid, ns: String) -> Self { + let attrs = [ + ("to".to_owned(), jid.clone().domain()), + ("version".to_owned(), "1.0".to_owned()), + ("xmlns".to_owned(), ns.clone()), + ("xmlns:stream".to_owned(), NS_XMPP_STREAM.to_owned()), + ] + .iter() + .cloned() + .collect(); + let send = stream.send(Packet::StreamStart(attrs)); + + StreamStart { + state: StreamStartState::SendStart(send), + jid, + ns, + } + } +} + +impl Future for StreamStart { + type Item = XMPPStream; + type Error = Error; + + fn poll(&mut self) -> Poll { + let old_state = replace(&mut self.state, StreamStartState::Invalid); + let mut retry = false; + + let (new_state, result) = match old_state { + StreamStartState::SendStart(mut send) => match send.poll() { + Ok(Async::Ready(stream)) => { + retry = true; + (StreamStartState::RecvStart(stream), Ok(Async::NotReady)) + } + Ok(Async::NotReady) => (StreamStartState::SendStart(send), Ok(Async::NotReady)), + Err(e) => (StreamStartState::Invalid, Err(e.into())), + }, + StreamStartState::RecvStart(mut stream) => match stream.poll() { + Ok(Async::Ready(Some(Packet::StreamStart(stream_attrs)))) => { + let stream_ns = stream_attrs + .get("xmlns") + .ok_or(ProtocolError::NoStreamNamespace)? + .clone(); + if self.ns == "jabber:client" { + retry = true; + // TODO: skip RecvFeatures for version < 1.0 + ( + StreamStartState::RecvFeatures(stream, stream_ns), + Ok(Async::NotReady), + ) + } else { + let id = stream_attrs + .get("id") + .ok_or(ProtocolError::NoStreamId)? + .clone(); + // FIXME: huge hack, shouldn’t be an element! + let stream = XMPPStream::new( + self.jid.clone(), + stream, + self.ns.clone(), + Element::builder(id).build(), + ); + (StreamStartState::Invalid, Ok(Async::Ready(stream))) + } + } + Ok(Async::Ready(_)) => return Err(ProtocolError::InvalidToken.into()), + Ok(Async::NotReady) => (StreamStartState::RecvStart(stream), Ok(Async::NotReady)), + Err(e) => return Err(ProtocolError::from(e).into()), + }, + StreamStartState::RecvFeatures(mut stream, stream_ns) => match stream.poll() { + Ok(Async::Ready(Some(Packet::Stanza(stanza)))) => { + if stanza.is("features", NS_XMPP_STREAM) { + let stream = + XMPPStream::new(self.jid.clone(), stream, self.ns.clone(), stanza); + (StreamStartState::Invalid, Ok(Async::Ready(stream))) + } else { + ( + StreamStartState::RecvFeatures(stream, stream_ns), + Ok(Async::NotReady), + ) + } + } + Ok(Async::Ready(_)) | Ok(Async::NotReady) => ( + StreamStartState::RecvFeatures(stream, stream_ns), + Ok(Async::NotReady), + ), + Err(e) => return Err(ProtocolError::from(e).into()), + }, + StreamStartState::Invalid => unreachable!(), + }; + + self.state = new_state; + if retry { + self.poll() + } else { + result + } + } +} diff --git a/tokio-xmpp/src/xmpp_codec.rs b/tokio-xmpp/src/xmpp_codec.rs new file mode 100644 index 0000000000000000000000000000000000000000..588d947d5c09898da7e6bbba36f3e6ff618fc218 --- /dev/null +++ b/tokio-xmpp/src/xmpp_codec.rs @@ -0,0 +1,532 @@ +//! XML stream parser for XMPP + +use crate::{ParseError, ParserError}; +use bytes::{BufMut, BytesMut}; +use xmpp_parsers::Element; +use quick_xml::Writer as EventWriter; +use std; +use std::cell::RefCell; +use std::collections::vec_deque::VecDeque; +use std::collections::HashMap; +use std::default::Default; +use std::fmt::Write; +use std::io; +use std::iter::FromIterator; +use std::rc::Rc; +use std::str::from_utf8; +use std::borrow::Cow; +use tokio_codec::{Decoder, Encoder}; +use xml5ever::interface::Attribute; +use xml5ever::tokenizer::{Tag, TagKind, Token, TokenSink, XmlTokenizer}; +use xml5ever::buffer_queue::BufferQueue; + +/// Anything that can be sent or received on an XMPP/XML stream +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Packet { + /// `` start tag + StreamStart(HashMap), + /// A complete stanza or nonza + Stanza(Element), + /// Plain text (think whitespace keep-alive) + Text(String), + /// `` closing tag + StreamEnd, +} + +type QueueItem = Result; + +/// Parser state +struct ParserSink { + // Ready stanzas, shared with XMPPCodec + queue: Rc>>, + // Parsing stack + stack: Vec, + ns_stack: Vec, String>>, +} + +impl ParserSink { + pub fn new(queue: Rc>>) -> Self { + ParserSink { + queue, + stack: vec![], + ns_stack: vec![], + } + } + + fn push_queue(&self, pkt: Packet) { + self.queue.borrow_mut().push_back(Ok(pkt)); + } + + fn push_queue_error(&self, e: ParserError) { + self.queue.borrow_mut().push_back(Err(e)); + } + + /// Lookup XML namespace declaration for given prefix (or no prefix) + fn lookup_ns(&self, prefix: &Option) -> Option<&str> { + for nss in self.ns_stack.iter().rev() { + if let Some(ns) = nss.get(prefix) { + return Some(ns); + } + } + + None + } + + fn handle_start_tag(&mut self, tag: Tag) { + let mut nss = HashMap::new(); + let is_prefix_xmlns = |attr: &Attribute| { + attr.name + .prefix + .as_ref() + .map(|prefix| prefix.eq_str_ignore_ascii_case("xmlns")) + .unwrap_or(false) + }; + for attr in &tag.attrs { + match attr.name.local.as_ref() { + "xmlns" => { + nss.insert(None, attr.value.as_ref().to_owned()); + } + prefix if is_prefix_xmlns(attr) => { + nss.insert(Some(prefix.to_owned()), attr.value.as_ref().to_owned()); + } + _ => (), + } + } + self.ns_stack.push(nss); + + let el = { + let mut el_builder = Element::builder(tag.name.local.as_ref()); + if let Some(el_ns) = + self.lookup_ns(&tag.name.prefix.map(|prefix| prefix.as_ref().to_owned())) + { + el_builder = el_builder.ns(el_ns); + } + for attr in &tag.attrs { + match attr.name.local.as_ref() { + "xmlns" => (), + _ if is_prefix_xmlns(attr) => (), + _ => { + let attr_name = if let Some(ref prefix) = attr.name.prefix { + Cow::Owned(format!("{}:{}", prefix, attr.name.local)) + } else { + Cow::Borrowed(attr.name.local.as_ref()) + }; + el_builder = el_builder.attr(attr_name, attr.value.as_ref()); + } + } + } + el_builder.build() + }; + + if self.stack.is_empty() { + let attrs = HashMap::from_iter(tag.attrs.iter().map(|attr| { + ( + attr.name.local.as_ref().to_owned(), + attr.value.as_ref().to_owned(), + ) + })); + self.push_queue(Packet::StreamStart(attrs)); + } + + self.stack.push(el); + } + + fn handle_end_tag(&mut self) { + let el = self.stack.pop().unwrap(); + self.ns_stack.pop(); + + match self.stack.len() { + // + 0 => self.push_queue(Packet::StreamEnd), + // + 1 => self.push_queue(Packet::Stanza(el)), + len => { + let parent = &mut self.stack[len - 1]; + parent.append_child(el); + } + } + } +} + +impl TokenSink for ParserSink { + fn process_token(&mut self, token: Token) { + match token { + Token::TagToken(tag) => match tag.kind { + TagKind::StartTag => self.handle_start_tag(tag), + TagKind::EndTag => self.handle_end_tag(), + TagKind::EmptyTag => { + self.handle_start_tag(tag); + self.handle_end_tag(); + } + TagKind::ShortTag => self.push_queue_error(ParserError::ShortTag), + }, + Token::CharacterTokens(tendril) => match self.stack.len() { + 0 | 1 => self.push_queue(Packet::Text(tendril.into())), + len => { + let el = &mut self.stack[len - 1]; + el.append_text_node(tendril); + } + }, + Token::EOFToken => self.push_queue(Packet::StreamEnd), + Token::ParseError(s) => { + // println!("ParseError: {:?}", s); + self.push_queue_error(ParserError::Parse(ParseError(s))); + } + _ => (), + } + } + + // fn end(&mut self) { + // } +} + +/// Stateful encoder/decoder for a bytestream from/to XMPP `Packet` +pub struct XMPPCodec { + /// Outgoing + ns: Option, + /// Incoming + parser: XmlTokenizer, + /// For handling incoming truncated utf8 + // TODO: optimize using tendrils? + buf: Vec, + /// Shared with ParserSink + queue: Rc>>, +} + +impl XMPPCodec { + /// Constructor + pub fn new() -> Self { + let queue = Rc::new(RefCell::new(VecDeque::new())); + let sink = ParserSink::new(queue.clone()); + // TODO: configure parser? + let parser = XmlTokenizer::new(sink, Default::default()); + XMPPCodec { + ns: None, + parser, + queue, + buf: vec![], + } + } +} + +impl Default for XMPPCodec { + fn default() -> Self { + Self::new() + } +} + +impl Decoder for XMPPCodec { + type Item = Packet; + type Error = ParserError; + + fn decode(&mut self, buf: &mut BytesMut) -> Result, Self::Error> { + let buf1: Box> = if !self.buf.is_empty() && !buf.is_empty() { + let mut prefix = std::mem::replace(&mut self.buf, vec![]); + prefix.extend_from_slice(buf.take().as_ref()); + Box::new(prefix) + } else { + Box::new(buf.take()) + }; + let buf1 = buf1.as_ref().as_ref(); + match from_utf8(buf1) { + Ok(mut s) => { + s = s.trim(); + if !s.is_empty() { + // println!("<< {}", s); + let mut buffer_queue = BufferQueue::new(); + let tendril = FromIterator::from_iter(s.chars()); + buffer_queue.push_back(tendril); + self.parser.feed(&mut buffer_queue); + } + } + // Remedies for truncated utf8 + Err(e) if e.valid_up_to() >= buf1.len() - 3 => { + // Prepare all the valid data + let mut b = BytesMut::with_capacity(e.valid_up_to()); + b.put(&buf1[0..e.valid_up_to()]); + + // Retry + let result = self.decode(&mut b); + + // Keep the tail back in + self.buf.extend_from_slice(&buf1[e.valid_up_to()..]); + + return result; + } + Err(e) => { + // println!("error {} at {}/{} in {:?}", e, e.valid_up_to(), buf1.len(), buf1); + return Err(ParserError::Utf8(e)); + } + } + + match self.queue.borrow_mut().pop_front() { + None => Ok(None), + Some(result) => result.map(|pkt| Some(pkt)), + } + } + + fn decode_eof(&mut self, buf: &mut BytesMut) -> Result, Self::Error> { + self.decode(buf) + } +} + +impl Encoder for XMPPCodec { + type Item = Packet; + type Error = io::Error; + + fn encode(&mut self, item: Self::Item, dst: &mut BytesMut) -> Result<(), Self::Error> { + let remaining = dst.capacity() - dst.len(); + let max_stanza_size: usize = 2usize.pow(16); + if remaining < max_stanza_size { + dst.reserve(max_stanza_size - remaining); + } + + fn to_io_err>>(e: E) -> io::Error { + io::Error::new(io::ErrorKind::InvalidInput, e) + } + + match item { + Packet::StreamStart(start_attrs) => { + let mut buf = String::new(); + write!(buf, "\n") + .map_err(to_io_err)?; + + // print!(">> {}", buf); + write!(dst, "{}", buf) + .map_err(to_io_err) + } + Packet::Stanza(stanza) => { + stanza + .write_to_inner(&mut EventWriter::new(WriteBytes::new(dst))) + .and_then(|_| { + // println!(">> {:?}", dst); + Ok(()) + }) + .map_err(|e| to_io_err(format!("{}", e))) + } + Packet::Text(text) => { + write_text(&text, dst) + .and_then(|_| { + // println!(">> {:?}", dst); + Ok(()) + }) + .map_err(to_io_err) + } + Packet::StreamEnd => { + write!(dst, "\n") + .map_err(to_io_err) + } + } + } +} + +/// Write XML-escaped text string +pub fn write_text(text: &str, writer: &mut W) -> Result<(), std::fmt::Error> { + write!(writer, "{}", escape(text)) +} + +/// Copied from `RustyXML` for now +pub fn escape(input: &str) -> String { + let mut result = String::with_capacity(input.len()); + + for c in input.chars() { + match c { + '&' => result.push_str("&"), + '<' => result.push_str("<"), + '>' => result.push_str(">"), + '\'' => result.push_str("'"), + '"' => result.push_str("""), + o => result.push(o), + } + } + result +} + +/// BytesMut impl only std::fmt::Write but not std::io::Write. The +/// latter trait is required for minidom's +/// `Element::write_to_inner()`. +struct WriteBytes<'a> { + dst: &'a mut BytesMut, +} + +impl<'a> WriteBytes<'a> { + fn new(dst: &'a mut BytesMut) -> Self { + WriteBytes { dst } + } +} + +impl<'a> std::io::Write for WriteBytes<'a> { + fn write(&mut self, buf: &[u8]) -> std::result::Result { + self.dst.put_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::result::Result<(), std::io::Error> { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bytes::BytesMut; + + #[test] + fn test_stream_start() { + let mut c = XMPPCodec::new(); + let mut b = BytesMut::with_capacity(1024); + b.put(r""); + let r = c.decode(&mut b); + assert!(match r { + Ok(Some(Packet::StreamStart(_))) => true, + _ => false, + }); + } + + #[test] + fn test_stream_end() { + let mut c = XMPPCodec::new(); + let mut b = BytesMut::with_capacity(1024); + b.put(r""); + let r = c.decode(&mut b); + assert!(match r { + Ok(Some(Packet::StreamStart(_))) => true, + _ => false, + }); + b.clear(); + b.put(r""); + let r = c.decode(&mut b); + assert!(match r { + Ok(Some(Packet::StreamEnd)) => true, + _ => false, + }); + } + + #[test] + fn test_truncated_stanza() { + let mut c = XMPPCodec::new(); + let mut b = BytesMut::with_capacity(1024); + b.put(r""); + let r = c.decode(&mut b); + assert!(match r { + Ok(Some(Packet::StreamStart(_))) => true, + _ => false, + }); + + b.clear(); + b.put(r"ß true, + _ => false, + }); + + b.clear(); + b.put(r">"); + let r = c.decode(&mut b); + assert!(match r { + Ok(Some(Packet::Stanza(ref el))) if el.name() == "test" && el.text() == "ß" => true, + _ => false, + }); + } + + #[test] + fn test_truncated_utf8() { + let mut c = XMPPCodec::new(); + let mut b = BytesMut::with_capacity(1024); + b.put(r""); + let r = c.decode(&mut b); + assert!(match r { + Ok(Some(Packet::StreamStart(_))) => true, + _ => false, + }); + + b.clear(); + b.put(&b"\xc3"[..]); + let r = c.decode(&mut b); + assert!(match r { + Ok(None) => true, + _ => false, + }); + + b.clear(); + b.put(&b"\x9f"[..]); + let r = c.decode(&mut b); + assert!(match r { + Ok(Some(Packet::Stanza(ref el))) if el.name() == "test" && el.text() == "ß" => true, + _ => false, + }); + } + + /// test case for https://gitlab.com/xmpp-rs/tokio-xmpp/issues/3 + #[test] + fn test_atrribute_prefix() { + let mut c = XMPPCodec::new(); + let mut b = BytesMut::with_capacity(1024); + b.put(r""); + let r = c.decode(&mut b); + assert!(match r { + Ok(Some(Packet::StreamStart(_))) => true, + _ => false, + }); + + b.clear(); + b.put(r"Test status"); + let r = c.decode(&mut b); + assert!(match r { + Ok(Some(Packet::Stanza(ref el))) if el.name() == "status" && el.text() == "Test status" && el.attr("xml:lang").map_or(false, |a| a == "en") => true, + _ => false, + }); + + } + + /// By default, encode() only get's a BytesMut that has 8kb space reserved. + #[test] + fn test_large_stanza() { + use futures::{Future, Sink}; + use std::io::Cursor; + use tokio_codec::FramedWrite; + let framed = FramedWrite::new(Cursor::new(vec![]), XMPPCodec::new()); + let mut text = "".to_owned(); + for _ in 0..2usize.pow(15) { + text = text + "A"; + } + let stanza = Element::builder("message") + .append(Element::builder("body").append(text.as_ref()).build()) + .build(); + let framed = framed.send(Packet::Stanza(stanza)).wait().expect("send"); + assert_eq!( + framed.get_ref().get_ref(), + &("".to_owned() + &text + "").as_bytes() + ); + } + + #[test] + fn test_lone_whitespace() { + let mut c = XMPPCodec::new(); + let mut b = BytesMut::with_capacity(1024); + b.put(r""); + let r = c.decode(&mut b); + assert!(match r { + Ok(Some(Packet::StreamStart(_))) => true, + _ => false, + }); + + b.clear(); + b.put(r" "); + let r = c.decode(&mut b); + assert!(match r { + Ok(None) => true, + _ => false, + }); + } +} diff --git a/tokio-xmpp/src/xmpp_stream.rs b/tokio-xmpp/src/xmpp_stream.rs new file mode 100644 index 0000000000000000000000000000000000000000..129dd1d4fe8825b06409cf52fac2f145f6678969 --- /dev/null +++ b/tokio-xmpp/src/xmpp_stream.rs @@ -0,0 +1,92 @@ +//! `XMPPStream` is the common container for all XMPP network connections + +use futures::sink::Send; +use futures::{Poll, Sink, StartSend, Stream}; +use xmpp_parsers::{Jid, Element}; +use tokio_codec::Framed; +use tokio_io::{AsyncRead, AsyncWrite}; + +use crate::stream_start::StreamStart; +use crate::xmpp_codec::{Packet, XMPPCodec}; + +/// namespace +pub const NS_XMPP_STREAM: &str = "http://etherx.jabber.org/streams"; + +/// Wraps a `stream` +pub struct XMPPStream { + /// The local Jabber-Id + pub jid: Jid, + /// Codec instance + pub stream: Framed, + /// `` for XMPP version 1.0 + pub stream_features: Element, + /// Root namespace + /// + /// This is different for either c2s, s2s, or component + /// connections. + pub ns: String, +} + +impl XMPPStream { + /// Constructor + pub fn new( + jid: Jid, + stream: Framed, + ns: String, + stream_features: Element, + ) -> Self { + XMPPStream { + jid, + stream, + stream_features, + ns, + } + } + + /// Send a `` start tag + pub fn start(stream: S, jid: Jid, ns: String) -> StreamStart { + let xmpp_stream = Framed::new(stream, XMPPCodec::new()); + StreamStart::from_stream(xmpp_stream, jid, ns) + } + + /// Unwraps the inner stream + pub fn into_inner(self) -> S { + self.stream.into_inner() + } + + /// Re-run `start()` + pub fn restart(self) -> StreamStart { + Self::start(self.stream.into_inner(), self.jid, self.ns) + } +} + +impl XMPPStream { + /// Convenience method + pub fn send_stanza>(self, e: E) -> Send { + self.send(Packet::Stanza(e.into())) + } +} + +/// Proxy to self.stream +impl Sink for XMPPStream { + type SinkItem = as Sink>::SinkItem; + type SinkError = as Sink>::SinkError; + + fn start_send(&mut self, item: Self::SinkItem) -> StartSend { + self.stream.start_send(item) + } + + fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { + self.stream.poll_complete() + } +} + +/// Proxy to self.stream +impl Stream for XMPPStream { + type Item = as Stream>::Item; + type Error = as Stream>::Error; + + fn poll(&mut self) -> Poll, Self::Error> { + self.stream.poll() + } +} diff --git a/xmpp-parsers/.gitlab-ci.yml b/xmpp-parsers/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..cd0be07974f035ea057fe9f435605d282972c53a --- /dev/null +++ b/xmpp-parsers/.gitlab-ci.yml @@ -0,0 +1,62 @@ +stages: + - build + - test + +variables: + FEATURES: "" + RUST_BACKTRACE: "full" + +.stable: + image: rust:latest + cache: + key: stable + paths: + - target/ + +.nightly: + image: rustlang/rust:nightly + cache: + key: nightly + paths: + - target/ + +.build: + stage: build + script: + - cargo build --verbose --no-default-features --features=$FEATURES + +.test: + stage: test + script: + - cargo test --verbose --no-default-features --features=$FEATURES + +rust-latest-build: + extends: + - .build + - .stable + +rust-nightly-build: + extends: + - .build + - .nightly + + +rust-latest-test: + extends: + - .test + - .stable + +rust-nightly-test: + extends: + - .test + - .nightly + +rust-latest-build with features=disable-validation: + extends: rust-latest-build + variables: + FEATURES: "disable-validation" + +rust-latest-test with features=disable-validation: + extends: rust-latest-test + variables: + FEATURES: "disable-validation" diff --git a/xmpp-parsers/Cargo.toml b/xmpp-parsers/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..e02950d45585dbada1aabbd99692d34d870b0db2 --- /dev/null +++ b/xmpp-parsers/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "xmpp-parsers" +version = "0.16.0" +authors = [ + "Emmanuel Gil Peyrot ", + "Maxime “pep” Buquet ", +] +description = "Collection of parsers and serialisers for XMPP extensions" +homepage = "https://gitlab.com/xmpp-rs/xmpp-parsers" +repository = "https://gitlab.com/xmpp-rs/xmpp-parsers" +keywords = ["xmpp", "jabber", "xml"] +categories = ["parsing", "network-programming"] +license = "MPL-2.0" +edition = "2018" + +[dependencies] +minidom = "0.11.0" +jid = { version = "0.8", features = ["minidom"] } +base64 = "0.10" +digest = "0.8" +sha-1 = "0.8" +sha2 = "0.8" +sha3 = "0.8" +blake2 = "0.8" +chrono = "0.4.5" + +[features] +# Build xmpp-parsers to make components instead of clients. +component = [] +# Disable validation of unknown attributes. +disable-validation = [] + +[package.metadata.docs.rs] +rustdoc-args = [ "--sort-modules-by-appearance", "-Zunstable-options" ] diff --git a/xmpp-parsers/ChangeLog b/xmpp-parsers/ChangeLog new file mode 100644 index 0000000000000000000000000000000000000000..9f9ea7df6583936a9a9cf34315aa2f869fdd807a --- /dev/null +++ b/xmpp-parsers/ChangeLog @@ -0,0 +1,325 @@ +Version 0.16.0: +2019-10-15 Emmanuel Gil Peyrot + * New parsers/serialisers: + - Client Certificate Management for SASL EXTERNAL (XEP-0257) + - JID Prep (XEP-0328) + - Client State Indication (XEP-0352) + - OpenPGP for XMPP (XEP-0373) + - Bookmarks 2 (This Time it's Serious) (XEP-0402) + - Anonymous unique occupant identifiers for MUCs (XEP-0421) + - Source-Specific Media Attributes in Jingle (XEP-0339) + - Jingle RTP Feedback Negotiation (XEP-0293) + * Breaking changes: + - Presence constructors now take Into and assume Some. + * Improvements: + - CI: refactor, add caching + - Update jid-rs to 0.8 + +Version 0.15.0: +2019-09-06 Emmanuel Gil Peyrot + * New parsers/serialisers: + - XHTML-IM (XEP-0071) + - User Tune (XEP-0118) + - Bits of Binary (XEP-0231) + - Message Carbons (XEP-0280) + * Breaking changes: + - Stop reexporting TryFrom and TryInto, they are available in + std::convert nowadays. + - Bind has been split into BindQuery and BindResponse. + * Improvements: + - New DOAP file for a machine-readable description of the features. + - Add various parser and formatter helpers on Hash. + +Version 0.14.0: +2019-07-13 Emmanuel Gil Peyrot , Maxime “pep” Buquet + * New parsers/serialisers: + - Entity Time (XEP-0202). + * Improvements: + - Microblog NS (XEP-0227). + - Update jid-rs dependency with jid split change (Jid, FullJid, + BareJid) and reexport them. + - Fix rustdoc options in Cargo.toml for docs.rs + * Breaking changes: + - Presence's show attribute is now Option and Show::None is no + more. + +Version 0.13.1: +2019-04-12 Emmanuel Gil Peyrot + * Bugfixes: + - Fix invalid serialisation of priority in presence. + - Bump image size to u16 from u8, as per XEP-0084 version 1.1.2. + * Improvements: + - Drop try_from dependency, as std::convert::TryFrom got + stabilised. + +Version 0.13.0: +2019-03-20 Emmanuel Gil Peyrot + * New parsers/serialisers: + - User Avatar (XEP-0084). + - Contact Addresses for XMPP Services (XEP-0157). + - Jingle RTP Sessions (XEP-0167). + - Jingle ICE-UDP Transport Method (XEP-0176). + - Use of DTLS-SRTP in Jingle Sessions (XEP-0320). + * Breaking changes: + - Make 'id' required on iq, as per RFC6120 §8.1.3. + - Refactor PubSub to have more type-safety. + - Treat FORM_TYPE as a special case in data forms, to avoid + duplicating it into a field. + - Add forgotten i18n to Jingle text element. + * Improvements: + - Add various helpers for hash representations. + - Add helpers constructors for multiple extensions (disco, caps, + pubsub, stanza_error). + - Use Into in more constructors. + - Internal change on attribute declaration in macros. + - Reexport missing try_from::TryInto. + +Version 0.12.2: +2019-01-16 Emmanuel Gil Peyrot + * Improvements: + - Reexport missing util::error::Error and try_from::TryFrom. + +Version 0.12.1: +2019-01-16 Emmanuel Gil Peyrot + * Improvements: + - Reexport missing JidParseError from the jid crate. + +Version 0.12.0: +2019-01-16 Emmanuel Gil Peyrot + * Breaking changes: + - Update dependencies. + - Switch to git, upstream is now available at + https://gitlab.com/xmpp-rs/xmpp-parsers + - Switch to Edition 2018, this removes support for rustc + versions older than 1.31. + - Implement support for XEP-0030 2.5rc3, relaxing the ordering + of children in disco#info. + * Improvements: + - Test for struct size, to keep them known and avoid bloat. + - Add various constructors to make the API easier to use. + - Reexport Jid from the jid crate, to avoid any weird issue on + using different incompatible versions of the same crate. + - Add forgotten 'ask' attribute on roster item (thanks O01eg!). + - Use cargo-fmt on the codebase, to lower the barrier of entry. + - Add a disable-validation feature, disabling many checks + xmpp-parsers is doing. This should be used for software + which want to let invalid XMPP pass through instead of being + rejected as invalid (thanks Astro-!). + +Version 0.11.1: +2018-09-20 Emmanuel Gil Peyrot + * Improvements: + - Document all of the modules. + +Version 0.11.0: +2018-08-03 Emmanuel Gil Peyrot + * Breaking changes: + - Split Software Version (XEP-0092) into a query and response + elements. + - Split RSM (XEP-0059) into a query and response elements. + - Fix type safety and spec issues in RSM and MAM (XEP-0313). + - Remove item@node and EmptyItems from PubSub events + (XEP-0060). + * Improvements: + - Document many additional modules. + - Add the SASL nonza, as well as the SCRAM-SHA-256 + and the two -PLUS mechanisms. + +Version 0.10.0: +2018-07-31 Emmanuel Gil Peyrot + * New parsers/serialisers: + - Added , SASL and bind (RFC6120) parsers. + - Added a WebSocket (RFC7395) implementation. + - Added a Jabber Component (XEP-0114). + - Added support for User Nickname (XEP-0172). + - Added support for Stream Management (XEP-0198). + - Added support for Bookmarks (XEP-0048). + - Publish-Subscribe (XEP-0060) now supports requests in + addition to events. + * Breaking changes: + - Switch from std::error to failure to report better errors. + - Bump to minidom 0.9.1, and reexport minidom::Element. + * Improvements: + - Add getters for the best body and subject in message, to make + it easier to determine which one the user wants based on + their language preferences. + - Add constructors and setters for most Jingle elements, to + ease their creation. + - Add constructors for hash, MUC item, iq and more. + - Use more macros to simplify and factorise the code. + - Use traits to define iq payloads. + - Document more modules. + +Version 0.9.0: +2017-10-31 Emmanuel Gil Peyrot + * New parsers/serialisers: + - Blocking Command (XEP-0191) has been added. + - Date and Time Profiles (XEP-0082) has been added, replacing + ad-hoc use of chrono in various places. + - User Mood (XEP-0107) has been added. + * Breaking changes: + - Fix subscription="none" not being the default. + - Add more type safety to pubsub#event. + - Reuse Jingle’s ContentId type in JingleFT. + - Import the disposition attribute values in Jingle. + * Improvements: + - Refactor a good part of the code using macros. + - Simplify the parsing code wherever it makes sense. + - Check for children ordering in disco#info result. + - Finish implementation of , and + in JingleFT. + - Correctly serialise , and test it. + +Version 0.8.0: +2017-08-27 Emmanuel Gil Peyrot + * New parsers/serialisers: + - iq:version (XEP-0092) has been added. + - Finally implement extension serialisation in disco. + * Breaking changes: + - Wrap even more elements into their own type, in jingle, + jingle_ft, roster, message. + - Split loose enums into multiple structs where it makes sense, + such as for IBB, StanzaId, Receipts. + - Split disco query and answer elements into their own struct, + to enforce more guarantees on both. + * Improvements: + - Use Vec::into_iter() more to avoid references and clones. + - Make data_forms propagate a media_element error. + - Document more of disco, roster, chatstates. + - Use the minidom feature of jid, for IntoAttributeValue. + - Add a component feature, changing the default namespace to + jabber:component:accept. + - Add support for indicating ranged transfers in jingle_ft. + +Version 0.7.1: +2017-07-24 Emmanuel Gil Peyrot + * Hotfixes: + - Stub out blake2 support, since the blake2 crate broke its API + between their 0.6.0 and 0.6.1 releases… + +Version 0.7.0: +2017-07-23 Emmanuel Gil Peyrot + * New parsers/serialisers: + - Jingle Message Initialisation (XEP-0353) was added. + - The disco#items query (XEP-0030) is now supported, in + addition to the existing disco#info one. + * Breaking changes: + - Replaced many type aliases with proper wrapping structs. + - Split Disco into a query and a result part, since they have + very different constraints. + - Split IqPayload in three to avoid parsing queries as results + for example. + * Improvements: + - Use TryFrom from the try_from crate, thus removing the + dependency on nightly! + - Always implement From instead of Into, the latter is + generated anyway. + - Add helpers to construct your Presence stanza. + +Version 0.6.0: +2017-06-27 Emmanuel Gil Peyrot + * New parsers/serialisers: + - In-Band Registration (XEP-0077) was added. + - Multi-User Chat (XEP-0045) got expanded a lot, thanks pep.! + * Breaking changes: + - Added wrappers for Strings used as identifiers, to add type + safety. + - Use chrono’s DateTime for JingleFT’s date element. + - Use Jid for JingleS5B’s jid attribute. + * Improvements: + - Use more macros for common tasks. + - Add a constructor for Message and Presence. + - Implement std::fmt::Display and std::error::Error on our + error type. + - Fix DataForms serialisation. + - Fix roster group serialisation. + - Update libraries, notably chrono whose version 0.3.1 got + yanked. + +Version 0.5.0: +2017-06-11 Emmanuel Gil Peyrot + * New parsers/serialisers: + - Implementation of the roster management protocol defined in + RFC 6121 §2. + - Implementation of PubSub events (except collections). + - Early implementation of MUC. + * Breaking changes: + - Rename presence enums to make them easier to use. + * Improvements: + - Make hashes comparable and hashable. + - Make data forms embeddable easily into minidom + Element::builder. + +Version 0.4.0: +2017-05-28 Emmanuel Gil Peyrot + * Incompatible changes: + - Receipts now make the id optional, as per the specification. + - Hashes now expose their raw binary value, instead of staying + base64-encoded. + - Parse dates (XEP-0082) in delayed delivery (XEP-0203) and + last user interaction (XEP-0319), using the chrono crate. + * Improvements: + - Removal of most of the remaining clones, the only ones left + are due to minidom not exposing a draining iterator over the + children. + - Finish to parse all of the attributes using get_attr!(). + - More attribute checks. + - Split more parsers into one parser per element. + - Rely on minidom 0.4.3 to serialise more standard types + automatically. + - Implement forgotten serialisation for data forms (XEP-0004). + - Implement legacy capabilities (XEP-0115) for compatibility + with older software. + +Version 0.3.0: +2017-05-23 Emmanuel Gil Peyrot + * Big changes: + - All parsers and serialisers now consume their argument, this + makes the API way more efficient, but you will have to clone + before passing your structs in it if you want to keep them. + - Payloads of stanzas are not parsed automatically anymore, to + let applications which want to forward them as-is do so more + easily. Parsing now always succeeds on unknown payloads, it + just puts them into an Unknown value containing the existing + minidom Element. + * New parsers/serialisers: + - Last User Interaction in Presence, XEP-0319. + * Improved parsers/serialisers: + - Message now supports subject, bodies and threads as per + RFC 6121 §5.2. + - Replace most attribute reads with a nice macro. + - Use enums for more enum-like things, for example Algo in + Hash, or FieldType in DataForm. + - Wire up stanza-id and origin-id to MessagePayload. + - Wire up MAM elements to message and iq payloads. + - Changes in the RSM API. + - Add support for more data forms elements, but still not the + complete set. + - Thanks to minidom 0.3.1, check for explicitly disallowed + extra attributes in some elements. + * Crate updates: + - minidom 0.4.1 + +Version 0.2.0: +2017-05-06 Emmanuel Gil Peyrot + * New parsers/serialisers: + - Stanza error, as per RFC 6120 §8.3. + - Jingle SOCKS5 Transport, XEP-0260. + * Incompatible changes: + - Parsers and serialisers now all implement TryFrom + and Into, instead of the old parse_* and serialise_* + functions. + - Presence has got an overhaul, it now hosts show, statuses and + priority in its struct. The status module has also been + dropped. + - Message now supports multiple bodies, each in a different + language. The body module has also been dropped. + - Iq now gets a proper StanzaError when the type is error. + - Fix bogus Jingle payload, which was requiring both + description and transport. + * Crate updates: + - minidom 0.3.0 + +Version 0.1.0: +2017-04-29 Emmanuel Gil Peyrot + * Implement many extensions. diff --git a/xmpp-parsers/LICENSE b/xmpp-parsers/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..14e2f777f6c395e7e04ab4aa306bbcc4b0c1120e --- /dev/null +++ b/xmpp-parsers/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + 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/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/xmpp-parsers/doap.xml b/xmpp-parsers/doap.xml new file mode 100644 index 0000000000000000000000000000000000000000..87e6fe952cf5f9b24e2d41a032844835673beb0c --- /dev/null +++ b/xmpp-parsers/doap.xml @@ -0,0 +1,643 @@ + + + + + xmpp-parsers + + 2017-04-18 + + Collection of parsers and serialisers for XMPP extensions + Collection de parseurs et de sérialiseurs pour extensions XMPP + + TODO + TODO + + + + + + + + + + + + + + + + Rust + + + + + + Link Mauve + + aaa4dac2b31c1be4ee8f8e2ab986d34fb261974f + + + + + pep. + + 99bcf9784288e323b0d2dea9c9ac7a2ede98395a + + + + + + + + + + + + + + + + + partial + 2.9 + 0.1.0 + + + + + + complete + 2.5rc3 + 0.1.0 + + + + + + complete + 1.32.0 + 0.5.0 + + + + + + complete + 2.0 + 0.1.0 + + + + + + complete + 1.1 + 0.10.0 + + + + + + complete + 1.0 + 0.1.0 + + + + + + partial + 1.15.8 + 0.5.0 + + + + + + complete + 1.2 + 0.1.0 + there is no specific module for this, the feature is all in the XEP-0004 module + + + + + + complete + 1.5.4 + 0.15.0 + + + + + + complete + 2.4 + 0.6.0 + + + + + + complete + 1.1 + 0.9.0 + + + + + + complete + 1.1.2 + 0.13.0 + + + + + + complete + 2.1 + 0.1.0 + + + + + + complete + 1.1 + 0.8.0 + + + + + + complete + 1.2.1 + 0.9.0 + + + + + + complete + 1.6 + 0.10.0 + + + + + + complete + 1.5.1 + 0.4.0 + + + + + + complete + 1.2 + 0.15.0 + + + + + + complete + 1.0.1 + 0.13.0 + + + + + + complete + 1.1.2 + 0.1.0 + + + + + + complete + 1.1.1 + 0.13.0 + + + + + + complete + 1.1 + 0.10.0 + + + + + + complete + 1.0 + 0.13.0 + + + + + + complete + 1.4.0 + 0.1.0 + + + + + + complete + 1.3 + 0.9.0 + + + + + + complete + 1.6 + 0.10.0 + + + + + + complete + 2.0.1 + 0.1.0 + + + + + + complete + 2.0 + 0.14.0 + + + + + + complete + 2.0 + 0.1.0 + + + + + + complete + 1.0 + 0.1.0 + + + + + + complete + 1.0 + 0.1.0 + + + + + + complete + 1.0 + 0.15.0 + + + + + + complete + 0.19.1 + 0.1.0 + + + + + + complete + 0.3 + 0.16.0 + + + + + + complete + 1.0.3 + 0.2.0 + + + + + + complete + 1.0 + 0.1.0 + + + + + + partial + 0.6.3 + 0.14.0 + only the namespace is included for now + + + + + + complete + 0.13.0 + 0.15.0 + + + + + + partial + 1.0.1 + 0.16.0 + Only supported in payload-type, and only for rtcp-fb. + + + + + + complete + 1.0 + 0.1.0 + + + + + + complete + 0.6.0 + 0.1.0 + + + + + + complete + 1.1.0 + 0.1.0 + + + + + + complete + 0.6.3 + 0.1.0 + + + + + + complete + 1.0.2 + 0.3.0 + + + + + + complete + 0.3.1 + 0.13.0 + + + + + + complete + 0.1 + 0.16.0 + + + + + + complete + 0.3 + 0.16.0 + + + + + + complete + 0.3.0 + 0.16.0 + + + + + + complete + 0.3 + 0.7.0 + + + + + + complete + 0.6.0 + 0.1.0 + + + + + + partial + 0.4.0 + 0.16.0 + + + + + + complete + 0.2.0 + 0.1.0 + + + + + + complete + 0.3.0 + 0.1.0 + + + + + + complete + 0.3.0 + 0.16.0 + + + + + + complete + 0.1.0 + 0.16.0 + + + + + + 0.15.0 + 2019-09-06 + + + + + + 0.14.0 + 2019-07-13 + + + + + + 0.13.1 + 2019-04-12 + + + + + + 0.13.0 + 2019-03-20 + + + + + + 0.12.2 + 2019-01-16 + + + + + + 0.12.1 + 2019-01-16 + + + + + + 0.12.0 + 2019-01-16 + + + + + + 0.11.1 + 2018-09-20 + + + + + + 0.11.0 + 2018-08-02 + + + + + + 0.10.0 + 2018-07-31 + + + + + + 0.9.0 + 2017-12-27 + + + + + + 0.8.0 + 2017-11-30 + + + + + + 0.7.1 + 2017-11-30 + + + + + + 0.7.0 + 2017-11-30 + + + + + + 0.6.0 + 2017-11-30 + + + + + + 0.5.0 + 2017-11-30 + + + + + + 0.4.0 + 2017-11-30 + + + + + + 0.3.0 + 2017-11-30 + + + + + + 0.2.0 + 2017-11-30 + + + + + + 0.1.0 + 2017-11-30 + + + + + diff --git a/xmpp-parsers/examples/generate-caps.rs b/xmpp-parsers/examples/generate-caps.rs new file mode 100644 index 0000000000000000000000000000000000000000..80ba863f12be8e5f9f229638966fef8ac1d8f800 --- /dev/null +++ b/xmpp-parsers/examples/generate-caps.rs @@ -0,0 +1,62 @@ +// Copyright (c) 2019 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::convert::TryFrom; +use std::io::{self, Read}; +use std::env; +use xmpp_parsers::{ + Element, + disco::DiscoInfoResult, + caps::{Caps, compute_disco as compute_disco_caps, hash_caps}, + ecaps2::{ECaps2, compute_disco as compute_disco_ecaps2, hash_ecaps2}, + hashes::Algo, +}; + +fn get_caps(disco: &DiscoInfoResult, node: String) -> Result { + let caps_data = compute_disco_caps(&disco); + let caps_hash = hash_caps(&caps_data, Algo::Sha_1)?; + Ok(Caps::new(node, caps_hash)) +} + +fn get_ecaps2(disco: &DiscoInfoResult) -> Result { + let ecaps2_data = compute_disco_ecaps2(&disco).unwrap(); + let ecaps2_sha256 = hash_ecaps2(&ecaps2_data, Algo::Sha_256)?; + let ecaps2_sha3_256 = hash_ecaps2(&ecaps2_data, Algo::Sha3_256)?; + Ok(ECaps2::new(vec![ecaps2_sha256, ecaps2_sha3_256])) +} + +fn main() -> Result<(), ()> { + let args: Vec<_> = env::args().collect(); + if args.len() != 2 { + println!("Usage: {} ", args[0]); + return Err(()); + } + let node = args[1].clone(); + + eprintln!("Reading a disco#info payload from stdin..."); + + // Read from stdin. + let stdin = io::stdin(); + let mut data = String::new(); + let mut handle = stdin.lock(); + handle.read_to_string(&mut data).unwrap(); + + // Parse the payload into a DiscoInfoResult. + let elem: Element = data.parse().unwrap(); + let disco = DiscoInfoResult::try_from(elem).unwrap(); + + // Compute both kinds of caps. + let caps = get_caps(&disco, node).unwrap(); + let ecaps2 = get_ecaps2(&disco).unwrap(); + + // Print them. + let caps_elem = Element::from(caps); + let ecaps2_elem = Element::from(ecaps2); + println!("{}", String::from(&caps_elem)); + println!("{}", String::from(&ecaps2_elem)); + + Ok(()) +} diff --git a/xmpp-parsers/src/attention.rs b/xmpp-parsers/src/attention.rs new file mode 100644 index 0000000000000000000000000000000000000000..83a871ccfba3a0dc3c31e4e71839b8e157512dde --- /dev/null +++ b/xmpp-parsers/src/attention.rs @@ -0,0 +1,72 @@ +// 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 crate::message::MessagePayload; + +generate_empty_element!( + /// Requests the attention of the recipient. + Attention, + "attention", + ATTENTION +); + +impl MessagePayload for Attention {} + +#[cfg(test)] +mod tests { + use super::*; + #[cfg(not(feature = "disable-validation"))] + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[test] + fn test_size() { + assert_size!(Attention, 0); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + Attention::try_from(elem).unwrap(); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Attention::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in attention element."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_attribute() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Attention::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in attention element."); + } + + #[test] + fn test_serialise() { + let elem: Element = "".parse().unwrap(); + let attention = Attention; + let elem2: Element = attention.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/avatar.rs b/xmpp-parsers/src/avatar.rs new file mode 100644 index 0000000000000000000000000000000000000000..c475db82ac767caee63f9300a30717d3dfecef66 --- /dev/null +++ b/xmpp-parsers/src/avatar.rs @@ -0,0 +1,128 @@ +// Copyright (c) 2019 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 crate::hashes::Sha1HexAttribute; +use crate::pubsub::PubSubPayload; +use crate::util::helpers::WhitespaceAwareBase64; + +generate_element!( + /// Communicates information about an avatar. + Metadata, "metadata", AVATAR_METADATA, + children: [ + /// List of information elements describing this avatar. + infos: Vec = ("info", AVATAR_METADATA) => Info + ] +); + +impl PubSubPayload for Metadata {} + +generate_element!( + /// Communicates avatar metadata. + Info, "info", AVATAR_METADATA, + attributes: [ + /// The size of the image data in bytes. + bytes: Required = "bytes", + + /// The width of the image in pixels. + width: Option = "width", + + /// The height of the image in pixels. + height: Option = "height", + + /// The SHA-1 hash of the image data for the specified content-type. + id: Required = "id", + + /// The IANA-registered content type of the image data. + type_: Required = "type", + + /// The http: or https: URL at which the image data file is hosted. + url: Option = "url", + ] +); + +generate_element!( + /// The actual avatar data. + Data, "data", AVATAR_DATA, + text: ( + /// Vector of bytes representing the avatar’s image. + data: WhitespaceAwareBase64> + ) +); + +impl PubSubPayload for Data {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hashes::Algo; + #[cfg(not(feature = "disable-validation"))] + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Metadata, 12); + assert_size!(Info, 64); + assert_size!(Data, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Metadata, 24); + assert_size!(Info, 120); + assert_size!(Data, 24); + } + + #[test] + fn test_simple() { + let elem: Element = " + + " + .parse() + .unwrap(); + let metadata = Metadata::try_from(elem).unwrap(); + assert_eq!(metadata.infos.len(), 1); + let info = &metadata.infos[0]; + assert_eq!(info.bytes, 12345); + assert_eq!(info.width, Some(64)); + assert_eq!(info.height, Some(64)); + assert_eq!(info.id.algo, Algo::Sha_1); + assert_eq!(info.type_, "image/png"); + assert_eq!(info.url, None); + assert_eq!( + info.id.hash, + [ + 17, 31, 75, 60, 80, 215, 176, 223, 114, 157, 41, 155, 198, 248, 233, 239, 144, 102, + 151, 31 + ] + ); + + let elem: Element = "AAAA" + .parse() + .unwrap(); + let data = Data::try_from(elem).unwrap(); + assert_eq!(data.data, b"\0\0\0"); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Data::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in data element.") + } +} diff --git a/xmpp-parsers/src/bind.rs b/xmpp-parsers/src/bind.rs new file mode 100644 index 0000000000000000000000000000000000000000..4da3564156abb8abd6025e0fa49fa9442508d11f --- /dev/null +++ b/xmpp-parsers/src/bind.rs @@ -0,0 +1,198 @@ +// Copyright (c) 2018 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 crate::util::error::Error; +use crate::iq::{IqResultPayload, IqSetPayload}; +use crate::ns; +use jid::{FullJid, Jid}; +use crate::Element; +use std::str::FromStr; +use std::convert::TryFrom; + +/// The request for resource binding, which is the process by which a client +/// can obtain a full JID and start exchanging on the XMPP network. +/// +/// See https://xmpp.org/rfcs/rfc6120.html#bind +#[derive(Debug, Clone, PartialEq)] +pub struct BindQuery { + /// Requests this resource, the server may associate another one though. + /// + /// If this is None, we request no particular resource, and a random one + /// will be affected by the server. + resource: Option, +} + +impl BindQuery { + /// Creates a resource binding request. + pub fn new(resource: Option) -> BindQuery { + BindQuery { resource } + } +} + +impl IqSetPayload for BindQuery {} + +impl TryFrom for BindQuery { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "bind", BIND); + check_no_attributes!(elem, "bind"); + + let mut resource = None; + for child in elem.children() { + if resource.is_some() { + return Err(Error::ParseError("Bind can only have one child.")); + } + if child.is("resource", ns::BIND) { + check_no_attributes!(child, "resource"); + check_no_children!(child, "resource"); + resource = Some(child.text()); + } else { + return Err(Error::ParseError("Unknown element in bind request.")); + } + } + + Ok(BindQuery { resource }) + } +} + +impl From for Element { + fn from(bind: BindQuery) -> Element { + Element::builder("bind") + .ns(ns::BIND) + .append_all(bind.resource.map(|resource| + Element::builder("resource") + .ns(ns::BIND) + .append(resource))) + .build() + } +} + +/// The response for resource binding, containing the client’s full JID. +/// +/// See https://xmpp.org/rfcs/rfc6120.html#bind +#[derive(Debug, Clone, PartialEq)] +pub struct BindResponse { + /// The full JID returned by the server for this client. + jid: FullJid, +} + +impl IqResultPayload for BindResponse {} + +impl From for FullJid { + fn from(bind: BindResponse) -> FullJid { + bind.jid + } +} + +impl From for Jid { + fn from(bind: BindResponse) -> Jid { + Jid::Full(bind.jid) + } +} + +impl TryFrom for BindResponse { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "bind", BIND); + check_no_attributes!(elem, "bind"); + + let mut jid = None; + for child in elem.children() { + if jid.is_some() { + return Err(Error::ParseError("Bind can only have one child.")); + } + if child.is("jid", ns::BIND) { + check_no_attributes!(child, "jid"); + check_no_children!(child, "jid"); + jid = Some(FullJid::from_str(&child.text())?); + } else { + return Err(Error::ParseError("Unknown element in bind response.")); + } + } + + Ok(BindResponse { jid: match jid { + None => return Err(Error::ParseError("Bind response must contain a jid element.")), + Some(jid) => jid, + } }) + } +} + +impl From for Element { + fn from(bind: BindResponse) -> Element { + Element::builder("bind") + .ns(ns::BIND) + .append(Element::builder("jid").ns(ns::BIND).append(bind.jid)) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(BindQuery, 12); + assert_size!(BindResponse, 36); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(BindQuery, 24); + assert_size!(BindResponse, 72); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let bind = BindQuery::try_from(elem).unwrap(); + assert_eq!(bind.resource, None); + + let elem: Element = "Hello™" + .parse() + .unwrap(); + let bind = BindQuery::try_from(elem).unwrap(); + // FIXME: “™” should be resourceprep’d into “TM” here… + //assert_eq!(bind.resource.unwrap(), "HelloTM"); + assert_eq!(bind.resource.unwrap(), "Hello™"); + + let elem: Element = "coucou@linkmauve.fr/HelloTM" + .parse() + .unwrap(); + let bind = BindResponse::try_from(elem).unwrap(); + assert_eq!(bind.jid, FullJid::new("coucou", "linkmauve.fr", "HelloTM")); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_resource() { + let elem: Element = "resource" + .parse() + .unwrap(); + let error = BindQuery::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in resource element."); + + let elem: Element = "resource" + .parse() + .unwrap(); + let error = BindQuery::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in resource element."); + } +} diff --git a/xmpp-parsers/src/blocking.rs b/xmpp-parsers/src/blocking.rs new file mode 100644 index 0000000000000000000000000000000000000000..1ddfa5c9a6d7cddc6212577292308983aefbd009 --- /dev/null +++ b/xmpp-parsers/src/blocking.rs @@ -0,0 +1,223 @@ +// 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 crate::util::error::Error; +use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; +use crate::ns; +use jid::Jid; +use crate::Element; +use std::convert::TryFrom; + +generate_empty_element!( + /// The element requesting the blocklist, the result iq will contain a + /// [BlocklistResult]. + BlocklistRequest, + "blocklist", + BLOCKING +); + +impl IqGetPayload for BlocklistRequest {} + +macro_rules! generate_blocking_element { + ($(#[$meta:meta])* $elem:ident, $name:tt) => ( + $(#[$meta])* + #[derive(Debug, Clone)] + pub struct $elem { + /// List of JIDs affected by this command. + pub items: Vec, + } + + impl TryFrom for $elem { + type Error = Error; + + fn try_from(elem: Element) -> Result<$elem, Error> { + check_self!(elem, $name, BLOCKING); + check_no_attributes!(elem, $name); + let mut items = vec!(); + for child in elem.children() { + check_self!(child, "item", BLOCKING); + check_no_unknown_attributes!(child, "item", ["jid"]); + check_no_children!(child, "item"); + items.push(get_attr!(child, "jid", Required)); + } + Ok($elem { items }) + } + } + + impl From<$elem> for Element { + fn from(elem: $elem) -> Element { + Element::builder($name) + .ns(ns::BLOCKING) + .append_all(elem.items.into_iter().map(|jid| { + Element::builder("item") + .ns(ns::BLOCKING) + .attr("jid", jid) + })) + .build() + } + } + ); +} + +generate_blocking_element!( + /// The element containing the current blocklist, as a reply from + /// [BlocklistRequest]. + BlocklistResult, + "blocklist" +); + +impl IqResultPayload for BlocklistResult {} + +// TODO: Prevent zero elements from being allowed. +generate_blocking_element!( + /// A query to block one or more JIDs. + Block, + "block" +); + +impl IqSetPayload for Block {} + +generate_blocking_element!( + /// A query to unblock one or more JIDs, or all of them. + /// + /// Warning: not putting any JID there means clearing out the blocklist. + Unblock, + "unblock" +); + +impl IqSetPayload for Unblock {} + +generate_empty_element!( + /// The application-specific error condition when a message is blocked. + Blocked, + "blocked", + BLOCKING_ERRORS +); + +#[cfg(test)] +mod tests { + use super::*; + use jid::BareJid; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(BlocklistRequest, 0); + assert_size!(BlocklistResult, 12); + assert_size!(Block, 12); + assert_size!(Unblock, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(BlocklistRequest, 0); + assert_size!(BlocklistResult, 24); + assert_size!(Block, 24); + assert_size!(Unblock, 24); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + let request_elem = elem.clone(); + BlocklistRequest::try_from(request_elem).unwrap(); + + let result_elem = elem.clone(); + let result = BlocklistResult::try_from(result_elem).unwrap(); + assert_eq!(result.items, vec!()); + + let elem: Element = "".parse().unwrap(); + let block = Block::try_from(elem).unwrap(); + assert_eq!(block.items, vec!()); + + let elem: Element = "".parse().unwrap(); + let unblock = Unblock::try_from(elem).unwrap(); + assert_eq!(unblock.items, vec!()); + } + + #[test] + fn test_items() { + let elem: Element = "".parse().unwrap(); + let two_items = vec![ + Jid::Bare(BareJid { + node: Some(String::from("coucou")), + domain: String::from("coucou"), + }), + Jid::Bare(BareJid { + node: None, + domain: String::from("domain"), + }), + ]; + + let result_elem = elem.clone(); + let result = BlocklistResult::try_from(result_elem).unwrap(); + assert_eq!(result.items, two_items); + + let elem: Element = "".parse().unwrap(); + let block = Block::try_from(elem).unwrap(); + assert_eq!(block.items, two_items); + + let elem: Element = "".parse().unwrap(); + let unblock = Unblock::try_from(elem).unwrap(); + assert_eq!(unblock.items, two_items); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid() { + let elem: Element = "" + .parse() + .unwrap(); + let request_elem = elem.clone(); + let error = BlocklistRequest::try_from(request_elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in blocklist element."); + + let result_elem = elem.clone(); + let error = BlocklistResult::try_from(result_elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in blocklist element."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = Block::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in block element."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = Unblock::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in unblock element."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_non_empty_blocklist_request() { + let elem: Element = "".parse().unwrap(); + let error = BlocklistRequest::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in blocklist element."); + } +} diff --git a/xmpp-parsers/src/bob.rs b/xmpp-parsers/src/bob.rs new file mode 100644 index 0000000000000000000000000000000000000000..c78f6c34ed274ecf8c06fe498aa14385cb862b92 --- /dev/null +++ b/xmpp-parsers/src/bob.rs @@ -0,0 +1,168 @@ +// Copyright (c) 2019 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 crate::hashes::{Hash, Algo}; +use crate::util::helpers::Base64; +use crate::util::error::Error; +use minidom::IntoAttributeValue; +use std::str::FromStr; + +/// A Content-ID, as defined in RFC2111. +/// +/// The text value SHOULD be of the form algo+hash@bob.xmpp.org, this struct +/// enforces that format. +#[derive(Clone, Debug)] +pub struct ContentId { + hash: Hash, +} + +impl FromStr for ContentId { + type Err = Error; + + fn from_str(s: &str) -> Result { + let temp: Vec<_> = s.splitn(2, '@').collect(); + let temp: Vec<_> = match temp[..] { + [lhs, rhs] => { + if rhs != "bob.xmpp.org" { + return Err(Error::ParseError("Wrong domain for cid URI.")) + } + lhs.splitn(2, '+').collect() + }, + _ => return Err(Error::ParseError("Missing @ in cid URI.")) + }; + let (algo, hex) = match temp[..] { + [lhs, rhs] => { + let algo = match lhs { + "sha1" => Algo::Sha_1, + "sha256" => Algo::Sha_256, + _ => unimplemented!(), + }; + (algo, rhs) + }, + _ => return Err(Error::ParseError("Missing + in cid URI.")) + }; + let hash = Hash::from_hex(algo, hex)?; + Ok(ContentId { hash }) + } +} + +impl IntoAttributeValue for ContentId { + fn into_attribute_value(self) -> Option { + let algo = match self.hash.algo { + Algo::Sha_1 => "sha1", + Algo::Sha_256 => "sha256", + _ => unimplemented!(), + }; + Some(format!("{}+{}@bob.xmpp.org", algo, self.hash.to_hex())) + } +} + +generate_element!( + /// Request for an uncached cid file. + Data, "data", BOB, + attributes: [ + /// The cid in question. + cid: Required = "cid", + + /// How long to cache it (in seconds). + max_age: Option = "max-age", + + /// The MIME type of the data being transmitted. + /// + /// See the [IANA MIME Media Types Registry][1] for a list of + /// registered types, but unregistered or yet-to-be-registered are + /// accepted too. + /// + /// [1]: https://www.iana.org/assignments/media-types/media-types.xhtml + type_: Option = "type" + ], + text: ( + /// The actual data. + data: Base64> + ) +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + use std::error::Error as StdError; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(ContentId, 28); + assert_size!(Data, 60); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(ContentId, 56); + assert_size!(Data, 120); + } + + #[test] + fn test_simple() { + let cid: ContentId = "sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org".parse().unwrap(); + assert_eq!(cid.hash.algo, Algo::Sha_1); + assert_eq!(cid.hash.hash, b"\x8f\x35\xfe\xf1\x10\xff\xc5\xdf\x08\xd5\x79\xa5\x00\x83\xff\x93\x08\xfb\x62\x42"); + assert_eq!(cid.into_attribute_value().unwrap(), "sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org"); + + let elem: Element = "".parse().unwrap(); + let data = Data::try_from(elem).unwrap(); + assert_eq!(data.cid.hash.algo, Algo::Sha_1); + assert_eq!(data.cid.hash.hash, b"\x8f\x35\xfe\xf1\x10\xff\xc5\xdf\x08\xd5\x79\xa5\x00\x83\xff\x93\x08\xfb\x62\x42"); + assert!(data.max_age.is_none()); + assert!(data.type_.is_none()); + assert!(data.data.is_empty()); + } + + #[test] + fn invalid_cid() { + let error = "Hello world!".parse::().unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Missing @ in cid URI."); + + let error = "Hello world@bob.xmpp.org".parse::().unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Missing + in cid URI."); + + let error = "sha1+1234@coucou.linkmauve.fr".parse::().unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Wrong domain for cid URI."); + + let error = "sha1+invalid@bob.xmpp.org".parse::().unwrap_err(); + let message = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!(message.description(), "invalid digit found in string"); + } + + #[test] + fn unknown_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Data::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in data element."); + } +} diff --git a/xmpp-parsers/src/bookmarks.rs b/xmpp-parsers/src/bookmarks.rs new file mode 100644 index 0000000000000000000000000000000000000000..900a3a911656feccdd6bce3c556ac9a892eea570 --- /dev/null +++ b/xmpp-parsers/src/bookmarks.rs @@ -0,0 +1,122 @@ +// Copyright (c) 2018 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 jid::BareJid; + +generate_attribute!( + /// Whether a conference bookmark should be joined automatically. + Autojoin, + "autojoin", + bool +); + +generate_element!( + /// A conference bookmark. + Conference, "conference", BOOKMARKS, + attributes: [ + /// Whether a conference bookmark should be joined automatically. + autojoin: Default = "autojoin", + + /// The JID of the conference. + jid: Required = "jid", + + /// A user-defined name for this conference. + name: Required = "name", + ], + children: [ + /// The nick the user will use to join this conference. + nick: Option = ("nick", BOOKMARKS) => String, + + /// The password required to join this conference. + password: Option = ("password", BOOKMARKS) => String + ] +); + +generate_element!( + /// An URL bookmark. + Url, "url", BOOKMARKS, + attributes: [ + /// A user-defined name for this URL. + name: Required = "name", + + /// The URL of this bookmark. + url: Required = "url", + ] +); + +generate_element!( + /// Container element for multiple bookmarks. + #[derive(Default)] + Storage, "storage", BOOKMARKS, + children: [ + /// Conferences the user has expressed an interest in. + conferences: Vec = ("conference", BOOKMARKS) => Conference, + + /// URLs the user is interested in. + urls: Vec = ("url", BOOKMARKS) => Url + ] +); + +impl Storage { + /// Create an empty bookmarks storage. + pub fn new() -> Storage { + Storage::default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::compare_elements::NamespaceAwareCompare; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Conference, 64); + assert_size!(Url, 24); + assert_size!(Storage, 24); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Conference, 128); + assert_size!(Url, 48); + assert_size!(Storage, 48); + } + + #[test] + fn empty() { + let elem: Element = "".parse().unwrap(); + let elem1 = elem.clone(); + let storage = Storage::try_from(elem).unwrap(); + assert_eq!(storage.conferences.len(), 0); + assert_eq!(storage.urls.len(), 0); + + let elem2 = Element::from(Storage::new()); + assert!(elem1.compare_to(&elem2)); + } + + #[test] + fn complete() { + let elem: Element = "Coucousecret".parse().unwrap(); + let storage = Storage::try_from(elem).unwrap(); + assert_eq!(storage.urls.len(), 1); + assert_eq!(storage.urls[0].name, "Example"); + assert_eq!(storage.urls[0].url, "https://example.org/"); + assert_eq!(storage.conferences.len(), 1); + assert_eq!(storage.conferences[0].autojoin, Autojoin::True); + assert_eq!( + storage.conferences[0].jid, + BareJid::new("test-muc", "muc.localhost") + ); + assert_eq!(storage.conferences[0].name, "Test MUC"); + assert_eq!(storage.conferences[0].clone().nick.unwrap(), "Coucou"); + assert_eq!(storage.conferences[0].clone().password.unwrap(), "secret"); + } +} diff --git a/xmpp-parsers/src/bookmarks2.rs b/xmpp-parsers/src/bookmarks2.rs new file mode 100644 index 0000000000000000000000000000000000000000..2a182f12a613dfcd9580e4490bf3a07df17959d3 --- /dev/null +++ b/xmpp-parsers/src/bookmarks2.rs @@ -0,0 +1,119 @@ +// Copyright (c) 2019 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/. + +generate_attribute!( + /// Whether a conference bookmark should be joined automatically. + Autojoin, + "autojoin", + bool +); + +generate_element!( + /// A conference bookmark. + Conference, "conference", BOOKMARKS2, + attributes: [ + /// Whether a conference bookmark should be joined automatically. + autojoin: Default = "autojoin", + + /// A user-defined name for this conference. + name: Option = "name", + ], + children: [ + /// The nick the user will use to join this conference. + nick: Option = ("nick", BOOKMARKS2) => String, + + /// The password required to join this conference. + password: Option = ("password", BOOKMARKS2) => String + ] +); + +impl Conference { + /// Create a new conference. + pub fn new() -> Conference { + Conference { + autojoin: Autojoin::False, + name: None, + nick: None, + password: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::compare_elements::NamespaceAwareCompare; + use crate::Element; + use std::convert::TryFrom; + use crate::pubsub::pubsub::Item as PubSubItem; + use crate::pubsub::event::PubSubEvent; + use crate::ns; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Conference, 40); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Conference, 80); + } + + #[test] + fn simple() { + let elem: Element = "".parse().unwrap(); + let elem1 = elem.clone(); + let conference = Conference::try_from(elem).unwrap(); + assert_eq!(conference.autojoin, Autojoin::False); + assert_eq!(conference.name, None); + assert_eq!(conference.nick, None); + assert_eq!(conference.password, None); + + let elem2 = Element::from(Conference::new()); + assert!(elem1.compare_to(&elem2)); + } + + #[test] + fn complete() { + let elem: Element = "Coucousecret".parse().unwrap(); + let conference = Conference::try_from(elem).unwrap(); + assert_eq!(conference.autojoin, Autojoin::True); + assert_eq!(conference.name, Some(String::from("Test MUC"))); + assert_eq!(conference.clone().nick.unwrap(), "Coucou"); + assert_eq!(conference.clone().password.unwrap(), "secret"); + } + + #[test] + fn wrapped() { + let elem: Element = "Coucousecret".parse().unwrap(); + let item = PubSubItem::try_from(elem).unwrap(); + let payload = item.payload.clone().unwrap(); + let conference = Conference::try_from(payload).unwrap(); + assert_eq!(conference.autojoin, Autojoin::True); + assert_eq!(conference.name, Some(String::from("Test MUC"))); + assert_eq!(conference.clone().nick.unwrap(), "Coucou"); + assert_eq!(conference.clone().password.unwrap(), "secret"); + + let elem: Element = "Coucousecret".parse().unwrap(); + let mut items = match PubSubEvent::try_from(elem) { + Ok(PubSubEvent::PublishedItems { node, items }) => { + assert_eq!(&node.0, ns::BOOKMARKS2); + items + }, + _ => panic!(), + }; + assert_eq!(items.len(), 1); + let item = items.pop().unwrap(); + let payload = item.payload.clone().unwrap(); + let conference = Conference::try_from(payload).unwrap(); + assert_eq!(conference.autojoin, Autojoin::True); + assert_eq!(conference.name, Some(String::from("Test MUC"))); + assert_eq!(conference.clone().nick.unwrap(), "Coucou"); + assert_eq!(conference.clone().password.unwrap(), "secret"); + } +} diff --git a/xmpp-parsers/src/caps.rs b/xmpp-parsers/src/caps.rs new file mode 100644 index 0000000000000000000000000000000000000000..96ffa22adefe47fcf24891c7df2aec091ab14cba --- /dev/null +++ b/xmpp-parsers/src/caps.rs @@ -0,0 +1,341 @@ +// 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 crate::data_forms::DataForm; +use crate::disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity}; +use crate::util::error::Error; +use crate::hashes::{Algo, Hash}; +use crate::ns; +use crate::presence::PresencePayload; +use blake2::VarBlake2b; +use digest::{Digest, Input, VariableOutput}; +use crate::Element; +use sha1::Sha1; +use sha2::{Sha256, Sha512}; +use sha3::{Sha3_256, Sha3_512}; +use std::convert::TryFrom; + +/// Represents a capability hash for a given client. +#[derive(Debug, Clone)] +pub struct Caps { + /// Deprecated list of additional feature bundles. + pub ext: Option, + + /// A URI identifying an XMPP application. + pub node: String, + + /// The hash of that application’s + /// [disco#info](../disco/struct.DiscoInfoResult.html). + /// + /// Warning: This protocol is insecure, you may want to switch to + /// [ecaps2](../ecaps2/index.html) instead, see [this + /// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html). + pub hash: Hash, +} + +impl PresencePayload for Caps {} + +impl TryFrom for Caps { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "c", CAPS, "caps"); + check_no_children!(elem, "caps"); + check_no_unknown_attributes!(elem, "caps", ["hash", "ver", "ext", "node"]); + let ver: String = get_attr!(elem, "ver", Required); + let hash = Hash { + algo: get_attr!(elem, "hash", Required), + hash: base64::decode(&ver)?, + }; + Ok(Caps { + ext: get_attr!(elem, "ext", Option), + node: get_attr!(elem, "node", Required), + hash, + }) + } +} + +impl From for Element { + fn from(caps: Caps) -> Element { + Element::builder("c") + .ns(ns::CAPS) + .attr("ext", caps.ext) + .attr("hash", caps.hash.algo) + .attr("node", caps.node) + .attr("ver", base64::encode(&caps.hash.hash)) + .build() + } +} + +impl Caps { + /// Create a Caps element from its node and hash. + pub fn new>(node: N, hash: Hash) -> Caps { + Caps { + ext: None, + node: node.into(), + hash, + } + } +} + +fn compute_item(field: &str) -> Vec { + let mut bytes = field.as_bytes().to_vec(); + bytes.push(b'<'); + bytes +} + +fn compute_items Vec>(things: &[T], encode: F) -> Vec { + let mut string: Vec = vec![]; + let mut accumulator: Vec> = vec![]; + for thing in things { + let bytes = encode(thing); + accumulator.push(bytes); + } + // This works using the expected i;octet collation. + accumulator.sort(); + for mut bytes in accumulator { + string.append(&mut bytes); + } + string +} + +fn compute_features(features: &[Feature]) -> Vec { + compute_items(features, |feature| compute_item(&feature.var)) +} + +fn compute_identities(identities: &[Identity]) -> Vec { + compute_items(identities, |identity| { + let lang = identity.lang.clone().unwrap_or_default(); + let name = identity.name.clone().unwrap_or_default(); + let string = format!("{}/{}/{}/{}", identity.category, identity.type_, lang, name); + let bytes = string.as_bytes(); + let mut vec = Vec::with_capacity(bytes.len()); + vec.extend_from_slice(bytes); + vec.push(b'<'); + vec + }) +} + +fn compute_extensions(extensions: &[DataForm]) -> Vec { + compute_items(extensions, |extension| { + let mut bytes = vec![]; + // TODO: maybe handle the error case? + if let Some(ref form_type) = extension.form_type { + bytes.extend_from_slice(form_type.as_bytes()); + } + bytes.push(b'<'); + for field in extension.fields.clone() { + if field.var == "FORM_TYPE" { + continue; + } + bytes.append(&mut compute_item(&field.var)); + bytes.append(&mut compute_items(&field.values, |value| { + compute_item(value) + })); + } + bytes + }) +} + +/// Applies the caps algorithm on the provided disco#info result, to generate +/// the hash input. +/// +/// Warning: This protocol is insecure, you may want to switch to +/// [ecaps2](../ecaps2/index.html) instead, see [this +/// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html). +pub fn compute_disco(disco: &DiscoInfoResult) -> Vec { + let identities_string = compute_identities(&disco.identities); + let features_string = compute_features(&disco.features); + let extensions_string = compute_extensions(&disco.extensions); + + let mut final_string = vec![]; + final_string.extend(identities_string); + final_string.extend(features_string); + final_string.extend(extensions_string); + final_string +} + +fn get_hash_vec(hash: &[u8]) -> Vec { + let mut vec = Vec::with_capacity(hash.len()); + vec.extend_from_slice(hash); + vec +} + +/// Hashes the result of [compute_disco()] with one of the supported [hash +/// algorithms](../hashes/enum.Algo.html). +pub fn hash_caps(data: &[u8], algo: Algo) -> Result { + Ok(Hash { + hash: match algo { + Algo::Sha_1 => { + let hash = Sha1::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Sha_256 => { + let hash = Sha256::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Sha_512 => { + let hash = Sha512::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Sha3_256 => { + let hash = Sha3_256::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Sha3_512 => { + let hash = Sha3_512::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Blake2b_256 => { + let mut hasher = VarBlake2b::new(32).unwrap(); + hasher.input(data); + hasher.vec_result() + } + Algo::Blake2b_512 => { + let mut hasher = VarBlake2b::new(64).unwrap(); + hasher.input(data); + hasher.vec_result() + } + Algo::Unknown(algo) => return Err(format!("Unknown algorithm: {}.", algo)), + }, + algo, + }) +} + +/// Helper function to create the query for the disco#info corresponding to a +/// caps hash. +pub fn query_caps(caps: Caps) -> DiscoInfoQuery { + DiscoInfoQuery { + node: Some(format!("{}#{}", caps.node, base64::encode(&caps.hash.hash))), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::caps; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Caps, 52); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Caps, 104); + } + + #[test] + fn test_parse() { + let elem: Element = "".parse().unwrap(); + let caps = Caps::try_from(elem).unwrap(); + assert_eq!(caps.node, String::from("coucou")); + assert_eq!(caps.hash.algo, Algo::Sha_256); + assert_eq!( + caps.hash.hash, + base64::decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=").unwrap() + ); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_child() { + let elem: Element = "K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=".parse().unwrap(); + let error = Caps::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in caps element."); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + let disco = DiscoInfoResult::try_from(elem).unwrap(); + let caps = caps::compute_disco(&disco); + assert_eq!(caps.len(), 50); + } + + #[test] + fn test_xep_5_2() { + let elem: Element = r#" + + + + + + + +"# + .parse() + .unwrap(); + + let data = b"client/pc//Exodus 0.9.1 + + + + + + + + + urn:xmpp:dataforms:softwareinfo + + + ipv4 + ipv6 + + + Mac + + + 10.5.1 + + + Psi + + + 0.11 + + + +"# + .parse() + .unwrap(); + let expected = b"client/pc/el/\xce\xa8 0.11 = ("forwarded", FORWARD) => Forwarded + ] +); + +impl MessagePayload for Received {} + +generate_element!( + /// Wrapper for a message sent from another resource. + Sent, "sent", CARBONS, + + children: [ + /// Wrapper for the enclosed message. + forwarded: Required = ("forwarded", FORWARD) => Forwarded + ] +); + +impl MessagePayload for Sent {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Enable, 0); + assert_size!(Disable, 0); + assert_size!(Private, 0); + assert_size!(Received, 212); + assert_size!(Sent, 212); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Enable, 0); + assert_size!(Disable, 0); + assert_size!(Private, 0); + assert_size!(Received, 408); + assert_size!(Sent, 408); + } + + #[test] + fn empty_elements() { + let elem: Element = "".parse().unwrap(); + Enable::try_from(elem).unwrap(); + + let elem: Element = "".parse().unwrap(); + Disable::try_from(elem).unwrap(); + + let elem: Element = "".parse().unwrap(); + Private::try_from(elem).unwrap(); + } + + #[test] + fn forwarded_elements() { + let elem: Element = " + + + +" + .parse() + .unwrap(); + let received = Received::try_from(elem).unwrap(); + assert!(received.forwarded.stanza.is_some()); + + let elem: Element = " + + + +" + .parse() + .unwrap(); + let sent = Sent::try_from(elem).unwrap(); + assert!(sent.forwarded.stanza.is_some()); + } +} diff --git a/xmpp-parsers/src/cert_management.rs b/xmpp-parsers/src/cert_management.rs new file mode 100644 index 0000000000000000000000000000000000000000..817a01bb284fcdb81cff9a2e36a6e8ec333aa79d --- /dev/null +++ b/xmpp-parsers/src/cert_management.rs @@ -0,0 +1,214 @@ +// Copyright (c) 2019 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 crate::iq::{IqSetPayload, IqGetPayload, IqResultPayload}; +use crate::util::helpers::Base64; + +generate_elem_id!( + /// The name of a certificate. + Name, "name", SASL_CERT +); + +generate_element!( + /// An X.509 certificate. + Cert, "x509cert", SASL_CERT, + text: ( + /// The BER X.509 data. + data: Base64> + ) +); + +generate_element!( + /// For the client to upload an X.509 certificate. + Append, "append", SASL_CERT, + children: [ + /// The name of this certificate. + name: Required = ("name", SASL_CERT) => Name, + + /// The X.509 certificate to set. + cert: Required = ("x509cert", SASL_CERT) => Cert, + + /// This client is forbidden from managing certificates. + no_cert_management: Present<_> = ("no-cert-management", SASL_CERT) => bool + ] +); + +impl IqSetPayload for Append {} + +generate_empty_element!( + /// Client requests the current list of X.509 certificates. + ListCertsQuery, "items", SASL_CERT +); + +impl IqGetPayload for ListCertsQuery {} + +generate_elem_id!( + /// One resource currently using a certificate. + Resource, "resource", SASL_CERT +); + +generate_element!( + /// A list of resources currently using this certificate. + Users, "users", SASL_CERT, + children: [ + /// Resources currently using this certificate. + resources: Vec = ("resource", SASL_CERT) => Resource + ] +); + +generate_element!( + /// An X.509 certificate being set for this user. + Item, "item", SASL_CERT, + children: [ + /// The name of this certificate. + name: Required = ("name", SASL_CERT) => Name, + + /// The X.509 certificate to set. + cert: Required = ("x509cert", SASL_CERT) => Cert, + + /// This client is forbidden from managing certificates. + no_cert_management: Present<_> = ("no-cert-management", SASL_CERT) => bool, + + /// List of resources currently using this certificate. + users: Option = ("users", SASL_CERT) => Users + ] +); + +generate_element!( + /// Server answers with the current list of X.509 certificates. + ListCertsResponse, "items", SASL_CERT, + children: [ + /// List of certificates. + items: Vec = ("item", SASL_CERT) => Item + ] +); + +impl IqResultPayload for ListCertsResponse {} + +generate_element!( + /// Client disables an X.509 certificate. + Disable, "disable", SASL_CERT, + children: [ + /// Name of the certificate to disable. + name: Required = ("name", SASL_CERT) => Name + ] +); + +impl IqSetPayload for Disable {} + +generate_element!( + /// Client revokes an X.509 certificate. + Revoke, "revoke", SASL_CERT, + children: [ + /// Name of the certificate to revoke. + name: Required = ("name", SASL_CERT) => Name + ] +); + +impl IqSetPayload for Revoke {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + use std::str::FromStr; + use crate::ns; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Append, 28); + assert_size!(Disable, 12); + assert_size!(Revoke, 12); + assert_size!(ListCertsQuery, 0); + assert_size!(ListCertsResponse, 12); + assert_size!(Item, 40); + assert_size!(Resource, 12); + assert_size!(Users, 12); + assert_size!(Cert, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Append, 56); + assert_size!(Disable, 24); + assert_size!(Revoke, 24); + assert_size!(ListCertsQuery, 0); + assert_size!(ListCertsResponse, 24); + assert_size!(Item, 80); + assert_size!(Resource, 24); + assert_size!(Users, 24); + assert_size!(Cert, 24); + } + + #[test] + fn simple() { + let elem: Element = "Mobile ClientAAAA".parse().unwrap(); + let append = Append::try_from(elem).unwrap(); + assert_eq!(append.name.0, "Mobile Client"); + assert_eq!(append.cert.data, b"\0\0\0"); + + let elem: Element = "Mobile Client".parse().unwrap(); + let disable = Disable::try_from(elem).unwrap(); + assert_eq!(disable.name.0, "Mobile Client"); + + let elem: Element = "Mobile Client".parse().unwrap(); + let revoke = Revoke::try_from(elem).unwrap(); + assert_eq!(revoke.name.0, "Mobile Client"); + } + + #[test] + fn list() { + let elem: Element = r#" + + + Mobile Client + AAAA + + Phone + + + + Laptop + BBBB + + "#.parse().unwrap(); + let mut list = ListCertsResponse::try_from(elem).unwrap(); + assert_eq!(list.items.len(), 2); + + let item = list.items.pop().unwrap(); + assert_eq!(item.name.0, "Laptop"); + assert_eq!(item.cert.data, [4, 16, 65]); + assert!(item.users.is_none()); + + let item = list.items.pop().unwrap(); + assert_eq!(item.name.0, "Mobile Client"); + assert_eq!(item.cert.data, b"\0\0\0"); + assert_eq!(item.users.unwrap().resources.len(), 1); + } + + #[test] + fn test_serialise() { + let append = Append { + name: Name::from_str("Mobile Client").unwrap(), + cert: Cert { data: b"\0\0\0".to_vec() }, + no_cert_management: false, + }; + let elem: Element = append.into(); + assert!(elem.is("append", ns::SASL_CERT)); + + let disable = Disable { + name: Name::from_str("Mobile Client").unwrap(), + }; + let elem: Element = disable.into(); + assert!(elem.is("disable", ns::SASL_CERT)); + let elem = elem.children().cloned().collect::>().pop().unwrap(); + assert!(elem.is("name", ns::SASL_CERT)); + assert_eq!(elem.text(), "Mobile Client"); + } +} diff --git a/xmpp-parsers/src/chatstates.rs b/xmpp-parsers/src/chatstates.rs new file mode 100644 index 0000000000000000000000000000000000000000..4d8f9a4dbb7edcf17cb0919a6ee785407eb00359 --- /dev/null +++ b/xmpp-parsers/src/chatstates.rs @@ -0,0 +1,100 @@ +// 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 crate::message::MessagePayload; + +generate_element_enum!( + /// Enum representing chatstate elements part of the + /// `http://jabber.org/protocol/chatstates` namespace. + ChatState, "chatstate", CHATSTATES, { + /// `` + Active => "active", + + /// `` + Composing => "composing", + + /// `` + Gone => "gone", + + /// `` + Inactive => "inactive", + + /// `` + Paused => "paused", + } +); + +impl MessagePayload for ChatState {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::ns; + use crate::Element; + use std::convert::TryFrom; + + #[test] + fn test_size() { + assert_size!(ChatState, 1); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + ChatState::try_from(elem).unwrap(); + } + + #[test] + fn test_invalid() { + let elem: Element = "" + .parse() + .unwrap(); + let error = ChatState::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "This is not a chatstate element."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = ChatState::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in chatstate element."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_attribute() { + let elem: Element = "" + .parse() + .unwrap(); + let error = ChatState::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in chatstate element."); + } + + #[test] + fn test_serialise() { + let chatstate = ChatState::Active; + let elem: Element = chatstate.into(); + assert!(elem.is("active", ns::CHATSTATES)); + } +} diff --git a/xmpp-parsers/src/component.rs b/xmpp-parsers/src/component.rs new file mode 100644 index 0000000000000000000000000000000000000000..b8120ffde74e18f3ae3120489090525d6adfe6f4 --- /dev/null +++ b/xmpp-parsers/src/component.rs @@ -0,0 +1,87 @@ +// Copyright (c) 2018 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 crate::util::helpers::PlainText; +use digest::Digest; +use sha1::Sha1; + +generate_element!( + /// The main authentication mechanism for components. + #[derive(Default)] + Handshake, "handshake", COMPONENT, + text: ( + /// If Some, contains the hex-encoded SHA-1 of the concatenation of the + /// stream id and the password, and is used to authenticate against the + /// server. + /// + /// If None, it is the successful reply from the server, the stream is now + /// fully established and both sides can now exchange stanzas. + data: PlainText> + ) +); + +impl Handshake { + /// Creates a successful reply from a server. + pub fn new() -> Handshake { + Handshake::default() + } + + /// Creates an authentication request from the component. + pub fn from_password_and_stream_id(password: &str, stream_id: &str) -> Handshake { + let input = String::from(stream_id) + password; + let hash = Sha1::digest(input.as_bytes()); + let content = format!("{:x}", hash); + Handshake { + data: Some(content), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Handshake, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Handshake, 24); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let handshake = Handshake::try_from(elem).unwrap(); + assert_eq!(handshake.data, None); + + let elem: Element = "Coucou" + .parse() + .unwrap(); + let handshake = Handshake::try_from(elem).unwrap(); + assert_eq!(handshake.data, Some(String::from("Coucou"))); + } + + #[test] + fn test_constructors() { + let handshake = Handshake::new(); + assert_eq!(handshake.data, None); + + let handshake = Handshake::from_password_and_stream_id("123456", "sid"); + assert_eq!( + handshake.data, + Some(String::from("9accec263ab84a43c6037ccf7cd48cb1d3f6df8e")) + ); + } +} diff --git a/xmpp-parsers/src/csi.rs b/xmpp-parsers/src/csi.rs new file mode 100644 index 0000000000000000000000000000000000000000..f3a9130d4afca79f24f772517dbbead2de9ef501 --- /dev/null +++ b/xmpp-parsers/src/csi.rs @@ -0,0 +1,59 @@ +// Copyright (c) 2019 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/. + +generate_empty_element!( + /// Stream:feature sent by the server to advertise it supports CSI. + Feature, "csi", CSI +); + +generate_empty_element!( + /// Client indicates it is inactive. + Inactive, "inactive", CSI +); + +generate_empty_element!( + /// Client indicates it is active again. + Active, "active", CSI +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + use crate::ns; + + #[test] + fn test_size() { + assert_size!(Feature, 0); + assert_size!(Inactive, 0); + assert_size!(Active, 0); + } + + #[test] + fn parsing() { + let elem: Element = "".parse().unwrap(); + Feature::try_from(elem).unwrap(); + + let elem: Element = "".parse().unwrap(); + Inactive::try_from(elem).unwrap(); + + let elem: Element = "".parse().unwrap(); + Active::try_from(elem).unwrap(); + } + + #[test] + fn serialising() { + let elem: Element = Feature.into(); + assert!(elem.is("csi", ns::CSI)); + + let elem: Element = Inactive.into(); + assert!(elem.is("inactive", ns::CSI)); + + let elem: Element = Active.into(); + assert!(elem.is("active", ns::CSI)); + } +} diff --git a/xmpp-parsers/src/data_forms.rs b/xmpp-parsers/src/data_forms.rs new file mode 100644 index 0000000000000000000000000000000000000000..37f94dfa541e068694b8830b45eeddab99ddaff0 --- /dev/null +++ b/xmpp-parsers/src/data_forms.rs @@ -0,0 +1,395 @@ +// 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 crate::util::error::Error; +use crate::media_element::MediaElement; +use crate::ns; +use crate::Element; +use std::convert::TryFrom; + +generate_element!( + /// Represents one of the possible values for a list- field. + Option_, "option", DATA_FORMS, + attributes: [ + /// The optional label to be displayed to the user for this option. + label: Option = "label" + ], + children: [ + /// The value returned to the server when selecting this option. + value: Required = ("value", DATA_FORMS) => String + ] +); + +generate_attribute!( + /// The type of a [field](struct.Field.html) element. + FieldType, "type", { + /// This field can only take the values "0" or "false" for a false + /// value, and "1" or "true" for a true value. + Boolean => "boolean", + + /// This field describes data, it must not be sent back to the + /// requester. + Fixed => "fixed", + + /// This field is hidden, it should not be displayed to the user but + /// should be sent back to the requester. + Hidden => "hidden", + + /// This field accepts one or more [JIDs](../../jid/struct.Jid.html). + /// A client may want to let the user autocomplete them based on their + /// contacts list for instance. + JidMulti => "jid-multi", + + /// This field accepts one [JID](../../jid/struct.Jid.html). A client + /// may want to let the user autocomplete it based on their contacts + /// list for instance. + JidSingle => "jid-single", + + /// This field accepts one or more values from the list provided as + /// [options](struct.Option_.html). + ListMulti => "list-multi", + + /// This field accepts one value from the list provided as + /// [options](struct.Option_.html). + ListSingle => "list-single", + + /// This field accepts one or more free form text lines. + TextMulti => "text-multi", + + /// This field accepts one free form password, a client should hide it + /// in its user interface. + TextPrivate => "text-private", + + /// This field accepts one free form text line. + TextSingle => "text-single", + }, Default = TextSingle +); + +/// Represents a field in a [data form](struct.DataForm.html). +#[derive(Debug, Clone)] +pub struct Field { + /// The unique identifier for this field, in the form. + pub var: String, + + /// The type of this field. + pub type_: FieldType, + + /// The label to be possibly displayed to the user for this field. + pub label: Option, + + /// The form will be rejected if this field isn’t present. + pub required: bool, + + /// A list of allowed values. + pub options: Vec, + + /// The values provided for this field. + pub values: Vec, + + /// A list of media related to this field. + pub media: Vec, +} + +impl Field { + fn is_list(&self) -> bool { + self.type_ == FieldType::ListSingle || self.type_ == FieldType::ListMulti + } +} + +impl TryFrom for Field { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "field", DATA_FORMS); + check_no_unknown_attributes!(elem, "field", ["label", "type", "var"]); + let mut field = Field { + var: get_attr!(elem, "var", Required), + type_: get_attr!(elem, "type", Default), + label: get_attr!(elem, "label", Option), + required: false, + options: vec![], + values: vec![], + media: vec![], + }; + for element in elem.children() { + if element.is("value", ns::DATA_FORMS) { + check_no_children!(element, "value"); + check_no_attributes!(element, "value"); + field.values.push(element.text()); + } else if element.is("required", ns::DATA_FORMS) { + if field.required { + return Err(Error::ParseError("More than one required element.")); + } + check_no_children!(element, "required"); + check_no_attributes!(element, "required"); + field.required = true; + } else if element.is("option", ns::DATA_FORMS) { + if !field.is_list() { + return Err(Error::ParseError("Option element found in non-list field.")); + } + let option = Option_::try_from(element.clone())?; + field.options.push(option); + } else if element.is("media", ns::MEDIA_ELEMENT) { + let media_element = MediaElement::try_from(element.clone())?; + field.media.push(media_element); + } else { + return Err(Error::ParseError( + "Field child isn’t a value, option or media element.", + )); + } + } + Ok(field) + } +} + +impl From for Element { + fn from(field: Field) -> Element { + Element::builder("field") + .ns(ns::DATA_FORMS) + .attr("var", field.var) + .attr("type", field.type_) + .attr("label", field.label) + .append_all(if field.required { + Some(Element::builder("required").ns(ns::DATA_FORMS)) + } else { + None + }) + .append_all(field.options.iter().cloned().map(Element::from)) + .append_all( + field + .values + .into_iter() + .map(|value| { + Element::builder("value") + .ns(ns::DATA_FORMS) + .append(value) + }) + ) + .append_all(field.media.iter().cloned().map(Element::from)) + .build() + } +} + +generate_attribute!( + /// Represents the type of a [data form](struct.DataForm.html). + DataFormType, "type", { + /// This is a cancel request for a prior type="form" data form. + Cancel => "cancel", + + /// This is a request for the recipient to fill this form and send it + /// back as type="submit". + Form => "form", + + /// This is a result form, which contains what the requester asked for. + Result_ => "result", + + /// This is a complete response to a form received before. + Submit => "submit", + } +); + +/// This is a form to be sent to another entity for filling. +#[derive(Debug, Clone)] +pub struct DataForm { + /// The type of this form, telling the other party which action to execute. + pub type_: DataFormType, + + /// An easy accessor for the FORM_TYPE of this form, see + /// [XEP-0068](https://xmpp.org/extensions/xep-0068.html) for more + /// information. + pub form_type: Option, + + /// The title of this form. + pub title: Option, + + /// The instructions given with this form. + pub instructions: Option, + + /// A list of fields comprising this form. + pub fields: Vec, +} + +impl TryFrom for DataForm { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "x", DATA_FORMS); + check_no_unknown_attributes!(elem, "x", ["type"]); + let type_ = get_attr!(elem, "type", Required); + let mut form = DataForm { + type_, + form_type: None, + title: None, + instructions: None, + fields: vec![], + }; + for child in elem.children() { + if child.is("title", ns::DATA_FORMS) { + if form.title.is_some() { + return Err(Error::ParseError("More than one title in form element.")); + } + check_no_children!(child, "title"); + check_no_attributes!(child, "title"); + form.title = Some(child.text()); + } else if child.is("instructions", ns::DATA_FORMS) { + if form.instructions.is_some() { + return Err(Error::ParseError( + "More than one instructions in form element.", + )); + } + check_no_children!(child, "instructions"); + check_no_attributes!(child, "instructions"); + form.instructions = Some(child.text()); + } else if child.is("field", ns::DATA_FORMS) { + let field = Field::try_from(child.clone())?; + if field.var == "FORM_TYPE" { + let mut field = field; + if form.form_type.is_some() { + return Err(Error::ParseError("More than one FORM_TYPE in a data form.")); + } + if field.type_ != FieldType::Hidden { + return Err(Error::ParseError("Invalid field type for FORM_TYPE.")); + } + if field.values.len() != 1 { + return Err(Error::ParseError("Wrong number of values in FORM_TYPE.")); + } + form.form_type = field.values.pop(); + } else { + form.fields.push(field); + } + } else { + return Err(Error::ParseError("Unknown child in data form element.")); + } + } + Ok(form) + } +} + +impl From for Element { + fn from(form: DataForm) -> Element { + Element::builder("x") + .ns(ns::DATA_FORMS) + .attr("type", form.type_) + .append_all( + form.title + .map(|title| Element::builder("title").ns(ns::DATA_FORMS).append(title)), + ) + .append_all(form.instructions.map(|text| { + Element::builder("instructions") + .ns(ns::DATA_FORMS) + .append(text) + })) + .append_all(form.form_type.map(|form_type| { + Element::builder("field") + .ns(ns::DATA_FORMS) + .attr("var", "FORM_TYPE") + .attr("type", "hidden") + .append(Element::builder("value") + .ns(ns::DATA_FORMS) + .append(form_type)) + })) + .append_all(form.fields.iter().cloned().map(Element::from)) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Option_, 24); + assert_size!(FieldType, 1); + assert_size!(Field, 64); + assert_size!(DataFormType, 1); + assert_size!(DataForm, 52); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Option_, 48); + assert_size!(FieldType, 1); + assert_size!(Field, 128); + assert_size!(DataFormType, 1); + assert_size!(DataForm, 104); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + let form = DataForm::try_from(elem).unwrap(); + assert_eq!(form.type_, DataFormType::Result_); + assert!(form.form_type.is_none()); + assert!(form.fields.is_empty()); + } + + #[test] + fn test_invalid() { + let elem: Element = "".parse().unwrap(); + let error = DataForm::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'type' missing."); + + let elem: Element = "".parse().unwrap(); + let error = DataForm::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'type' attribute."); + } + + #[test] + fn test_wrong_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = DataForm::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in data form element."); + } + + #[test] + fn option() { + let elem: Element = + "" + .parse() + .unwrap(); + let option = Option_::try_from(elem).unwrap(); + assert_eq!(&option.label.unwrap(), "Coucou !"); + assert_eq!(&option.value, "coucou"); + + let elem: Element = "".parse().unwrap(); + let error = Option_::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!( + message, + "Element option must not have more than one value child." + ); + } +} diff --git a/xmpp-parsers/src/date.rs b/xmpp-parsers/src/date.rs new file mode 100644 index 0000000000000000000000000000000000000000..bfcf4e166de8cf1097a5d4f6010c87ae48536b7e --- /dev/null +++ b/xmpp-parsers/src/date.rs @@ -0,0 +1,138 @@ +// 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 crate::util::error::Error; +use chrono::{DateTime as ChronoDateTime, FixedOffset}; +use minidom::{IntoAttributeValue, Node}; +use std::str::FromStr; + +/// Implements the DateTime profile of XEP-0082, which represents a +/// non-recurring moment in time, with an accuracy of seconds or fraction of +/// seconds, and includes a timezone. +#[derive(Debug, Clone, PartialEq)] +pub struct DateTime(ChronoDateTime); + +impl DateTime { + /// Retrieves the associated timezone. + pub fn timezone(&self) -> FixedOffset { + self.0.timezone() + } + + /// Returns a new `DateTime` with a different timezone. + pub fn with_timezone(&self, tz: FixedOffset) -> DateTime { + DateTime(self.0.with_timezone(&tz)) + } + + /// Formats this `DateTime` with the specified format string. + pub fn format(&self, fmt: &str) -> String { + format!("{}", self.0.format(fmt)) + } +} + +impl FromStr for DateTime { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(DateTime(ChronoDateTime::parse_from_rfc3339(s)?)) + } +} + +impl IntoAttributeValue for DateTime { + fn into_attribute_value(self) -> Option { + Some(self.0.to_rfc3339()) + } +} + +impl Into for DateTime { + fn into(self) -> Node { + Node::Text(self.0.to_rfc3339()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::{Datelike, Timelike}; + use std::error::Error as StdError; + + // DateTime’s size doesn’t depend on the architecture. + #[test] + fn test_size() { + assert_size!(DateTime, 16); + } + + #[test] + fn test_simple() { + let date: DateTime = "2002-09-10T23:08:25Z".parse().unwrap(); + assert_eq!(date.0.year(), 2002); + assert_eq!(date.0.month(), 9); + assert_eq!(date.0.day(), 10); + assert_eq!(date.0.hour(), 23); + assert_eq!(date.0.minute(), 08); + assert_eq!(date.0.second(), 25); + assert_eq!(date.0.nanosecond(), 0); + assert_eq!(date.0.timezone(), FixedOffset::east(0)); + } + + #[test] + fn test_invalid_date() { + // There is no thirteenth month. + let error = DateTime::from_str("2017-13-01T12:23:34Z").unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.description(), "input is out of range"); + + // Timezone ≥24:00 aren’t allowed. + let error = DateTime::from_str("2017-05-27T12:11:02+25:00").unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.description(), "input is out of range"); + + // Timezone without the : separator aren’t allowed. + let error = DateTime::from_str("2017-05-27T12:11:02+0100").unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.description(), "input contains invalid characters"); + + // No seconds, error message could be improved. + let error = DateTime::from_str("2017-05-27T12:11+01:00").unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.description(), "input contains invalid characters"); + + // TODO: maybe we’ll want to support this one, as per XEP-0082 §4. + let error = DateTime::from_str("20170527T12:11:02+01:00").unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.description(), "input contains invalid characters"); + + // No timezone. + let error = DateTime::from_str("2017-05-27T12:11:02").unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.description(), "premature end of input"); + } + + #[test] + fn test_serialise() { + let date = + DateTime(ChronoDateTime::parse_from_rfc3339("2017-05-21T20:19:55+01:00").unwrap()); + let attr = date.into_attribute_value(); + assert_eq!(attr, Some(String::from("2017-05-21T20:19:55+01:00"))); + } +} diff --git a/xmpp-parsers/src/delay.rs b/xmpp-parsers/src/delay.rs new file mode 100644 index 0000000000000000000000000000000000000000..e7c7449a73d3e4fafc3832439fbaf520770828ca --- /dev/null +++ b/xmpp-parsers/src/delay.rs @@ -0,0 +1,118 @@ +// 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 crate::date::DateTime; +use crate::util::helpers::PlainText; +use crate::message::MessagePayload; +use crate::presence::PresencePayload; +use jid::Jid; + +generate_element!( + /// Notes when and by whom a message got stored for later delivery. + Delay, "delay", DELAY, + attributes: [ + /// The entity which delayed this message. + from: Option = "from", + + /// The time at which this message got stored. + stamp: Required = "stamp" + ], + text: ( + /// The optional reason this message got delayed. + data: PlainText> + ) +); + +impl MessagePayload for Delay {} +impl PresencePayload for Delay {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::str::FromStr; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Delay, 68); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Delay, 120); + } + + #[test] + fn test_simple() { + let elem: Element = + "" + .parse() + .unwrap(); + let delay = Delay::try_from(elem).unwrap(); + assert_eq!(delay.from, Some(Jid::from_str("capulet.com").unwrap())); + assert_eq!( + delay.stamp, + DateTime::from_str("2002-09-10T23:08:25Z").unwrap() + ); + assert_eq!(delay.data, None); + } + + #[test] + fn test_unknown() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Delay::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "This is not a delay element."); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Delay::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in delay element."); + } + + #[test] + fn test_serialise() { + let elem: Element = "" + .parse() + .unwrap(); + let delay = Delay { + from: None, + stamp: DateTime::from_str("2002-09-10T23:08:25Z").unwrap(), + data: None, + }; + let elem2 = delay.into(); + assert_eq!(elem, elem2); + } + + #[test] + fn test_serialise_data() { + let elem: Element = "Reason".parse().unwrap(); + let delay = Delay { + from: Some(Jid::from_str("juliet@example.org").unwrap()), + stamp: DateTime::from_str("2002-09-10T23:08:25Z").unwrap(), + data: Some(String::from("Reason")), + }; + let elem2 = delay.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/disco.rs b/xmpp-parsers/src/disco.rs new file mode 100644 index 0000000000000000000000000000000000000000..040ce8c42e9b4d94f242d09092240b3c80f9ca67 --- /dev/null +++ b/xmpp-parsers/src/disco.rs @@ -0,0 +1,457 @@ +// 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 crate::data_forms::{DataForm, DataFormType}; +use crate::util::error::Error; +use crate::iq::{IqGetPayload, IqResultPayload}; +use crate::ns; +use jid::Jid; +use crate::Element; +use std::convert::TryFrom; + +generate_element!( +/// Structure representing a `` element. +/// +/// It should only be used in an ``, as it can only represent +/// the request, and not a result. +DiscoInfoQuery, "query", DISCO_INFO, +attributes: [ + /// Node on which we are doing the discovery. + node: Option = "node", +]); + +impl IqGetPayload for DiscoInfoQuery {} + +generate_element!( +/// Structure representing a `` element. +#[derive(PartialEq)] +Feature, "feature", DISCO_INFO, +attributes: [ + /// Namespace of the feature we want to represent. + var: Required = "var", +]); + +impl Feature { + /// Create a new `` with the according `@var`. + pub fn new>(var: S) -> Feature { + Feature { + var: var.into(), + } + } +} + +generate_element!( + /// Structure representing an `` element. + Identity, "identity", DISCO_INFO, + attributes: [ + /// Category of this identity. + // TODO: use an enum here. + category: RequiredNonEmpty = "category", + + /// Type of this identity. + // TODO: use an enum here. + type_: RequiredNonEmpty = "type", + + /// Lang of the name of this identity. + lang: Option = "xml:lang", + + /// Name of this identity. + name: Option = "name", + ] +); + +impl Identity { + /// Create a new ``. + pub fn new(category: C, type_: T, lang: L, name: N) -> Identity + where C: Into, + T: Into, + L: Into, + N: Into, + { + Identity { + category: category.into(), + type_: type_.into(), + lang: Some(lang.into()), + name: Some(name.into()), + } + } + + /// Create a new `` without a name. + pub fn new_anonymous(category: C, type_: T) -> Identity + where C: Into, + T: Into, + { + Identity { + category: category.into(), + type_: type_.into(), + lang: None, + name: None, + } + } +} + +/// Structure representing a `` element. +/// +/// It should only be used in an ``, as it can only +/// represent the result, and not a request. +#[derive(Debug, Clone)] +pub struct DiscoInfoResult { + /// Node on which we have done this discovery. + pub node: Option, + + /// List of identities exposed by this entity. + pub identities: Vec, + + /// List of features supported by this entity. + pub features: Vec, + + /// List of extensions reported by this entity. + pub extensions: Vec, +} + +impl IqResultPayload for DiscoInfoResult {} + +impl TryFrom for DiscoInfoResult { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "query", DISCO_INFO, "disco#info result"); + check_no_unknown_attributes!(elem, "disco#info result", ["node"]); + + let mut result = DiscoInfoResult { + node: get_attr!(elem, "node", Option), + identities: vec![], + features: vec![], + extensions: vec![], + }; + + for child in elem.children() { + if child.is("identity", ns::DISCO_INFO) { + let identity = Identity::try_from(child.clone())?; + result.identities.push(identity); + } else if child.is("feature", ns::DISCO_INFO) { + let feature = Feature::try_from(child.clone())?; + result.features.push(feature); + } else if child.is("x", ns::DATA_FORMS) { + let data_form = DataForm::try_from(child.clone())?; + if data_form.type_ != DataFormType::Result_ { + return Err(Error::ParseError( + "Data form must have a 'result' type in disco#info.", + )); + } + if data_form.form_type.is_none() { + return Err(Error::ParseError("Data form found without a FORM_TYPE.")); + } + result.extensions.push(data_form); + } else { + return Err(Error::ParseError("Unknown element in disco#info.")); + } + } + + if result.identities.is_empty() { + return Err(Error::ParseError( + "There must be at least one identity in disco#info.", + )); + } + if result.features.is_empty() { + return Err(Error::ParseError( + "There must be at least one feature in disco#info.", + )); + } + if !result.features.contains(&Feature { + var: ns::DISCO_INFO.to_owned(), + }) { + return Err(Error::ParseError( + "disco#info feature not present in disco#info.", + )); + } + + Ok(result) + } +} + +impl From for Element { + fn from(disco: DiscoInfoResult) -> Element { + Element::builder("query") + .ns(ns::DISCO_INFO) + .attr("node", disco.node) + .append_all(disco.identities.into_iter()) + .append_all(disco.features.into_iter()) + .append_all(disco.extensions.iter().cloned().map(Element::from)) + .build() + } +} + +generate_element!( +/// Structure representing a `` element. +/// +/// It should only be used in an ``, as it can only represent +/// the request, and not a result. +DiscoItemsQuery, "query", DISCO_ITEMS, +attributes: [ + /// Node on which we are doing the discovery. + node: Option = "node", +]); + +impl IqGetPayload for DiscoItemsQuery {} + +generate_element!( +/// Structure representing an `` element. +Item, "item", DISCO_ITEMS, +attributes: [ + /// JID of the entity pointed by this item. + jid: Required = "jid", + /// Node of the entity pointed by this item. + node: Option = "node", + /// Name of the entity pointed by this item. + name: Option = "name", +]); + +generate_element!( + /// Structure representing a `` element. + /// + /// It should only be used in an ``, as it can only + /// represent the result, and not a request. + DiscoItemsResult, "query", DISCO_ITEMS, + attributes: [ + /// Node on which we have done this discovery. + node: Option = "node" + ], + children: [ + /// List of items pointed by this entity. + items: Vec = ("item", DISCO_ITEMS) => Item + ] +); + +impl IqResultPayload for DiscoItemsResult {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::compare_elements::NamespaceAwareCompare; + use std::str::FromStr; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Identity, 48); + assert_size!(Feature, 12); + assert_size!(DiscoInfoQuery, 12); + assert_size!(DiscoInfoResult, 48); + + assert_size!(Item, 64); + assert_size!(DiscoItemsQuery, 12); + assert_size!(DiscoItemsResult, 24); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Identity, 96); + assert_size!(Feature, 24); + assert_size!(DiscoInfoQuery, 24); + assert_size!(DiscoInfoResult, 96); + + assert_size!(Item, 128); + assert_size!(DiscoItemsQuery, 24); + assert_size!(DiscoItemsResult, 48); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + let query = DiscoInfoResult::try_from(elem).unwrap(); + assert!(query.node.is_none()); + assert_eq!(query.identities.len(), 1); + assert_eq!(query.features.len(), 1); + assert!(query.extensions.is_empty()); + } + + #[test] + fn test_identity_after_feature() { + let elem: Element = "".parse().unwrap(); + let query = DiscoInfoResult::try_from(elem).unwrap(); + assert_eq!(query.identities.len(), 1); + assert_eq!(query.features.len(), 1); + assert!(query.extensions.is_empty()); + } + + #[test] + fn test_feature_after_dataform() { + let elem: Element = "coucou".parse().unwrap(); + let query = DiscoInfoResult::try_from(elem).unwrap(); + assert_eq!(query.identities.len(), 1); + assert_eq!(query.features.len(), 1); + assert_eq!(query.extensions.len(), 1); + } + + #[test] + fn test_extension() { + let elem: Element = "example".parse().unwrap(); + let elem1 = elem.clone(); + let query = DiscoInfoResult::try_from(elem).unwrap(); + assert!(query.node.is_none()); + assert_eq!(query.identities.len(), 1); + assert_eq!(query.features.len(), 1); + assert_eq!(query.extensions.len(), 1); + assert_eq!(query.extensions[0].form_type, Some(String::from("example"))); + + let elem2 = query.into(); + assert!(elem1.compare_to(&elem2)); + } + + #[test] + fn test_invalid() { + let elem: Element = + "" + .parse() + .unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown element in disco#info."); + } + + #[test] + fn test_invalid_identity() { + let elem: Element = + "" + .parse() + .unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'category' missing."); + + let elem: Element = + "" + .parse() + .unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!( + message, + "Required attribute 'category' must not be empty." + ); + + let elem: Element = "".parse().unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'type' missing."); + + let elem: Element = "".parse().unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'type' must not be empty."); + } + + #[test] + fn test_invalid_feature() { + let elem: Element = + "" + .parse() + .unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'var' missing."); + } + + #[test] + fn test_invalid_result() { + let elem: Element = "" + .parse() + .unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!( + message, + "There must be at least one identity in disco#info." + ); + + let elem: Element = "".parse().unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "There must be at least one feature in disco#info."); + + let elem: Element = "".parse().unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "disco#info feature not present in disco#info."); + } + + #[test] + fn test_simple_items() { + let elem: Element = "" + .parse() + .unwrap(); + let query = DiscoItemsQuery::try_from(elem).unwrap(); + assert!(query.node.is_none()); + + let elem: Element = "" + .parse() + .unwrap(); + let query = DiscoItemsQuery::try_from(elem).unwrap(); + assert_eq!(query.node, Some(String::from("coucou"))); + } + + #[test] + fn test_simple_items_result() { + let elem: Element = "" + .parse() + .unwrap(); + let query = DiscoItemsResult::try_from(elem).unwrap(); + assert!(query.node.is_none()); + assert!(query.items.is_empty()); + + let elem: Element = "" + .parse() + .unwrap(); + let query = DiscoItemsResult::try_from(elem).unwrap(); + assert_eq!(query.node, Some(String::from("coucou"))); + assert!(query.items.is_empty()); + } + + #[test] + fn test_answers_items_result() { + let elem: Element = "".parse().unwrap(); + let query = DiscoItemsResult::try_from(elem).unwrap(); + let elem2 = Element::from(query); + let query = DiscoItemsResult::try_from(elem2).unwrap(); + assert_eq!(query.items.len(), 2); + assert_eq!(query.items[0].jid, Jid::from_str("component").unwrap()); + assert_eq!(query.items[0].node, None); + assert_eq!(query.items[0].name, None); + assert_eq!(query.items[1].jid, Jid::from_str("component2").unwrap()); + assert_eq!(query.items[1].node, Some(String::from("test"))); + assert_eq!(query.items[1].name, Some(String::from("A component"))); + } +} diff --git a/xmpp-parsers/src/ecaps2.rs b/xmpp-parsers/src/ecaps2.rs new file mode 100644 index 0000000000000000000000000000000000000000..cfcb340f34a1891465f015736174104d6c51beb8 --- /dev/null +++ b/xmpp-parsers/src/ecaps2.rs @@ -0,0 +1,472 @@ +// 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 crate::data_forms::DataForm; +use crate::disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity}; +use crate::hashes::{Algo, Hash}; +use crate::ns; +use crate::presence::PresencePayload; +use blake2::VarBlake2b; +use digest::{Digest, Input, VariableOutput}; +use sha2::{Sha256, Sha512}; +use sha3::{Sha3_256, Sha3_512}; + +generate_element!( + /// Represents a set of capability hashes, all of them must correspond to + /// the same input [disco#info](../disco/struct.DiscoInfoResult.html), + /// using different [algorithms](../hashes/enum.Algo.html). + ECaps2, "c", ECAPS2, + children: [ + /// Hashes of the [disco#info](../disco/struct.DiscoInfoResult.html). + hashes: Vec = ("hash", HASHES) => Hash + ] +); + +impl PresencePayload for ECaps2 {} + +impl ECaps2 { + /// Create an ECaps2 element from a list of hashes. + pub fn new(hashes: Vec) -> ECaps2 { + ECaps2 { + hashes, + } + } +} + +fn compute_item(field: &str) -> Vec { + let mut bytes = field.as_bytes().to_vec(); + bytes.push(0x1f); + bytes +} + +fn compute_items Vec>(things: &[T], separator: u8, encode: F) -> Vec { + let mut string: Vec = vec![]; + let mut accumulator: Vec> = vec![]; + for thing in things { + let bytes = encode(thing); + accumulator.push(bytes); + } + // This works using the expected i;octet collation. + accumulator.sort(); + for mut bytes in accumulator { + string.append(&mut bytes); + } + string.push(separator); + string +} + +fn compute_features(features: &[Feature]) -> Vec { + compute_items(features, 0x1c, |feature| compute_item(&feature.var)) +} + +fn compute_identities(identities: &[Identity]) -> Vec { + compute_items(identities, 0x1c, |identity| { + let mut bytes = compute_item(&identity.category); + bytes.append(&mut compute_item(&identity.type_)); + bytes.append(&mut compute_item( + &identity.lang.clone().unwrap_or_default(), + )); + bytes.append(&mut compute_item( + &identity.name.clone().unwrap_or_default(), + )); + bytes.push(0x1e); + bytes + }) +} + +fn compute_extensions(extensions: &[DataForm]) -> Result, ()> { + for extension in extensions { + if extension.form_type.is_none() { + return Err(()); + } + } + Ok(compute_items(extensions, 0x1c, |extension| { + let mut bytes = compute_item("FORM_TYPE"); + bytes.append(&mut compute_item(if let Some(ref form_type) = extension.form_type { form_type } else { unreachable!() })); + bytes.push(0x1e); + bytes.append(&mut compute_items(&extension.fields, 0x1d, |field| { + let mut bytes = compute_item(&field.var); + bytes.append(&mut compute_items(&field.values, 0x1e, |value| { + compute_item(value) + })); + bytes + })); + bytes + })) +} + +/// Applies the [algorithm from +/// XEP-0390](https://xmpp.org/extensions/xep-0390.html#algorithm-input) on a +/// [disco#info query element](../disco/struct.DiscoInfoResult.html). +pub fn compute_disco(disco: &DiscoInfoResult) -> Result, ()> { + let features_string = compute_features(&disco.features); + let identities_string = compute_identities(&disco.identities); + let extensions_string = compute_extensions(&disco.extensions)?; + + let mut final_string = vec![]; + final_string.extend(features_string); + final_string.extend(identities_string); + final_string.extend(extensions_string); + Ok(final_string) +} + +fn get_hash_vec(hash: &[u8]) -> Vec { + let mut vec = Vec::with_capacity(hash.len()); + vec.extend_from_slice(hash); + vec +} + +/// Hashes the result of [compute_disco()] with one of the supported [hash +/// algorithms](../hashes/enum.Algo.html). +pub fn hash_ecaps2(data: &[u8], algo: Algo) -> Result { + Ok(Hash { + hash: match algo { + Algo::Sha_256 => { + let hash = Sha256::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Sha_512 => { + let hash = Sha512::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Sha3_256 => { + let hash = Sha3_256::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Sha3_512 => { + let hash = Sha3_512::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Blake2b_256 => { + let mut hasher = VarBlake2b::new(32).unwrap(); + hasher.input(data); + hasher.vec_result() + } + Algo::Blake2b_512 => { + let mut hasher = VarBlake2b::new(64).unwrap(); + hasher.input(data); + hasher.vec_result() + } + Algo::Sha_1 => return Err(String::from("Disabled algorithm sha-1: unsafe.")), + Algo::Unknown(algo) => return Err(format!("Unknown algorithm: {}.", algo)), + }, + algo, + }) +} + +/// Helper function to create the query for the disco#info corresponding to an +/// ecaps2 hash. +pub fn query_ecaps2(hash: Hash) -> DiscoInfoQuery { + DiscoInfoQuery { + node: Some(format!( + "{}#{}.{}", + ns::ECAPS2, + String::from(hash.algo), + base64::encode(&hash.hash) + )), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(ECaps2, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(ECaps2, 24); + } + + #[test] + fn test_parse() { + let elem: Element = "K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=".parse().unwrap(); + let ecaps2 = ECaps2::try_from(elem).unwrap(); + assert_eq!(ecaps2.hashes.len(), 2); + assert_eq!(ecaps2.hashes[0].algo, Algo::Sha_256); + assert_eq!( + ecaps2.hashes[0].hash, + base64::decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=").unwrap() + ); + assert_eq!(ecaps2.hashes[1].algo, Algo::Sha3_256); + assert_eq!( + ecaps2.hashes[1].hash, + base64::decode("+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=").unwrap() + ); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=".parse().unwrap(); + let error = ECaps2::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in c element."); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + let disco = DiscoInfoResult::try_from(elem).unwrap(); + let ecaps2 = compute_disco(&disco).unwrap(); + assert_eq!(ecaps2.len(), 54); + } + + #[test] + fn test_xep_ex1() { + let elem: Element = r#" + + + + + + + + + + + + + + + + + + + + +"# + .parse() + .unwrap(); + let expected = vec![ + 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, + 114, 111, 116, 111, 99, 111, 108, 47, 98, 121, 116, 101, 115, 116, 114, 101, 97, 109, + 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, + 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 99, 104, 97, 116, 115, 116, 97, 116, + 101, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, + 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, + 110, 102, 111, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, + 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, + 105, 116, 101, 109, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, + 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 105, 98, 98, 31, 104, + 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, + 111, 116, 111, 99, 111, 108, 47, 114, 111, 115, 116, 101, 114, 120, 31, 104, 116, 116, + 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, + 111, 99, 111, 108, 47, 115, 105, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, + 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 115, 105, + 47, 112, 114, 111, 102, 105, 108, 101, 47, 102, 105, 108, 101, 45, 116, 114, 97, 110, + 115, 102, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 108, 97, 115, 116, + 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 112, 114, 105, 118, 97, 99, 121, 31, + 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 114, 111, 115, 116, 101, 114, 31, 106, 97, + 98, 98, 101, 114, 58, 105, 113, 58, 116, 105, 109, 101, 31, 106, 97, 98, 98, 101, 114, + 58, 105, 113, 58, 118, 101, 114, 115, 105, 111, 110, 31, 106, 97, 98, 98, 101, 114, 58, + 120, 58, 111, 111, 98, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 112, 105, 110, + 103, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 114, 101, 99, 101, 105, 112, 116, + 115, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 116, 105, 109, 101, 31, 28, 99, + 108, 105, 101, 110, 116, 31, 109, 111, 98, 105, 108, 101, 31, 31, 66, 111, 109, 98, + 117, 115, 77, 111, 100, 31, 30, 28, 28, + ]; + let disco = DiscoInfoResult::try_from(elem).unwrap(); + let ecaps2 = compute_disco(&disco).unwrap(); + assert_eq!(ecaps2.len(), 0x1d9); + assert_eq!(ecaps2, expected); + + let sha_256 = hash_ecaps2(&ecaps2, Algo::Sha_256).unwrap(); + assert_eq!( + sha_256.hash, + base64::decode("kzBZbkqJ3ADrj7v08reD1qcWUwNGHaidNUgD7nHpiw8=").unwrap() + ); + let sha3_256 = hash_ecaps2(&ecaps2, Algo::Sha3_256).unwrap(); + assert_eq!( + sha3_256.hash, + base64::decode("79mdYAfU9rEdTOcWDO7UEAt6E56SUzk/g6TnqUeuD9Q=").unwrap() + ); + } + + #[test] + fn test_xep_ex2() { + let elem: Element = r#" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + urn:xmpp:dataforms:softwareinfo + + + Tkabber + + + 0.11.1-svn-20111216-mod (Tcl/Tk 8.6b2) + + + Windows + + + XP + + + +"# + .parse() + .unwrap(); + let expected = vec![ + 103, 97, 109, 101, 115, 58, 98, 111, 97, 114, 100, 31, 104, 116, 116, 112, 58, 47, 47, + 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, + 108, 47, 97, 99, 116, 105, 118, 105, 116, 121, 31, 104, 116, 116, 112, 58, 47, 47, 106, + 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, + 97, 99, 116, 105, 118, 105, 116, 121, 43, 110, 111, 116, 105, 102, 121, 31, 104, 116, + 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, + 116, 111, 99, 111, 108, 47, 98, 121, 116, 101, 115, 116, 114, 101, 97, 109, 115, 31, + 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, + 114, 111, 116, 111, 99, 111, 108, 47, 99, 104, 97, 116, 115, 116, 97, 116, 101, 115, + 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, + 112, 114, 111, 116, 111, 99, 111, 108, 47, 99, 111, 109, 109, 97, 110, 100, 115, 31, + 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, + 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, 110, 102, 111, + 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, + 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, 116, 101, + 109, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, + 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 101, 118, 105, 108, 31, 104, 116, + 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, + 116, 111, 99, 111, 108, 47, 102, 101, 97, 116, 117, 114, 101, 45, 110, 101, 103, 31, + 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, + 114, 111, 116, 111, 99, 111, 108, 47, 103, 101, 111, 108, 111, 99, 31, 104, 116, 116, + 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, + 111, 99, 111, 108, 47, 103, 101, 111, 108, 111, 99, 43, 110, 111, 116, 105, 102, 121, + 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, + 112, 114, 111, 116, 111, 99, 111, 108, 47, 105, 98, 98, 31, 104, 116, 116, 112, 58, 47, + 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, + 108, 47, 105, 113, 105, 98, 98, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, + 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 109, 111, + 111, 100, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, + 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 109, 111, 111, 100, 43, 110, 111, + 116, 105, 102, 121, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, + 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 114, 111, 115, 116, 101, + 114, 120, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, + 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 115, 105, 31, 104, 116, 116, 112, + 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, + 99, 111, 108, 47, 115, 105, 47, 112, 114, 111, 102, 105, 108, 101, 47, 102, 105, 108, + 101, 45, 116, 114, 97, 110, 115, 102, 101, 114, 31, 104, 116, 116, 112, 58, 47, 47, + 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, + 108, 47, 116, 117, 110, 101, 31, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119, 46, + 102, 97, 99, 101, 98, 111, 111, 107, 46, 99, 111, 109, 47, 120, 109, 112, 112, 47, 109, + 101, 115, 115, 97, 103, 101, 115, 31, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119, + 46, 120, 109, 112, 112, 46, 111, 114, 103, 47, 101, 120, 116, 101, 110, 115, 105, 111, + 110, 115, 47, 120, 101, 112, 45, 48, 48, 56, 52, 46, 104, 116, 109, 108, 35, 110, 115, + 45, 109, 101, 116, 97, 100, 97, 116, 97, 43, 110, 111, 116, 105, 102, 121, 31, 106, 97, + 98, 98, 101, 114, 58, 105, 113, 58, 97, 118, 97, 116, 97, 114, 31, 106, 97, 98, 98, + 101, 114, 58, 105, 113, 58, 98, 114, 111, 119, 115, 101, 31, 106, 97, 98, 98, 101, 114, + 58, 105, 113, 58, 100, 116, 99, 112, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, + 102, 105, 108, 101, 120, 102, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, + 58, 105, 98, 98, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 105, 110, 98, 97, + 110, 100, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 106, 105, 100, 108, 105, + 110, 107, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 108, 97, 115, 116, 31, 106, + 97, 98, 98, 101, 114, 58, 105, 113, 58, 111, 111, 98, 31, 106, 97, 98, 98, 101, 114, + 58, 105, 113, 58, 112, 114, 105, 118, 97, 99, 121, 31, 106, 97, 98, 98, 101, 114, 58, + 105, 113, 58, 114, 111, 115, 116, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, + 113, 58, 116, 105, 109, 101, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 118, 101, + 114, 115, 105, 111, 110, 31, 106, 97, 98, 98, 101, 114, 58, 120, 58, 100, 97, 116, 97, + 31, 106, 97, 98, 98, 101, 114, 58, 120, 58, 101, 118, 101, 110, 116, 31, 106, 97, 98, + 98, 101, 114, 58, 120, 58, 111, 111, 98, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, + 97, 118, 97, 116, 97, 114, 58, 109, 101, 116, 97, 100, 97, 116, 97, 43, 110, 111, 116, + 105, 102, 121, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 112, 105, 110, 103, 31, + 117, 114, 110, 58, 120, 109, 112, 112, 58, 114, 101, 99, 101, 105, 112, 116, 115, 31, + 117, 114, 110, 58, 120, 109, 112, 112, 58, 116, 105, 109, 101, 31, 28, 99, 108, 105, + 101, 110, 116, 31, 112, 99, 31, 101, 110, 31, 84, 107, 97, 98, 98, 101, 114, 31, 30, + 99, 108, 105, 101, 110, 116, 31, 112, 99, 31, 114, 117, 31, 208, 162, 208, 186, 208, + 176, 208, 177, 208, 177, 208, 181, 209, 128, 31, 30, 28, 70, 79, 82, 77, 95, 84, 89, + 80, 69, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 100, 97, 116, 97, 102, 111, 114, + 109, 115, 58, 115, 111, 102, 116, 119, 97, 114, 101, 105, 110, 102, 111, 31, 30, 111, + 115, 31, 87, 105, 110, 100, 111, 119, 115, 31, 30, 111, 115, 95, 118, 101, 114, 115, + 105, 111, 110, 31, 88, 80, 31, 30, 115, 111, 102, 116, 119, 97, 114, 101, 31, 84, 107, + 97, 98, 98, 101, 114, 31, 30, 115, 111, 102, 116, 119, 97, 114, 101, 95, 118, 101, 114, + 115, 105, 111, 110, 31, 48, 46, 49, 49, 46, 49, 45, 115, 118, 110, 45, 50, 48, 49, 49, + 49, 50, 49, 54, 45, 109, 111, 100, 32, 40, 84, 99, 108, 47, 84, 107, 32, 56, 46, 54, + 98, 50, 41, 31, 30, 29, 28, + ]; + let disco = DiscoInfoResult::try_from(elem).unwrap(); + let ecaps2 = compute_disco(&disco).unwrap(); + assert_eq!(ecaps2.len(), 0x543); + assert_eq!(ecaps2, expected); + + let sha_256 = hash_ecaps2(&ecaps2, Algo::Sha_256).unwrap(); + assert_eq!( + sha_256.hash, + base64::decode("u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY=").unwrap() + ); + let sha3_256 = hash_ecaps2(&ecaps2, Algo::Sha3_256).unwrap(); + assert_eq!( + sha3_256.hash, + base64::decode("XpUJzLAc93258sMECZ3FJpebkzuyNXDzRNwQog8eycg=").unwrap() + ); + } + + #[test] + fn test_blake2b_512() { + let hash = hash_ecaps2("abc".as_bytes(), Algo::Blake2b_512).unwrap(); + let known_hash: Vec = vec![ + 0xBA, 0x80, 0xA5, 0x3F, 0x98, 0x1C, 0x4D, 0x0D, 0x6A, 0x27, 0x97, 0xB6, 0x9F, 0x12, + 0xF6, 0xE9, 0x4C, 0x21, 0x2F, 0x14, 0x68, 0x5A, 0xC4, 0xB7, 0x4B, 0x12, 0xBB, 0x6F, + 0xDB, 0xFF, 0xA2, 0xD1, 0x7D, 0x87, 0xC5, 0x39, 0x2A, 0xAB, 0x79, 0x2D, 0xC2, 0x52, + 0xD5, 0xDE, 0x45, 0x33, 0xCC, 0x95, 0x18, 0xD3, 0x8A, 0xA8, 0xDB, 0xF1, 0x92, 0x5A, + 0xB9, 0x23, 0x86, 0xED, 0xD4, 0x00, 0x99, 0x23, + ]; + assert_eq!(hash.hash, known_hash); + } +} diff --git a/xmpp-parsers/src/eme.rs b/xmpp-parsers/src/eme.rs new file mode 100644 index 0000000000000000000000000000000000000000..1014318e36eceb79dd3ef0313969aa7d3102596a --- /dev/null +++ b/xmpp-parsers/src/eme.rs @@ -0,0 +1,96 @@ +// 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 crate::message::MessagePayload; + +generate_element!( + /// Structure representing an `` element. + ExplicitMessageEncryption, "encryption", EME, + attributes: [ + /// Namespace of the encryption scheme used. + namespace: Required = "namespace", + + /// User-friendly name for the encryption scheme, should be `None` for OTR, + /// legacy OpenPGP and OX. + name: Option = "name", + ] +); + +impl MessagePayload for ExplicitMessageEncryption {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(ExplicitMessageEncryption, 24); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(ExplicitMessageEncryption, 48); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let encryption = ExplicitMessageEncryption::try_from(elem).unwrap(); + assert_eq!(encryption.namespace, "urn:xmpp:otr:0"); + assert_eq!(encryption.name, None); + + let elem: Element = "".parse().unwrap(); + let encryption = ExplicitMessageEncryption::try_from(elem).unwrap(); + assert_eq!(encryption.namespace, "some.unknown.mechanism"); + assert_eq!(encryption.name, Some(String::from("SuperMechanism"))); + } + + #[test] + fn test_unknown() { + let elem: Element = "" + .parse() + .unwrap(); + let error = ExplicitMessageEncryption::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "This is not a encryption element."); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = ExplicitMessageEncryption::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in encryption element."); + } + + #[test] + fn test_serialise() { + let elem: Element = "" + .parse() + .unwrap(); + let eme = ExplicitMessageEncryption { + namespace: String::from("coucou"), + name: None, + }; + let elem2 = eme.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/forwarding.rs b/xmpp-parsers/src/forwarding.rs new file mode 100644 index 0000000000000000000000000000000000000000..6517d22f9f280d27fb4d339d99312a11efca36df --- /dev/null +++ b/xmpp-parsers/src/forwarding.rs @@ -0,0 +1,74 @@ +// 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 crate::delay::Delay; +use crate::message::Message; + +generate_element!( + /// Contains a forwarded stanza, either standalone or part of another + /// extension (such as carbons). + Forwarded, "forwarded", FORWARD, + children: [ + /// When the stanza originally got sent. + delay: Option = ("delay", DELAY) => Delay, + + // XXX: really? Option? + /// The stanza being forwarded. + stanza: Option = ("message", DEFAULT_NS) => Message + + // TODO: also handle the two other stanza possibilities. + ] +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Forwarded, 212); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Forwarded, 408); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + Forwarded::try_from(elem).unwrap(); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Forwarded::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in forwarded element."); + } + + #[test] + fn test_serialise() { + let elem: Element = "".parse().unwrap(); + let forwarded = Forwarded { + delay: None, + stanza: None, + }; + let elem2 = forwarded.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/hashes.rs b/xmpp-parsers/src/hashes.rs new file mode 100644 index 0000000000000000000000000000000000000000..56b61355507471a549f96caf3125910009b5d307 --- /dev/null +++ b/xmpp-parsers/src/hashes.rs @@ -0,0 +1,269 @@ +// 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 crate::util::error::Error; +use crate::util::helpers::Base64; +use minidom::IntoAttributeValue; +use std::num::ParseIntError; +use std::ops::{Deref, DerefMut}; +use std::str::FromStr; + +/// List of the algorithms we support, or Unknown. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Algo { + /// The Secure Hash Algorithm 1, with known vulnerabilities, do not use it. + /// + /// See https://tools.ietf.org/html/rfc3174 + Sha_1, + + /// The Secure Hash Algorithm 2, in its 256-bit version. + /// + /// See https://tools.ietf.org/html/rfc6234 + Sha_256, + + /// The Secure Hash Algorithm 2, in its 512-bit version. + /// + /// See https://tools.ietf.org/html/rfc6234 + Sha_512, + + /// The Secure Hash Algorithm 3, based on Keccak, in its 256-bit version. + /// + /// See https://keccak.team/files/Keccak-submission-3.pdf + Sha3_256, + + /// The Secure Hash Algorithm 3, based on Keccak, in its 512-bit version. + /// + /// See https://keccak.team/files/Keccak-submission-3.pdf + Sha3_512, + + /// The BLAKE2 hash algorithm, for a 256-bit output. + /// + /// See https://tools.ietf.org/html/rfc7693 + Blake2b_256, + + /// The BLAKE2 hash algorithm, for a 512-bit output. + /// + /// See https://tools.ietf.org/html/rfc7693 + Blake2b_512, + + /// An unknown hash not in this list, you can probably reject it. + Unknown(String), +} + +impl FromStr for Algo { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "" => return Err(Error::ParseError("'algo' argument can’t be empty.")), + + "sha-1" => Algo::Sha_1, + "sha-256" => Algo::Sha_256, + "sha-512" => Algo::Sha_512, + "sha3-256" => Algo::Sha3_256, + "sha3-512" => Algo::Sha3_512, + "blake2b-256" => Algo::Blake2b_256, + "blake2b-512" => Algo::Blake2b_512, + value => Algo::Unknown(value.to_owned()), + }) + } +} + +impl From for String { + fn from(algo: Algo) -> String { + String::from(match algo { + Algo::Sha_1 => "sha-1", + Algo::Sha_256 => "sha-256", + Algo::Sha_512 => "sha-512", + Algo::Sha3_256 => "sha3-256", + Algo::Sha3_512 => "sha3-512", + Algo::Blake2b_256 => "blake2b-256", + Algo::Blake2b_512 => "blake2b-512", + Algo::Unknown(text) => return text, + }) + } +} + +impl IntoAttributeValue for Algo { + fn into_attribute_value(self) -> Option { + Some(String::from(self)) + } +} + +generate_element!( + /// This element represents a hash of some data, defined by the hash + /// algorithm used and the computed value. + #[derive(PartialEq)] + Hash, "hash", HASHES, + attributes: [ + /// The algorithm used to create this hash. + algo: Required = "algo" + ], + text: ( + /// The hash value, as a vector of bytes. + hash: Base64> + ) +); + +impl Hash { + /// Creates a [Hash] element with the given algo and data. + pub fn new(algo: Algo, hash: Vec) -> Hash { + Hash { algo, hash } + } + + /// Like [new](#method.new) but takes base64-encoded data before decoding + /// it. + pub fn from_base64(algo: Algo, hash: &str) -> Result { + Ok(Hash::new(algo, base64::decode(hash)?)) + } + + /// Like [new](#method.new) but takes hex-encoded data before decoding it. + pub fn from_hex(algo: Algo, hex: &str) -> Result { + let mut bytes = vec![]; + for i in 0..hex.len() / 2 { + let byte = u8::from_str_radix(&hex[2 * i..2 * i + 2], 16)?; + bytes.push(byte); + } + Ok(Hash::new(algo, bytes)) + } + + /// Like [new](#method.new) but takes hex-encoded data before decoding it. + pub fn from_colon_separated_hex(algo: Algo, hex: &str) -> Result { + let mut bytes = vec![]; + for i in 0..(1 + hex.len()) / 3 { + let byte = u8::from_str_radix(&hex[3 * i..3 * i + 2], 16)?; + if 3 * i + 2 < hex.len() { + assert_eq!(&hex[3 * i + 2..3 * i + 3], ":"); + } + bytes.push(byte); + } + Ok(Hash::new(algo, bytes)) + } + + /// Formats this hash into base64. + pub fn to_base64(&self) -> String { + base64::encode(&self.hash[..]) + } + + /// Formats this hash into hexadecimal. + pub fn to_hex(&self) -> String { + let mut bytes = vec![]; + for byte in self.hash.iter() { + bytes.push(format!("{:02x}", byte)); + } + bytes.join("") + } + + /// Formats this hash into colon-separated hexadecimal. + pub fn to_colon_separated_hex(&self) -> String { + let mut bytes = vec![]; + for byte in self.hash.iter() { + bytes.push(format!("{:02x}", byte)); + } + bytes.join(":") + } +} + +/// Helper for parsing and serialising a SHA-1 attribute. +#[derive(Debug, Clone, PartialEq)] +pub struct Sha1HexAttribute(Hash); + +impl FromStr for Sha1HexAttribute { + type Err = ParseIntError; + + fn from_str(hex: &str) -> Result { + let hash = Hash::from_hex(Algo::Sha_1, hex)?; + Ok(Sha1HexAttribute(hash)) + } +} + +impl IntoAttributeValue for Sha1HexAttribute { + fn into_attribute_value(self) -> Option { + Some(self.to_hex()) + } +} + +impl DerefMut for Sha1HexAttribute { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Deref for Sha1HexAttribute { + type Target = Hash; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Algo, 16); + assert_size!(Hash, 28); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Algo, 32); + assert_size!(Hash, 56); + } + + #[test] + fn test_simple() { + let elem: Element = "2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=".parse().unwrap(); + let hash = Hash::try_from(elem).unwrap(); + assert_eq!(hash.algo, Algo::Sha_256); + assert_eq!( + hash.hash, + base64::decode("2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=").unwrap() + ); + } + + #[test] + fn value_serialisation() { + let elem: Element = "2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=".parse().unwrap(); + let hash = Hash::try_from(elem).unwrap(); + assert_eq!(hash.to_base64(), "2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU="); + assert_eq!(hash.to_hex(), "d976ab9b04e53710c0324bf29a5a17dd2e7e55bca536b26dfe5e50c8f6be6285"); + assert_eq!(hash.to_colon_separated_hex(), "d9:76:ab:9b:04:e5:37:10:c0:32:4b:f2:9a:5a:17:dd:2e:7e:55:bc:a5:36:b2:6d:fe:5e:50:c8:f6:be:62:85"); + } + + #[test] + fn test_unknown() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Hash::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "This is not a hash element."); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Hash::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in hash element."); + } +} diff --git a/xmpp-parsers/src/ibb.rs b/xmpp-parsers/src/ibb.rs new file mode 100644 index 0000000000000000000000000000000000000000..719fd8537e66390ba8722dc8c9edaafecad4c6ec --- /dev/null +++ b/xmpp-parsers/src/ibb.rs @@ -0,0 +1,172 @@ +// 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 crate::util::helpers::Base64; +use crate::iq::IqSetPayload; + +generate_id!( + /// An identifier matching a stream. + StreamId +); + +generate_attribute!( +/// Which stanza type to use to exchange data. +Stanza, "stanza", { + /// `` gives a feedback on whether the chunk has been received or not, + /// which is useful in the case the recipient might not receive them in a + /// timely manner, or to do your own throttling based on the results. + Iq => "iq", + + /// `` can be faster, since it doesn’t require any feedback, but in + /// practice it will be throttled by the servers on the way. + Message => "message", +}, Default = Iq); + +generate_element!( +/// Starts an In-Band Bytestream session with the given parameters. +Open, "open", IBB, +attributes: [ + /// Maximum size in bytes for each chunk. + block_size: Required = "block-size", + + /// The identifier to be used to create a stream. + sid: Required = "sid", + + /// Which stanza type to use to exchange data. + stanza: Default = "stanza", +]); + +impl IqSetPayload for Open {} + +generate_element!( +/// Exchange a chunk of data in an open stream. +Data, "data", IBB, + attributes: [ + /// Sequence number of this chunk, must wraparound after 65535. + seq: Required = "seq", + + /// The identifier of the stream on which data is being exchanged. + sid: Required = "sid" + ], + text: ( + /// Vector of bytes to be exchanged. + data: Base64> + ) +); + +impl IqSetPayload for Data {} + +generate_element!( +/// Close an open stream. +Close, "close", IBB, +attributes: [ + /// The identifier of the stream to be closed. + sid: Required = "sid", +]); + +impl IqSetPayload for Close {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::error::Error as StdError; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(StreamId, 12); + assert_size!(Stanza, 1); + assert_size!(Open, 16); + assert_size!(Data, 28); + assert_size!(Close, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(StreamId, 24); + assert_size!(Stanza, 1); + assert_size!(Open, 32); + assert_size!(Data, 56); + assert_size!(Close, 24); + } + + #[test] + fn test_simple() { + let sid = StreamId(String::from("coucou")); + + let elem: Element = + "" + .parse() + .unwrap(); + let open = Open::try_from(elem).unwrap(); + assert_eq!(open.block_size, 3); + assert_eq!(open.sid, sid); + assert_eq!(open.stanza, Stanza::Iq); + + let elem: Element = + "AAAA" + .parse() + .unwrap(); + let data = Data::try_from(elem).unwrap(); + assert_eq!(data.seq, 0); + assert_eq!(data.sid, sid); + assert_eq!(data.data, vec!(0, 0, 0)); + + let elem: Element = "" + .parse() + .unwrap(); + let close = Close::try_from(elem).unwrap(); + assert_eq!(close.sid, sid); + } + + #[test] + fn test_invalid() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Open::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'block-size' missing."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = Open::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!(message.description(), "invalid digit found in string"); + + let elem: Element = "" + .parse() + .unwrap(); + let error = Open::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(error) => error, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'sid' missing."); + } + + #[test] + fn test_invalid_stanza() { + let elem: Element = "".parse().unwrap(); + let error = Open::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'stanza' attribute."); + } +} diff --git a/xmpp-parsers/src/ibr.rs b/xmpp-parsers/src/ibr.rs new file mode 100644 index 0000000000000000000000000000000000000000..b1b15286653395d7b63bb619d342f91ecd3a2bd8 --- /dev/null +++ b/xmpp-parsers/src/ibr.rs @@ -0,0 +1,247 @@ +// 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 crate::data_forms::DataForm; +use crate::util::error::Error; +use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; +use crate::ns; +use crate::Element; +use std::collections::HashMap; +use std::convert::TryFrom; + +/// Query for registering against a service. +#[derive(Debug, Clone)] +pub struct Query { + /// Deprecated fixed list of possible fields to fill before the user can + /// register. + pub fields: HashMap, + + /// Whether this account is already registered. + pub registered: bool, + + /// Whether to remove this account. + pub remove: bool, + + /// A data form the user must fill before being allowed to register. + pub form: Option, + // Not yet implemented. + //pub oob: Option, +} + +impl IqGetPayload for Query {} +impl IqSetPayload for Query {} +impl IqResultPayload for Query {} + +impl TryFrom for Query { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "query", REGISTER, "IBR query"); + let mut query = Query { + registered: false, + fields: HashMap::new(), + remove: false, + form: None, + }; + for child in elem.children() { + let namespace = child.ns().unwrap(); + if namespace == ns::REGISTER { + let name = child.name(); + let fields = vec![ + "address", + "city", + "date", + "email", + "first", + "instructions", + "key", + "last", + "misc", + "name", + "nick", + "password", + "phone", + "state", + "text", + "url", + "username", + "zip", + ]; + if fields.binary_search(&name).is_ok() { + query.fields.insert(name.to_owned(), child.text()); + } else if name == "registered" { + query.registered = true; + } else if name == "remove" { + query.remove = true; + } else { + return Err(Error::ParseError("Wrong field in ibr element.")); + } + } else if child.is("x", ns::DATA_FORMS) { + query.form = Some(DataForm::try_from(child.clone())?); + } else { + return Err(Error::ParseError("Unknown child in ibr element.")); + } + } + Ok(query) + } +} + +impl From for Element { + fn from(query: Query) -> Element { + Element::builder("query") + .ns(ns::REGISTER) + .append_all(if query.registered { + Some(Element::builder("registered").ns(ns::REGISTER)) + } else { + None + }) + .append_all( + query + .fields + .into_iter() + .map(|(name, value)| Element::builder(name).ns(ns::REGISTER).append(value)) + ) + .append_all(if query.remove { + Some(Element::builder("remove").ns(ns::REGISTER)) + } else { + None + }) + .append_all(query.form.map(Element::from)) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::compare_elements::NamespaceAwareCompare; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Query, 96); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Query, 168); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + Query::try_from(elem).unwrap(); + } + + #[test] + fn test_ex2() { + let elem: Element = r#" + + + Choose a username and password for use with this service. + Please also provide your email address. + + + + + +"# + .parse() + .unwrap(); + let query = Query::try_from(elem).unwrap(); + assert_eq!(query.registered, false); + assert_eq!(query.fields["instructions"], "\n Choose a username and password for use with this service.\n Please also provide your email address.\n "); + assert_eq!(query.fields["username"], ""); + assert_eq!(query.fields["password"], ""); + assert_eq!(query.fields["email"], ""); + assert_eq!(query.fields.contains_key("name"), false); + + // FIXME: HashMap doesn’t keep the order right. + //let elem2 = query.into(); + //assert_eq!(elem, elem2); + } + + #[test] + fn test_ex9() { + let elem: Element = r#" + + + Use the enclosed form to register. If your Jabber client does not + support Data Forms, visit http://www.shakespeare.lit/contests.php + + + Contest Registration + + Please provide the following information + to sign up for our special contests! + + + jabber:iq:register + + + + + + + + + + + + + + + + +"# + .parse() + .unwrap(); + let elem1 = elem.clone(); + let query = Query::try_from(elem).unwrap(); + assert_eq!(query.registered, false); + assert!(!query.fields["instructions"].is_empty()); + let form = query.form.clone().unwrap(); + assert!(!form.instructions.unwrap().is_empty()); + let elem2 = query.into(); + assert!(elem1.compare_to(&elem2)); + } + + #[test] + fn test_ex10() { + let elem: Element = r#" + + + + jabber:iq:register + + + Juliet + + + Capulet + + + juliet@capulet.com + + + F + + + +"# + .parse() + .unwrap(); + let elem1 = elem.clone(); + let query = Query::try_from(elem).unwrap(); + assert_eq!(query.registered, false); + for _ in &query.fields { + panic!(); + } + let elem2 = query.into(); + assert!(elem1.compare_to(&elem2)); + } +} diff --git a/xmpp-parsers/src/idle.rs b/xmpp-parsers/src/idle.rs new file mode 100644 index 0000000000000000000000000000000000000000..47d1c18335c7627c7f20449b21eba42cd491927d --- /dev/null +++ b/xmpp-parsers/src/idle.rs @@ -0,0 +1,147 @@ +// 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 crate::date::DateTime; +use crate::presence::PresencePayload; + +generate_element!( + /// Represents the last time the user interacted with their system. + Idle, "idle", IDLE, + attributes: [ + /// The time at which the user stopped interacting. + since: Required = "since", + ] +); + +impl PresencePayload for Idle {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::error::Error as StdError; + use std::str::FromStr; + use std::convert::TryFrom; + + #[test] + fn test_size() { + assert_size!(Idle, 16); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + Idle::try_from(elem).unwrap(); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Idle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in idle element."); + } + + #[test] + fn test_invalid_id() { + let elem: Element = "".parse().unwrap(); + let error = Idle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'since' missing."); + } + + #[test] + fn test_invalid_date() { + // There is no thirteenth month. + let elem: Element = "" + .parse() + .unwrap(); + let error = Idle::try_from(elem).unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.description(), "input is out of range"); + + // Timezone ≥24:00 aren’t allowed. + let elem: Element = "" + .parse() + .unwrap(); + let error = Idle::try_from(elem).unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.description(), "input is out of range"); + + // Timezone without the : separator aren’t allowed. + let elem: Element = "" + .parse() + .unwrap(); + let error = Idle::try_from(elem).unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.description(), "input contains invalid characters"); + + // No seconds, error message could be improved. + let elem: Element = "" + .parse() + .unwrap(); + let error = Idle::try_from(elem).unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.description(), "input contains invalid characters"); + + // TODO: maybe we’ll want to support this one, as per XEP-0082 §4. + let elem: Element = "" + .parse() + .unwrap(); + let error = Idle::try_from(elem).unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.description(), "input contains invalid characters"); + + // No timezone. + let elem: Element = "" + .parse() + .unwrap(); + let error = Idle::try_from(elem).unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.description(), "premature end of input"); + } + + #[test] + fn test_serialise() { + let elem: Element = "" + .parse() + .unwrap(); + let idle = Idle { + since: DateTime::from_str("2017-05-21T20:19:55+01:00").unwrap(), + }; + let elem2 = idle.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/iq.rs b/xmpp-parsers/src/iq.rs new file mode 100644 index 0000000000000000000000000000000000000000..644926163157e0e0dedea535fe9adbb5c7db5023 --- /dev/null +++ b/xmpp-parsers/src/iq.rs @@ -0,0 +1,447 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// Copyright (c) 2017 Maxime “pep” Buquet +// +// 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 crate::util::error::Error; +use crate::ns; +use crate::stanza_error::StanzaError; +use jid::Jid; +use crate::Element; +use minidom::IntoAttributeValue; +use std::convert::TryFrom; + +/// Should be implemented on every known payload of an ``. +pub trait IqGetPayload: TryFrom + Into {} + +/// Should be implemented on every known payload of an ``. +pub trait IqSetPayload: TryFrom + Into {} + +/// Should be implemented on every known payload of an ``. +pub trait IqResultPayload: TryFrom + Into {} + +/// Represents one of the four possible iq types. +#[derive(Debug, Clone)] +pub enum IqType { + /// This is a request for accessing some data. + Get(Element), + + /// This is a request for modifying some data. + Set(Element), + + /// This is a result containing some data. + Result(Option), + + /// A get or set request failed. + Error(StanzaError), +} + +impl<'a> IntoAttributeValue for &'a IqType { + fn into_attribute_value(self) -> Option { + Some( + match *self { + IqType::Get(_) => "get", + IqType::Set(_) => "set", + IqType::Result(_) => "result", + IqType::Error(_) => "error", + } + .to_owned(), + ) + } +} + +/// The main structure representing the `` stanza. +#[derive(Debug, Clone)] +pub struct Iq { + /// The JID emitting this stanza. + pub from: Option, + + /// The recipient of this stanza. + pub to: Option, + + /// The @id attribute of this stanza, which is required in order to match a + /// request with its result/error. + pub id: String, + + /// The payload content of this stanza. + pub payload: IqType, +} + +impl Iq { + /// Creates an `` stanza containing a get request. + pub fn from_get>(id: S, payload: impl IqGetPayload) -> Iq { + Iq { + from: None, + to: None, + id: id.into(), + payload: IqType::Get(payload.into()), + } + } + + /// Creates an `` stanza containing a set request. + pub fn from_set>(id: S, payload: impl IqSetPayload) -> Iq { + Iq { + from: None, + to: None, + id: id.into(), + payload: IqType::Set(payload.into()), + } + } + + /// Creates an `` stanza containing a result. + pub fn from_result>(id: S, payload: Option) -> Iq { + Iq { + from: None, + to: None, + id: id.into(), + payload: IqType::Result(payload.map(Into::into)), + } + } + + /// Creates an `` stanza containing an error. + pub fn from_error>(id: S, payload: StanzaError) -> Iq { + Iq { + from: None, + to: None, + id: id.into(), + payload: IqType::Error(payload), + } + } + + /// Sets the recipient of this stanza. + pub fn with_to(mut self, to: Jid) -> Iq { + self.to = Some(to); + self + } + + /// Sets the emitter of this stanza. + pub fn with_from(mut self, from: Jid) -> Iq { + self.from = Some(from); + self + } + + /// Sets the id of this stanza, in order to later match its response. + pub fn with_id(mut self, id: String) -> Iq { + self.id = id; + self + } +} + +impl TryFrom for Iq { + type Error = Error; + + fn try_from(root: Element) -> Result { + check_self!(root, "iq", DEFAULT_NS); + let from = get_attr!(root, "from", Option); + let to = get_attr!(root, "to", Option); + let id = get_attr!(root, "id", Required); + let type_: String = get_attr!(root, "type", Required); + + let mut payload = None; + let mut error_payload = None; + for elem in root.children() { + if payload.is_some() { + return Err(Error::ParseError("Wrong number of children in iq element.")); + } + if type_ == "error" { + if elem.is("error", ns::DEFAULT_NS) { + if error_payload.is_some() { + return Err(Error::ParseError("Wrong number of children in iq element.")); + } + error_payload = Some(StanzaError::try_from(elem.clone())?); + } else if root.children().count() != 2 { + return Err(Error::ParseError("Wrong number of children in iq element.")); + } + } else { + payload = Some(elem.clone()); + } + } + + let type_ = if type_ == "get" { + if let Some(payload) = payload { + IqType::Get(payload) + } else { + return Err(Error::ParseError("Wrong number of children in iq element.")); + } + } else if type_ == "set" { + if let Some(payload) = payload { + IqType::Set(payload) + } else { + return Err(Error::ParseError("Wrong number of children in iq element.")); + } + } else if type_ == "result" { + if let Some(payload) = payload { + IqType::Result(Some(payload)) + } else { + IqType::Result(None) + } + } else if type_ == "error" { + if let Some(payload) = error_payload { + IqType::Error(payload) + } else { + return Err(Error::ParseError("Wrong number of children in iq element.")); + } + } else { + return Err(Error::ParseError("Unknown iq type.")); + }; + + Ok(Iq { + from, + to, + id, + payload: type_, + }) + } +} + +impl From for Element { + fn from(iq: Iq) -> Element { + let mut stanza = Element::builder("iq") + .ns(ns::DEFAULT_NS) + .attr("from", iq.from) + .attr("to", iq.to) + .attr("id", iq.id) + .attr("type", &iq.payload) + .build(); + let elem = match iq.payload { + IqType::Get(elem) | IqType::Set(elem) | IqType::Result(Some(elem)) => elem, + IqType::Error(error) => error.into(), + IqType::Result(None) => return stanza, + }; + stanza.append_child(elem); + stanza + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::compare_elements::NamespaceAwareCompare; + use crate::disco::DiscoInfoQuery; + use crate::stanza_error::{DefinedCondition, ErrorType}; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(IqType, 112); + assert_size!(Iq, 204); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(IqType, 224); + assert_size!(Iq, 408); + } + + #[test] + fn test_require_type() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "".parse().unwrap(); + let error = Iq::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'id' missing."); + + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "".parse().unwrap(); + let error = Iq::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'type' missing."); + } + + #[test] + fn test_get() { + #[cfg(not(feature = "component"))] + let elem: Element = " + + " + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = " + + " + .parse() + .unwrap(); + let iq = Iq::try_from(elem).unwrap(); + let query: Element = "".parse().unwrap(); + assert_eq!(iq.from, None); + assert_eq!(iq.to, None); + assert_eq!(&iq.id, "foo"); + assert!(match iq.payload { + IqType::Get(element) => element.compare_to(&query), + _ => false, + }); + } + + #[test] + fn test_set() { + #[cfg(not(feature = "component"))] + let elem: Element = " + + " + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = " + + " + .parse() + .unwrap(); + let iq = Iq::try_from(elem).unwrap(); + let vcard: Element = "".parse().unwrap(); + assert_eq!(iq.from, None); + assert_eq!(iq.to, None); + assert_eq!(&iq.id, "vcard"); + assert!(match iq.payload { + IqType::Set(element) => element.compare_to(&vcard), + _ => false, + }); + } + + #[test] + fn test_result_empty() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let iq = Iq::try_from(elem).unwrap(); + assert_eq!(iq.from, None); + assert_eq!(iq.to, None); + assert_eq!(&iq.id, "res"); + assert!(match iq.payload { + IqType::Result(None) => true, + _ => false, + }); + } + + #[test] + fn test_result() { + #[cfg(not(feature = "component"))] + let elem: Element = " + + " + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = " + + " + .parse() + .unwrap(); + let iq = Iq::try_from(elem).unwrap(); + let query: Element = "" + .parse() + .unwrap(); + assert_eq!(iq.from, None); + assert_eq!(iq.to, None); + assert_eq!(&iq.id, "res"); + assert!(match iq.payload { + IqType::Result(Some(element)) => element.compare_to(&query), + _ => false, + }); + } + + #[test] + fn test_error() { + #[cfg(not(feature = "component"))] + let elem: Element = " + + + + + " + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = " + + + + + " + .parse() + .unwrap(); + let iq = Iq::try_from(elem).unwrap(); + assert_eq!(iq.from, None); + assert_eq!(iq.to, None); + assert_eq!(iq.id, "err1"); + match iq.payload { + IqType::Error(error) => { + assert_eq!(error.type_, ErrorType::Cancel); + assert_eq!(error.by, None); + assert_eq!( + error.defined_condition, + DefinedCondition::ServiceUnavailable + ); + assert_eq!(error.texts.len(), 0); + assert_eq!(error.other, None); + } + _ => panic!(), + } + } + + #[test] + fn test_children_invalid() { + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let error = Iq::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Wrong number of children in iq element."); + } + + #[test] + fn test_serialise() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let iq2 = Iq { + from: None, + to: None, + id: String::from("res"), + payload: IqType::Result(None), + }; + let elem2 = iq2.into(); + assert_eq!(elem, elem2); + } + + #[test] + fn test_disco() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "".parse().unwrap(); + let iq = Iq::try_from(elem).unwrap(); + let disco_info = match iq.payload { + IqType::Get(payload) => DiscoInfoQuery::try_from(payload).unwrap(), + _ => panic!(), + }; + assert!(disco_info.node.is_none()); + } +} diff --git a/xmpp-parsers/src/jid_prep.rs b/xmpp-parsers/src/jid_prep.rs new file mode 100644 index 0000000000000000000000000000000000000000..5fbc0a0c0f3f7ab29bf42f978cd329ab248af273 --- /dev/null +++ b/xmpp-parsers/src/jid_prep.rs @@ -0,0 +1,73 @@ +// Copyright (c) 2019 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 crate::iq::{IqGetPayload, IqResultPayload}; +use crate::util::helpers::{Text, JidCodec}; +use jid::Jid; + +generate_element!( + /// Request from a client to stringprep/PRECIS a string into a JID. + JidPrepQuery, "jid", JID_PREP, + text: ( + /// The potential JID. + data: Text + ) +); + +impl IqGetPayload for JidPrepQuery {} + +impl JidPrepQuery { + /// Create a new JID Prep query. + pub fn new>(jid: J) -> JidPrepQuery { + JidPrepQuery { + data: jid.into(), + } + } +} + +generate_element!( + /// Response from the server with the stringprep’d/PRECIS’d JID. + JidPrepResponse, "jid", JID_PREP, + text: ( + /// The JID. + jid: JidCodec + ) +); + +impl IqResultPayload for JidPrepResponse {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + use std::str::FromStr; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(JidPrepQuery, 12); + assert_size!(JidPrepResponse, 40); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(JidPrepQuery, 24); + assert_size!(JidPrepResponse, 80); + } + + #[test] + fn simple() { + let elem: Element = "ROMeo@montague.lit/orchard".parse().unwrap(); + let query = JidPrepQuery::try_from(elem).unwrap(); + assert_eq!(query.data, "ROMeo@montague.lit/orchard"); + + let elem: Element = "romeo@montague.lit/orchard".parse().unwrap(); + let response = JidPrepResponse::try_from(elem).unwrap(); + assert_eq!(response.jid, Jid::from_str("romeo@montague.lit/orchard").unwrap()); + } +} diff --git a/xmpp-parsers/src/jingle.rs b/xmpp-parsers/src/jingle.rs new file mode 100644 index 0000000000000000000000000000000000000000..82875686b8d6835eb4e0d2a894ac4eec03730429 --- /dev/null +++ b/xmpp-parsers/src/jingle.rs @@ -0,0 +1,867 @@ +// 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 crate::util::error::Error; +use crate::iq::IqSetPayload; +use crate::jingle_rtp::Description as RtpDescription; +use crate::jingle_ice_udp::Transport as IceUdpTransport; +use crate::jingle_ibb::Transport as IbbTransport; +use crate::jingle_s5b::Transport as Socks5Transport; +use crate::ns; +use jid::Jid; +use crate::Element; +use std::collections::BTreeMap; +use std::str::FromStr; +use std::convert::TryFrom; + +generate_attribute!( + /// The action attribute. + Action, "action", { + /// Accept a content-add action received from another party. + ContentAccept => "content-accept", + + /// Add one or more new content definitions to the session. + ContentAdd => "content-add", + + /// Change the directionality of media sending. + ContentModify => "content-modify", + + /// Reject a content-add action received from another party. + ContentReject => "content-reject", + + /// Remove one or more content definitions from the session. + ContentRemove => "content-remove", + + /// Exchange information about parameters for an application type. + DescriptionInfo => "description-info", + + /// Exchange information about security preconditions. + SecurityInfo => "security-info", + + /// Definitively accept a session negotiation. + SessionAccept => "session-accept", + + /// Send session-level information, such as a ping or a ringing message. + SessionInfo => "session-info", + + /// Request negotiation of a new Jingle session. + SessionInitiate => "session-initiate", + + /// End an existing session. + SessionTerminate => "session-terminate", + + /// Accept a transport-replace action received from another party. + TransportAccept => "transport-accept", + + /// Exchange transport candidates. + TransportInfo => "transport-info", + + /// Reject a transport-replace action received from another party. + TransportReject => "transport-reject", + + /// Redefine a transport method or replace it with a different method. + TransportReplace => "transport-replace", + } +); + +generate_attribute!( + /// Which party originally generated the content type. + Creator, "creator", { + /// This content was created by the initiator of this session. + Initiator => "initiator", + + /// This content was created by the responder of this session. + Responder => "responder", + } +); + +generate_attribute!( + /// Which parties in the session will be generating content. + Senders, "senders", { + /// Both parties can send for this content. + Both => "both", + + /// Only the initiator can send for this content. + Initiator => "initiator", + + /// No one can send for this content. + None => "none", + + /// Only the responder can send for this content. + Responder => "responder", + }, Default = Both +); + +generate_attribute!( + /// How the content definition is to be interpreted by the recipient. The + /// meaning of this attribute matches the "Content-Disposition" header as + /// defined in RFC 2183 and applied to SIP by RFC 3261. + /// + /// Possible values are defined here: + /// https://www.iana.org/assignments/cont-disp/cont-disp.xhtml + Disposition, "disposition", { + /// Displayed automatically. + Inline => "inline", + + /// User controlled display. + Attachment => "attachment", + + /// Process as form response. + FormData => "form-data", + + /// Tunneled content to be processed silently. + Signal => "signal", + + /// The body is a custom ring tone to alert the user. + Alert => "alert", + + /// The body is displayed as an icon to the user. + Icon => "icon", + + /// The body should be displayed to the user. + Render => "render", + + /// The body contains a list of URIs that indicates the recipients of + /// the request. + RecipientListHistory => "recipient-list-history", + + /// The body describes a communications session, for example, an + /// RFC2327 SDP body. + Session => "session", + + /// Authenticated Identity Body. + Aib => "aib", + + /// The body describes an early communications session, for example, + /// and [RFC2327] SDP body. + EarlySession => "early-session", + + /// The body includes a list of URIs to which URI-list services are to + /// be applied. + RecipientList => "recipient-list", + + /// The payload of the message carrying this Content-Disposition header + /// field value is an Instant Message Disposition Notification as + /// requested in the corresponding Instant Message. + Notification => "notification", + + /// The body needs to be handled according to a reference to the body + /// that is located in the same SIP message as the body. + ByReference => "by-reference", + + /// The body contains information associated with an Info Package. + InfoPackage => "info-package", + + /// The body describes either metadata about the RS or the reason for + /// the metadata snapshot request as determined by the MIME value + /// indicated in the Content-Type. + RecordingSession => "recording-session", + }, Default = Session +); + +generate_id!( + /// An unique identifier in a session, referencing a + /// [struct.Content.html](Content element). + ContentId +); + +/// Enum wrapping all of the various supported descriptions of a Content. +#[derive(Debug, Clone)] +pub enum Description { + /// Jingle RTP Sessions (XEP-0167) description. + Rtp(RtpDescription), + + /// To be used for any description that isn’t known at compile-time. + Unknown(Element), +} + +impl TryFrom for Description { + type Error = Error; + + fn try_from(elem: Element) -> Result { + Ok(if elem.is("description", ns::JINGLE_RTP) { + Description::Rtp(RtpDescription::try_from(elem)?) + } else { + Description::Unknown(elem) + }) + } +} + +impl From for Description { + fn from(desc: RtpDescription) -> Description { + Description::Rtp(desc) + } +} + +impl From for Element { + fn from(desc: Description) -> Element { + match desc { + Description::Rtp(desc) => desc.into(), + Description::Unknown(elem) => elem, + } + } +} + +impl Description { + fn get_ns(&self) -> String { + match self { + Description::Rtp(_) => String::from(ns::JINGLE_RTP), + Description::Unknown(elem) => elem.ns().unwrap_or_else(|| String::new()), + } + } +} + +/// Enum wrapping all of the various supported transports of a Content. +#[derive(Debug, Clone)] +pub enum Transport { + /// Jingle ICE-UDP Bytestreams (XEP-0176) transport. + IceUdp(IceUdpTransport), + + /// Jingle In-Band Bytestreams (XEP-0261) transport. + Ibb(IbbTransport), + + /// Jingle SOCKS5 Bytestreams (XEP-0260) transport. + Socks5(Socks5Transport), + + /// To be used for any transport that isn’t known at compile-time. + Unknown(Element), +} + +impl TryFrom for Transport { + type Error = Error; + + fn try_from(elem: Element) -> Result { + Ok(if elem.is("transport", ns::JINGLE_ICE_UDP) { + Transport::IceUdp(IceUdpTransport::try_from(elem)?) + } else if elem.is("transport", ns::JINGLE_IBB) { + Transport::Ibb(IbbTransport::try_from(elem)?) + } else if elem.is("transport", ns::JINGLE_S5B) { + Transport::Socks5(Socks5Transport::try_from(elem)?) + } else { + Transport::Unknown(elem) + }) + } +} + +impl From for Transport { + fn from(transport: IceUdpTransport) -> Transport { + Transport::IceUdp(transport) + } +} + +impl From for Transport { + fn from(transport: IbbTransport) -> Transport { + Transport::Ibb(transport) + } +} + +impl From for Transport { + fn from(transport: Socks5Transport) -> Transport { + Transport::Socks5(transport) + } +} + +impl From for Element { + fn from(transport: Transport) -> Element { + match transport { + Transport::IceUdp(transport) => transport.into(), + Transport::Ibb(transport) => transport.into(), + Transport::Socks5(transport) => transport.into(), + Transport::Unknown(elem) => elem, + } + } +} + +impl Transport { + fn get_ns(&self) -> String { + match self { + Transport::IceUdp(_) => String::from(ns::JINGLE_ICE_UDP), + Transport::Ibb(_) => String::from(ns::JINGLE_IBB), + Transport::Socks5(_) => String::from(ns::JINGLE_S5B), + Transport::Unknown(elem) => elem.ns().unwrap_or_else(|| String::new()), + } + } +} + +generate_element!( + /// Describes a session’s content, there can be multiple content in one + /// session. + Content, "content", JINGLE, + attributes: [ + /// Who created this content. + creator: Required = "creator", + + /// How the content definition is to be interpreted by the recipient. + disposition: Default = "disposition", + + /// A per-session unique identifier for this content. + name: Required = "name", + + /// Who can send data for this content. + senders: Default = "senders", + ], + children: [ + /// What to send. + description: Option = ("description", *) => Description, + + /// How to send it. + transport: Option = ("transport", *) => Transport, + + /// With which security. + security: Option = ("security", JINGLE) => Element + ] +); + +impl Content { + /// Create a new content. + pub fn new(creator: Creator, name: ContentId) -> Content { + Content { + creator, + name, + disposition: Disposition::Session, + senders: Senders::Both, + description: None, + transport: None, + security: None, + } + } + + /// Set how the content is to be interpreted by the recipient. + pub fn with_disposition(mut self, disposition: Disposition) -> Content { + self.disposition = disposition; + self + } + + /// Specify who can send data for this content. + pub fn with_senders(mut self, senders: Senders) -> Content { + self.senders = senders; + self + } + + /// Set the description of this content. + pub fn with_description>(mut self, description: D) -> Content { + self.description = Some(description.into()); + self + } + + /// Set the transport of this content. + pub fn with_transport>(mut self, transport: T) -> Content { + self.transport = Some(transport.into()); + self + } + + /// Set the security of this content. + pub fn with_security(mut self, security: Element) -> Content { + self.security = Some(security); + self + } +} + +/// Lists the possible reasons to be included in a Jingle iq. +#[derive(Debug, Clone, PartialEq)] +pub enum Reason { + /// The party prefers to use an existing session with the peer rather than + /// initiate a new session; the Jingle session ID of the alternative + /// session SHOULD be provided as the XML character data of the + /// child. + AlternativeSession, //(String), + + /// The party is busy and cannot accept a session. + Busy, + + /// The initiator wishes to formally cancel the session initiation request. + Cancel, + + /// The action is related to connectivity problems. + ConnectivityError, + + /// The party wishes to formally decline the session. + Decline, + + /// The session length has exceeded a pre-defined time limit (e.g., a + /// meeting hosted at a conference service). + Expired, + + /// The party has been unable to initialize processing related to the + /// application type. + FailedApplication, + + /// The party has been unable to establish connectivity for the transport + /// method. + FailedTransport, + + /// The action is related to a non-specific application error. + GeneralError, + + /// The entity is going offline or is no longer available. + Gone, + + /// The party supports the offered application type but does not support + /// the offered or negotiated parameters. + IncompatibleParameters, + + /// The action is related to media processing problems. + MediaError, + + /// The action is related to a violation of local security policies. + SecurityError, + + /// The action is generated during the normal course of state management + /// and does not reflect any error. + Success, + + /// A request has not been answered so the sender is timing out the + /// request. + Timeout, + + /// The party supports none of the offered application types. + UnsupportedApplications, + + /// The party supports none of the offered transport methods. + UnsupportedTransports, +} + +impl FromStr for Reason { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "alternative-session" => Reason::AlternativeSession, + "busy" => Reason::Busy, + "cancel" => Reason::Cancel, + "connectivity-error" => Reason::ConnectivityError, + "decline" => Reason::Decline, + "expired" => Reason::Expired, + "failed-application" => Reason::FailedApplication, + "failed-transport" => Reason::FailedTransport, + "general-error" => Reason::GeneralError, + "gone" => Reason::Gone, + "incompatible-parameters" => Reason::IncompatibleParameters, + "media-error" => Reason::MediaError, + "security-error" => Reason::SecurityError, + "success" => Reason::Success, + "timeout" => Reason::Timeout, + "unsupported-applications" => Reason::UnsupportedApplications, + "unsupported-transports" => Reason::UnsupportedTransports, + + _ => return Err(Error::ParseError("Unknown reason.")), + }) + } +} + +impl From for Element { + fn from(reason: Reason) -> Element { + Element::builder(match reason { + Reason::AlternativeSession => "alternative-session", + Reason::Busy => "busy", + Reason::Cancel => "cancel", + Reason::ConnectivityError => "connectivity-error", + Reason::Decline => "decline", + Reason::Expired => "expired", + Reason::FailedApplication => "failed-application", + Reason::FailedTransport => "failed-transport", + Reason::GeneralError => "general-error", + Reason::Gone => "gone", + Reason::IncompatibleParameters => "incompatible-parameters", + Reason::MediaError => "media-error", + Reason::SecurityError => "security-error", + Reason::Success => "success", + Reason::Timeout => "timeout", + Reason::UnsupportedApplications => "unsupported-applications", + Reason::UnsupportedTransports => "unsupported-transports", + }) + .ns(ns::JINGLE) + .build() + } +} + +type Lang = String; + +/// Informs the recipient of something. +#[derive(Debug, Clone)] +pub struct ReasonElement { + /// The list of possible reasons to be included in a Jingle iq. + pub reason: Reason, + + /// A human-readable description of this reason. + pub texts: BTreeMap, +} + +impl TryFrom for ReasonElement { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "reason", JINGLE); + check_no_attributes!(elem, "reason"); + let mut reason = None; + let mut texts = BTreeMap::new(); + for child in elem.children() { + if child.is("text", ns::JINGLE) { + check_no_children!(child, "text"); + check_no_unknown_attributes!(child, "text", ["xml:lang"]); + let lang = get_attr!(elem, "xml:lang", Default); + if texts.insert(lang, child.text()).is_some() { + return Err(Error::ParseError( + "Text element present twice for the same xml:lang.", + )); + } + } else if child.has_ns(ns::JINGLE) { + if reason.is_some() { + return Err(Error::ParseError( + "Reason must not have more than one reason.", + )); + } + check_no_children!(child, "reason"); + check_no_attributes!(child, "reason"); + reason = Some(child.name().parse()?); + } else { + return Err(Error::ParseError("Reason contains a foreign element.")); + } + } + let reason = reason.ok_or(Error::ParseError( + "Reason doesn’t contain a valid reason.", + ))?; + Ok(ReasonElement { + reason, + texts, + }) + } +} + +impl From for Element { + fn from(reason: ReasonElement) -> Element { + Element::builder("reason") + .ns(ns::JINGLE) + .append(Element::from(reason.reason)) + .append_all( + reason.texts.into_iter().map(|(lang, text)| { + Element::builder("text") + .ns(ns::JINGLE) + .attr("xml:lang", lang) + .append(text) + })) + .build() + } +} + +generate_id!( + /// Unique identifier for a session between two JIDs. + SessionId +); + +/// The main Jingle container, to be included in an iq stanza. +#[derive(Debug, Clone)] +pub struct Jingle { + /// The action to execute on both ends. + pub action: Action, + + /// Who the initiator is. + pub initiator: Option, + + /// Who the responder is. + pub responder: Option, + + /// Unique session identifier between two entities. + pub sid: SessionId, + + /// A list of contents to be negotiated in this session. + pub contents: Vec, + + /// An optional reason. + pub reason: Option, + + /// Payloads to be included. + pub other: Vec, +} + +impl IqSetPayload for Jingle {} + +impl Jingle { + /// Create a new Jingle element. + pub fn new(action: Action, sid: SessionId) -> Jingle { + Jingle { + action, + sid, + initiator: None, + responder: None, + contents: Vec::new(), + reason: None, + other: Vec::new(), + } + } + + /// Set the initiator’s JID. + pub fn with_initiator(mut self, initiator: Jid) -> Jingle { + self.initiator = Some(initiator); + self + } + + /// Set the responder’s JID. + pub fn with_responder(mut self, responder: Jid) -> Jingle { + self.responder = Some(responder); + self + } + + /// Add a content to this Jingle container. + pub fn add_content(mut self, content: Content) -> Jingle { + self.contents.push(content); + self + } + + /// Set the reason in this Jingle container. + pub fn set_reason(mut self, content: Content) -> Jingle { + self.contents.push(content); + self + } +} + +impl TryFrom for Jingle { + type Error = Error; + + fn try_from(root: Element) -> Result { + check_self!(root, "jingle", JINGLE, "Jingle"); + check_no_unknown_attributes!(root, "Jingle", ["action", "initiator", "responder", "sid"]); + + let mut jingle = Jingle { + action: get_attr!(root, "action", Required), + initiator: get_attr!(root, "initiator", Option), + responder: get_attr!(root, "responder", Option), + sid: get_attr!(root, "sid", Required), + contents: vec![], + reason: None, + other: vec![], + }; + + for child in root.children().cloned() { + if child.is("content", ns::JINGLE) { + let content = Content::try_from(child)?; + jingle.contents.push(content); + } else if child.is("reason", ns::JINGLE) { + if jingle.reason.is_some() { + return Err(Error::ParseError( + "Jingle must not have more than one reason.", + )); + } + let reason = ReasonElement::try_from(child)?; + jingle.reason = Some(reason); + } else { + jingle.other.push(child); + } + } + + Ok(jingle) + } +} + +impl From for Element { + fn from(jingle: Jingle) -> Element { + Element::builder("jingle") + .ns(ns::JINGLE) + .attr("action", jingle.action) + .attr("initiator", jingle.initiator) + .attr("responder", jingle.responder) + .attr("sid", jingle.sid) + .append_all(jingle.contents) + .append_all(jingle.reason.map(Element::from)) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Action, 1); + assert_size!(Creator, 1); + assert_size!(Senders, 1); + assert_size!(Disposition, 1); + assert_size!(ContentId, 12); + assert_size!(Content, 172); + assert_size!(Reason, 1); + assert_size!(ReasonElement, 16); + assert_size!(SessionId, 12); + assert_size!(Jingle, 136); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Action, 1); + assert_size!(Creator, 1); + assert_size!(Senders, 1); + assert_size!(Disposition, 1); + assert_size!(ContentId, 24); + assert_size!(Content, 384); + assert_size!(Reason, 1); + assert_size!(ReasonElement, 32); + assert_size!(SessionId, 24); + assert_size!(Jingle, 272); + } + + #[test] + fn test_simple() { + let elem: Element = + "" + .parse() + .unwrap(); + let jingle = Jingle::try_from(elem).unwrap(); + assert_eq!(jingle.action, Action::SessionInitiate); + assert_eq!(jingle.sid, SessionId(String::from("coucou"))); + } + + #[test] + fn test_invalid_jingle() { + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'action' missing."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'sid' missing."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'action' attribute."); + } + + #[test] + fn test_content() { + let elem: Element = "".parse().unwrap(); + let jingle = Jingle::try_from(elem).unwrap(); + assert_eq!(jingle.contents[0].creator, Creator::Initiator); + assert_eq!(jingle.contents[0].name, ContentId(String::from("coucou"))); + assert_eq!(jingle.contents[0].senders, Senders::Both); + assert_eq!(jingle.contents[0].disposition, Disposition::Session); + + let elem: Element = "".parse().unwrap(); + let jingle = Jingle::try_from(elem).unwrap(); + assert_eq!(jingle.contents[0].senders, Senders::Both); + + let elem: Element = "".parse().unwrap(); + let jingle = Jingle::try_from(elem).unwrap(); + assert_eq!(jingle.contents[0].disposition, Disposition::EarlySession); + } + + #[test] + fn test_invalid_content() { + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'creator' missing."); + + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'name' missing."); + + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'creator' attribute."); + + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'senders' attribute."); + + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'senders' attribute."); + } + + #[test] + fn test_reason() { + let elem: Element = "".parse().unwrap(); + let jingle = Jingle::try_from(elem).unwrap(); + let reason = jingle.reason.unwrap(); + assert_eq!(reason.reason, Reason::Success); + assert_eq!(reason.texts, BTreeMap::new()); + + let elem: Element = "coucou".parse().unwrap(); + let jingle = Jingle::try_from(elem).unwrap(); + let reason = jingle.reason.unwrap(); + assert_eq!(reason.reason, Reason::Success); + assert_eq!(reason.texts.get(""), Some(&String::from("coucou"))); + } + + #[test] + fn test_invalid_reason() { + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Reason doesn’t contain a valid reason."); + + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown reason."); + + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Reason contains a foreign element."); + + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Jingle must not have more than one reason."); + + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Text element present twice for the same xml:lang."); + } +} diff --git a/xmpp-parsers/src/jingle_dtls_srtp.rs b/xmpp-parsers/src/jingle_dtls_srtp.rs new file mode 100644 index 0000000000000000000000000000000000000000..e6a3ee235730fa3c102e41064c6b07a87764da0f --- /dev/null +++ b/xmpp-parsers/src/jingle_dtls_srtp.rs @@ -0,0 +1,98 @@ +// Copyright (c) 2019 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 crate::util::helpers::ColonSeparatedHex; +use crate::util::error::Error; +use crate::hashes::{Hash, Algo}; + +generate_attribute!( + /// Indicates which of the end points should initiate the TCP connection establishment. + Setup, "setup", { + /// The endpoint will initiate an outgoing connection. + Active => "active", + + /// The endpoint will accept an incoming connection. + Passive => "passive", + + /// The endpoint is willing to accept an incoming connection or to initiate an outgoing + /// connection. + Actpass => "actpass", + + /* + /// The endpoint does not want the connection to be established for the time being. + /// + /// Note that this value isn’t used, as per the XEP. + Holdconn => "holdconn", + */ + } +); + +// TODO: use a hashes::Hash instead of two different fields here. +generate_element!( + /// Fingerprint of the key used for a DTLS handshake. + Fingerprint, "fingerprint", JINGLE_DTLS, + attributes: [ + /// The hash algorithm used for this fingerprint. + hash: Required = "hash", + + /// Indicates which of the end points should initiate the TCP connection establishment. + setup: Required = "setup" + ], + text: ( + /// Hash value of this fingerprint. + value: ColonSeparatedHex> + ) +); + +impl Fingerprint { + /// Create a new Fingerprint from a Setup and a Hash. + pub fn from_hash(setup: Setup, hash: Hash) -> Fingerprint { + Fingerprint { + hash: hash.algo, + setup, + value: hash.hash, + } + } + + /// Create a new Fingerprint from a Setup and parsing the hash. + pub fn from_colon_separated_hex(setup: Setup, algo: &str, hash: &str) -> Result { + let algo = algo.parse()?; + let hash = Hash::from_colon_separated_hex(algo, hash)?; + Ok(Fingerprint::from_hash(setup, hash)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Setup, 1); + assert_size!(Fingerprint, 32); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Setup, 1); + assert_size!(Fingerprint, 64); + } + + #[test] + fn test_ex1() { + let elem: Element = "02:1A:CC:54:27:AB:EB:9C:53:3F:3E:4B:65:2E:7D:46:3F:54:42:CD:54:F1:7A:03:A2:7D:F9:B0:7F:46:19:B2" + .parse() + .unwrap(); + let fingerprint = Fingerprint::try_from(elem).unwrap(); + assert_eq!(fingerprint.setup, Setup::Actpass); + assert_eq!(fingerprint.hash, Algo::Sha_256); + assert_eq!(fingerprint.value, [2, 26, 204, 84, 39, 171, 235, 156, 83, 63, 62, 75, 101, 46, 125, 70, 63, 84, 66, 205, 84, 241, 122, 3, 162, 125, 249, 176, 127, 70, 25, 178]); + } +} diff --git a/xmpp-parsers/src/jingle_ft.rs b/xmpp-parsers/src/jingle_ft.rs new file mode 100644 index 0000000000000000000000000000000000000000..f3fc9a33fdad6ac7d5e221df9e7d2f31586e6434 --- /dev/null +++ b/xmpp-parsers/src/jingle_ft.rs @@ -0,0 +1,616 @@ +// 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 crate::date::DateTime; +use crate::util::error::Error; +use crate::hashes::Hash; +use crate::jingle::{ContentId, Creator}; +use crate::ns; +use minidom::{Element, Node}; +use std::collections::BTreeMap; +use std::str::FromStr; +use std::convert::TryFrom; + +generate_element!( + /// Represents a range in a file. + #[derive(PartialEq, Default)] + Range, "range", JINGLE_FT, + attributes: [ + /// The offset in bytes from the beginning of the file. + offset: Default = "offset", + + /// The length in bytes of the range, or None to be the entire + /// remaining of the file. + length: Option = "length" + ], + children: [ + /// List of hashes for this range. + hashes: Vec = ("hash", HASHES) => Hash + ] +); + +impl Range { + /// Creates a new range. + pub fn new() -> Range { + Default::default() + } +} + +type Lang = String; + +generate_id!( + /// Wrapper for a file description. + Desc +); + +/// Represents a file to be transferred. +#[derive(Debug, Clone, Default)] +pub struct File { + /// The date of last modification of this file. + pub date: Option, + + /// The MIME type of this file. + pub media_type: Option, + + /// The name of this file. + pub name: Option, + + /// The description of this file, possibly localised. + pub descs: BTreeMap, + + /// The size of this file, in bytes. + pub size: Option, + + /// Used to request only a part of this file. + pub range: Option, + + /// A list of hashes matching this entire file. + pub hashes: Vec, +} + +impl File { + /// Creates a new file descriptor. + pub fn new() -> File { + File::default() + } + + /// Sets the date of last modification on this file. + pub fn with_date(mut self, date: DateTime) -> File { + self.date = Some(date); + self + } + + /// Sets the date of last modification on this file from an ISO-8601 + /// string. + pub fn with_date_str(mut self, date: &str) -> Result { + self.date = Some(DateTime::from_str(date)?); + Ok(self) + } + + /// Sets the MIME type of this file. + pub fn with_media_type(mut self, media_type: String) -> File { + self.media_type = Some(media_type); + self + } + + /// Sets the name of this file. + pub fn with_name(mut self, name: String) -> File { + self.name = Some(name); + self + } + + /// Sets a description for this file. + pub fn add_desc(mut self, lang: &str, desc: Desc) -> File { + self.descs.insert(Lang::from(lang), desc); + self + } + + /// Sets the file size of this file, in bytes. + pub fn with_size(mut self, size: u64) -> File { + self.size = Some(size); + self + } + + /// Request only a range of this file. + pub fn with_range(mut self, range: Range) -> File { + self.range = Some(range); + self + } + + /// Add a hash on this file. + pub fn add_hash(mut self, hash: Hash) -> File { + self.hashes.push(hash); + self + } +} + +impl TryFrom for File { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "file", JINGLE_FT); + check_no_attributes!(elem, "file"); + + let mut file = File { + date: None, + media_type: None, + name: None, + descs: BTreeMap::new(), + size: None, + range: None, + hashes: vec![], + }; + + for child in elem.children() { + if child.is("date", ns::JINGLE_FT) { + if file.date.is_some() { + return Err(Error::ParseError("File must not have more than one date.")); + } + file.date = Some(child.text().parse()?); + } else if child.is("media-type", ns::JINGLE_FT) { + if file.media_type.is_some() { + return Err(Error::ParseError( + "File must not have more than one media-type.", + )); + } + file.media_type = Some(child.text()); + } else if child.is("name", ns::JINGLE_FT) { + if file.name.is_some() { + return Err(Error::ParseError("File must not have more than one name.")); + } + file.name = Some(child.text()); + } else if child.is("desc", ns::JINGLE_FT) { + let lang = get_attr!(child, "xml:lang", Default); + let desc = Desc(child.text()); + if file.descs.insert(lang, desc).is_some() { + return Err(Error::ParseError( + "Desc element present twice for the same xml:lang.", + )); + } + } else if child.is("size", ns::JINGLE_FT) { + if file.size.is_some() { + return Err(Error::ParseError("File must not have more than one size.")); + } + file.size = Some(child.text().parse()?); + } else if child.is("range", ns::JINGLE_FT) { + if file.range.is_some() { + return Err(Error::ParseError("File must not have more than one range.")); + } + file.range = Some(Range::try_from(child.clone())?); + } else if child.is("hash", ns::HASHES) { + file.hashes.push(Hash::try_from(child.clone())?); + } else { + return Err(Error::ParseError("Unknown element in JingleFT file.")); + } + } + + Ok(file) + } +} + +impl From for Element { + fn from(file: File) -> Element { + Element::builder("file") + .ns(ns::JINGLE_FT) + .append_all(file.date.map(|date| + Element::builder("date") + .append(date))) + .append_all(file.media_type.map(|media_type| + Element::builder("media-type") + .append(media_type))) + .append_all(file.name.map(|name| + Element::builder("name") + .append(name))) + .append_all(file.descs.into_iter().map(|(lang, desc)| + Element::builder("desc") + .attr("xml:lang", lang) + .append(desc.0))) + .append_all(file.size.map(|size| + Element::builder("size") + .append(format!("{}", size)))) + .append_all(file.range) + .append_all(file.hashes) + .build() + } +} + +/// A wrapper element for a file. +#[derive(Debug, Clone)] +pub struct Description { + /// The actual file descriptor. + pub file: File, +} + +impl TryFrom for Description { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "description", JINGLE_FT, "JingleFT description"); + check_no_attributes!(elem, "JingleFT description"); + let mut file = None; + for child in elem.children() { + if file.is_some() { + return Err(Error::ParseError( + "JingleFT description element must have exactly one child.", + )); + } + file = Some(File::try_from(child.clone())?); + } + if file.is_none() { + return Err(Error::ParseError( + "JingleFT description element must have exactly one child.", + )); + } + Ok(Description { + file: file.unwrap(), + }) + } +} + +impl From for Element { + fn from(description: Description) -> Element { + Element::builder("description") + .ns(ns::JINGLE_FT) + .append(Node::Element(description.file.into())) + .build() + } +} + +/// A checksum for checking that the file has been transferred correctly. +#[derive(Debug, Clone)] +pub struct Checksum { + /// The identifier of the file transfer content. + pub name: ContentId, + + /// The creator of this file transfer. + pub creator: Creator, + + /// The file being checksummed. + pub file: File, +} + +impl TryFrom for Checksum { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "checksum", JINGLE_FT); + check_no_unknown_attributes!(elem, "checksum", ["name", "creator"]); + let mut file = None; + for child in elem.children() { + if file.is_some() { + return Err(Error::ParseError( + "JingleFT checksum element must have exactly one child.", + )); + } + file = Some(File::try_from(child.clone())?); + } + if file.is_none() { + return Err(Error::ParseError( + "JingleFT checksum element must have exactly one child.", + )); + } + Ok(Checksum { + name: get_attr!(elem, "name", Required), + creator: get_attr!(elem, "creator", Required), + file: file.unwrap(), + }) + } +} + +impl From for Element { + fn from(checksum: Checksum) -> Element { + Element::builder("checksum") + .ns(ns::JINGLE_FT) + .attr("name", checksum.name) + .attr("creator", checksum.creator) + .append(Node::Element(checksum.file.into())) + .build() + } +} + +generate_element!( + /// A notice that the file transfer has been completed. + Received, "received", JINGLE_FT, + attributes: [ + /// The content identifier of this Jingle session. + name: Required = "name", + + /// The creator of this file transfer. + creator: Required = "creator", + ] +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::hashes::Algo; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Range, 40); + assert_size!(File, 128); + assert_size!(Description, 128); + assert_size!(Checksum, 144); + assert_size!(Received, 16); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Range, 48); + assert_size!(File, 184); + assert_size!(Description, 184); + assert_size!(Checksum, 216); + assert_size!(Received, 32); + } + + #[test] + fn test_description() { + let elem: Element = r#" + + + text/plain + test.txt + 2015-07-26T21:46:00+01:00 + 6144 + w0mcJylzCn+AfvuGdqkty2+KP48= + + +"# + .parse() + .unwrap(); + let desc = Description::try_from(elem).unwrap(); + assert_eq!(desc.file.media_type, Some(String::from("text/plain"))); + assert_eq!(desc.file.name, Some(String::from("test.txt"))); + assert_eq!(desc.file.descs, BTreeMap::new()); + assert_eq!( + desc.file.date, + DateTime::from_str("2015-07-26T21:46:00+01:00").ok() + ); + assert_eq!(desc.file.size, Some(6144u64)); + assert_eq!(desc.file.range, None); + assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1); + assert_eq!( + desc.file.hashes[0].hash, + base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap() + ); + } + + #[test] + fn test_request() { + let elem: Element = r#" + + + w0mcJylzCn+AfvuGdqkty2+KP48= + + +"# + .parse() + .unwrap(); + let desc = Description::try_from(elem).unwrap(); + assert_eq!(desc.file.media_type, None); + assert_eq!(desc.file.name, None); + assert_eq!(desc.file.descs, BTreeMap::new()); + assert_eq!(desc.file.date, None); + assert_eq!(desc.file.size, None); + assert_eq!(desc.file.range, None); + assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1); + assert_eq!( + desc.file.hashes[0].hash, + base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap() + ); + } + + #[test] + fn test_descs() { + let elem: Element = r#" + + + text/plain + Fichier secret ! + Secret file! + w0mcJylzCn+AfvuGdqkty2+KP48= + + +"# + .parse() + .unwrap(); + let desc = Description::try_from(elem).unwrap(); + assert_eq!( + desc.file.descs.keys().cloned().collect::>(), + ["en", "fr"] + ); + assert_eq!(desc.file.descs["en"], Desc(String::from("Secret file!"))); + assert_eq!( + desc.file.descs["fr"], + Desc(String::from("Fichier secret !")) + ); + + let elem: Element = r#" + + + text/plain + Fichier secret ! + Secret file! + w0mcJylzCn+AfvuGdqkty2+KP48= + + +"# + .parse() + .unwrap(); + let error = Description::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Desc element present twice for the same xml:lang."); + } + + #[test] + fn test_received() { + let elem: Element = "".parse().unwrap(); + let received = Received::try_from(elem).unwrap(); + assert_eq!(received.name, ContentId(String::from("coucou"))); + assert_eq!(received.creator, Creator::Initiator); + let elem2 = Element::from(received.clone()); + let received2 = Received::try_from(elem2).unwrap(); + assert_eq!(received2.name, ContentId(String::from("coucou"))); + assert_eq!(received2.creator, Creator::Initiator); + + let elem: Element = "".parse().unwrap(); + let error = Received::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in received element."); + + let elem: Element = + "" + .parse() + .unwrap(); + let error = Received::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'name' missing."); + + let elem: Element = "".parse().unwrap(); + let error = Received::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'creator' attribute."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_received() { + let elem: Element = "".parse().unwrap(); + let error = Received::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in received element."); + } + + #[test] + fn test_checksum() { + let elem: Element = "w0mcJylzCn+AfvuGdqkty2+KP48=".parse().unwrap(); + let hash = vec![ + 195, 73, 156, 39, 41, 115, 10, 127, 128, 126, 251, 134, 118, 169, 45, 203, 111, 138, + 63, 143, + ]; + let checksum = Checksum::try_from(elem).unwrap(); + assert_eq!(checksum.name, ContentId(String::from("coucou"))); + assert_eq!(checksum.creator, Creator::Initiator); + assert_eq!( + checksum.file.hashes, + vec!(Hash { + algo: Algo::Sha_1, + hash: hash.clone() + }) + ); + let elem2 = Element::from(checksum); + let checksum2 = Checksum::try_from(elem2).unwrap(); + assert_eq!(checksum2.name, ContentId(String::from("coucou"))); + assert_eq!(checksum2.creator, Creator::Initiator); + assert_eq!( + checksum2.file.hashes, + vec!(Hash { + algo: Algo::Sha_1, + hash: hash.clone() + }) + ); + + let elem: Element = "".parse().unwrap(); + let error = Checksum::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "This is not a file element."); + + let elem: Element = "w0mcJylzCn+AfvuGdqkty2+KP48=".parse().unwrap(); + let error = Checksum::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'name' missing."); + + let elem: Element = "w0mcJylzCn+AfvuGdqkty2+KP48=".parse().unwrap(); + let error = Checksum::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'creator' attribute."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_checksum() { + let elem: Element = "w0mcJylzCn+AfvuGdqkty2+KP48=".parse().unwrap(); + let error = Checksum::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in checksum element."); + } + + #[test] + fn test_range() { + let elem: Element = "" + .parse() + .unwrap(); + let range = Range::try_from(elem).unwrap(); + assert_eq!(range.offset, 0); + assert_eq!(range.length, None); + assert_eq!(range.hashes, vec!()); + + let elem: Element = "kHp5RSzW/h7Gm1etSf90Mr5PC/k=".parse().unwrap(); + let hashes = vec![Hash { + algo: Algo::Sha_1, + hash: vec![ + 144, 122, 121, 69, 44, 214, 254, 30, 198, 155, 87, 173, 73, 255, 116, 50, 190, 79, + 11, 249, + ], + }]; + let range = Range::try_from(elem).unwrap(); + assert_eq!(range.offset, 2048); + assert_eq!(range.length, Some(1024)); + assert_eq!(range.hashes, hashes); + let elem2 = Element::from(range); + let range2 = Range::try_from(elem2).unwrap(); + assert_eq!(range2.offset, 2048); + assert_eq!(range2.length, Some(1024)); + assert_eq!(range2.hashes, hashes); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_range() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Range::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in range element."); + } +} diff --git a/xmpp-parsers/src/jingle_ibb.rs b/xmpp-parsers/src/jingle_ibb.rs new file mode 100644 index 0000000000000000000000000000000000000000..f0fb90f0f8a12b43758d046b8f1cf7933fa8e99d --- /dev/null +++ b/xmpp-parsers/src/jingle_ibb.rs @@ -0,0 +1,114 @@ +// 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 crate::ibb::{Stanza, StreamId}; + +generate_element!( +/// Describes an [In-Band Bytestream](https://xmpp.org/extensions/xep-0047.html) +/// Jingle transport, see also the [IBB module](../ibb.rs). +Transport, "transport", JINGLE_IBB, +attributes: [ + /// Maximum size in bytes for each chunk. + block_size: Required = "block-size", + + /// The identifier to be used to create a stream. + sid: Required = "sid", + + /// Which stanza type to use to exchange data. + stanza: Default = "stanza", +]); + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::error::Error as StdError; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Transport, 16); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Transport, 32); + } + + #[test] + fn test_simple() { + let elem: Element = + "" + .parse() + .unwrap(); + let transport = Transport::try_from(elem).unwrap(); + assert_eq!(transport.block_size, 3); + assert_eq!(transport.sid, StreamId(String::from("coucou"))); + assert_eq!(transport.stanza, Stanza::Iq); + } + + #[test] + fn test_invalid() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Transport::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'block-size' missing."); + + let elem: Element = + "" + .parse() + .unwrap(); + let error = Transport::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!( + message.description(), + "number too large to fit in target type" + ); + + let elem: Element = "" + .parse() + .unwrap(); + let error = Transport::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!(message.description(), "invalid digit found in string"); + + let elem: Element = + "" + .parse() + .unwrap(); + let error = Transport::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'sid' missing."); + } + + #[test] + fn test_invalid_stanza() { + let elem: Element = "".parse().unwrap(); + let error = Transport::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'stanza' attribute."); + } +} diff --git a/xmpp-parsers/src/jingle_ice_udp.rs b/xmpp-parsers/src/jingle_ice_udp.rs new file mode 100644 index 0000000000000000000000000000000000000000..b7f9e777886a2e686355b88a96b1d15f3b6620e5 --- /dev/null +++ b/xmpp-parsers/src/jingle_ice_udp.rs @@ -0,0 +1,189 @@ +// Copyright (c) 2019 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 crate::jingle_dtls_srtp::Fingerprint; +use std::net::IpAddr; + +generate_element!( + /// Wrapper element for an ICE-UDP transport. + Transport, "transport", JINGLE_ICE_UDP, + attributes: [ + /// A Password as defined in ICE-CORE. + pwd: Option = "pwd", + + /// A User Fragment as defined in ICE-CORE. + ufrag: Option = "ufrag", + ], + children: [ + /// List of candidates for this ICE-UDP session. + candidates: Vec = ("candidate", JINGLE_ICE_UDP) => Candidate, + + /// Fingerprint of the key used for the DTLS handshake. + fingerprint: Option = ("fingerprint", JINGLE_DTLS) => Fingerprint + ] +); + +impl Transport { + /// Create a new ICE-UDP transport. + pub fn new() -> Transport { + Transport { + pwd: None, + ufrag: None, + candidates: Vec::new(), + fingerprint: None, + } + } + + /// Add a candidate to this transport. + pub fn add_candidate(mut self, candidate: Candidate) -> Self { + self.candidates.push(candidate); + self + } + + /// Set the DTLS-SRTP fingerprint of this transport. + pub fn with_fingerprint(mut self, fingerprint: Fingerprint) -> Self { + self.fingerprint = Some(fingerprint); + self + } +} + +generate_attribute!( + /// A Candidate Type as defined in ICE-CORE. + Type, "type", { + /// Host candidate. + Host => "host", + + /// Peer reflexive candidate. + Prflx => "prflx", + + /// Relayed candidate. + Relay => "relay", + + /// Server reflexive candidate. + Srflx => "srflx", + } +); + +generate_element!( + /// A candidate for an ICE-UDP session. + Candidate, "candidate", JINGLE_ICE_UDP, + attributes: [ + /// A Component ID as defined in ICE-CORE. + component: Required = "component", + + /// A Foundation as defined in ICE-CORE. + foundation: Required = "foundation", + + /// An index, starting at 0, that enables the parties to keep track of updates to the + /// candidate throughout the life of the session. + generation: Required = "generation", + + /// A unique identifier for the candidate. + id: Required = "id", + + /// The Internet Protocol (IP) address for the candidate transport mechanism; this can be + /// either an IPv4 address or an IPv6 address. + ip: Required = "ip", + + /// An index, starting at 0, referencing which network this candidate is on for a given + /// peer. + network: Required = "network", + + /// The port at the candidate IP address. + port: Required = "port", + + /// A Priority as defined in ICE-CORE. + priority: Required = "priority", + + /// The protocol to be used. The only value defined by this specification is "udp". + protocol: Required = "protocol", + + /// A related address as defined in ICE-CORE. + rel_addr: Option = "rel-addr", + + /// A related port as defined in ICE-CORE. + rel_port: Option = "rel-port", + + /// A Candidate Type as defined in ICE-CORE. + type_: Required = "type", + ] +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + use crate::hashes::Algo; + use crate::jingle_dtls_srtp::Setup; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Transport, 68); + assert_size!(Type, 1); + assert_size!(Candidate, 80); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Transport, 136); + assert_size!(Type, 1); + assert_size!(Candidate, 104); + } + + #[test] + fn test_gajim() { + let elem: Element = " + + + + + + + + + + + + + + + + + + + +" + .parse() + .unwrap(); + let transport = Transport::try_from(elem).unwrap(); + assert_eq!(transport.pwd.unwrap(), "wakMJ8Ydd5rqnPaFerws5o"); + assert_eq!(transport.ufrag.unwrap(), "aeXX"); + } + + #[test] + fn test_jitsi_meet() { + let elem: Element = " + + 97:F2:B5:BE:DB:A6:00:B1:3E:40:B2:41:3C:0D:FC:E0:BD:B2:A0:E8 + + + +" + .parse() + .unwrap(); + let transport = Transport::try_from(elem).unwrap(); + assert_eq!(transport.pwd.unwrap(), "7lk9uul39gckit6t02oavv2r9j"); + assert_eq!(transport.ufrag.unwrap(), "2acq51d4p07v2m"); + + let fingerprint = transport.fingerprint.unwrap(); + assert_eq!(fingerprint.hash, Algo::Sha_1); + assert_eq!(fingerprint.setup, Setup::Actpass); + assert_eq!(fingerprint.value, [151, 242, 181, 190, 219, 166, 0, 177, 62, 64, 178, 65, 60, 13, 252, 224, 189, 178, 160, 232]); + } +} diff --git a/xmpp-parsers/src/jingle_message.rs b/xmpp-parsers/src/jingle_message.rs new file mode 100644 index 0000000000000000000000000000000000000000..78bf1db22ca38112eae8b3a363ce9563e8515b9d --- /dev/null +++ b/xmpp-parsers/src/jingle_message.rs @@ -0,0 +1,146 @@ +// 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 crate::util::error::Error; +use crate::jingle::SessionId; +use crate::ns; +use crate::Element; +use std::convert::TryFrom; + +/// Defines a protocol for broadcasting Jingle requests to all of the clients +/// of a user. +#[derive(Debug, Clone)] +pub enum JingleMI { + /// Indicates we want to start a Jingle session. + Propose { + /// The generated session identifier, must be unique between two users. + sid: SessionId, + + /// The application description of the proposed session. + // TODO: Use a more specialised type here. + description: Element, + }, + + /// Cancels a previously proposed session. + Retract(SessionId), + + /// Accepts a session proposed by the other party. + Accept(SessionId), + + /// Proceed with a previously proposed session. + Proceed(SessionId), + + /// Rejects a session proposed by the other party. + Reject(SessionId), +} + +fn get_sid(elem: Element) -> Result { + check_no_unknown_attributes!(elem, "Jingle message", ["id"]); + Ok(SessionId(get_attr!(elem, "id", Required))) +} + +fn check_empty_and_get_sid(elem: Element) -> Result { + check_no_children!(elem, "Jingle message"); + get_sid(elem) +} + +impl TryFrom for JingleMI { + type Error = Error; + + fn try_from(elem: Element) -> Result { + if !elem.has_ns(ns::JINGLE_MESSAGE) { + return Err(Error::ParseError("This is not a Jingle message element.")); + } + Ok(match elem.name() { + "propose" => { + let mut description = None; + for child in elem.children() { + if child.name() != "description" { + return Err(Error::ParseError("Unknown child in propose element.")); + } + if description.is_some() { + return Err(Error::ParseError("Too many children in propose element.")); + } + description = Some(child.clone()); + } + JingleMI::Propose { + sid: get_sid(elem)?, + description: description.ok_or(Error::ParseError( + "Propose element doesn’t contain a description.", + ))?, + } + } + "retract" => JingleMI::Retract(check_empty_and_get_sid(elem)?), + "accept" => JingleMI::Accept(check_empty_and_get_sid(elem)?), + "proceed" => JingleMI::Proceed(check_empty_and_get_sid(elem)?), + "reject" => JingleMI::Reject(check_empty_and_get_sid(elem)?), + _ => return Err(Error::ParseError("This is not a Jingle message element.")), + }) + } +} + +impl From for Element { + fn from(jingle_mi: JingleMI) -> Element { + match jingle_mi { + JingleMI::Propose { sid, description } => Element::builder("propose") + .ns(ns::JINGLE_MESSAGE) + .attr("id", sid) + .append(description), + JingleMI::Retract(sid) => Element::builder("retract") + .ns(ns::JINGLE_MESSAGE) + .attr("id", sid), + JingleMI::Accept(sid) => Element::builder("accept") + .ns(ns::JINGLE_MESSAGE) + .attr("id", sid), + JingleMI::Proceed(sid) => Element::builder("proceed") + .ns(ns::JINGLE_MESSAGE) + .attr("id", sid), + JingleMI::Reject(sid) => Element::builder("reject") + .ns(ns::JINGLE_MESSAGE) + .attr("id", sid), + } + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(JingleMI, 68); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(JingleMI, 136); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + JingleMI::try_from(elem).unwrap(); + } + + #[test] + fn test_invalid_child() { + let elem: Element = + "" + .parse() + .unwrap(); + let error = JingleMI::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in propose element."); + } +} diff --git a/xmpp-parsers/src/jingle_rtcp_fb.rs b/xmpp-parsers/src/jingle_rtcp_fb.rs new file mode 100644 index 0000000000000000000000000000000000000000..da52b88794b23c581bc39334f4bf4a817504db79 --- /dev/null +++ b/xmpp-parsers/src/jingle_rtcp_fb.rs @@ -0,0 +1,46 @@ +// Copyright (c) 2019 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/. + +generate_element!( + /// Wrapper element for a rtcp-fb. + RtcpFb, "rtcp-fb", JINGLE_RTCP_FB, + attributes: [ + /// Type of this rtcp-fb. + type_: Required = "type", + + /// Subtype of this rtcp-fb, if relevant. + subtype: Option = "subtype", + ] +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(RtcpFb, 24); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(RtcpFb, 48); + } + + #[test] + fn parse_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let rtcp_fb = RtcpFb::try_from(elem).unwrap(); + assert_eq!(rtcp_fb.type_, "nack"); + assert_eq!(rtcp_fb.subtype.unwrap(), "sli"); + } +} diff --git a/xmpp-parsers/src/jingle_rtp.rs b/xmpp-parsers/src/jingle_rtp.rs new file mode 100644 index 0000000000000000000000000000000000000000..4fa596a991a12db2bc049eeae18f18c239cc38a0 --- /dev/null +++ b/xmpp-parsers/src/jingle_rtp.rs @@ -0,0 +1,173 @@ +// Copyright (c) 2019 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 crate::jingle_ssma::{Source, Group}; +use crate::jingle_rtcp_fb::RtcpFb; + +generate_element!( + /// Wrapper element describing an RTP session. + Description, "description", JINGLE_RTP, + attributes: [ + /// Namespace of the encryption scheme used. + media: Required = "media", + + /// User-friendly name for the encryption scheme, should be `None` for OTR, + /// legacy OpenPGP and OX. + // XXX: is this a String or an u32?! Refer to RFC 3550. + ssrc: Option = "ssrc", + ], + children: [ + /// List of encodings that can be used for this RTP stream. + payload_types: Vec = ("payload-type", JINGLE_RTP) => PayloadType, + + /// List of ssrc-group. + ssrc_groups: Vec = ("ssrc-group", JINGLE_SSMA) => Group, + + /// List of ssrc. + ssrcs: Vec = ("source", JINGLE_SSMA) => Source + + // TODO: Add support for and . + ] +); + +impl Description { + /// Create a new RTP description. + pub fn new(media: String) -> Description { + Description { + media, + ssrc: None, + payload_types: Vec::new(), + ssrc_groups: Vec::new(), + ssrcs: Vec::new(), + } + } +} + +generate_attribute!( + /// The number of channels. + Channels, "channels", u8, Default = 1 +); + +generate_element!( + /// An encoding that can be used for an RTP stream. + PayloadType, "payload-type", JINGLE_RTP, + attributes: [ + /// The number of channels. + channels: Default = "channels", + + /// The sampling frequency in Hertz. + clockrate: Option = "clockrate", + + /// The payload identifier. + id: Required = "id", + + /// Maximum packet time as specified in RFC 4566. + maxptime: Option = "maxptime", + + /// The appropriate subtype of the MIME type. + name: Option = "name", + + /// Packet time as specified in RFC 4566. + ptime: Option = "ptime", + ], + children: [ + /// List of parameters specifying this payload-type. + /// + /// Their order MUST be ignored. + parameters: Vec = ("parameter", JINGLE_RTP) => Parameter, + + /// List of rtcp-fb parameters from XEP-0293. + rtcp_fbs: Vec = ("rtcp-fb", JINGLE_RTCP_FB) => RtcpFb + ] +); + +impl PayloadType { + /// Create a new RTP payload-type. + pub fn new(id: u8, name: String) -> PayloadType { + PayloadType { + channels: Default::default(), + clockrate: None, + id, + maxptime: None, + name: Some(name), + ptime: None, + parameters: Vec::new(), + rtcp_fbs: Vec::new(), + } + } +} + +generate_element!( + /// Parameter related to a payload. + Parameter, "parameter", JINGLE_RTP, + attributes: [ + /// The name of the parameter, from the list at + /// https://www.iana.org/assignments/sdp-parameters/sdp-parameters.xhtml + name: Required = "name", + + /// The value of this parameter. + value: Required = "value", + ] +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Description, 36); + assert_size!(Channels, 1); + assert_size!(PayloadType, 52); + assert_size!(Parameter, 24); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Description, 120); + assert_size!(Channels, 1); + assert_size!(PayloadType, 104); + assert_size!(Parameter, 48); + } + + #[test] + fn test_simple() { + let elem: Element = " + + + + + + + + + + + + + + + + + + + + + + + +" + .parse() + .unwrap(); + let desc = Description::try_from(elem).unwrap(); + assert_eq!(desc.media, "audio"); + assert_eq!(desc.ssrc, None); + } +} diff --git a/xmpp-parsers/src/jingle_s5b.rs b/xmpp-parsers/src/jingle_s5b.rs new file mode 100644 index 0000000000000000000000000000000000000000..b6df19ed6d835abb8f7b5e4779366b9ea715d2c9 --- /dev/null +++ b/xmpp-parsers/src/jingle_s5b.rs @@ -0,0 +1,352 @@ +// 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 crate::util::error::Error; +use crate::ns; +use jid::Jid; +use crate::Element; +use std::net::IpAddr; +use std::convert::TryFrom; + +generate_attribute!( + /// The type of the connection being proposed by this candidate. + Type, "type", { + /// Direct connection using NAT assisting technologies like NAT-PMP or + /// UPnP-IGD. + Assisted => "assisted", + + /// Direct connection using the given interface. + Direct => "direct", + + /// SOCKS5 relay. + Proxy => "proxy", + + /// Tunnel protocol such as Teredo. + Tunnel => "tunnel", + }, Default = Direct +); + +generate_attribute!( + /// Which mode to use for the connection. + Mode, "mode", { + /// Use TCP, which is the default. + Tcp => "tcp", + + /// Use UDP. + Udp => "udp", + }, Default = Tcp +); + +generate_id!( + /// An identifier for a candidate. + CandidateId +); + +generate_id!( + /// An identifier for a stream. + StreamId +); + +generate_element!( + /// A candidate for a connection. + Candidate, "candidate", JINGLE_S5B, + attributes: [ + /// The identifier for this candidate. + cid: Required = "cid", + + /// The host to connect to. + host: Required = "host", + + /// The JID to request at the given end. + jid: Required = "jid", + + /// The port to connect to. + port: Option = "port", + + /// The priority of this candidate, computed using this formula: + /// priority = (2^16)*(type preference) + (local preference) + priority: Required = "priority", + + /// The type of the connection being proposed by this candidate. + type_: Default = "type", + ] +); + +impl Candidate { + /// Creates a new candidate with the given parameters. + pub fn new(cid: CandidateId, host: IpAddr, jid: Jid, priority: u32) -> Candidate { + Candidate { + cid, + host, + jid, + priority, + port: Default::default(), + type_: Default::default(), + } + } + + /// Sets the port of this candidate. + pub fn with_port(mut self, port: u16) -> Candidate { + self.port = Some(port); + self + } + + /// Sets the type of this candidate. + pub fn with_type(mut self, type_: Type) -> Candidate { + self.type_ = type_; + self + } +} + +/// The payload of a transport. +#[derive(Debug, Clone)] +pub enum TransportPayload { + /// The responder informs the initiator that the bytestream pointed by this + /// candidate has been activated. + Activated(CandidateId), + + /// A list of suggested candidates. + Candidates(Vec), + + /// Both parties failed to use a candidate, they should fallback to another + /// transport. + CandidateError, + + /// The candidate pointed here should be used by both parties. + CandidateUsed(CandidateId), + + /// This entity can’t connect to the SOCKS5 proxy. + ProxyError, + + /// XXX: Invalid, should not be found in the wild. + None, +} + +/// Describes a Jingle transport using a direct or proxied connection. +#[derive(Debug, Clone)] +pub struct Transport { + /// The stream identifier for this transport. + pub sid: StreamId, + + /// The destination address. + pub dstaddr: Option, + + /// The mode to be used for the transfer. + pub mode: Mode, + + /// The payload of this transport. + pub payload: TransportPayload, +} + +impl Transport { + /// Creates a new transport element. + pub fn new(sid: StreamId) -> Transport { + Transport { + sid, + dstaddr: None, + mode: Default::default(), + payload: TransportPayload::None, + } + } + + /// Sets the destination address of this transport. + pub fn with_dstaddr(mut self, dstaddr: String) -> Transport { + self.dstaddr = Some(dstaddr); + self + } + + /// Sets the mode of this transport. + pub fn with_mode(mut self, mode: Mode) -> Transport { + self.mode = mode; + self + } + + /// Sets the payload of this transport. + pub fn with_payload(mut self, payload: TransportPayload) -> Transport { + self.payload = payload; + self + } +} + +impl TryFrom for Transport { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "transport", JINGLE_S5B); + check_no_unknown_attributes!(elem, "transport", ["sid", "dstaddr", "mode"]); + let sid = get_attr!(elem, "sid", Required); + let dstaddr = get_attr!(elem, "dstaddr", Option); + let mode = get_attr!(elem, "mode", Default); + + let mut payload = None; + for child in elem.children() { + payload = Some(if child.is("candidate", ns::JINGLE_S5B) { + let mut candidates = + match payload { + Some(TransportPayload::Candidates(candidates)) => candidates, + Some(_) => return Err(Error::ParseError( + "Non-candidate child already present in JingleS5B transport element.", + )), + None => vec![], + }; + candidates.push(Candidate::try_from(child.clone())?); + TransportPayload::Candidates(candidates) + } else if child.is("activated", ns::JINGLE_S5B) { + if payload.is_some() { + return Err(Error::ParseError( + "Non-activated child already present in JingleS5B transport element.", + )); + } + let cid = get_attr!(child, "cid", Required); + TransportPayload::Activated(cid) + } else if child.is("candidate-error", ns::JINGLE_S5B) { + if payload.is_some() { + return Err(Error::ParseError( + "Non-candidate-error child already present in JingleS5B transport element.", + )); + } + TransportPayload::CandidateError + } else if child.is("candidate-used", ns::JINGLE_S5B) { + if payload.is_some() { + return Err(Error::ParseError( + "Non-candidate-used child already present in JingleS5B transport element.", + )); + } + let cid = get_attr!(child, "cid", Required); + TransportPayload::CandidateUsed(cid) + } else if child.is("proxy-error", ns::JINGLE_S5B) { + if payload.is_some() { + return Err(Error::ParseError( + "Non-proxy-error child already present in JingleS5B transport element.", + )); + } + TransportPayload::ProxyError + } else { + return Err(Error::ParseError( + "Unknown child in JingleS5B transport element.", + )); + }); + } + let payload = payload.unwrap_or(TransportPayload::None); + Ok(Transport { + sid, + dstaddr, + mode, + payload, + }) + } +} + +impl From for Element { + fn from(transport: Transport) -> Element { + Element::builder("transport") + .ns(ns::JINGLE_S5B) + .attr("sid", transport.sid) + .attr("dstaddr", transport.dstaddr) + .attr("mode", transport.mode) + .append_all(match transport.payload { + TransportPayload::Candidates(candidates) => candidates + .into_iter() + .map(Element::from) + .collect::>(), + TransportPayload::Activated(cid) => vec![Element::builder("activated") + .ns(ns::JINGLE_S5B) + .attr("cid", cid) + .build()], + TransportPayload::CandidateError => vec![Element::builder("candidate-error") + .ns(ns::JINGLE_S5B) + .build()], + TransportPayload::CandidateUsed(cid) => vec![Element::builder("candidate-used") + .ns(ns::JINGLE_S5B) + .attr("cid", cid) + .build()], + TransportPayload::ProxyError => vec![Element::builder("proxy-error") + .ns(ns::JINGLE_S5B) + .build()], + TransportPayload::None => vec![], + }) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::compare_elements::NamespaceAwareCompare; + use std::str::FromStr; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Type, 1); + assert_size!(Mode, 1); + assert_size!(CandidateId, 12); + assert_size!(StreamId, 12); + assert_size!(Candidate, 84); + assert_size!(TransportPayload, 16); + assert_size!(Transport, 44); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Type, 1); + assert_size!(Mode, 1); + assert_size!(CandidateId, 24); + assert_size!(StreamId, 24); + assert_size!(Candidate, 136); + assert_size!(TransportPayload, 32); + assert_size!(Transport, 88); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let transport = Transport::try_from(elem).unwrap(); + assert_eq!(transport.sid, StreamId(String::from("coucou"))); + assert_eq!(transport.dstaddr, None); + assert_eq!(transport.mode, Mode::Tcp); + match transport.payload { + TransportPayload::None => (), + _ => panic!("Wrong element inside transport!"), + } + } + + #[test] + fn test_serialise_activated() { + let elem: Element = "".parse().unwrap(); + let transport = Transport { + sid: StreamId(String::from("coucou")), + dstaddr: None, + mode: Mode::Tcp, + payload: TransportPayload::Activated(CandidateId(String::from("coucou"))), + }; + let elem2: Element = transport.into(); + assert!(elem.compare_to(&elem2)); + } + + #[test] + fn test_serialise_candidate() { + let elem: Element = "".parse().unwrap(); + let transport = Transport { + sid: StreamId(String::from("coucou")), + dstaddr: None, + mode: Mode::Tcp, + payload: TransportPayload::Candidates(vec![Candidate { + cid: CandidateId(String::from("coucou")), + host: IpAddr::from_str("127.0.0.1").unwrap(), + jid: Jid::from_str("coucou@coucou").unwrap(), + port: None, + priority: 0u32, + type_: Type::Direct, + }]), + }; + let elem2: Element = transport.into(); + assert!(elem.compare_to(&elem2)); + } +} diff --git a/xmpp-parsers/src/jingle_ssma.rs b/xmpp-parsers/src/jingle_ssma.rs new file mode 100644 index 0000000000000000000000000000000000000000..4d966b86ff8dd0676150363ee352b0228ee7fede --- /dev/null +++ b/xmpp-parsers/src/jingle_ssma.rs @@ -0,0 +1,114 @@ +// Copyright (c) 2019 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/. + +generate_element!( + /// Source element for the ssrc SDP attribute. + Source, "source", JINGLE_SSMA, + attributes: [ + /// Maps to the ssrc-id parameter. + id: Required = "ssrc", + ], + children: [ + /// List of attributes for this source. + parameters: Vec = ("parameter", JINGLE_SSMA) => Parameter + ] +); + +impl Source { + /// Create a new SSMA Source element. + pub fn new(id: String) -> Source { + Source { + id, + parameters: Vec::new(), + } + } +} + +generate_element!( + /// Parameter associated with a ssrc. + Parameter, "parameter", JINGLE_SSMA, + attributes: [ + /// The name of the parameter. + name: Required = "name", + + /// The optional value of the parameter. + value: Option = "value", + ] +); + +generate_element!( + /// Element grouping multiple ssrc. + Group, "ssrc-group", JINGLE_SSMA, + attributes: [ + /// The semantics of this group. + semantics: Required = "semantics", + ], + children: [ + /// The various ssrc concerned by this group. + sources: Vec = ("source", JINGLE_SSMA) => Source + ] +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Source, 24); + assert_size!(Parameter, 24); + assert_size!(Group, 24); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Source, 48); + assert_size!(Parameter, 48); + assert_size!(Group, 48); + } + + #[test] + fn parse_source() { + let elem: Element = " + + + +" + .parse() + .unwrap(); + let mut ssrc = Source::try_from(elem).unwrap(); + assert_eq!(ssrc.id, "1656081975"); + assert_eq!(ssrc.parameters.len(), 2); + let parameter = ssrc.parameters.pop().unwrap(); + assert_eq!(parameter.name, "msid"); + assert_eq!(parameter.value.unwrap(), "MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIv MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIva0"); + let parameter = ssrc.parameters.pop().unwrap(); + assert_eq!(parameter.name, "cname"); + assert_eq!(parameter.value.unwrap(), "Yv/wvbCdsDW2Prgd"); + } + + #[test] + fn parse_source_group() { + let elem: Element = " + + + +" + .parse() + .unwrap(); + let mut group = Group::try_from(elem).unwrap(); + assert_eq!(group.semantics, "FID"); + assert_eq!(group.sources.len(), 2); + let source = group.sources.pop().unwrap(); + assert_eq!(source.id, "386328120"); + let source = group.sources.pop().unwrap(); + assert_eq!(source.id, "2301230316"); + } +} diff --git a/xmpp-parsers/src/lib.rs b/xmpp-parsers/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..4977a8bc9937aab5a9840bb20e52d42602a052e8 --- /dev/null +++ b/xmpp-parsers/src/lib.rs @@ -0,0 +1,214 @@ +//! A crate parsing common XMPP elements into Rust structures. +//! +//! Each module implements the `TryFrom` trait, which takes a +//! minidom [`Element`] and returns a `Result` whose value is `Ok` if the +//! element parsed correctly, `Err(error::Error)` otherwise. +//! +//! The returned structure can be manipuled as any Rust structure, with each +//! field being public. You can also create the same structure manually, with +//! some having `new()` and `with_*()` helper methods to create them. +//! +//! Once you are happy with your structure, you can serialise it back to an +//! [`Element`], using either `From` or `Into`, which give you what +//! you want to be sending on the wire. +//! +//! [`Element`]: ../minidom/element/struct.Element.html + +// Copyright (c) 2017-2019 Emmanuel Gil Peyrot +// Copyright (c) 2017-2019 Maxime “pep” Buquet +// +// 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/. + +#![deny(missing_docs)] + +pub use minidom::Element; +pub use jid::{BareJid, FullJid, Jid, JidParseError}; +pub use crate::util::error::Error; + +/// XML namespace definitions used through XMPP. +pub mod ns; + +#[macro_use] +mod util; + +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub mod bind; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub mod iq; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub mod message; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub mod presence; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub mod sasl; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub mod stanza_error; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub mod stream; + +/// RFC 6121: Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence +pub mod roster; + +/// RFC 7395: An Extensible Messaging and Presence Protocol (XMPP) Subprotocol for WebSocket +pub mod websocket; + +/// XEP-0004: Data Forms +pub mod data_forms; + +/// XEP-0030: Service Discovery +pub mod disco; + +/// XEP-0045: Multi-User Chat +pub mod muc; + +/// XEP-0047: In-Band Bytestreams +pub mod ibb; + +/// XEP-0048: Bookmarks +pub mod bookmarks; + +/// XEP-0059: Result Set Management +pub mod rsm; + +/// XEP-0060: Publish-Subscribe +pub mod pubsub; + +/// XEP-0071: XHTML-IM +pub mod xhtml; + +/// XEP-0077: In-Band Registration +pub mod ibr; + +/// XEP-0082: XMPP Date and Time Profiles +pub mod date; + +/// XEP-0084: User Avatar +pub mod avatar; + +/// XEP-0085: Chat State Notifications +pub mod chatstates; + +/// XEP-0092: Software Version +pub mod version; + +/// XEP-0107: User Mood +pub mod mood; + +/// XEP-0114: Jabber Component Protocol +pub mod component; + +/// XEP-0115: Entity Capabilities +pub mod caps; + +/// XEP-0118: User Tune +pub mod tune; + +/// XEP-0157: Contact Addresses for XMPP Services +pub mod server_info; + +/// XEP-0166: Jingle +pub mod jingle; + +/// XEP-0167: Jingle RTP Sessions +pub mod jingle_rtp; + +/// XEP-0172: User Nickname +pub mod nick; + +/// XEP-0176: Jingle ICE-UDP Transport Method +pub mod jingle_ice_udp; + +/// XEP-0184: Message Delivery Receipts +pub mod receipts; + +/// XEP-0191: Blocking Command +pub mod blocking; + +/// XEP-0198: Stream Management +pub mod sm; + +/// XEP-0199: XMPP Ping +pub mod ping; + +/// XEP-0202: Entity Time +pub mod time; + +/// XEP-0203: Delayed Delivery +pub mod delay; + +/// XEP-0221: Data Forms Media Element +pub mod media_element; + +/// XEP-0224: Attention +pub mod attention; + +/// XEP-0231: Bits of Binary +pub mod bob; + +/// XEP-0234: Jingle File Transfer +pub mod jingle_ft; + +/// XEP-0257: Client Certificate Management for SASL EXTERNAL +pub mod cert_management; + +/// XEP-0260: Jingle SOCKS5 Bytestreams Transport Method +pub mod jingle_s5b; + +/// XEP-0261: Jingle In-Band Bytestreams Transport Method +pub mod jingle_ibb; + +/// XEP-0280: Message Carbons +pub mod carbons; + +/// XEP-0293: Jingle RTP Feedback Negotiation +pub mod jingle_rtcp_fb; + +/// XEP-0297: Stanza Forwarding +pub mod forwarding; + +/// XEP-0300: Use of Cryptographic Hash Functions in XMPP +pub mod hashes; + +/// XEP-0308: Last Message Correction +pub mod message_correct; + +/// XEP-0313: Message Archive Management +pub mod mam; + +/// XEP-0319: Last User Interaction in Presence +pub mod idle; + +/// XEP-0320: Use of DTLS-SRTP in Jingle Sessions +pub mod jingle_dtls_srtp; + +/// XEP-0328: JID Prep +pub mod jid_prep; + +/// XEP-0339: Source-Specific Media Attributes in Jingle +pub mod jingle_ssma; + +/// XEP-0352: Client State Indication +pub mod csi; + +/// XEP-0353: Jingle Message Initiation +pub mod jingle_message; + +/// XEP-0359: Unique and Stable Stanza IDs +pub mod stanza_id; + +/// XEP-0373: OpenPGP for XMPP +pub mod openpgp; + +/// XEP-0380: Explicit Message Encryption +pub mod eme; + +/// XEP-0390: Entity Capabilities 2.0 +pub mod ecaps2; + +/// XEP-0402: Bookmarks 2 (This Time it's Serious) +pub mod bookmarks2; + +/// XEP-0421: Anonymous unique occupant identifiers for MUCs +pub mod occupant_id; diff --git a/xmpp-parsers/src/mam.rs b/xmpp-parsers/src/mam.rs new file mode 100644 index 0000000000000000000000000000000000000000..28362bf917c7081d49c28757434af018a485c9b6 --- /dev/null +++ b/xmpp-parsers/src/mam.rs @@ -0,0 +1,392 @@ +// 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 crate::data_forms::DataForm; +use crate::util::error::Error; +use crate::forwarding::Forwarded; +use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; +use crate::message::MessagePayload; +use crate::ns; +use crate::pubsub::NodeName; +use crate::rsm::{SetQuery, SetResult}; +use jid::Jid; +use minidom::{Element, Node}; +use std::convert::TryFrom; + +generate_id!( + /// An identifier matching a result message to the query requesting it. + QueryId +); + +generate_element!( + /// Starts a query to the archive. + Query, "query", MAM, + attributes: [ + /// An optional identifier for matching forwarded messages to this + /// query. + queryid: Option = "queryid", + + /// Must be set to Some when querying a PubSub node’s archive. + node: Option = "node" + ], + children: [ + /// Used for filtering the results. + form: Option = ("x", DATA_FORMS) => DataForm, + + /// Used for paging through results. + set: Option = ("set", RSM) => SetQuery + ] +); + +impl IqGetPayload for Query {} +impl IqSetPayload for Query {} +impl IqResultPayload for Query {} + +generate_element!( + /// The wrapper around forwarded stanzas. + Result_, "result", MAM, + attributes: [ + /// The stanza-id under which the archive stored this stanza. + id: Required = "id", + + /// The same queryid as the one requested in the + /// [query](struct.Query.html). + queryid: Option = "queryid", + ], + children: [ + /// The actual stanza being forwarded. + forwarded: Required = ("forwarded", FORWARD) => Forwarded + ] +); + +impl MessagePayload for Result_ {} + +generate_attribute!( + /// True when the end of a MAM query has been reached. + Complete, + "complete", + bool +); + +generate_element!( + /// Notes the end of a page in a query. + Fin, "fin", MAM, + attributes: [ + /// True when the end of a MAM query has been reached. + complete: Default = "complete", + ], + children: [ + /// Describes the current page, it should contain at least [first] + /// (with an [index]) and [last], and generally [count]. + /// + /// [first]: ../rsm/struct.SetResult.html#structfield.first + /// [index]: ../rsm/struct.SetResult.html#structfield.first_index + /// [last]: ../rsm/struct.SetResult.html#structfield.last + /// [count]: ../rsm/struct.SetResult.html#structfield.count + set: Required = ("set", RSM) => SetResult + ] +); + +impl IqResultPayload for Fin {} + +generate_attribute!( + /// Notes the default archiving preference for the user. + DefaultPrefs, "default", { + /// The default is to always log messages in the archive. + Always => "always", + + /// The default is to never log messages in the archive. + Never => "never", + + /// The default is to log messages in the archive only for contacts + /// present in the user’s [roster](../roster/index.html). + Roster => "roster", + } +); + +/// Controls the archiving preferences of the user. +#[derive(Debug, Clone)] +pub struct Prefs { + /// The default preference for JIDs in neither + /// [always](#structfield.always) or [never](#structfield.never) lists. + pub default_: DefaultPrefs, + + /// The set of JIDs for which to always store messages in the archive. + pub always: Vec, + + /// The set of JIDs for which to never store messages in the archive. + pub never: Vec, +} + +impl IqGetPayload for Prefs {} +impl IqSetPayload for Prefs {} +impl IqResultPayload for Prefs {} + +impl TryFrom for Prefs { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "prefs", MAM); + check_no_unknown_attributes!(elem, "prefs", ["default"]); + let mut always = vec![]; + let mut never = vec![]; + for child in elem.children() { + if child.is("always", ns::MAM) { + for jid_elem in child.children() { + if !jid_elem.is("jid", ns::MAM) { + return Err(Error::ParseError("Invalid jid element in always.")); + } + always.push(jid_elem.text().parse()?); + } + } else if child.is("never", ns::MAM) { + for jid_elem in child.children() { + if !jid_elem.is("jid", ns::MAM) { + return Err(Error::ParseError("Invalid jid element in never.")); + } + never.push(jid_elem.text().parse()?); + } + } else { + return Err(Error::ParseError("Unknown child in prefs element.")); + } + } + let default_ = get_attr!(elem, "default", Required); + Ok(Prefs { + default_, + always, + never, + }) + } +} + +fn serialise_jid_list(name: &str, jids: Vec) -> ::std::option::IntoIter { + if jids.is_empty() { + None.into_iter() + } else { + Some( + Element::builder(name) + .ns(ns::MAM) + .append_all( + jids.into_iter() + .map(|jid| + Element::builder("jid") + .ns(ns::MAM) + .append(String::from(jid)))) + .into(), + ).into_iter() + } +} + +impl From for Element { + fn from(prefs: Prefs) -> Element { + Element::builder("prefs") + .ns(ns::MAM) + .attr("default", prefs.default_) + .append_all(serialise_jid_list("always", prefs.always)) + .append_all(serialise_jid_list("never", prefs.never)) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(QueryId, 12); + assert_size!(Query, 116); + assert_size!(Result_, 236); + assert_size!(Complete, 1); + assert_size!(Fin, 44); + assert_size!(DefaultPrefs, 1); + assert_size!(Prefs, 28); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(QueryId, 24); + assert_size!(Query, 232); + assert_size!(Result_, 456); + assert_size!(Complete, 1); + assert_size!(Fin, 88); + assert_size!(DefaultPrefs, 1); + assert_size!(Prefs, 56); + } + + #[test] + fn test_query() { + let elem: Element = "".parse().unwrap(); + Query::try_from(elem).unwrap(); + } + + #[test] + fn test_result() { + #[cfg(not(feature = "component"))] + let elem: Element = r#" + + + + + Hail to thee + + + +"# + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = r#" + + + + + Hail to thee + + + +"#.parse().unwrap(); + Result_::try_from(elem).unwrap(); + } + + #[test] + fn test_fin() { + let elem: Element = r#" + + + 28482-98726-73623 + 09af3-cc343-b409f + + +"# + .parse() + .unwrap(); + Fin::try_from(elem).unwrap(); + } + + #[test] + fn test_query_x() { + let elem: Element = r#" + + + + urn:xmpp:mam:2 + + + juliet@capulet.lit + + + +"# + .parse() + .unwrap(); + Query::try_from(elem).unwrap(); + } + + #[test] + fn test_query_x_set() { + let elem: Element = r#" + + + + urn:xmpp:mam:2 + + + 2010-08-07T00:00:00Z + + + + 10 + + +"# + .parse() + .unwrap(); + Query::try_from(elem).unwrap(); + } + + #[test] + fn test_prefs_get() { + let elem: Element = "" + .parse() + .unwrap(); + let prefs = Prefs::try_from(elem).unwrap(); + assert_eq!(prefs.always, vec!()); + assert_eq!(prefs.never, vec!()); + + let elem: Element = r#" + + + + +"# + .parse() + .unwrap(); + let prefs = Prefs::try_from(elem).unwrap(); + assert_eq!(prefs.always, vec!()); + assert_eq!(prefs.never, vec!()); + } + + #[test] + fn test_prefs_result() { + let elem: Element = r#" + + + romeo@montague.lit + + + montague@montague.lit + + +"# + .parse() + .unwrap(); + let prefs = Prefs::try_from(elem).unwrap(); + assert_eq!( + prefs.always, + vec!(Jid::from_str("romeo@montague.lit").unwrap()) + ); + assert_eq!( + prefs.never, + vec!(Jid::from_str("montague@montague.lit").unwrap()) + ); + + let elem2 = Element::from(prefs.clone()); + println!("{:?}", elem2); + let prefs2 = Prefs::try_from(elem2).unwrap(); + assert_eq!(prefs.default_, prefs2.default_); + assert_eq!(prefs.always, prefs2.always); + assert_eq!(prefs.never, prefs2.never); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Query::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in query element."); + } + + #[test] + fn test_serialise() { + let elem: Element = "".parse().unwrap(); + let replace = Query { + queryid: None, + node: None, + form: None, + set: None, + }; + let elem2 = replace.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/media_element.rs b/xmpp-parsers/src/media_element.rs new file mode 100644 index 0000000000000000000000000000000000000000..099dadae5ab37387540ca22170afbec6d5f7eeaa --- /dev/null +++ b/xmpp-parsers/src/media_element.rs @@ -0,0 +1,256 @@ +// 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 crate::util::helpers::TrimmedPlainText; + +generate_element!( + /// Represents an URI used in a media element. + URI, "uri", MEDIA_ELEMENT, + attributes: [ + /// The MIME type of the URI referenced. + /// + /// See the [IANA MIME Media Types Registry][1] for a list of + /// registered types, but unregistered or yet-to-be-registered are + /// accepted too. + /// + /// [1]: https://www.iana.org/assignments/media-types/media-types.xhtml + type_: Required = "type" + ], + text: ( + /// The actual URI contained. + uri: TrimmedPlainText + ) +); + +generate_element!( + /// References a media element, to be used in [data + /// forms](../data_forms/index.html). + MediaElement, "media", MEDIA_ELEMENT, + attributes: [ + /// The recommended display width in pixels. + width: Option = "width", + + /// The recommended display height in pixels. + height: Option = "height" + ], + children: [ + /// A list of URIs referencing this media. + uris: Vec = ("uri", MEDIA_ELEMENT) => URI + ] +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_forms::DataForm; + use crate::util::error::Error; + use crate::Element; + use std::error::Error as StdError; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(URI, 24); + assert_size!(MediaElement, 28); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(URI, 48); + assert_size!(MediaElement, 56); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + let media = MediaElement::try_from(elem).unwrap(); + assert!(media.width.is_none()); + assert!(media.height.is_none()); + assert!(media.uris.is_empty()); + } + + #[test] + fn test_width_height() { + let elem: Element = "" + .parse() + .unwrap(); + let media = MediaElement::try_from(elem).unwrap(); + assert_eq!(media.width.unwrap(), 32); + assert_eq!(media.height.unwrap(), 32); + } + + #[test] + fn test_uri() { + let elem: Element = "https://example.org/".parse().unwrap(); + let media = MediaElement::try_from(elem).unwrap(); + assert_eq!(media.uris.len(), 1); + assert_eq!(media.uris[0].type_, "text/html"); + assert_eq!(media.uris[0].uri, "https://example.org/"); + } + + #[test] + fn test_invalid_width_height() { + let elem: Element = "" + .parse() + .unwrap(); + let error = MediaElement::try_from(elem).unwrap_err(); + let error = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!( + error.description(), + "cannot parse integer from empty string" + ); + + let elem: Element = "" + .parse() + .unwrap(); + let error = MediaElement::try_from(elem).unwrap_err(); + let error = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!(error.description(), "invalid digit found in string"); + + let elem: Element = "" + .parse() + .unwrap(); + let error = MediaElement::try_from(elem).unwrap_err(); + let error = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!( + error.description(), + "cannot parse integer from empty string" + ); + + let elem: Element = "" + .parse() + .unwrap(); + let error = MediaElement::try_from(elem).unwrap_err(); + let error = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!(error.description(), "invalid digit found in string"); + } + + #[test] + fn test_unknown_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = MediaElement::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in media element."); + } + + #[test] + fn test_bad_uri() { + let elem: Element = + "https://example.org/" + .parse() + .unwrap(); + let error = MediaElement::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'type' missing."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = MediaElement::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "URI missing in uri."); + } + + #[test] + fn test_xep_ex1() { + let elem: Element = r#" + + + http://victim.example.com/challenges/speech.wav?F3A6292C + + + cid:sha1+a15a505e360702b79c75a5f67773072ed392f52a@bob.xmpp.org + + + http://victim.example.com/challenges/speech.mp3?F3A6292C + +"# + .parse() + .unwrap(); + let media = MediaElement::try_from(elem).unwrap(); + assert!(media.width.is_none()); + assert!(media.height.is_none()); + assert_eq!(media.uris.len(), 3); + assert_eq!(media.uris[0].type_, "audio/x-wav"); + assert_eq!( + media.uris[0].uri, + "http://victim.example.com/challenges/speech.wav?F3A6292C" + ); + assert_eq!(media.uris[1].type_, "audio/ogg; codecs=speex"); + assert_eq!( + media.uris[1].uri, + "cid:sha1+a15a505e360702b79c75a5f67773072ed392f52a@bob.xmpp.org" + ); + assert_eq!(media.uris[2].type_, "audio/mpeg"); + assert_eq!( + media.uris[2].uri, + "http://victim.example.com/challenges/speech.mp3?F3A6292C" + ); + } + + #[test] + fn test_xep_ex2() { + let elem: Element = r#" + + [ ... ] + + + + http://www.victim.com/challenges/ocr.jpeg?F3A6292C + + + cid:sha1+f24030b8d91d233bac14777be5ab531ca3b9f102@bob.xmpp.org + + + + [ ... ] +"# + .parse() + .unwrap(); + let form = DataForm::try_from(elem).unwrap(); + assert_eq!(form.fields.len(), 1); + assert_eq!(form.fields[0].var, "ocr"); + assert_eq!(form.fields[0].media[0].width, Some(290)); + assert_eq!(form.fields[0].media[0].height, Some(80)); + assert_eq!(form.fields[0].media[0].uris[0].type_, "image/jpeg"); + assert_eq!( + form.fields[0].media[0].uris[0].uri, + "http://www.victim.com/challenges/ocr.jpeg?F3A6292C" + ); + assert_eq!(form.fields[0].media[0].uris[1].type_, "image/jpeg"); + assert_eq!( + form.fields[0].media[0].uris[1].uri, + "cid:sha1+f24030b8d91d233bac14777be5ab531ca3b9f102@bob.xmpp.org" + ); + } +} diff --git a/xmpp-parsers/src/message.rs b/xmpp-parsers/src/message.rs new file mode 100644 index 0000000000000000000000000000000000000000..2c7eb29946abca98cfa5926ab459a109c78e9a78 --- /dev/null +++ b/xmpp-parsers/src/message.rs @@ -0,0 +1,418 @@ +// 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 crate::util::error::Error; +use crate::ns; +use jid::Jid; +use crate::Element; +use std::collections::BTreeMap; +use std::convert::TryFrom; + +/// Should be implemented on every known payload of a ``. +pub trait MessagePayload: TryFrom + Into {} + +generate_attribute!( + /// The type of a message. + MessageType, "type", { + /// Standard instant messaging message. + Chat => "chat", + + /// Notifies that an error happened. + Error => "error", + + /// Standard group instant messaging message. + Groupchat => "groupchat", + + /// Used by servers to notify users when things happen. + Headline => "headline", + + /// This is an email-like message, it usually contains a + /// [subject](struct.Subject.html). + Normal => "normal", + }, Default = Normal +); + +type Lang = String; + +generate_elem_id!( + /// Represents one `` element, that is the free form text content of + /// a message. + Body, + "body", + DEFAULT_NS +); + +generate_elem_id!( + /// Defines the subject of a room, or of an email-like normal message. + Subject, + "subject", + DEFAULT_NS +); + +generate_elem_id!( + /// A thread identifier, so that other people can specify to which message + /// they are replying. + Thread, + "thread", + DEFAULT_NS +); + +/// The main structure representing the `` stanza. +#[derive(Debug, Clone)] +pub struct Message { + /// The JID emitting this stanza. + pub from: Option, + + /// The recipient of this stanza. + pub to: Option, + + /// The @id attribute of this stanza, which is required in order to match a + /// request with its response. + pub id: Option, + + /// The type of this message. + pub type_: MessageType, + + /// A list of bodies, sorted per language. Use + /// [get_best_body()](#method.get_best_body) to access them on reception. + pub bodies: BTreeMap, + + /// A list of subjects, sorted per language. Use + /// [get_best_subject()](#method.get_best_subject) to access them on + /// reception. + pub subjects: BTreeMap, + + /// An optional thread identifier, so that other people can reply directly + /// to this message. + pub thread: Option, + + /// A list of the extension payloads contained in this stanza. + pub payloads: Vec, +} + +impl Message { + /// Creates a new `` stanza for the given recipient. + pub fn new(to: Option) -> Message { + Message { + from: None, + to, + id: None, + type_: MessageType::Chat, + bodies: BTreeMap::new(), + subjects: BTreeMap::new(), + thread: None, + payloads: vec![], + } + } + + fn get_best<'a, T>( + map: &'a BTreeMap, + preferred_langs: Vec<&str>, + ) -> Option<(Lang, &'a T)> { + if map.is_empty() { + return None; + } + for lang in preferred_langs { + if let Some(value) = map.get(lang) { + return Some((Lang::from(lang), value)); + } + } + if let Some(value) = map.get("") { + return Some((Lang::new(), value)); + } + map.iter().map(|(lang, value)| (lang.clone(), value)).next() + } + + /// Returns the best matching body from a list of languages. + /// + /// For instance, if a message contains both an xml:lang='de', an xml:lang='fr' and an English + /// body without an xml:lang attribute, and you pass ["fr", "en"] as your preferred languages, + /// `Some(("fr", the_second_body))` will be returned. + /// + /// If no body matches, an undefined body will be returned. + pub fn get_best_body(&self, preferred_langs: Vec<&str>) -> Option<(Lang, &Body)> { + Message::get_best::(&self.bodies, preferred_langs) + } + + /// Returns the best matching subject from a list of languages. + /// + /// For instance, if a message contains both an xml:lang='de', an xml:lang='fr' and an English + /// subject without an xml:lang attribute, and you pass ["fr", "en"] as your preferred + /// languages, `Some(("fr", the_second_subject))` will be returned. + /// + /// If no subject matches, an undefined subject will be returned. + pub fn get_best_subject(&self, preferred_langs: Vec<&str>) -> Option<(Lang, &Subject)> { + Message::get_best::(&self.subjects, preferred_langs) + } +} + +impl TryFrom for Message { + type Error = Error; + + fn try_from(root: Element) -> Result { + check_self!(root, "message", DEFAULT_NS); + let from = get_attr!(root, "from", Option); + let to = get_attr!(root, "to", Option); + let id = get_attr!(root, "id", Option); + let type_ = get_attr!(root, "type", Default); + let mut bodies = BTreeMap::new(); + let mut subjects = BTreeMap::new(); + let mut thread = None; + let mut payloads = vec![]; + for elem in root.children() { + if elem.is("body", ns::DEFAULT_NS) { + check_no_children!(elem, "body"); + let lang = get_attr!(elem, "xml:lang", Default); + let body = Body(elem.text()); + if bodies.insert(lang, body).is_some() { + return Err(Error::ParseError( + "Body element present twice for the same xml:lang.", + )); + } + } else if elem.is("subject", ns::DEFAULT_NS) { + check_no_children!(elem, "subject"); + let lang = get_attr!(elem, "xml:lang", Default); + let subject = Subject(elem.text()); + if subjects.insert(lang, subject).is_some() { + return Err(Error::ParseError( + "Subject element present twice for the same xml:lang.", + )); + } + } else if elem.is("thread", ns::DEFAULT_NS) { + if thread.is_some() { + return Err(Error::ParseError("Thread element present twice.")); + } + check_no_children!(elem, "thread"); + thread = Some(Thread(elem.text())); + } else { + payloads.push(elem.clone()) + } + } + Ok(Message { + from, + to, + id, + type_, + bodies, + subjects, + thread, + payloads, + }) + } +} + +impl From for Element { + fn from(message: Message) -> Element { + Element::builder("message") + .ns(ns::DEFAULT_NS) + .attr("from", message.from) + .attr("to", message.to) + .attr("id", message.id) + .attr("type", message.type_) + .append_all( + message + .subjects + .into_iter() + .map(|(lang, subject)| { + let mut subject = Element::from(subject); + subject.set_attr( + "xml:lang", + match lang.as_ref() { + "" => None, + lang => Some(lang), + }, + ); + subject + }) + ) + .append_all( + message + .bodies + .into_iter() + .map(|(lang, body)| { + let mut body = Element::from(body); + body.set_attr( + "xml:lang", + match lang.as_ref() { + "" => None, + lang => Some(lang), + }, + ); + body + }) + ) + .append_all(message.payloads.into_iter()) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::compare_elements::NamespaceAwareCompare; + use std::str::FromStr; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(MessageType, 1); + assert_size!(Body, 12); + assert_size!(Subject, 12); + assert_size!(Thread, 12); + assert_size!(Message, 144); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(MessageType, 1); + assert_size!(Body, 24); + assert_size!(Subject, 24); + assert_size!(Thread, 24); + assert_size!(Message, 288); + } + + #[test] + fn test_simple() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let message = Message::try_from(elem).unwrap(); + assert_eq!(message.from, None); + assert_eq!(message.to, None); + assert_eq!(message.id, None); + assert_eq!(message.type_, MessageType::Normal); + assert!(message.payloads.is_empty()); + } + + #[test] + fn test_serialise() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let mut message = Message::new(None); + message.type_ = MessageType::Normal; + let elem2 = message.into(); + assert_eq!(elem, elem2); + } + + #[test] + fn test_body() { + #[cfg(not(feature = "component"))] + let elem: Element = "Hello world!".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "Hello world!".parse().unwrap(); + let elem1 = elem.clone(); + let message = Message::try_from(elem).unwrap(); + assert_eq!(message.bodies[""], Body::from_str("Hello world!").unwrap()); + + { + let (lang, body) = message.get_best_body(vec!["en"]).unwrap(); + assert_eq!(lang, ""); + assert_eq!(body, &Body::from_str("Hello world!").unwrap()); + } + + let elem2 = message.into(); + assert!(elem1.compare_to(&elem2)); + } + + #[test] + fn test_serialise_body() { + #[cfg(not(feature = "component"))] + let elem: Element = "Hello world!".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "Hello world!".parse().unwrap(); + let mut message = Message::new(Some(Jid::from_str("coucou@example.org").unwrap())); + message + .bodies + .insert(String::from(""), Body::from_str("Hello world!").unwrap()); + let elem2 = message.into(); + assert!(elem.compare_to(&elem2)); + } + + #[test] + fn test_subject() { + #[cfg(not(feature = "component"))] + let elem: Element = "Hello world!".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "Hello world!".parse().unwrap(); + let elem1 = elem.clone(); + let message = Message::try_from(elem).unwrap(); + assert_eq!( + message.subjects[""], + Subject::from_str("Hello world!").unwrap() + ); + + { + let (lang, subject) = message.get_best_subject(vec!["en"]).unwrap(); + assert_eq!(lang, ""); + assert_eq!(subject, &Subject::from_str("Hello world!").unwrap()); + } + + let elem2 = message.into(); + assert!(elem1.compare_to(&elem2)); + } + + #[test] + fn get_best_body() { + #[cfg(not(feature = "component"))] + let elem: Element = "Hallo Welt!Salut le monde !Hello world!".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "Hello world!".parse().unwrap(); + let message = Message::try_from(elem).unwrap(); + + // Tests basic feature. + { + let (lang, body) = message.get_best_body(vec!["fr"]).unwrap(); + assert_eq!(lang, "fr"); + assert_eq!(body, &Body::from_str("Salut le monde !").unwrap()); + } + + // Tests order. + { + let (lang, body) = message.get_best_body(vec!["en", "de"]).unwrap(); + assert_eq!(lang, "de"); + assert_eq!(body, &Body::from_str("Hallo Welt!").unwrap()); + } + + // Tests fallback. + { + let (lang, body) = message.get_best_body(vec![]).unwrap(); + assert_eq!(lang, ""); + assert_eq!(body, &Body::from_str("Hello world!").unwrap()); + } + + // Tests fallback. + { + let (lang, body) = message.get_best_body(vec!["ja"]).unwrap(); + assert_eq!(lang, ""); + assert_eq!(body, &Body::from_str("Hello world!").unwrap()); + } + + let message = Message::new(None); + + // Tests without a body. + assert_eq!(message.get_best_body(vec!("ja")), None); + } + + #[test] + fn test_attention() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "".parse().unwrap(); + let elem1 = elem.clone(); + let message = Message::try_from(elem).unwrap(); + let elem2 = message.into(); + assert_eq!(elem1, elem2); + } +} diff --git a/xmpp-parsers/src/message_correct.rs b/xmpp-parsers/src/message_correct.rs new file mode 100644 index 0000000000000000000000000000000000000000..374900f41304c35e879e6d5980a9aa767d20fedf --- /dev/null +++ b/xmpp-parsers/src/message_correct.rs @@ -0,0 +1,99 @@ +// 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 crate::message::MessagePayload; + +generate_element!( + /// Defines that the message containing this payload should replace a + /// previous message, identified by the id. + Replace, "replace", MESSAGE_CORRECT, + attributes: [ + /// The 'id' attribute of the message getting corrected. + id: Required = "id", + ] +); + +impl MessagePayload for Replace {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Replace, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Replace, 24); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + Replace::try_from(elem).unwrap(); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_attribute() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Replace::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in replace element."); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Replace::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in replace element."); + } + + #[test] + fn test_invalid_id() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Replace::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'id' missing."); + } + + #[test] + fn test_serialise() { + let elem: Element = "" + .parse() + .unwrap(); + let replace = Replace { + id: String::from("coucou"), + }; + let elem2 = replace.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/mood.rs b/xmpp-parsers/src/mood.rs new file mode 100644 index 0000000000000000000000000000000000000000..4270c5889642bbce1c2f2b631175615a38e14586 --- /dev/null +++ b/xmpp-parsers/src/mood.rs @@ -0,0 +1,312 @@ +// 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/. + +generate_element_enum!( + /// Enum representing all of the possible values of the XEP-0107 moods. + MoodEnum, "mood", MOOD, { + /// Impressed with fear or apprehension; in fear; apprehensive. + Afraid => "afraid", + + /// Astonished; confounded with fear, surprise or wonder. + Amazed => "amazed", + + /// Inclined to love; having a propensity to love, or to sexual enjoyment; loving, fond, affectionate, passionate, lustful, sexual, etc. + Amorous => "amorous", + + /// Displaying or feeling anger, i.e., a strong feeling of displeasure, hostility or antagonism towards someone or something, usually combined with an urge to harm. + Angry => "angry", + + /// To be disturbed or irritated, especially by continued or repeated acts. + Annoyed => "annoyed", + + /// Full of anxiety or disquietude; greatly concerned or solicitous, esp. respecting something future or unknown; being in painful suspense. + Anxious => "anxious", + + /// To be stimulated in one's feelings, especially to be sexually stimulated. + Aroused => "aroused", + + /// Feeling shame or guilt. + Ashamed => "ashamed", + + /// Suffering from boredom; uninterested, without attention. + Bored => "bored", + + /// Strong in the face of fear; courageous. + Brave => "brave", + + /// Peaceful, quiet. + Calm => "calm", + + /// Taking care or caution; tentative. + Cautious => "cautious", + + /// Feeling the sensation of coldness, especially to the point of discomfort. + Cold => "cold", + + /// Feeling very sure of or positive about something, especially about one's own capabilities. + Confident => "confident", + + /// Chaotic, jumbled or muddled. + Confused => "confused", + + /// Feeling introspective or thoughtful. + Contemplative => "contemplative", + + /// Pleased at the satisfaction of a want or desire; satisfied. + Contented => "contented", + + /// Grouchy, irritable; easily upset. + Cranky => "cranky", + + /// Feeling out of control; feeling overly excited or enthusiastic. + Crazy => "crazy", + + /// Feeling original, expressive, or imaginative. + Creative => "creative", + + /// Inquisitive; tending to ask questions, investigate, or explore. + Curious => "curious", + + /// Feeling sad and dispirited. + Dejected => "dejected", + + /// Severely despondent and unhappy. + Depressed => "depressed", + + /// Defeated of expectation or hope; let down. + Disappointed => "disappointed", + + /// Filled with disgust; irritated and out of patience. + Disgusted => "disgusted", + + /// Feeling a sudden or complete loss of courage in the face of trouble or danger. + Dismayed => "dismayed", + + /// Having one's attention diverted; preoccupied. + Distracted => "distracted", + + /// Having a feeling of shameful discomfort. + Embarrassed => "embarrassed", + + /// Feeling pain by the excellence or good fortune of another. + Envious => "envious", + + /// Having great enthusiasm. + Excited => "excited", + + /// In the mood for flirting. + Flirtatious => "flirtatious", + + /// Suffering from frustration; dissatisfied, agitated, or discontented because one is unable to perform an action or fulfill a desire. + Frustrated => "frustrated", + + /// Feeling appreciation or thanks. + Grateful => "grateful", + + /// Feeling very sad about something, especially something lost; mournful; sorrowful. + Grieving => "grieving", + + /// Unhappy and irritable. + Grumpy => "grumpy", + + /// Feeling responsible for wrongdoing; feeling blameworthy. + Guilty => "guilty", + + /// Experiencing the effect of favourable fortune; having the feeling arising from the consciousness of well-being or of enjoyment; enjoying good of any kind, as peace, tranquillity, comfort; contented; joyous. + Happy => "happy", + + /// Having a positive feeling, belief, or expectation that something wished for can or will happen. + Hopeful => "hopeful", + + /// Feeling the sensation of heat, especially to the point of discomfort. + Hot => "hot", + + /// Having or showing a modest or low estimate of one's own importance; feeling lowered in dignity or importance. + Humbled => "humbled", + + /// Feeling deprived of dignity or self-respect. + Humiliated => "humiliated", + + /// Having a physical need for food. + Hungry => "hungry", + + /// Wounded, injured, or pained, whether physically or emotionally. + Hurt => "hurt", + + /// Favourably affected by something or someone. + Impressed => "impressed", + + /// Feeling amazement at something or someone; or feeling a combination of fear and reverence. + InAwe => "in_awe", + + /// Feeling strong affection, care, liking, or attraction.. + InLove => "in_love", + + /// Showing anger or indignation, especially at something unjust or wrong. + Indignant => "indignant", + + /// Showing great attention to something or someone; having or showing interest. + Interested => "interested", + + /// Under the influence of alcohol; drunk. + Intoxicated => "intoxicated", + + /// Feeling as if one cannot be defeated, overcome or denied. + Invincible => "invincible", + + /// Fearful of being replaced in position or affection. + Jealous => "jealous", + + /// Feeling isolated, empty, or abandoned. + Lonely => "lonely", + + /// Unable to find one's way, either physically or emotionally. + Lost => "lost", + + /// Feeling as if one will be favored by luck. + Lucky => "lucky", + + /// Causing or intending to cause intentional harm; bearing ill will towards another; cruel; malicious. + Mean => "mean", + + /// Given to sudden or frequent changes of mind or feeling; temperamental. + Moody => "moody", + + /// Easily agitated or alarmed; apprehensive or anxious. + Nervous => "nervous", + + /// Not having a strong mood or emotional state. + Neutral => "neutral", + + /// Feeling emotionally hurt, displeased, or insulted. + Offended => "offended", + + /// Feeling resentful anger caused by an extremely violent or vicious attack, or by an offensive, immoral, or indecent act. + Outraged => "outraged", + + /// Interested in play; fun, recreational, unserious, lighthearted; joking, silly. + Playful => "playful", + + /// Feeling a sense of one's own worth or accomplishment. + Proud => "proud", + + /// Having an easy-going mood; not stressed; calm. + Relaxed => "relaxed", + + /// Feeling uplifted because of the removal of stress or discomfort. + Relieved => "relieved", + + /// Feeling regret or sadness for doing something wrong. + Remorseful => "remorseful", + + /// Without rest; unable to be still or quiet; uneasy; continually moving. + Restless => "restless", + + /// Feeling sorrow; sorrowful, mournful. + Sad => "sad", + + /// Mocking and ironical. + Sarcastic => "sarcastic", + + /// Pleased at the fulfillment of a need or desire. + Satisfied => "satisfied", + + /// Without humor or expression of happiness; grave in manner or disposition; earnest; thoughtful; solemn. + Serious => "serious", + + /// Surprised, startled, confused, or taken aback. + Shocked => "shocked", + + /// Feeling easily frightened or scared; timid; reserved or coy. + Shy => "shy", + + /// Feeling in poor health; ill. + Sick => "sick", + + /// Feeling the need for sleep. + Sleepy => "sleepy", + + /// Acting without planning; natural; impulsive. + Spontaneous => "spontaneous", + + /// Suffering emotional pressure. + Stressed => "stressed", + + /// Capable of producing great physical force; or, emotionally forceful, able, determined, unyielding. + Strong => "strong", + + /// Experiencing a feeling caused by something unexpected. + Surprised => "surprised", + + /// Showing appreciation or gratitude. + Thankful => "thankful", + + /// Feeling the need to drink. + Thirsty => "thirsty", + + /// In need of rest or sleep. + Tired => "tired", + + /// [Feeling any emotion not defined here.] + Undefined => "undefined", + + /// Lacking in force or ability, either physical or emotional. + Weak => "weak", + + /// Thinking about unpleasant things that have happened or that might happen; feeling afraid and unhappy. + Worried => "worried", + } +); + +generate_elem_id!( + /// Free-form text description of the mood. + Text, + "text", + MOOD +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(MoodEnum, 1); + assert_size!(Text, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(MoodEnum, 1); + assert_size!(Text, 24); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let mood = MoodEnum::try_from(elem).unwrap(); + assert_eq!(mood, MoodEnum::Happy); + } + + #[test] + fn test_text() { + let elem: Element = "Yay!" + .parse() + .unwrap(); + let elem2 = elem.clone(); + let text = Text::try_from(elem).unwrap(); + assert_eq!(text.0, String::from("Yay!")); + + let elem3 = text.into(); + assert_eq!(elem2, elem3); + } +} diff --git a/xmpp-parsers/src/muc/mod.rs b/xmpp-parsers/src/muc/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..5875e3bab750c79f8dc9e48e8e4c6f8de4ef79f0 --- /dev/null +++ b/xmpp-parsers/src/muc/mod.rs @@ -0,0 +1,14 @@ +// Copyright (c) 2017 Maxime “pep” Buquet +// +// 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/. + +/// The http://jabber.org/protocol/muc protocol. +pub mod muc; + +/// The http://jabber.org/protocol/muc#user protocol. +pub mod user; + +pub use self::muc::Muc; +pub use self::user::MucUser; diff --git a/xmpp-parsers/src/muc/muc.rs b/xmpp-parsers/src/muc/muc.rs new file mode 100644 index 0000000000000000000000000000000000000000..01688ec3623c4a073a8482fcd7592e520695c3fd --- /dev/null +++ b/xmpp-parsers/src/muc/muc.rs @@ -0,0 +1,197 @@ +// Copyright (c) 2017 Maxime “pep” Buquet +// 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 crate::date::DateTime; +use crate::presence::PresencePayload; + +generate_element!( + /// Represents the query for messages before our join. + #[derive(PartialEq, Default)] + History, "history", MUC, + attributes: [ + /// How many characters of history to send, in XML characters. + maxchars: Option = "maxchars", + + /// How many messages to send. + maxstanzas: Option = "maxstanzas", + + /// Only send messages received in these last seconds. + seconds: Option = "seconds", + + /// Only send messages after this date. + since: Option = "since", + ] +); + +impl History { + /// Create a new empty history element. + pub fn new() -> Self { + History::default() + } + + /// Set how many characters of history to send. + pub fn with_maxchars(mut self, maxchars: u32) -> Self { + self.maxchars = Some(maxchars); + self + } + + /// Set how many messages to send. + pub fn with_maxstanzas(mut self, maxstanzas: u32) -> Self { + self.maxstanzas = Some(maxstanzas); + self + } + + /// Only send messages received in these last seconds. + pub fn with_seconds(mut self, seconds: u32) -> Self { + self.seconds = Some(seconds); + self + } + + /// Only send messages received since this date. + pub fn with_since(mut self, since: DateTime) -> Self { + self.since = Some(since); + self + } +} + +generate_element!( + /// Represents a room join request. + #[derive(PartialEq, Default)] + Muc, "x", MUC, children: [ + /// Password to use when the room is protected by a password. + password: Option = ("password", MUC) => String, + + /// Controls how much and how old we want to receive history on join. + history: Option = ("history", MUC) => History + ] +); + +impl PresencePayload for Muc {} + +impl Muc { + /// Create a new MUC join element. + pub fn new() -> Self { + Muc::default() + } + + /// Join a room with this password. + pub fn with_password(mut self, password: String) -> Self { + self.password = Some(password); + self + } + + /// Join a room with only that much history. + pub fn with_history(mut self, history: History) -> Self { + self.history = Some(history); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::compare_elements::NamespaceAwareCompare; + use crate::util::error::Error; + use crate::Element; + use std::str::FromStr; + use std::convert::TryFrom; + + #[test] + fn test_muc_simple() { + let elem: Element = "" + .parse() + .unwrap(); + Muc::try_from(elem).unwrap(); + } + + #[test] + fn test_muc_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Muc::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in x element."); + } + + #[test] + fn test_muc_serialise() { + let elem: Element = "" + .parse() + .unwrap(); + let muc = Muc { + password: None, + history: None, + }; + let elem2 = muc.into(); + assert_eq!(elem, elem2); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_muc_invalid_attribute() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Muc::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in x element."); + } + + #[test] + fn test_muc_simple_password() { + let elem: Element = " + + coucou + " + .parse() + .unwrap(); + let elem1 = elem.clone(); + let muc = Muc::try_from(elem).unwrap(); + assert_eq!(muc.password, Some("coucou".to_owned())); + + let elem2 = Element::from(muc); + assert!(elem1.compare_to(&elem2)); + } + + #[test] + fn history() { + let elem: Element = " + + + " + .parse() + .unwrap(); + let muc = Muc::try_from(elem).unwrap(); + let muc2 = Muc::new().with_history(History::new().with_maxstanzas(0)); + assert_eq!(muc, muc2); + + let history = muc.history.unwrap(); + assert_eq!(history.maxstanzas, Some(0)); + assert_eq!(history.maxchars, None); + assert_eq!(history.seconds, None); + assert_eq!(history.since, None); + + let elem: Element = " + + + " + .parse() + .unwrap(); + let muc = Muc::try_from(elem).unwrap(); + assert_eq!( + muc.history.unwrap().since.unwrap(), + DateTime::from_str("1970-01-01T00:00:00+00:00").unwrap() + ); + } +} diff --git a/xmpp-parsers/src/muc/user.rs b/xmpp-parsers/src/muc/user.rs new file mode 100644 index 0000000000000000000000000000000000000000..31157f7681d7b367f98336d24ec11344cb27e93d --- /dev/null +++ b/xmpp-parsers/src/muc/user.rs @@ -0,0 +1,693 @@ +// Copyright (c) 2017 Maxime “pep” Buquet +// 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 crate::util::error::Error; +use crate::ns; +use jid::FullJid; +use crate::Element; +use std::convert::TryFrom; + +generate_attribute_enum!( +/// Lists all of the possible status codes used in MUC presences. +Status, "status", MUC_USER, "code", { + /// Inform user that any occupant is allowed to see the user's full JID + NonAnonymousRoom => 100, + + /// Inform user that his or her affiliation changed while not in the room + AffiliationChange => 101, + + /// Inform occupants that room now shows unavailable members + ConfigShowsUnavailableMembers => 102, + + /// Inform occupants that room now does not show unavailable members + ConfigHidesUnavailableMembers => 103, + + /// Inform occupants that a non-privacy-related room configuration change has occurred + ConfigNonPrivacyRelated => 104, + + /// Inform user that presence refers to itself + SelfPresence => 110, + + /// Inform occupants that room logging is now enabled + ConfigRoomLoggingEnabled => 170, + + /// Inform occupants that room logging is now disabled + ConfigRoomLoggingDisabled => 171, + + /// Inform occupants that the room is now non-anonymous + ConfigRoomNonAnonymous => 172, + + /// Inform occupants that the room is now semi-anonymous + ConfigRoomSemiAnonymous => 173, + + /// Inform user that a new room has been created + RoomHasBeenCreated => 201, + + /// Inform user that service has assigned or modified occupant's roomnick + AssignedNick => 210, + + /// Inform user that he or she has been banned from the room + Banned => 301, + + /// Inform all occupants of new room nickname + NewNick => 303, + + /// Inform user that he or she has been kicked from the room + Kicked => 307, + + /// Inform user that he or she is being removed from the room + /// because of an affiliation change + RemovalFromRoom => 321, + + /// Inform user that he or she is being removed from the room + /// because the room has been changed to members-only and the + /// user is not a member + ConfigMembersOnly => 322, + + /// Inform user that he or she is being removed from the room + /// because the MUC service is being shut down + ServiceShutdown => 332, +}); + +/// Optional element used in elements inside presence stanzas of type +/// "unavailable" that are sent to users who are kick or banned, as well as within IQs for tracking +/// purposes. -- CHANGELOG 0.17 (2002-10-23) +/// +/// Possesses a 'jid' and a 'nick' attribute, so that an action can be attributed either to a real +/// JID or to a roomnick. -- CHANGELOG 1.25 (2012-02-08) +#[derive(Debug, Clone, PartialEq)] +pub enum Actor { + /// The full JID associated with this user. + Jid(FullJid), + + /// The nickname of this user. + Nick(String), +} + +impl TryFrom for Actor { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "actor", MUC_USER); + check_no_unknown_attributes!(elem, "actor", ["jid", "nick"]); + check_no_children!(elem, "actor"); + let jid: Option = get_attr!(elem, "jid", Option); + let nick = get_attr!(elem, "nick", Option); + + match (jid, nick) { + (Some(_), Some(_)) | (None, None) => { + Err(Error::ParseError( + "Either 'jid' or 'nick' attribute is required.", + )) + } + (Some(jid), _) => Ok(Actor::Jid(jid)), + (_, Some(nick)) => Ok(Actor::Nick(nick)), + } + } +} + +impl From for Element { + fn from(actor: Actor) -> Element { + let elem = Element::builder("actor").ns(ns::MUC_USER); + + (match actor { + Actor::Jid(jid) => elem.attr("jid", jid), + Actor::Nick(nick) => elem.attr("nick", nick), + }) + .build() + } +} + +generate_element!( + /// Used to continue a one-to-one discussion in a room, with more than one + /// participant. + Continue, "continue", MUC_USER, + attributes: [ + /// The thread to continue in this room. + thread: Option = "thread", + ] +); + +generate_elem_id!( + /// A reason for inviting, declining, etc. a request. + Reason, + "reason", + MUC_USER +); + +generate_attribute!( + /// The affiliation of an entity with a room, which isn’t tied to its + /// presence in it. + Affiliation, "affiliation", { + /// The user who created the room, or who got appointed by its creator + /// to be their equal. + Owner => "owner", + + /// A user who has been empowered by an owner to do administrative + /// operations. + Admin => "admin", + + /// A user who is whitelisted to speak in moderated rooms, or to join a + /// member-only room. + Member => "member", + + /// A user who has been banned from this room. + Outcast => "outcast", + + /// A normal participant. + None => "none", + }, Default = None +); + +generate_attribute!( + /// The current role of an entity in a room, it can be changed by an owner + /// or an administrator but will be lost once they leave the room. + Role, "role", { + /// This user can kick other participants, as well as grant and revoke + /// them voice. + Moderator => "moderator", + + /// A user who can speak in this room. + Participant => "participant", + + /// A user who cannot speak in this room, and must request voice before + /// doing so. + Visitor => "visitor", + + /// A user who is absent from the room. + None => "none", + }, Default = None +); + +generate_element!( + /// An item representing a user in a room. + Item, "item", MUC_USER, attributes: [ + /// The affiliation of this user with the room. + affiliation: Required = "affiliation", + + /// The real JID of this user, if you are allowed to see it. + jid: Option = "jid", + + /// The current nickname of this user. + nick: Option = "nick", + + /// The current role of this user. + role: Required = "role", + ], children: [ + /// The actor affected by this item. + actor: Option = ("actor", MUC_USER) => Actor, + + /// Whether this continues a one-to-one discussion. + continue_: Option = ("continue", MUC_USER) => Continue, + + /// A reason for this item. + reason: Option = ("reason", MUC_USER) => Reason + ] +); + +impl Item { + /// Creates a new item with the given affiliation and role. + pub fn new(affiliation: Affiliation, role: Role) -> Item { + Item { + affiliation, + role, + jid: None, + nick: None, + actor: None, + continue_: None, + reason: None, + } + } +} + +generate_element!( + /// The main muc#user element. + MucUser, "x", MUC_USER, children: [ + /// List of statuses applying to this item. + status: Vec = ("status", MUC_USER) => Status, + + /// List of items. + items: Vec = ("item", MUC_USER) => Item + ] +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::compare_elements::NamespaceAwareCompare; + use std::error::Error as StdError; + + #[test] + fn test_simple() { + let elem: Element = " + + " + .parse() + .unwrap(); + MucUser::try_from(elem).unwrap(); + } + + #[test] + fn statuses_and_items() { + let elem: Element = " + + + + + + " + .parse() + .unwrap(); + let muc_user = MucUser::try_from(elem).unwrap(); + assert_eq!(muc_user.status.len(), 2); + assert_eq!(muc_user.status[0], Status::AffiliationChange); + assert_eq!(muc_user.status[1], Status::ConfigShowsUnavailableMembers); + assert_eq!(muc_user.items.len(), 1); + assert_eq!(muc_user.items[0].affiliation, Affiliation::Member); + assert_eq!(muc_user.items[0].role, Role::Moderator); + } + + #[test] + fn test_invalid_child() { + let elem: Element = " + + + + " + .parse() + .unwrap(); + let error = MucUser::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in x element."); + } + + #[test] + fn test_serialise() { + let elem: Element = " + + " + .parse() + .unwrap(); + let muc = MucUser { + status: vec![], + items: vec![], + }; + let elem2 = muc.into(); + assert!(elem.compare_to(&elem2)); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_attribute() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = MucUser::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in x element."); + } + + #[test] + fn test_status_simple() { + let elem: Element = " + + " + .parse() + .unwrap(); + Status::try_from(elem).unwrap(); + } + + #[test] + fn test_status_invalid() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Status::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'code' missing."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_status_invalid_child() { + let elem: Element = " + + + + " + .parse() + .unwrap(); + let error = Status::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in status element."); + } + + #[test] + fn test_status_simple_code() { + let elem: Element = " + + " + .parse() + .unwrap(); + let status = Status::try_from(elem).unwrap(); + assert_eq!(status, Status::Kicked); + } + + #[test] + fn test_status_invalid_code() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Status::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Invalid status code value."); + } + + #[test] + fn test_status_invalid_code2() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Status::try_from(elem).unwrap_err(); + let error = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!(error.description(), "invalid digit found in string"); + } + + #[test] + fn test_actor_required_attributes() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Actor::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Either 'jid' or 'nick' attribute is required."); + } + + #[test] + fn test_actor_required_attributes2() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Actor::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Either 'jid' or 'nick' attribute is required."); + } + + #[test] + fn test_actor_jid() { + let elem: Element = " + + " + .parse() + .unwrap(); + let actor = Actor::try_from(elem).unwrap(); + let jid = match actor { + Actor::Jid(jid) => jid, + _ => panic!(), + }; + assert_eq!(jid, "foo@bar/baz".parse::().unwrap()); + } + + #[test] + fn test_actor_nick() { + let elem: Element = " + + " + .parse() + .unwrap(); + let actor = Actor::try_from(elem).unwrap(); + let nick = match actor { + Actor::Nick(nick) => nick, + _ => panic!(), + }; + assert_eq!(nick, "baz".to_owned()); + } + + #[test] + fn test_continue_simple() { + let elem: Element = " + + " + .parse() + .unwrap(); + Continue::try_from(elem).unwrap(); + } + + #[test] + fn test_continue_thread_attribute() { + let elem: Element = " + + " + .parse() + .unwrap(); + let continue_ = Continue::try_from(elem).unwrap(); + assert_eq!(continue_.thread, Some("foo".to_owned())); + } + + #[test] + fn test_continue_invalid() { + let elem: Element = " + + + + " + .parse() + .unwrap(); + let continue_ = Continue::try_from(elem).unwrap_err(); + let message = match continue_ { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in continue element.".to_owned()); + } + + #[test] + fn test_reason_simple() { + let elem: Element = " + Reason" + .parse() + .unwrap(); + let elem2 = elem.clone(); + let reason = Reason::try_from(elem).unwrap(); + assert_eq!(reason.0, "Reason".to_owned()); + + let elem3 = reason.into(); + assert_eq!(elem2, elem3); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_reason_invalid_attribute() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Reason::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in reason element.".to_owned()); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_reason_invalid() { + let elem: Element = " + + + + " + .parse() + .unwrap(); + let error = Reason::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in reason element.".to_owned()); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_item_invalid_attr() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Item::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in item element.".to_owned()); + } + + #[test] + fn test_item_affiliation_role_attr() { + let elem: Element = " + + " + .parse() + .unwrap(); + Item::try_from(elem).unwrap(); + } + + #[test] + fn test_item_affiliation_role_invalid_attr() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Item::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'role' missing.".to_owned()); + } + + #[test] + fn test_item_nick_attr() { + let elem: Element = " + + " + .parse() + .unwrap(); + let item = Item::try_from(elem).unwrap(); + match item { + Item { nick, .. } => assert_eq!(nick, Some("foobar".to_owned())), + } + } + + #[test] + fn test_item_affiliation_role_invalid_attr2() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Item::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!( + message, + "Required attribute 'affiliation' missing.".to_owned() + ); + } + + #[test] + fn test_item_role_actor_child() { + let elem: Element = " + + + + " + .parse() + .unwrap(); + let item = Item::try_from(elem).unwrap(); + match item { + Item { actor, .. } => assert_eq!(actor, Some(Actor::Nick("foobar".to_owned()))), + } + } + + #[test] + fn test_item_role_continue_child() { + let elem: Element = " + + + + " + .parse() + .unwrap(); + let item = Item::try_from(elem).unwrap(); + let continue_1 = Continue { + thread: Some("foobar".to_owned()), + }; + match item { + Item { + continue_: Some(continue_2), + .. + } => assert_eq!(continue_2.thread, continue_1.thread), + _ => panic!(), + } + } + + #[test] + fn test_item_role_reason_child() { + let elem: Element = " + + foobar + + " + .parse() + .unwrap(); + let item = Item::try_from(elem).unwrap(); + match item { + Item { reason, .. } => assert_eq!(reason, Some(Reason("foobar".to_owned()))), + } + } +} diff --git a/xmpp-parsers/src/nick.rs b/xmpp-parsers/src/nick.rs new file mode 100644 index 0000000000000000000000000000000000000000..20ae7a94c9cf03eb34751f4b87c2647af521fe2e --- /dev/null +++ b/xmpp-parsers/src/nick.rs @@ -0,0 +1,79 @@ +// Copyright (c) 2018 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/. + +generate_elem_id!( + /// Represents a global, memorable, friendly or informal name chosen by a user. + Nick, + "nick", + NICK +); + +#[cfg(test)] +mod tests { + use super::*; + #[cfg(not(feature = "disable-validation"))] + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Nick, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Nick, 24); + } + + #[test] + fn test_simple() { + let elem: Element = "Link Mauve" + .parse() + .unwrap(); + let nick = Nick::try_from(elem).unwrap(); + assert_eq!(&nick.0, "Link Mauve"); + } + + #[test] + fn test_serialise() { + let elem1 = Element::from(Nick(String::from("Link Mauve"))); + let elem2: Element = "Link Mauve" + .parse() + .unwrap(); + assert_eq!(elem1, elem2); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Nick::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in nick element."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_attribute() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Nick::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in nick element."); + } +} diff --git a/xmpp-parsers/src/ns.rs b/xmpp-parsers/src/ns.rs new file mode 100644 index 0000000000000000000000000000000000000000..231d78ed4168eaae90989b0feb169289cd335660 --- /dev/null +++ b/xmpp-parsers/src/ns.rs @@ -0,0 +1,235 @@ +// Copyright (c) 2017-2018 Emmanuel Gil Peyrot +// Copyright (c) 2017 Maxime “pep” Buquet +// +// 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/. + +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub const JABBER_CLIENT: &str = "jabber:client"; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub const XMPP_STANZAS: &str = "urn:ietf:params:xml:ns:xmpp-stanzas"; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub const STREAM: &str = "http://etherx.jabber.org/streams"; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub const SASL: &str = "urn:ietf:params:xml:ns:xmpp-sasl"; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub const BIND: &str = "urn:ietf:params:xml:ns:xmpp-bind"; + +/// RFC 6121: Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence +pub const ROSTER: &str = "jabber:iq:roster"; + +/// RFC 7395: An Extensible Messaging and Presence Protocol (XMPP) Subprotocol for WebSocket +pub const WEBSOCKET: &str = "urn:ietf:params:xml:ns:xmpp-framing"; + +/// XEP-0004: Data Forms +pub const DATA_FORMS: &str = "jabber:x:data"; + +/// XEP-0030: Service Discovery +pub const DISCO_INFO: &str = "http://jabber.org/protocol/disco#info"; +/// XEP-0030: Service Discovery +pub const DISCO_ITEMS: &str = "http://jabber.org/protocol/disco#items"; + +/// XEP-0045: Multi-User Chat +pub const MUC: &str = "http://jabber.org/protocol/muc"; +/// XEP-0045: Multi-User Chat +pub const MUC_USER: &str = "http://jabber.org/protocol/muc#user"; + +/// XEP-0047: In-Band Bytestreams +pub const IBB: &str = "http://jabber.org/protocol/ibb"; + +/// XEP-0048: Bookmarks +pub const BOOKMARKS: &str = "storage:bookmarks"; + +/// XEP-0059: Result Set Management +pub const RSM: &str = "http://jabber.org/protocol/rsm"; + +/// XEP-0060: Publish-Subscribe +pub const PUBSUB: &str = "http://jabber.org/protocol/pubsub"; +/// XEP-0060: Publish-Subscribe +pub const PUBSUB_ERRORS: &str = "http://jabber.org/protocol/pubsub#errors"; +/// XEP-0060: Publish-Subscribe +pub const PUBSUB_EVENT: &str = "http://jabber.org/protocol/pubsub#event"; +/// XEP-0060: Publish-Subscribe +pub const PUBSUB_OWNER: &str = "http://jabber.org/protocol/pubsub#owner"; + +/// XEP-0071: XHTML-IM +pub const XHTML_IM: &str = "http://jabber.org/protocol/xhtml-im"; +/// XEP-0071: XHTML-IM +pub const XHTML: &str = "http://www.w3.org/1999/xhtml"; + +/// XEP-0077: In-Band Registration +pub const REGISTER: &str = "jabber:iq:register"; + +/// XEP-0084: User Avatar +pub const AVATAR_DATA: &str = "urn:xmpp:avatar:data"; +/// XEP-0084: User Avatar +pub const AVATAR_METADATA: &str = "urn:xmpp:avatar:metadata"; + +/// XEP-0085: Chat State Notifications +pub const CHATSTATES: &str = "http://jabber.org/protocol/chatstates"; + +/// XEP-0092: Software Version +pub const VERSION: &str = "jabber:iq:version"; + +/// XEP-0107: User Mood +pub const MOOD: &str = "http://jabber.org/protocol/mood"; + +/// XEP-0114: Jabber Component Protocol +pub const COMPONENT_ACCEPT: &str = "jabber:component:accept"; + +/// XEP-0114: Jabber Component Protocol +pub const COMPONENT: &str = "jabber:component:accept"; + +/// XEP-0115: Entity Capabilities +pub const CAPS: &str = "http://jabber.org/protocol/caps"; + +/// XEP-0118: User Tune +pub const TUNE: &str = "http://jabber.org/protocol/tune"; + +/// XEP-0157: Contact Addresses for XMPP Services +pub const SERVER_INFO: &str = "http://jabber.org/network/serverinfo"; + +/// XEP-0166: Jingle +pub const JINGLE: &str = "urn:xmpp:jingle:1"; + +/// XEP-0167: Jingle RTP Sessions +pub const JINGLE_RTP: &str = "urn:xmpp:jingle:apps:rtp:1"; +/// XEP-0167: Jingle RTP Sessions +pub const JINGLE_RTP_AUDIO: &str = "urn:xmpp:jingle:apps:rtp:audio"; +/// XEP-0167: Jingle RTP Sessions +pub const JINGLE_RTP_VIDEO: &str = "urn:xmpp:jingle:apps:rtp:video"; + +/// XEP-0172: User Nickname +pub const NICK: &str = "http://jabber.org/protocol/nick"; + +/// XEP-0176: Jingle ICE-UDP Transport Method +pub const JINGLE_ICE_UDP: &str = "urn:xmpp:jingle:transports:ice-udp:1"; + +/// XEP-0184: Message Delivery Receipts +pub const RECEIPTS: &str = "urn:xmpp:receipts"; + +/// XEP-0191: Blocking Command +pub const BLOCKING: &str = "urn:xmpp:blocking"; +/// XEP-0191: Blocking Command +pub const BLOCKING_ERRORS: &str = "urn:xmpp:blocking:errors"; + +/// XEP-0198: Stream Management +pub const SM: &str = "urn:xmpp:sm:3"; + +/// XEP-0199: XMPP Ping +pub const PING: &str = "urn:xmpp:ping"; + +/// XEP-0202: Entity Time +pub const TIME: &str = "urn:xmpp:time"; + +/// XEP-0203: Delayed Delivery +pub const DELAY: &str = "urn:xmpp:delay"; + +/// XEP-0221: Data Forms Media Element +pub const MEDIA_ELEMENT: &str = "urn:xmpp:media-element"; + +/// XEP-0224: Attention +pub const ATTENTION: &str = "urn:xmpp:attention:0"; + +/// XEP-0231: Bits of Binary +pub const BOB: &str = "urn:xmpp:bob"; + +/// XEP-0234: Jingle File Transfer +pub const JINGLE_FT: &str = "urn:xmpp:jingle:apps:file-transfer:5"; +/// XEP-0234: Jingle File Transfer +pub const JINGLE_FT_ERROR: &str = "urn:xmpp:jingle:apps:file-transfer:errors:0"; + +/// XEP-0257: Client Certificate Management for SASL EXTERNAL +pub const SASL_CERT: &str = "urn:xmpp:saslcert:1"; + +/// XEP-0260: Jingle SOCKS5 Bytestreams Transport Method +pub const JINGLE_S5B: &str = "urn:xmpp:jingle:transports:s5b:1"; + +/// XEP-0261: Jingle In-Band Bytestreams Transport Method +pub const JINGLE_IBB: &str = "urn:xmpp:jingle:transports:ibb:1"; + +/// XEP-0277: Microblogging over XMPP +pub const MICROBLOG: &str = "urn:xmpp:microblog:0"; + +/// XEP-0280: Message Carbons +pub const CARBONS: &str = "urn:xmpp:carbons:2"; + +/// XEP-0293: Jingle RTP Feedback Negotiation +pub const JINGLE_RTCP_FB: &str = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0"; + +/// XEP-0297: Stanza Forwarding +pub const FORWARD: &str = "urn:xmpp:forward:0"; + +/// XEP-0300: Use of Cryptographic Hash Functions in XMPP +pub const HASHES: &str = "urn:xmpp:hashes:2"; +/// XEP-0300: Use of Cryptographic Hash Functions in XMPP +pub const HASH_ALGO_SHA_256: &str = "urn:xmpp:hash-function-text-names:sha-256"; +/// XEP-0300: Use of Cryptographic Hash Functions in XMPP +pub const HASH_ALGO_SHA_512: &str = "urn:xmpp:hash-function-text-names:sha-512"; +/// XEP-0300: Use of Cryptographic Hash Functions in XMPP +pub const HASH_ALGO_SHA3_256: &str = "urn:xmpp:hash-function-text-names:sha3-256"; +/// XEP-0300: Use of Cryptographic Hash Functions in XMPP +pub const HASH_ALGO_SHA3_512: &str = "urn:xmpp:hash-function-text-names:sha3-512"; +/// XEP-0300: Use of Cryptographic Hash Functions in XMPP +pub const HASH_ALGO_BLAKE2B_256: &str = "urn:xmpp:hash-function-text-names:id-blake2b256"; +/// XEP-0300: Use of Cryptographic Hash Functions in XMPP +pub const HASH_ALGO_BLAKE2B_512: &str = "urn:xmpp:hash-function-text-names:id-blake2b512"; + +/// XEP-0308: Last Message Correction +pub const MESSAGE_CORRECT: &str = "urn:xmpp:message-correct:0"; + +/// XEP-0313: Message Archive Management +pub const MAM: &str = "urn:xmpp:mam:2"; + +/// XEP-0319: Last User Interaction in Presence +pub const IDLE: &str = "urn:xmpp:idle:1"; + +/// XEP-0320: Use of DTLS-SRTP in Jingle Sessions +pub const JINGLE_DTLS: &str = "urn:xmpp:jingle:apps:dtls:0"; + +/// XEP-0328: JID Prep +pub const JID_PREP: &str = "urn:xmpp:jidprep:0"; + +/// XEP-0339: Source-Specific Media Attributes in Jingle +pub const JINGLE_SSMA: &str = "urn:xmpp:jingle:apps:rtp:ssma:0"; + +/// XEP-0352: Client State Indication +pub const CSI: &str = "urn:xmpp:csi:0"; + +/// XEP-0353: Jingle Message Initiation +pub const JINGLE_MESSAGE: &str = "urn:xmpp:jingle-message:0"; + +/// XEP-0359: Unique and Stable Stanza IDs +pub const SID: &str = "urn:xmpp:sid:0"; + +/// XEP-0373: OpenPGP for XMPP +pub const OX: &str = "urn:xmpp:openpgp:0"; +/// XEP-0373: OpenPGP for XMPP +pub const OX_PUBKEYS: &str = "urn:xmpp:openpgp:0:public-keys"; + +/// XEP-0380: Explicit Message Encryption +pub const EME: &str = "urn:xmpp:eme:0"; + +/// XEP-0390: Entity Capabilities 2.0 +pub const ECAPS2: &str = "urn:xmpp:caps"; +/// XEP-0390: Entity Capabilities 2.0 +pub const ECAPS2_OPTIMIZE: &str = "urn:xmpp:caps:optimize"; + +/// XEP-0402: Bookmarks 2 (This Time it's Serious) +pub const BOOKMARKS2: &str = "urn:xmpp:bookmarks:0"; +/// XEP-0402: Bookmarks 2 (This Time it's Serious) +pub const BOOKMARKS2_COMPAT: &str = "urn:xmpp:bookmarks:0#compat"; + +/// XEP-0421: Anonymous unique occupant identifiers for MUCs +pub const OID: &str = "urn:xmpp:occupant-id:0"; + +/// Alias for the main namespace of the stream, that is "jabber:client" when +/// the component feature isn’t enabled. +#[cfg(not(feature = "component"))] +pub const DEFAULT_NS: &str = JABBER_CLIENT; + +/// Alias for the main namespace of the stream, that is +/// "jabber:component:accept" when the component feature is enabled. +#[cfg(feature = "component")] +pub const DEFAULT_NS: &str = COMPONENT_ACCEPT; diff --git a/xmpp-parsers/src/occupant_id.rs b/xmpp-parsers/src/occupant_id.rs new file mode 100644 index 0000000000000000000000000000000000000000..418a878bd5729fb44d0d8b266cb32a8083aee383 --- /dev/null +++ b/xmpp-parsers/src/occupant_id.rs @@ -0,0 +1,89 @@ +// Copyright (c) 2019 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 crate::message::MessagePayload; +use crate::presence::PresencePayload; + +generate_element!( + /// Unique identifier given to a MUC participant. + /// + /// It allows clients to identify a MUC participant across reconnects and + /// renames. It thus prevents impersonification of anonymous users. + OccupantId, "occupant-id", OID, + + attributes: [ + /// The id associated to the sending user by the MUC service. + id: Required = "id", + ] +); + +impl MessagePayload for OccupantId {} +impl PresencePayload for OccupantId {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(OccupantId, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(OccupantId, 24); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let origin_id = OccupantId::try_from(elem).unwrap(); + assert_eq!(origin_id.id, "coucou"); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = OccupantId::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in occupant-id element."); + } + + #[test] + fn test_invalid_id() { + let elem: Element = "".parse().unwrap(); + let error = OccupantId::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'id' missing."); + } + + #[test] + fn test_serialise() { + let elem: Element = "" + .parse() + .unwrap(); + let occupant_id = OccupantId { + id: String::from("coucou"), + }; + let elem2 = occupant_id.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/openpgp.rs b/xmpp-parsers/src/openpgp.rs new file mode 100644 index 0000000000000000000000000000000000000000..86f4829a77fa8af1fae6448a3aa27b3635418e6b --- /dev/null +++ b/xmpp-parsers/src/openpgp.rs @@ -0,0 +1,104 @@ +// Copyright (c) 2019 Maxime “pep” Buquet +// +// 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 crate::date::DateTime; +use crate::util::helpers::Base64; +use crate::pubsub::PubSubPayload; + +// TODO: Merge this container with the PubKey struct +generate_element!( + /// Data contained in the PubKey element + PubKeyData, "data", OX, + text: ( + /// Base64 data + data: Base64> + ) +); + +generate_element!( + /// Pubkey element to be used in PubSub publish payloads. + PubKey, "pubkey", OX, + attributes: [ + /// Last updated date + date: Option = "date" + ], + children: [ + /// Public key as base64 data + data: Required = ("data", OX) => PubKeyData + ] +); + +impl PubSubPayload for PubKey {} + +generate_element!( + /// Public key metadata + PubKeyMeta, "pubkey-metadata", OX, + attributes: [ + /// OpenPGP v4 fingerprint + v4fingerprint: Required = "v4-fingerprint", + /// Time the key was published or updated + date: Required = "date", + ] +); + +generate_element!( + /// List of public key metadata + PubKeysMeta, "public-key-list", OX, + children: [ + /// Public keys + pubkeys: Vec = ("pubkey-metadata", OX) => PubKeyMeta + ] +); + +impl PubSubPayload for PubKeysMeta {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ns; + use std::str::FromStr; + use crate::pubsub::{NodeName, Item, pubsub::{Item as PubSubItem, Publish}}; + + #[test] + fn pubsub_publish_pubkey_data() { + let pubkey = PubKey { + date: None, + data: PubKeyData { + data: (&"Foo").as_bytes().to_vec(), + } + }; + println!("Foo1: {:?}", pubkey); + + let pubsub = Publish { + node: NodeName(format!("{}:{}", ns::OX_PUBKEYS, "some-fingerprint")), + items: vec![ + PubSubItem(Item::new(None, None, Some(pubkey))), + ], + }; + println!("Foo2: {:?}", pubsub); + } + + #[test] + fn pubsub_publish_pubkey_meta() { + let pubkeymeta = PubKeysMeta { + pubkeys: vec![ + PubKeyMeta { + v4fingerprint: "some-fingerprint".to_owned(), + date: DateTime::from_str("2019-03-30T18:30:25Z").unwrap(), + }, + ], + }; + println!("Foo1: {:?}", pubkeymeta); + + let pubsub = Publish { + node: NodeName("foo".to_owned()), + items: vec![ + PubSubItem(Item::new(None, None, Some(pubkeymeta))), + ], + }; + println!("Foo2: {:?}", pubsub); + } +} diff --git a/xmpp-parsers/src/ping.rs b/xmpp-parsers/src/ping.rs new file mode 100644 index 0000000000000000000000000000000000000000..70663fa3a79d483da66f040b3b2419c4245272f6 --- /dev/null +++ b/xmpp-parsers/src/ping.rs @@ -0,0 +1,71 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// Copyright (c) 2017 Maxime “pep” Buquet +// +// 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 crate::iq::IqGetPayload; + +generate_empty_element!( + /// Represents a ping to the recipient, which must be answered with an + /// empty `` or with an error. + Ping, + "ping", + PING +); + +impl IqGetPayload for Ping {} + +#[cfg(test)] +mod tests { + use super::*; + #[cfg(not(feature = "disable-validation"))] + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[test] + fn test_size() { + assert_size!(Ping, 0); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + Ping::try_from(elem).unwrap(); + } + + #[test] + fn test_serialise() { + let elem1 = Element::from(Ping); + let elem2: Element = "".parse().unwrap(); + assert_eq!(elem1, elem2); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Ping::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in ping element."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_attribute() { + let elem: Element = "".parse().unwrap(); + let error = Ping::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in ping element."); + } +} diff --git a/xmpp-parsers/src/presence.rs b/xmpp-parsers/src/presence.rs new file mode 100644 index 0000000000000000000000000000000000000000..b096a7b0967cee24c0c7f21c04a08fb286246dc5 --- /dev/null +++ b/xmpp-parsers/src/presence.rs @@ -0,0 +1,661 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// Copyright (c) 2017 Maxime “pep” Buquet +// +// 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 crate::util::error::Error; +use crate::ns; +use jid::Jid; +use minidom::{Element, IntoAttributeValue, Node}; +use std::collections::BTreeMap; +use std::str::FromStr; +use std::convert::TryFrom; + +/// Should be implemented on every known payload of a ``. +pub trait PresencePayload: TryFrom + Into {} + +/// Specifies the availability of an entity or resource. +#[derive(Debug, Clone, PartialEq)] +pub enum Show { + /// The entity or resource is temporarily away. + Away, + + /// The entity or resource is actively interested in chatting. + Chat, + + /// The entity or resource is busy (dnd = "Do Not Disturb"). + Dnd, + + /// The entity or resource is away for an extended period (xa = "eXtended + /// Away"). + Xa, +} + +impl FromStr for Show { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "away" => Show::Away, + "chat" => Show::Chat, + "dnd" => Show::Dnd, + "xa" => Show::Xa, + + _ => return Err(Error::ParseError("Invalid value for show.")), + }) + } +} + +impl Into for Show { + fn into(self) -> Node { + Element::builder("show") + .append(match self { + Show::Away => "away", + Show::Chat => "chat", + Show::Dnd => "dnd", + Show::Xa => "xa", + }) + .build() + .into() + } +} + +type Lang = String; +type Status = String; + +type Priority = i8; + +/// +#[derive(Debug, Clone, PartialEq)] +pub enum Type { + /// This value is not an acceptable 'type' attribute, it is only used + /// internally to signal the absence of 'type'. + None, + + /// An error has occurred regarding processing of a previously sent + /// presence stanza; if the presence stanza is of type "error", it MUST + /// include an child element (refer to [XMPP‑CORE]). + Error, + + /// A request for an entity's current presence; SHOULD be generated only by + /// a server on behalf of a user. + Probe, + + /// The sender wishes to subscribe to the recipient's presence. + Subscribe, + + /// The sender has allowed the recipient to receive their presence. + Subscribed, + + /// The sender is no longer available for communication. + Unavailable, + + /// The sender is unsubscribing from the receiver's presence. + Unsubscribe, + + /// The subscription request has been denied or a previously granted + /// subscription has been canceled. + Unsubscribed, +} + +impl Default for Type { + fn default() -> Type { + Type::None + } +} + +impl FromStr for Type { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "error" => Type::Error, + "probe" => Type::Probe, + "subscribe" => Type::Subscribe, + "subscribed" => Type::Subscribed, + "unavailable" => Type::Unavailable, + "unsubscribe" => Type::Unsubscribe, + "unsubscribed" => Type::Unsubscribed, + + _ => { + return Err(Error::ParseError( + "Invalid 'type' attribute on presence element.", + )); + } + }) + } +} + +impl IntoAttributeValue for Type { + fn into_attribute_value(self) -> Option { + Some( + match self { + Type::None => return None, + + Type::Error => "error", + Type::Probe => "probe", + Type::Subscribe => "subscribe", + Type::Subscribed => "subscribed", + Type::Unavailable => "unavailable", + Type::Unsubscribe => "unsubscribe", + Type::Unsubscribed => "unsubscribed", + } + .to_owned(), + ) + } +} + +/// The main structure representing the `` stanza. +#[derive(Debug, Clone)] +pub struct Presence { + /// The sender of this presence. + pub from: Option, + + /// The recipient of this presence. + pub to: Option, + + /// The identifier, unique on this stream, of this stanza. + pub id: Option, + + /// The type of this presence stanza. + pub type_: Type, + + /// The availability of the sender of this presence. + pub show: Option, + + /// A localised list of statuses defined in this presence. + pub statuses: BTreeMap, + + /// The sender’s resource priority, if negative it won’t receive messages + /// that haven’t been directed to it. + pub priority: Priority, + + /// A list of payloads contained in this presence. + pub payloads: Vec, +} + +impl Presence { + /// Create a new presence of this type. + pub fn new(type_: Type) -> Presence { + Presence { + from: None, + to: None, + id: None, + type_, + show: None, + statuses: BTreeMap::new(), + priority: 0i8, + payloads: vec![], + } + } + + /// Set the emitter of this presence, this should only be useful for + /// servers and components, as clients can only send presences from their + /// own resource (which is implicit). + pub fn with_from>(mut self, from: J) -> Presence { + self.from = Some(from.into()); + self + } + + /// Set the recipient of this presence, this is only useful for directed + /// presences. + pub fn with_to>(mut self, to: J) -> Presence { + self.to = Some(to.into()); + self + } + + /// Set the identifier for this presence. + pub fn with_id(mut self, id: String) -> Presence { + self.id = Some(id); + self + } + + /// Set the availability information of this presence. + pub fn with_show(mut self, show: Show) -> Presence { + self.show = Some(show); + self + } + + /// Set the priority of this presence. + pub fn with_priority(mut self, priority: i8) -> Presence { + self.priority = priority; + self + } + + /// Set the payloads of this presence. + pub fn with_payloads(mut self, payloads: Vec) -> Presence { + self.payloads = payloads; + self + } + + /// Set the availability information of this presence. + pub fn set_status(&mut self, lang: L, status: S) + where L: Into, + S: Into, + { + self.statuses.insert(lang.into(), status.into()); + } + + /// Add a payload to this presence. + pub fn add_payload(&mut self, payload: P) { + self.payloads.push(payload.into()); + } +} + +impl TryFrom for Presence { + type Error = Error; + + fn try_from(root: Element) -> Result { + check_self!(root, "presence", DEFAULT_NS); + let mut show = None; + let mut priority = None; + let mut presence = Presence { + from: get_attr!(root, "from", Option), + to: get_attr!(root, "to", Option), + id: get_attr!(root, "id", Option), + type_: get_attr!(root, "type", Default), + show: None, + statuses: BTreeMap::new(), + priority: 0i8, + payloads: vec![], + }; + for elem in root.children() { + if elem.is("show", ns::DEFAULT_NS) { + if show.is_some() { + return Err(Error::ParseError( + "More than one show element in a presence.", + )); + } + check_no_attributes!(elem, "show"); + check_no_children!(elem, "show"); + show = Some(Show::from_str(elem.text().as_ref())?); + } else if elem.is("status", ns::DEFAULT_NS) { + check_no_unknown_attributes!(elem, "status", ["xml:lang"]); + check_no_children!(elem, "status"); + let lang = get_attr!(elem, "xml:lang", Default); + if presence.statuses.insert(lang, elem.text()).is_some() { + return Err(Error::ParseError( + "Status element present twice for the same xml:lang.", + )); + } + } else if elem.is("priority", ns::DEFAULT_NS) { + if priority.is_some() { + return Err(Error::ParseError( + "More than one priority element in a presence.", + )); + } + check_no_attributes!(elem, "priority"); + check_no_children!(elem, "priority"); + priority = Some(Priority::from_str(elem.text().as_ref())?); + } else { + presence.payloads.push(elem.clone()); + } + } + presence.show = show; + if let Some(priority) = priority { + presence.priority = priority; + } + Ok(presence) + } +} + +impl From for Element { + fn from(presence: Presence) -> Element { + Element::builder("presence") + .ns(ns::DEFAULT_NS) + .attr("from", presence.from) + .attr("to", presence.to) + .attr("id", presence.id) + .attr("type", presence.type_) + .append_all(presence.show.into_iter()) + .append_all( + presence + .statuses + .into_iter() + .map(|(lang, status)| { + Element::builder("status") + .attr( + "xml:lang", + match lang.as_ref() { + "" => None, + lang => Some(lang), + }, + ) + .append(status) + }) + ) + .append_all(if presence.priority == 0 { + None + } else { + Some(Element::builder("priority") + .append(format!("{}", presence.priority))) + }) + .append_all(presence.payloads.into_iter()) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::compare_elements::NamespaceAwareCompare; + use jid::{BareJid, FullJid}; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Show, 1); + assert_size!(Type, 1); + assert_size!(Presence, 120); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Show, 1); + assert_size!(Type, 1); + assert_size!(Presence, 240); + } + + #[test] + fn test_simple() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let presence = Presence::try_from(elem).unwrap(); + assert_eq!(presence.from, None); + assert_eq!(presence.to, None); + assert_eq!(presence.id, None); + assert_eq!(presence.type_, Type::None); + assert!(presence.payloads.is_empty()); + } + + #[test] + fn test_serialise() { + #[cfg(not(feature = "component"))] + let elem: Element = "/>" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = "/>" + .parse() + .unwrap(); + let presence = Presence::new(Type::Unavailable); + let elem2 = presence.into(); + assert!(elem.compare_to(&elem2)); + } + + #[test] + fn test_show() { + #[cfg(not(feature = "component"))] + let elem: Element = "chat" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = + "chat" + .parse() + .unwrap(); + let presence = Presence::try_from(elem).unwrap(); + assert_eq!(presence.payloads.len(), 0); + assert_eq!(presence.show, Some(Show::Chat)); + } + + #[test] + fn test_empty_show_value() { + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let presence = Presence::try_from(elem).unwrap(); + assert_eq!(presence.show, None); + } + + #[test] + fn test_missing_show_value() { + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let error = Presence::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Invalid value for show."); + } + + #[test] + fn test_invalid_show() { + // "online" used to be a pretty common mistake. + #[cfg(not(feature = "component"))] + let elem: Element = "online" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = + "online" + .parse() + .unwrap(); + let error = Presence::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Invalid value for show."); + } + + #[test] + fn test_empty_status() { + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let presence = Presence::try_from(elem).unwrap(); + assert_eq!(presence.payloads.len(), 0); + assert_eq!(presence.statuses.len(), 1); + assert_eq!(presence.statuses[""], ""); + } + + #[test] + fn test_status() { + #[cfg(not(feature = "component"))] + let elem: Element = "Here!" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = + "Here!" + .parse() + .unwrap(); + let presence = Presence::try_from(elem).unwrap(); + assert_eq!(presence.payloads.len(), 0); + assert_eq!(presence.statuses.len(), 1); + assert_eq!(presence.statuses[""], "Here!"); + } + + #[test] + fn test_multiple_statuses() { + #[cfg(not(feature = "component"))] + let elem: Element = "Here!Là!".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "Here!Là!".parse().unwrap(); + let presence = Presence::try_from(elem).unwrap(); + assert_eq!(presence.payloads.len(), 0); + assert_eq!(presence.statuses.len(), 2); + assert_eq!(presence.statuses[""], "Here!"); + assert_eq!(presence.statuses["fr"], "Là!"); + } + + #[test] + fn test_invalid_multiple_statuses() { + #[cfg(not(feature = "component"))] + let elem: Element = "Here!Là!".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "Here!Là!".parse().unwrap(); + let error = Presence::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!( + message, + "Status element present twice for the same xml:lang." + ); + } + + #[test] + fn test_priority() { + #[cfg(not(feature = "component"))] + let elem: Element = "-1" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = + "-1" + .parse() + .unwrap(); + let presence = Presence::try_from(elem).unwrap(); + assert_eq!(presence.payloads.len(), 0); + assert_eq!(presence.priority, -1i8); + } + + #[test] + fn test_invalid_priority() { + #[cfg(not(feature = "component"))] + let elem: Element = "128" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = + "128" + .parse() + .unwrap(); + let error = Presence::try_from(elem).unwrap_err(); + match error { + Error::ParseIntError(_) => (), + _ => panic!(), + }; + } + + #[test] + fn test_unknown_child() { + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = + "" + .parse() + .unwrap(); + let presence = Presence::try_from(elem).unwrap(); + let payload = &presence.payloads[0]; + assert!(payload.is("test", "invalid")); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_status_child() { + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = + "" + .parse() + .unwrap(); + let error = Presence::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in status element."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_attribute() { + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = + "" + .parse() + .unwrap(); + let error = Presence::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in status element."); + } + + #[test] + fn test_serialise_status() { + let status = Status::from("Hello world!"); + let mut presence = Presence::new(Type::Unavailable); + presence.statuses.insert(String::from(""), status); + let elem: Element = presence.into(); + assert!(elem.is("presence", ns::DEFAULT_NS)); + assert!(elem.children().next().unwrap().is("status", ns::DEFAULT_NS)); + } + + #[test] + fn test_serialise_priority() { + let presence = Presence::new(Type::None) + .with_priority(42); + let elem: Element = presence.into(); + assert!(elem.is("presence", ns::DEFAULT_NS)); + let priority = elem.children().next().unwrap(); + assert!(priority.is("priority", ns::DEFAULT_NS)); + assert_eq!(priority.text(), "42"); + } + + #[test] + fn presence_with_to() { + let presence = Presence::new(Type::None); + let elem: Element = presence.into(); + assert_eq!(elem.attr("to"), None); + + let presence = Presence::new(Type::None) + .with_to(Jid::Bare(BareJid::domain("localhost"))); + let elem: Element = presence.into(); + assert_eq!(elem.attr("to"), Some("localhost")); + + let presence = Presence::new(Type::None) + .with_to(BareJid::domain("localhost")); + let elem: Element = presence.into(); + assert_eq!(elem.attr("to"), Some("localhost")); + + let presence = Presence::new(Type::None) + .with_to(Jid::Full(FullJid::new("test", "localhost", "coucou"))); + let elem: Element = presence.into(); + assert_eq!(elem.attr("to"), Some("test@localhost/coucou")); + + let presence = Presence::new(Type::None) + .with_to(FullJid::new("test", "localhost", "coucou")); + let elem: Element = presence.into(); + assert_eq!(elem.attr("to"), Some("test@localhost/coucou")); + } +} diff --git a/xmpp-parsers/src/pubsub/event.rs b/xmpp-parsers/src/pubsub/event.rs new file mode 100644 index 0000000000000000000000000000000000000000..2228a067dd1ea49af5b73598414e3d3d72a5f5f3 --- /dev/null +++ b/xmpp-parsers/src/pubsub/event.rs @@ -0,0 +1,429 @@ +// 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 crate::data_forms::DataForm; +use crate::date::DateTime; +use crate::util::error::Error; +use crate::ns; +use crate::pubsub::{ItemId, NodeName, Subscription, SubscriptionId, Item as PubSubItem}; +use jid::Jid; +use crate::Element; +use std::convert::TryFrom; + +/// Event wrapper for a PubSub ``. +#[derive(Debug, Clone)] +pub struct Item(pub PubSubItem); + +impl_pubsub_item!(Item, PUBSUB_EVENT); + +/// Represents an event happening to a PubSub node. +#[derive(Debug, Clone)] +pub enum PubSubEvent { + /* + Collection { + }, + */ + /// This node’s configuration changed. + Configuration { + /// The node affected. + node: NodeName, + + /// The new configuration of this node. + form: Option, + }, + + /// This node has been deleted, with an optional redirect to another node. + Delete { + /// The node affected. + node: NodeName, + + /// The xmpp: URI of another node replacing this one. + redirect: Option, + }, + + /// Some items have been published on this node. + PublishedItems { + /// The node affected. + node: NodeName, + + /// The list of published items. + items: Vec, + }, + + /// Some items have been removed from this node. + RetractedItems { + /// The node affected. + node: NodeName, + + /// The list of retracted items. + items: Vec, + }, + + /// All items of this node just got removed at once. + Purge { + /// The node affected. + node: NodeName, + }, + + /// The user’s subscription to this node has changed. + Subscription { + /// The node affected. + node: NodeName, + + /// The time at which this subscription will expire. + expiry: Option, + + /// The JID of the user affected. + jid: Option, + + /// An identifier for this subscription. + subid: Option, + + /// The state of this subscription. + subscription: Option, + }, +} + +fn parse_items(elem: Element, node: NodeName) -> Result { + let mut is_retract = None; + let mut items = vec![]; + let mut retracts = vec![]; + for child in elem.children() { + if child.is("item", ns::PUBSUB_EVENT) { + match is_retract { + None => is_retract = Some(false), + Some(false) => (), + Some(true) => { + return Err(Error::ParseError( + "Mix of item and retract in items element.", + )); + } + } + items.push(Item::try_from(child.clone())?); + } else if child.is("retract", ns::PUBSUB_EVENT) { + match is_retract { + None => is_retract = Some(true), + Some(true) => (), + Some(false) => { + return Err(Error::ParseError( + "Mix of item and retract in items element.", + )); + } + } + check_no_children!(child, "retract"); + check_no_unknown_attributes!(child, "retract", ["id"]); + let id = get_attr!(child, "id", Required); + retracts.push(id); + } else { + return Err(Error::ParseError("Invalid child in items element.")); + } + } + Ok(match is_retract { + Some(false) => PubSubEvent::PublishedItems { node, items }, + Some(true) => PubSubEvent::RetractedItems { + node, + items: retracts, + }, + None => return Err(Error::ParseError("Missing children in items element.")), + }) +} + +impl TryFrom for PubSubEvent { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "event", PUBSUB_EVENT); + check_no_attributes!(elem, "event"); + + let mut payload = None; + for child in elem.children() { + let node = get_attr!(child, "node", Required); + if child.is("configuration", ns::PUBSUB_EVENT) { + let mut payloads = child.children().cloned().collect::>(); + let item = payloads.pop(); + if !payloads.is_empty() { + return Err(Error::ParseError( + "More than a single payload in configuration element.", + )); + } + let form = match item { + None => None, + Some(payload) => Some(DataForm::try_from(payload)?), + }; + payload = Some(PubSubEvent::Configuration { node, form }); + } else if child.is("delete", ns::PUBSUB_EVENT) { + let mut redirect = None; + for item in child.children() { + if item.is("redirect", ns::PUBSUB_EVENT) { + if redirect.is_some() { + return Err(Error::ParseError( + "More than one redirect in delete element.", + )); + } + let uri = get_attr!(item, "uri", Required); + redirect = Some(uri); + } else { + return Err(Error::ParseError("Unknown child in delete element.")); + } + } + payload = Some(PubSubEvent::Delete { node, redirect }); + } else if child.is("items", ns::PUBSUB_EVENT) { + payload = Some(parse_items(child.clone(), node)?); + } else if child.is("purge", ns::PUBSUB_EVENT) { + check_no_children!(child, "purge"); + payload = Some(PubSubEvent::Purge { node }); + } else if child.is("subscription", ns::PUBSUB_EVENT) { + check_no_children!(child, "subscription"); + payload = Some(PubSubEvent::Subscription { + node, + expiry: get_attr!(child, "expiry", Option), + jid: get_attr!(child, "jid", Option), + subid: get_attr!(child, "subid", Option), + subscription: get_attr!(child, "subscription", Option), + }); + } else { + return Err(Error::ParseError("Unknown child in event element.")); + } + } + Ok(payload.ok_or(Error::ParseError("No payload in event element."))?) + } +} + +impl From for Element { + fn from(event: PubSubEvent) -> Element { + let payload = match event { + PubSubEvent::Configuration { node, form } => Element::builder("configuration") + .ns(ns::PUBSUB_EVENT) + .attr("node", node) + .append_all(form.map(Element::from)), + PubSubEvent::Delete { node, redirect } => Element::builder("purge") + .ns(ns::PUBSUB_EVENT) + .attr("node", node) + .append_all(redirect.map(|redirect| { + Element::builder("redirect") + .ns(ns::PUBSUB_EVENT) + .attr("uri", redirect) + })), + PubSubEvent::PublishedItems { node, items } => Element::builder("items") + .ns(ns::PUBSUB_EVENT) + .attr("node", node) + .append_all(items.into_iter()), + PubSubEvent::RetractedItems { node, items } => Element::builder("items") + .ns(ns::PUBSUB_EVENT) + .attr("node", node) + .append_all( + items + .into_iter() + .map(|id| { + Element::builder("retract") + .ns(ns::PUBSUB_EVENT) + .attr("id", id) + }) + ), + PubSubEvent::Purge { node } => Element::builder("purge") + .ns(ns::PUBSUB_EVENT) + .attr("node", node), + PubSubEvent::Subscription { + node, + expiry, + jid, + subid, + subscription, + } => Element::builder("subscription") + .ns(ns::PUBSUB_EVENT) + .attr("node", node) + .attr("expiry", expiry) + .attr("jid", jid) + .attr("subid", subid) + .attr("subscription", subscription), + }; + Element::builder("event") + .ns(ns::PUBSUB_EVENT) + .append(payload) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::compare_elements::NamespaceAwareCompare; + use std::str::FromStr; + + #[test] + fn missing_items() { + let elem: Element = + "" + .parse() + .unwrap(); + let error = PubSubEvent::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Missing children in items element."); + } + + #[test] + fn test_simple_items() { + let elem: Element = "".parse().unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::PublishedItems { node, items } => { + assert_eq!(node, NodeName(String::from("coucou"))); + assert_eq!(items[0].id, Some(ItemId(String::from("test")))); + assert_eq!( + items[0].publisher, + Some(Jid::from_str("test@coucou").unwrap()) + ); + assert_eq!(items[0].payload, None); + } + _ => panic!(), + } + } + + #[test] + fn test_simple_pep() { + let elem: Element = "".parse().unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::PublishedItems { node, items } => { + assert_eq!(node, NodeName(String::from("something"))); + assert_eq!(items[0].id, None); + assert_eq!(items[0].publisher, None); + match items[0].payload { + Some(ref elem) => assert!(elem.is("foreign", "example:namespace")), + _ => panic!(), + } + } + _ => panic!(), + } + } + + #[test] + fn test_simple_retract() { + let elem: Element = "".parse().unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::RetractedItems { node, items } => { + assert_eq!(node, NodeName(String::from("something"))); + assert_eq!(items[0], ItemId(String::from("coucou"))); + assert_eq!(items[1], ItemId(String::from("test"))); + } + _ => panic!(), + } + } + + #[test] + fn test_simple_delete() { + let elem: Element = "".parse().unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::Delete { node, redirect } => { + assert_eq!(node, NodeName(String::from("coucou"))); + assert_eq!(redirect, Some(String::from("hello"))); + } + _ => panic!(), + } + } + + #[test] + fn test_simple_purge() { + let elem: Element = + "" + .parse() + .unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::Purge { node } => { + assert_eq!(node, NodeName(String::from("coucou"))); + } + _ => panic!(), + } + } + + #[test] + fn test_simple_configure() { + let elem: Element = "http://jabber.org/protocol/pubsub#node_config".parse().unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::Configuration { node, form: _ } => { + assert_eq!(node, NodeName(String::from("coucou"))); + //assert_eq!(form.type_, Result_); + } + _ => panic!(), + } + } + + #[test] + fn test_invalid() { + let elem: Element = + "" + .parse() + .unwrap(); + let error = PubSubEvent::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in event element."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_attribute() { + let elem: Element = "" + .parse() + .unwrap(); + let error = PubSubEvent::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in event element."); + } + + #[test] + fn test_ex221_subscription() { + let elem: Element = r#" + + + +"# + .parse() + .unwrap(); + let event = PubSubEvent::try_from(elem.clone()).unwrap(); + match event.clone() { + PubSubEvent::Subscription { + node, + expiry, + jid, + subid, + subscription, + } => { + assert_eq!(node, NodeName(String::from("princely_musings"))); + assert_eq!( + subid, + Some(SubscriptionId(String::from( + "ba49252aaa4f5d320c24d3766f0bdcade78c78d3" + ))) + ); + assert_eq!(subscription, Some(Subscription::Subscribed)); + assert_eq!(jid, Some(Jid::from_str("francisco@denmark.lit").unwrap())); + assert_eq!(expiry, Some("2006-02-28T23:59:59Z".parse().unwrap())); + } + _ => panic!(), + } + + let elem2: Element = event.into(); + assert!(elem.compare_to(&elem2)); + } +} diff --git a/xmpp-parsers/src/pubsub/mod.rs b/xmpp-parsers/src/pubsub/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..4b998b9febddc61c341e82d28edf88a1ebde497d --- /dev/null +++ b/xmpp-parsers/src/pubsub/mod.rs @@ -0,0 +1,76 @@ +// 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/. + +/// The `http://jabber.org/protocol/pubsub#event` protocol. +pub mod event; + +/// The `http://jabber.org/protocol/pubsub` protocol. +pub mod pubsub; + +pub use self::event::PubSubEvent; +pub use self::pubsub::PubSub; + +use crate::{Jid, Element}; + +generate_id!( + /// The name of a PubSub node, used to identify it on a JID. + NodeName +); + +generate_id!( + /// The identifier of an item, which is unique per node. + ItemId +); + +generate_id!( + /// The identifier of a subscription to a PubSub node. + SubscriptionId +); + +generate_attribute!( + /// The state of a subscription to a node. + Subscription, "subscription", { + /// The user is not subscribed to this node. + None => "none", + + /// The user’s subscription to this node is still pending. + Pending => "pending", + + /// The user is subscribed to this node. + Subscribed => "subscribed", + + /// The user’s subscription to this node will only be valid once + /// configured. + Unconfigured => "unconfigured", + }, Default = None +); + +/// An item from a PubSub node. +#[derive(Debug, Clone)] +pub struct Item { + /// The identifier for this item, unique per node. + pub id: Option, + + /// The JID of the entity who published this item. + pub publisher: Option, + + /// The payload of this item, in an arbitrary namespace. + pub payload: Option, +} + +impl Item { + /// Create a new item, accepting only payloads implementing `PubSubPayload`. + pub fn new(id: Option, publisher: Option, payload: Option

) -> Item { + Item { + id, + publisher, + payload: payload.map(Into::into), + } + } +} + +/// This trait should be implemented on any element which can be included as a PubSub payload. +pub trait PubSubPayload: ::std::convert::TryFrom + Into {} diff --git a/xmpp-parsers/src/pubsub/pubsub.rs b/xmpp-parsers/src/pubsub/pubsub.rs new file mode 100644 index 0000000000000000000000000000000000000000..39b30c7427ab6abf379d3d880e78cd71826cd927 --- /dev/null +++ b/xmpp-parsers/src/pubsub/pubsub.rs @@ -0,0 +1,657 @@ +// Copyright (c) 2018 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 crate::data_forms::DataForm; +use crate::util::error::Error; +use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; +use crate::ns; +use crate::pubsub::{NodeName, Subscription, SubscriptionId, Item as PubSubItem}; +use jid::Jid; +use crate::Element; +use std::convert::TryFrom; + +// TODO: a better solution would be to split this into a query and a result elements, like for +// XEP-0030. +generate_element!( + /// A list of affiliations you have on a service, or on a node. + Affiliations, "affiliations", PUBSUB, + attributes: [ + /// The optional node name this request pertains to. + node: Option = "node", + ], + children: [ + /// The actual list of affiliation elements. + affiliations: Vec = ("affiliation", PUBSUB) => Affiliation + ] +); + +generate_attribute!( + /// A list of possible affiliations to a node. + AffiliationAttribute, "affiliation", { + /// You are a member of this node, you can subscribe and retrieve items. + Member => "member", + + /// You don’t have a specific affiliation with this node, you can only subscribe to it. + None => "none", + + /// You are banned from this node. + Outcast => "outcast", + + /// You are an owner of this node, and can do anything with it. + Owner => "owner", + + /// You are a publisher on this node, you can publish and retract items to it. + Publisher => "publisher", + + /// You can publish and retract items on this node, but not subscribe or retrieve items. + PublishOnly => "publish-only", + } +); + +generate_element!( + /// An affiliation element. + Affiliation, "affiliation", PUBSUB, + attributes: [ + /// The node this affiliation pertains to. + node: Required = "node", + + /// The affiliation you currently have on this node. + affiliation: Required = "affiliation", + ] +); + +generate_element!( + /// Request to configure a new node. + Configure, "configure", PUBSUB, + children: [ + /// The form to configure it. + form: Option = ("x", DATA_FORMS) => DataForm + ] +); + +generate_element!( + /// Request to create a new node. + Create, "create", PUBSUB, + attributes: [ + /// The node name to create, if `None` the service will generate one. + node: Option = "node", + ] +); + +generate_element!( + /// Request for a default node configuration. + Default, "default", PUBSUB, + attributes: [ + /// The node targeted by this request, otherwise the entire service. + node: Option = "node", + + // TODO: do we really want to support collection nodes? + // type: Option = "type", + ] +); + +generate_element!( + /// A request for a list of items. + Items, "items", PUBSUB, + attributes: [ + // TODO: should be an xs:positiveInteger, that is, an unbounded int ≥ 1. + /// Maximum number of items returned. + max_items: Option = "max_items", + + /// The node queried by this request. + node: Required = "node", + + /// The subscription identifier related to this request. + subid: Option = "subid", + ], + children: [ + /// The actual list of items returned. + items: Vec = ("item", PUBSUB) => Item + ] +); + +impl Items { + /// Create a new items request. + pub fn new(node: &str) -> Items { + Items { + node: NodeName(String::from(node)), + max_items: None, + subid: None, + items: Vec::new(), + } + } +} + +/// Response wrapper for a PubSub ``. +#[derive(Debug, Clone)] +pub struct Item(pub PubSubItem); + +impl_pubsub_item!(Item, PUBSUB); + +generate_element!( + /// The options associated to a subscription request. + Options, "options", PUBSUB, + attributes: [ + /// The JID affected by this request. + jid: Required = "jid", + + /// The node affected by this request. + node: Option = "node", + + /// The subscription identifier affected by this request. + subid: Option = "subid", + ], + children: [ + /// The form describing the subscription. + form: Option = ("x", DATA_FORMS) => DataForm + ] +); + +generate_element!( + /// Request to publish items to a node. + Publish, "publish", PUBSUB, + attributes: [ + /// The target node for this operation. + node: Required = "node", + ], + children: [ + /// The items you want to publish. + items: Vec = ("item", PUBSUB) => Item + ] +); + +generate_element!( + /// The options associated to a publish request. + PublishOptions, "publish-options", PUBSUB, + children: [ + /// The form describing these options. + form: Option = ("x", DATA_FORMS) => DataForm + ] +); + +generate_attribute!( + /// Whether a retract request should notify subscribers or not. + Notify, + "notify", + bool +); + +generate_element!( + /// A request to retract some items from a node. + Retract, "retract", PUBSUB, + attributes: [ + /// The node affected by this request. + node: Required = "node", + + /// Whether a retract request should notify subscribers or not. + notify: Default = "notify", + ], + children: [ + /// The items affected by this request. + items: Vec = ("item", PUBSUB) => Item + ] +); + +/// Indicate that the subscription can be configured. +#[derive(Debug, Clone)] +pub struct SubscribeOptions { + /// If `true`, the configuration is actually required. + required: bool, +} + +impl TryFrom for SubscribeOptions { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "subscribe-options", PUBSUB); + check_no_attributes!(elem, "subscribe-options"); + let mut required = false; + for child in elem.children() { + if child.is("required", ns::PUBSUB) { + if required { + return Err(Error::ParseError( + "More than one required element in subscribe-options.", + )); + } + required = true; + } else { + return Err(Error::ParseError( + "Unknown child in subscribe-options element.", + )); + } + } + Ok(SubscribeOptions { required }) + } +} + +impl From for Element { + fn from(subscribe_options: SubscribeOptions) -> Element { + Element::builder("subscribe-options") + .ns(ns::PUBSUB) + .append_all(if subscribe_options.required { + Some(Element::builder("required").ns(ns::PUBSUB)) + } else { + None + }) + .build() + } +} + +generate_element!( + /// A request to subscribe a JID to a node. + Subscribe, "subscribe", PUBSUB, + attributes: [ + /// The JID being subscribed. + jid: Required = "jid", + + /// The node to subscribe to. + node: Option = "node", + ] +); + +generate_element!( + /// A request for current subscriptions. + Subscriptions, "subscriptions", PUBSUB, + attributes: [ + /// The node to query. + node: Option = "node", + ], + children: [ + /// The list of subscription elements returned. + subscription: Vec = ("subscription", PUBSUB) => SubscriptionElem + ] +); + +generate_element!( + /// A subscription element, describing the state of a subscription. + SubscriptionElem, "subscription", PUBSUB, + attributes: [ + /// The JID affected by this subscription. + jid: Required = "jid", + + /// The node affected by this subscription. + node: Option = "node", + + /// The subscription identifier for this subscription. + subid: Option = "subid", + + /// The state of the subscription. + subscription: Option = "subscription", + ], + children: [ + /// The options related to this subscription. + subscribe_options: Option = ("subscribe-options", PUBSUB) => SubscribeOptions + ] +); + +generate_element!( + /// An unsubscribe request. + Unsubscribe, "unsubscribe", PUBSUB, + attributes: [ + /// The JID affected by this request. + jid: Required = "jid", + + /// The node affected by this request. + node: Option = "node", + + /// The subscription identifier for this subscription. + subid: Option = "subid", + ] +); + +/// Main payload used to communicate with a PubSub service. +/// +/// `` +#[derive(Debug, Clone)] +pub enum PubSub { + /// Request to create a new node, with optional suggested name and suggested configuration. + Create { + /// The create request. + create: Create, + + /// The configure request for the new node. + configure: Option, + }, + + /// Request to publish items to a node, with optional options. + Publish { + /// The publish request. + publish: Publish, + + /// The options related to this publish request. + publish_options: Option, + }, + + /// A list of affiliations you have on a service, or on a node. + Affiliations(Affiliations), + + /// Request for a default node configuration. + Default(Default), + + /// A request for a list of items. + Items(Items), + + /// A request to retract some items from a node. + Retract(Retract), + + /// A request about a subscription. + Subscription(SubscriptionElem), + + /// A request for current subscriptions. + Subscriptions(Subscriptions), + + /// An unsubscribe request. + Unsubscribe(Unsubscribe), +} + +impl IqGetPayload for PubSub {} +impl IqSetPayload for PubSub {} +impl IqResultPayload for PubSub {} + +impl TryFrom for PubSub { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "pubsub", PUBSUB); + check_no_attributes!(elem, "pubsub"); + + let mut payload = None; + for child in elem.children() { + if child.is("create", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let create = Create::try_from(child.clone())?; + payload = Some(PubSub::Create { + create, + configure: None, + }); + } else if child.is("configure", ns::PUBSUB) { + if let Some(PubSub::Create { create, configure }) = payload { + if configure.is_some() { + return Err(Error::ParseError( + "Configure is already defined in pubsub element.", + )); + } + let configure = Some(Configure::try_from(child.clone())?); + payload = Some(PubSub::Create { create, configure }); + } else { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + } else if child.is("publish", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let publish = Publish::try_from(child.clone())?; + payload = Some(PubSub::Publish { + publish, + publish_options: None, + }); + } else if child.is("publish-options", ns::PUBSUB) { + if let Some(PubSub::Publish { + publish, + publish_options, + }) = payload + { + if publish_options.is_some() { + return Err(Error::ParseError( + "Publish-options are already defined in pubsub element.", + )); + } + let publish_options = Some(PublishOptions::try_from(child.clone())?); + payload = Some(PubSub::Publish { + publish, + publish_options, + }); + } else { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + } else if child.is("affiliations", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let affiliations = Affiliations::try_from(child.clone())?; + payload = Some(PubSub::Affiliations(affiliations)); + } else if child.is("default", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let default = Default::try_from(child.clone())?; + payload = Some(PubSub::Default(default)); + } else if child.is("items", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let items = Items::try_from(child.clone())?; + payload = Some(PubSub::Items(items)); + } else if child.is("retract", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let retract = Retract::try_from(child.clone())?; + payload = Some(PubSub::Retract(retract)); + } else if child.is("subscription", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let subscription = SubscriptionElem::try_from(child.clone())?; + payload = Some(PubSub::Subscription(subscription)); + } else if child.is("subscriptions", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let subscriptions = Subscriptions::try_from(child.clone())?; + payload = Some(PubSub::Subscriptions(subscriptions)); + } else if child.is("unsubscribe", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let unsubscribe = Unsubscribe::try_from(child.clone())?; + payload = Some(PubSub::Unsubscribe(unsubscribe)); + } else { + return Err(Error::ParseError("Unknown child in pubsub element.")); + } + } + Ok(payload.ok_or(Error::ParseError("No payload in pubsub element."))?) + } +} + +impl From for Element { + fn from(pubsub: PubSub) -> Element { + Element::builder("pubsub") + .ns(ns::PUBSUB) + .append_all(match pubsub { + PubSub::Create { create, configure } => { + let mut elems = vec![Element::from(create)]; + if let Some(configure) = configure { + elems.push(Element::from(configure)); + } + elems + } + PubSub::Publish { + publish, + publish_options, + } => { + let mut elems = vec![Element::from(publish)]; + if let Some(publish_options) = publish_options { + elems.push(Element::from(publish_options)); + } + elems + } + PubSub::Affiliations(affiliations) => vec![Element::from(affiliations)], + PubSub::Default(default) => vec![Element::from(default)], + PubSub::Items(items) => vec![Element::from(items)], + PubSub::Retract(retract) => vec![Element::from(retract)], + PubSub::Subscription(subscription) => vec![Element::from(subscription)], + PubSub::Subscriptions(subscriptions) => vec![Element::from(subscriptions)], + PubSub::Unsubscribe(unsubscribe) => vec![Element::from(unsubscribe)], + }) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::compare_elements::NamespaceAwareCompare; + + #[test] + fn create() { + let elem: Element = "" + .parse() + .unwrap(); + let elem1 = elem.clone(); + let pubsub = PubSub::try_from(elem).unwrap(); + match pubsub.clone() { + PubSub::Create { create, configure } => { + assert!(create.node.is_none()); + assert!(configure.is_none()); + } + _ => panic!(), + } + + let elem2 = Element::from(pubsub); + assert!(elem1.compare_to(&elem2)); + + let elem: Element = + "" + .parse() + .unwrap(); + let elem1 = elem.clone(); + let pubsub = PubSub::try_from(elem).unwrap(); + match pubsub.clone() { + PubSub::Create { create, configure } => { + assert_eq!(&create.node.unwrap().0, "coucou"); + assert!(configure.is_none()); + } + _ => panic!(), + } + + let elem2 = Element::from(pubsub); + assert!(elem1.compare_to(&elem2)); + } + + #[test] + fn create_and_configure() { + let elem: Element = + "" + .parse() + .unwrap(); + let elem1 = elem.clone(); + let pubsub = PubSub::try_from(elem).unwrap(); + match pubsub.clone() { + PubSub::Create { create, configure } => { + assert!(create.node.is_none()); + assert!(configure.unwrap().form.is_none()); + } + _ => panic!(), + } + + let elem2 = Element::from(pubsub); + assert!(elem1.compare_to(&elem2)); + } + + #[test] + fn publish() { + let elem: Element = + "" + .parse() + .unwrap(); + let elem1 = elem.clone(); + let pubsub = PubSub::try_from(elem).unwrap(); + match pubsub.clone() { + PubSub::Publish { + publish, + publish_options, + } => { + assert_eq!(&publish.node.0, "coucou"); + assert!(publish_options.is_none()); + } + _ => panic!(), + } + + let elem2 = Element::from(pubsub); + assert!(elem1.compare_to(&elem2)); + } + + #[test] + fn publish_with_publish_options() { + let elem: Element = "".parse().unwrap(); + let elem1 = elem.clone(); + let pubsub = PubSub::try_from(elem).unwrap(); + match pubsub.clone() { + PubSub::Publish { + publish, + publish_options, + } => { + assert_eq!(&publish.node.0, "coucou"); + assert!(publish_options.unwrap().form.is_none()); + } + _ => panic!(), + } + + let elem2 = Element::from(pubsub); + assert!(elem1.compare_to(&elem2)); + } + + #[test] + fn invalid_empty_pubsub() { + let elem: Element = "" + .parse() + .unwrap(); + let error = PubSub::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "No payload in pubsub element."); + } + + #[test] + fn publish_option() { + let elem: Element = "http://jabber.org/protocol/pubsub#publish-options".parse().unwrap(); + let publish_options = PublishOptions::try_from(elem).unwrap(); + assert_eq!( + &publish_options.form.unwrap().form_type.unwrap(), + "http://jabber.org/protocol/pubsub#publish-options" + ); + } + + #[test] + fn subscribe_options() { + let elem1: Element = "" + .parse() + .unwrap(); + let subscribe_options1 = SubscribeOptions::try_from(elem1).unwrap(); + assert_eq!(subscribe_options1.required, false); + + let elem2: Element = "".parse().unwrap(); + let subscribe_options2 = SubscribeOptions::try_from(elem2).unwrap(); + assert_eq!(subscribe_options2.required, true); + } +} diff --git a/xmpp-parsers/src/receipts.rs b/xmpp-parsers/src/receipts.rs new file mode 100644 index 0000000000000000000000000000000000000000..e837a17fdcedf984266206c513561f5433dc4847 --- /dev/null +++ b/xmpp-parsers/src/receipts.rs @@ -0,0 +1,89 @@ +// 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 crate::message::MessagePayload; + +generate_empty_element!( + /// Requests that this message is acked by the final recipient once + /// received. + Request, + "request", + RECEIPTS +); + +impl MessagePayload for Request {} + +generate_element!( + /// Notes that a previous message has correctly been received, it is + /// referenced by its 'id' attribute. + Received, "received", RECEIPTS, + attributes: [ + /// The 'id' attribute of the received message. + id: Required = "id", + ] +); + +impl MessagePayload for Received {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ns; + use crate::Element; + use std::convert::TryFrom; + use crate::util::error::Error; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Request, 0); + assert_size!(Received, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Request, 0); + assert_size!(Received, 24); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + Request::try_from(elem).unwrap(); + + let elem: Element = "" + .parse() + .unwrap(); + Received::try_from(elem).unwrap(); + } + + #[test] + fn test_missing_id() { + let elem: Element = "".parse().unwrap(); + let error = Received::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'id' missing."); + } + + #[test] + fn test_serialise() { + let receipt = Request; + let elem: Element = receipt.into(); + assert!(elem.is("request", ns::RECEIPTS)); + assert_eq!(elem.attrs().count(), 0); + + let receipt = Received { + id: String::from("coucou"), + }; + let elem: Element = receipt.into(); + assert!(elem.is("received", ns::RECEIPTS)); + assert_eq!(elem.attr("id"), Some("coucou")); + } +} diff --git a/xmpp-parsers/src/roster.rs b/xmpp-parsers/src/roster.rs new file mode 100644 index 0000000000000000000000000000000000000000..730da52df65f0b2f3d2e0a0ae9f9c607e3f1caa0 --- /dev/null +++ b/xmpp-parsers/src/roster.rs @@ -0,0 +1,337 @@ +// 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 crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; +use jid::BareJid; + +generate_elem_id!( + /// Represents a group a contact is part of. + Group, + "group", + ROSTER +); + +generate_attribute!( + /// The state of your mutual subscription with a contact. + Subscription, "subscription", { + /// The user doesn’t have any subscription to this contact’s presence, + /// and neither does this contact. + None => "none", + + /// Only this contact has a subscription with you, not the opposite. + From => "from", + + /// Only you have a subscription with this contact, not the opposite. + To => "to", + + /// Both you and your contact are subscribed to each other’s presence. + Both => "both", + + /// In a roster set, this asks the server to remove this contact item + /// from your roster. + Remove => "remove", + }, Default = None +); + +generate_attribute!( + /// The sub-state of subscription with a contact. + Ask, "ask", ( + /// Pending sub-state of the 'none' subscription state. + Subscribe => "subscribe" + ) +); + +generate_element!( + /// Contact from the user’s contact list. + #[derive(PartialEq)] + Item, "item", ROSTER, + attributes: [ + /// JID of this contact. + jid: Required = "jid", + + /// Name of this contact. + name: OptionEmpty = "name", + + /// Subscription status of this contact. + subscription: Default = "subscription", + + /// Indicates “Pending Out” sub-states for this contact. + ask: Default = "ask", + ], + + children: [ + /// Groups this contact is part of. + groups: Vec = ("group", ROSTER) => Group + ] +); + +generate_element!( + /// The contact list of the user. + Roster, "query", ROSTER, + attributes: [ + /// Version of the contact list. + /// + /// This is an opaque string that should only be sent back to the server on + /// a new connection, if this client is storing the contact list between + /// connections. + ver: Option = "ver" + ], + children: [ + /// List of the contacts of the user. + items: Vec = ("item", ROSTER) => Item + ] +); + +impl IqGetPayload for Roster {} +impl IqSetPayload for Roster {} +impl IqResultPayload for Roster {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::compare_elements::NamespaceAwareCompare; + use crate::util::error::Error; + use crate::Element; + use std::str::FromStr; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Group, 12); + assert_size!(Subscription, 1); + assert_size!(Ask, 1); + assert_size!(Item, 52); + assert_size!(Roster, 24); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Group, 24); + assert_size!(Subscription, 1); + assert_size!(Ask, 1); + assert_size!(Item, 104); + assert_size!(Roster, 48); + } + + #[test] + fn test_get() { + let elem: Element = "".parse().unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert!(roster.ver.is_none()); + assert!(roster.items.is_empty()); + } + + #[test] + fn test_result() { + let elem: Element = "".parse().unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert_eq!(roster.ver, Some(String::from("ver7"))); + assert_eq!(roster.items.len(), 2); + + let elem2: Element = "".parse().unwrap(); + let roster2 = Roster::try_from(elem2).unwrap(); + assert_eq!(roster.items, roster2.items); + + let elem: Element = "" + .parse() + .unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert_eq!(roster.ver, Some(String::from("ver9"))); + assert!(roster.items.is_empty()); + + let elem: Element = r#" + + + Friends + + + + + MyBuddies + + +"# + .parse() + .unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert_eq!(roster.ver, Some(String::from("ver11"))); + assert_eq!(roster.items.len(), 4); + assert_eq!( + roster.items[0].jid, + BareJid::from_str("romeo@example.net").unwrap() + ); + assert_eq!(roster.items[0].name, Some(String::from("Romeo"))); + assert_eq!(roster.items[0].subscription, Subscription::Both); + assert_eq!(roster.items[0].ask, Ask::None); + assert_eq!( + roster.items[0].groups, + vec!(Group::from_str("Friends").unwrap()) + ); + + assert_eq!( + roster.items[3].jid, + BareJid::from_str("contact@example.org").unwrap() + ); + assert_eq!(roster.items[3].name, Some(String::from("MyContact"))); + assert_eq!(roster.items[3].subscription, Subscription::None); + assert_eq!(roster.items[3].ask, Ask::Subscribe); + assert_eq!( + roster.items[3].groups, + vec!(Group::from_str("MyBuddies").unwrap()) + ); + } + + #[test] + fn test_multiple_groups() { + let elem: Element = r#" + + + A + B + + +"# + .parse() + .unwrap(); + let elem1 = elem.clone(); + let roster = Roster::try_from(elem).unwrap(); + assert!(roster.ver.is_none()); + assert_eq!(roster.items.len(), 1); + assert_eq!( + roster.items[0].jid, + BareJid::from_str("test@example.org").unwrap() + ); + assert_eq!(roster.items[0].name, None); + assert_eq!(roster.items[0].groups.len(), 2); + assert_eq!(roster.items[0].groups[0], Group::from_str("A").unwrap()); + assert_eq!(roster.items[0].groups[1], Group::from_str("B").unwrap()); + let elem2 = roster.into(); + assert!(elem1.compare_to(&elem2)); + } + + #[test] + fn test_set() { + let elem: Element = + "" + .parse() + .unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert!(roster.ver.is_none()); + assert_eq!(roster.items.len(), 1); + + let elem: Element = r#" + + + Servants + + +"# + .parse() + .unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert!(roster.ver.is_none()); + assert_eq!(roster.items.len(), 1); + assert_eq!( + roster.items[0].jid, + BareJid::from_str("nurse@example.com").unwrap() + ); + assert_eq!(roster.items[0].name, Some(String::from("Nurse"))); + assert_eq!(roster.items[0].groups.len(), 1); + assert_eq!( + roster.items[0].groups[0], + Group::from_str("Servants").unwrap() + ); + + let elem: Element = r#" + + + +"# + .parse() + .unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert!(roster.ver.is_none()); + assert_eq!(roster.items.len(), 1); + assert_eq!( + roster.items[0].jid, + BareJid::from_str("nurse@example.com").unwrap() + ); + assert!(roster.items[0].name.is_none()); + assert!(roster.items[0].groups.is_empty()); + assert_eq!(roster.items[0].subscription, Subscription::Remove); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Roster::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in query element."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = Roster::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in query element."); + } + + #[test] + fn test_invalid_item() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Roster::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'jid' missing."); + + /* + let elem: Element = "".parse().unwrap(); + let error = Roster::try_from(elem).unwrap_err(); + let error = match error { + Error::JidParseError(error) => error, + _ => panic!(), + }; + assert_eq!(error.description(), "Invalid JID, I guess?"); + */ + + let elem: Element = + "" + .parse() + .unwrap(); + let error = Roster::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in item element."); + } +} diff --git a/xmpp-parsers/src/rsm.rs b/xmpp-parsers/src/rsm.rs new file mode 100644 index 0000000000000000000000000000000000000000..169681d1e911f47aed85802c0cae31aaf92e12b0 --- /dev/null +++ b/xmpp-parsers/src/rsm.rs @@ -0,0 +1,310 @@ +// 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 crate::util::error::Error; +use crate::ns; +use crate::Element; +use std::convert::TryFrom; + +/// Requests paging through a potentially big set of items (represented by an +/// UID). +#[derive(Debug, Clone)] +pub struct SetQuery { + /// Limit the number of items, or use the recipient’s defaults if None. + pub max: Option, + + /// The UID after which to give results, or if None it is the element + /// “before” the first item, effectively an index of negative one. + pub after: Option, + + /// The UID before which to give results, or if None it starts with the + /// last page of the full set. + pub before: Option, + + /// Numerical index of the page (deprecated). + pub index: Option, +} + +impl TryFrom for SetQuery { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "set", RSM, "RSM set"); + let mut set = SetQuery { + max: None, + after: None, + before: None, + index: None, + }; + for child in elem.children() { + if child.is("max", ns::RSM) { + if set.max.is_some() { + return Err(Error::ParseError("Set can’t have more than one max.")); + } + set.max = Some(child.text().parse()?); + } else if child.is("after", ns::RSM) { + if set.after.is_some() { + return Err(Error::ParseError("Set can’t have more than one after.")); + } + set.after = Some(child.text()); + } else if child.is("before", ns::RSM) { + if set.before.is_some() { + return Err(Error::ParseError("Set can’t have more than one before.")); + } + set.before = Some(child.text()); + } else if child.is("index", ns::RSM) { + if set.index.is_some() { + return Err(Error::ParseError("Set can’t have more than one index.")); + } + set.index = Some(child.text().parse()?); + } else { + return Err(Error::ParseError("Unknown child in set element.")); + } + } + Ok(set) + } +} + +impl From for Element { + fn from(set: SetQuery) -> Element { + Element::builder("set") + .ns(ns::RSM) + .append_all(set.max.map(|max| { + Element::builder("max") + .ns(ns::RSM) + .append(format!("{}", max)) + })) + .append_all( + set.after + .map(|after| Element::builder("after").ns(ns::RSM).append(after)) + ) + .append_all(set.before.map(|before| { + Element::builder("before") + .ns(ns::RSM) + .append(before) + })) + .append_all(set.index.map(|index| { + Element::builder("index") + .ns(ns::RSM) + .append(format!("{}", index)) + })) + .build() + } +} + +/// Describes the paging result of a [query](struct.SetQuery.html). +#[derive(Debug, Clone)] +pub struct SetResult { + /// The UID of the first item of the page. + pub first: Option, + + /// The position of the [first item](#structfield.first) in the full set + /// (which may be approximate). + pub first_index: Option, + + /// The UID of the last item of the page. + pub last: Option, + + /// How many items there are in the full set (which may be approximate). + pub count: Option, +} + +impl TryFrom for SetResult { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "set", RSM, "RSM set"); + let mut set = SetResult { + first: None, + first_index: None, + last: None, + count: None, + }; + for child in elem.children() { + if child.is("first", ns::RSM) { + if set.first.is_some() { + return Err(Error::ParseError("Set can’t have more than one first.")); + } + set.first_index = get_attr!(child, "index", Option); + set.first = Some(child.text()); + } else if child.is("last", ns::RSM) { + if set.last.is_some() { + return Err(Error::ParseError("Set can’t have more than one last.")); + } + set.last = Some(child.text()); + } else if child.is("count", ns::RSM) { + if set.count.is_some() { + return Err(Error::ParseError("Set can’t have more than one count.")); + } + set.count = Some(child.text().parse()?); + } else { + return Err(Error::ParseError("Unknown child in set element.")); + } + } + Ok(set) + } +} + +impl From for Element { + fn from(set: SetResult) -> Element { + let first = set.first.clone().map(|first| { + Element::builder("first") + .ns(ns::RSM) + .attr("index", set.first_index) + .append(first) + }); + Element::builder("set") + .ns(ns::RSM) + .append_all(first) + .append_all( + set.last + .map(|last| Element::builder("last").ns(ns::RSM).append(last)), + ) + .append_all(set.count.map(|count| { + Element::builder("count") + .ns(ns::RSM) + .append(format!("{}", count)) + })) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::compare_elements::NamespaceAwareCompare; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(SetQuery, 40); + assert_size!(SetResult, 40); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(SetQuery, 80); + assert_size!(SetResult, 80); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let set = SetQuery::try_from(elem).unwrap(); + assert_eq!(set.max, None); + assert_eq!(set.after, None); + assert_eq!(set.before, None); + assert_eq!(set.index, None); + + let elem: Element = "" + .parse() + .unwrap(); + let set = SetResult::try_from(elem).unwrap(); + match set.first { + Some(_) => panic!(), + None => (), + } + assert_eq!(set.last, None); + assert_eq!(set.count, None); + } + + #[test] + fn test_unknown() { + let elem: Element = "" + .parse() + .unwrap(); + let error = SetQuery::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "This is not a RSM set element."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = SetResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "This is not a RSM set element."); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = SetQuery::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in set element."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = SetResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in set element."); + } + + #[test] + fn test_serialise() { + let elem: Element = "" + .parse() + .unwrap(); + let rsm = SetQuery { + max: None, + after: None, + before: None, + index: None, + }; + let elem2 = rsm.into(); + assert_eq!(elem, elem2); + + let elem: Element = "" + .parse() + .unwrap(); + let rsm = SetResult { + first: None, + first_index: None, + last: None, + count: None, + }; + let elem2 = rsm.into(); + assert_eq!(elem, elem2); + } + + #[test] + fn test_first_index() { + let elem: Element = + "coucou" + .parse() + .unwrap(); + let elem1 = elem.clone(); + let set = SetResult::try_from(elem).unwrap(); + assert_eq!(set.first, Some(String::from("coucou"))); + assert_eq!(set.first_index, Some(4)); + + let set2 = SetResult { + first: Some(String::from("coucou")), + first_index: Some(4), + last: None, + count: None, + }; + let elem2 = set2.into(); + assert!(elem1.compare_to(&elem2)); + } +} diff --git a/xmpp-parsers/src/sasl.rs b/xmpp-parsers/src/sasl.rs new file mode 100644 index 0000000000000000000000000000000000000000..e3f0900576adf0f29af36d679f68e398a16283bd --- /dev/null +++ b/xmpp-parsers/src/sasl.rs @@ -0,0 +1,308 @@ +// Copyright (c) 2018 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 crate::util::error::Error; +use crate::util::helpers::Base64; +use crate::ns; +use crate::Element; +use std::collections::BTreeMap; +use std::convert::TryFrom; + +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: Required = "mechanism" + ], + text: ( + /// The content of the handshake. + data: Base64> + ) +); + +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> + ) +); + +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> + ) +); + +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> + ) +); + +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, +} + +impl TryFrom for Failure { + type Error = Error; + + fn try_from(root: Element) -> Result { + 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, + texts, + }) + } +} + +impl From for Element { + fn from(failure: Failure) -> Element { + Element::builder("failure") + .ns(ns::SASL) + .append(failure.defined_condition) + .append_all( + failure + .texts + .into_iter() + .map(|(lang, text)| { + Element::builder("text") + .ns(ns::SASL) + .attr("xml:lang", lang) + .append(text) + }) + ) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Mechanism, 1); + assert_size!(Auth, 16); + assert_size!(Challenge, 12); + assert_size!(Response, 12); + assert_size!(Abort, 0); + assert_size!(Success, 12); + assert_size!(DefinedCondition, 1); + assert_size!(Failure, 16); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Mechanism, 1); + assert_size!(Auth, 32); + assert_size!(Challenge, 24); + assert_size!(Response, 24); + assert_size!(Abort, 0); + assert_size!(Success, 24); + assert_size!(DefinedCondition, 1); + assert_size!(Failure, 32); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let auth = Auth::try_from(elem).unwrap(); + assert_eq!(auth.mechanism, Mechanism::Plain); + assert!(auth.data.is_empty()); + } + + #[test] + fn section_6_5_1() { + let elem: Element = + "" + .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 = " + + Call 212-555-1212 for assistance. + " + .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.") + ); + } + + /// Some servers apparently use a non-namespaced 'lang' attribute, which is invalid as not part + /// of the schema. This tests whether we can parse it when disabling validation. + #[cfg(feature = "disable-validation")] + #[test] + fn invalid_failure_with_non_prefixed_text_lang() { + let elem: Element = " + + Invalid username or password + " + .parse() + .unwrap(); + let failure = Failure::try_from(elem).unwrap(); + assert_eq!(failure.defined_condition, DefinedCondition::NotAuthorized); + assert_eq!( + failure.texts[""], + String::from("Invalid username or password") + ); + } +} diff --git a/xmpp-parsers/src/server_info.rs b/xmpp-parsers/src/server_info.rs new file mode 100644 index 0000000000000000000000000000000000000000..3ad9e0008786092f03391d6a1617095447ec54dc --- /dev/null +++ b/xmpp-parsers/src/server_info.rs @@ -0,0 +1,209 @@ +// Copyright (C) 2019 Maxime “pep” Buquet +// 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 crate::data_forms::{DataForm, DataFormType, Field, FieldType}; +use crate::ns; +use crate::util::error::Error; +use std::convert::TryFrom; + +/// Structure representing a `http://jabber.org/network/serverinfo` form type. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ServerInfo { + /// Abuse addresses + pub abuse: Vec, + + /// Admin addresses + pub admin: Vec, + + /// Feedback addresses + pub feedback: Vec, + + /// Sales addresses + pub sales: Vec, + + /// Security addresses + pub security: Vec, + + /// Support addresses + pub support: Vec, +} + +impl TryFrom for ServerInfo { + type Error = Error; + + fn try_from(form: DataForm) -> Result { + if form.type_ != DataFormType::Result_ { + return Err(Error::ParseError("Wrong type of form.")); + } + if form.form_type != Some(String::from(ns::SERVER_INFO)) { + return Err(Error::ParseError("Wrong FORM_TYPE for form.")); + } + let mut server_info = ServerInfo::default(); + for field in form.fields { + if field.type_ != FieldType::ListMulti { + return Err(Error::ParseError("Field is not of the required type.")); + } + if field.var == "abuse-addresses" { + server_info.abuse = field.values; + } else if field.var == "admin-addresses" { + server_info.admin = field.values; + } else if field.var == "feedback-addresses" { + server_info.feedback = field.values; + } else if field.var == "sales-addresses" { + server_info.sales = field.values; + } else if field.var == "security-addresses" { + server_info.security = field.values; + } else if field.var == "support-addresses" { + server_info.support = field.values; + } else { + return Err(Error::ParseError("Unknown form field var.")); + } + } + + Ok(server_info) + } +} + +impl From for DataForm { + fn from(server_info: ServerInfo) -> DataForm { + DataForm { + type_: DataFormType::Result_, + form_type: Some(String::from(ns::SERVER_INFO)), + title: None, + instructions: None, + fields: vec![ + generate_address_field("abuse-addresses", server_info.abuse), + generate_address_field("admin-addresses", server_info.admin), + generate_address_field("feedback-addresses", server_info.feedback), + generate_address_field("sales-addresses", server_info.sales), + generate_address_field("security-addresses", server_info.security), + generate_address_field("support-addresses", server_info.support), + ], + } + } +} + +/// Generate `Field` for addresses +pub fn generate_address_field>(var: S, values: Vec) -> Field { + Field { + var: var.into(), + type_: FieldType::ListMulti, + label: None, + required: false, + options: vec![], + values, + media: vec![], + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_forms::{DataForm, DataFormType, Field, FieldType}; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(ServerInfo, 72); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(ServerInfo, 144); + } + + #[test] + fn test_simple() { + let form = DataForm { + type_: DataFormType::Result_, + form_type: Some(String::from(ns::SERVER_INFO)), + title: None, + instructions: None, + fields: vec![ + Field { + var: String::from("abuse-addresses"), + type_: FieldType::ListMulti, + label: None, + required: false, + options: vec![], + values: vec![], + media: vec![], + }, + Field { + var: String::from("admin-addresses"), + type_: FieldType::ListMulti, + label: None, + required: false, + options: vec![], + values: vec![ + String::from("xmpp:admin@foo.bar"), + String::from("https://foo.bar/chat/"), + String::from("mailto:admin@foo.bar"), + ], + media: vec![], + }, + Field { + var: String::from("feedback-addresses"), + type_: FieldType::ListMulti, + label: None, + required: false, + options: vec![], + values: vec![], + media: vec![], + }, + Field { + var: String::from("sales-addresses"), + type_: FieldType::ListMulti, + label: None, + required: false, + options: vec![], + values: vec![], + media: vec![], + }, + Field { + var: String::from("security-addresses"), + type_: FieldType::ListMulti, + label: None, + required: false, + options: vec![], + values: vec![ + String::from("xmpp:security@foo.bar"), + String::from("mailto:security@foo.bar"), + ], + media: vec![], + }, + Field { + var: String::from("support-addresses"), + type_: FieldType::ListMulti, + label: None, + required: false, + options: vec![], + values: vec![String::from("mailto:support@foo.bar")], + media: vec![], + }, + ], + }; + + let server_info = ServerInfo { + abuse: vec![], + admin: vec![ + String::from("xmpp:admin@foo.bar"), + String::from("https://foo.bar/chat/"), + String::from("mailto:admin@foo.bar"), + ], + feedback: vec![], + sales: vec![], + security: vec![ + String::from("xmpp:security@foo.bar"), + String::from("mailto:security@foo.bar"), + ], + support: vec![String::from("mailto:support@foo.bar")], + }; + + // assert_eq!(DataForm::from(server_info), form); + assert_eq!(ServerInfo::try_from(form).unwrap(), server_info); + } +} diff --git a/xmpp-parsers/src/sm.rs b/xmpp-parsers/src/sm.rs new file mode 100644 index 0000000000000000000000000000000000000000..a78320f22c54b2a33d64f1be1646a67cd5e580d6 --- /dev/null +++ b/xmpp-parsers/src/sm.rs @@ -0,0 +1,227 @@ +// Copyright (c) 2018 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 crate::stanza_error::DefinedCondition; + +generate_element!( + /// Acknowledgement of the currently received stanzas. + A, "a", SM, + attributes: [ + /// The last handled stanza. + h: Required = "h", + ] +); + +impl A { + /// Generates a new `` element. + pub fn new(h: u32) -> A { + A { h } + } +} + +generate_attribute!( + /// Whether to allow resumption of a previous stream. + ResumeAttr, + "resume", + bool +); + +generate_element!( + /// Client request for enabling stream management. + #[derive(Default)] + Enable, "enable", SM, + attributes: [ + /// The preferred resumption time in seconds by the client. + // TODO: should be the infinite integer set ≥ 1. + max: Option = "max", + + /// Whether the client wants to be allowed to resume the stream. + resume: Default = "resume", + ] +); + +impl Enable { + /// Generates a new `` element. + pub fn new() -> Self { + Enable::default() + } + + /// Sets the preferred resumption time in seconds. + pub fn with_max(mut self, max: u32) -> Self { + self.max = Some(max); + self + } + + /// Asks for resumption to be possible. + pub fn with_resume(mut self) -> Self { + self.resume = ResumeAttr::True; + self + } +} + +generate_id!( + /// A random identifier used for stream resumption. + StreamId +); + +generate_element!( + /// Server response once stream management is enabled. + Enabled, "enabled", SM, + attributes: [ + /// A random identifier used for stream resumption. + id: Option = "id", + + /// The preferred IP, domain, IP:port or domain:port location for + /// resumption. + location: Option = "location", + + /// The preferred resumption time in seconds by the server. + // TODO: should be the infinite integer set ≥ 1. + max: Option = "max", + + /// Whether stream resumption is allowed. + resume: Default = "resume", + ] +); + +generate_element!( + /// A stream management error happened. + Failed, "failed", SM, + attributes: [ + /// The last handled stanza. + h: Option = "h", + ], + children: [ + /// The error returned. + // XXX: implement the * handling. + error: Option = ("*", XMPP_STANZAS) => DefinedCondition + ] +); + +generate_empty_element!( + /// Requests the currently received stanzas by the other party. + R, + "r", + SM +); + +generate_element!( + /// Requests a stream resumption. + Resume, "resume", SM, + attributes: [ + /// The last handled stanza. + h: Required = "h", + + /// The previous id given by the server on + /// [enabled](struct.Enabled.html). + previd: Required = "previd", + ] +); + +generate_element!( + /// The response by the server for a successfully resumed stream. + Resumed, "resumed", SM, + attributes: [ + /// The last handled stanza. + h: Required = "h", + + /// The previous id given by the server on + /// [enabled](struct.Enabled.html). + previd: Required = "previd", + ] +); + +// TODO: add support for optional and required. +generate_empty_element!( + /// Represents availability of Stream Management in ``. + StreamManagement, + "sm", + SM +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(A, 4); + assert_size!(ResumeAttr, 1); + assert_size!(Enable, 12); + assert_size!(StreamId, 12); + assert_size!(Enabled, 36); + assert_size!(Failed, 12); + assert_size!(R, 0); + assert_size!(Resume, 16); + assert_size!(Resumed, 16); + assert_size!(StreamManagement, 0); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(A, 4); + assert_size!(ResumeAttr, 1); + assert_size!(Enable, 12); + assert_size!(StreamId, 24); + assert_size!(Enabled, 64); + assert_size!(Failed, 12); + assert_size!(R, 0); + assert_size!(Resume, 32); + assert_size!(Resumed, 32); + assert_size!(StreamManagement, 0); + } + + #[test] + fn a() { + let elem: Element = " +// +// 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 crate::util::error::Error; +use crate::message::MessagePayload; +use crate::ns; +use crate::presence::PresencePayload; +use jid::Jid; +use crate::Element; +use std::collections::BTreeMap; +use std::convert::TryFrom; + +generate_attribute!( + /// The type of the error. + ErrorType, "type", { + /// Retry after providing credentials. + Auth => "auth", + + /// Do not retry (the error cannot be remedied). + Cancel => "cancel", + + /// Proceed (the condition was only a warning). + Continue => "continue", + + /// Retry after changing the data sent. + Modify => "modify", + + /// Retry after waiting (the error is temporary). + Wait => "wait", + } +); + +generate_element_enum!( + /// List of valid error conditions. + DefinedCondition, "condition", XMPP_STANZAS, { + /// The sender has sent a stanza containing XML that does not conform + /// to the appropriate schema or that cannot be processed (e.g., an IQ + /// stanza that includes an unrecognized value of the 'type' attribute, + /// or an element that is qualified by a recognized namespace but that + /// violates the defined syntax for the element); the associated error + /// type SHOULD be "modify". + BadRequest => "bad-request", + + /// Access cannot be granted because an existing resource exists with + /// the same name or address; the associated error type SHOULD be + /// "cancel". + Conflict => "conflict", + + /// The feature represented in the XML stanza is not implemented by the + /// intended recipient or an intermediate server and therefore the + /// stanza cannot be processed (e.g., the entity understands the + /// namespace but does not recognize the element name); the associated + /// error type SHOULD be "cancel" or "modify". + FeatureNotImplemented => "feature-not-implemented", + + /// The requesting entity does not possess the necessary permissions to + /// perform an action that only certain authorized roles or individuals + /// are allowed to complete (i.e., it typically relates to + /// authorization rather than authentication); the associated error + /// type SHOULD be "auth". + Forbidden => "forbidden", + + /// The recipient or server can no longer be contacted at this address, + /// typically on a permanent basis (as opposed to the error + /// condition, which is used for temporary addressing failures); the + /// associated error type SHOULD be "cancel" and the error stanza + /// SHOULD include a new address (if available) as the XML character + /// data of the element (which MUST be a Uniform Resource + /// Identifier [URI] or Internationalized Resource Identifier [IRI] at + /// which the entity can be contacted, typically an XMPP IRI as + /// specified in [XMPP‑URI]). + Gone => "gone", + + /// The server has experienced a misconfiguration or other internal + /// error that prevents it from processing the stanza; the associated + /// error type SHOULD be "cancel". + InternalServerError => "internal-server-error", + + /// The addressed JID or item requested cannot be found; the associated + /// error type SHOULD be "cancel". + ItemNotFound => "item-not-found", + + /// The sending entity has provided (e.g., during resource binding) or + /// communicated (e.g., in the 'to' address of a stanza) an XMPP + /// address or aspect thereof that violates the rules defined in + /// [XMPP‑ADDR]; the associated error type SHOULD be "modify". + JidMalformed => "jid-malformed", + + /// The recipient or server understands the request but cannot process + /// it because the request does not meet criteria defined by the + /// recipient or server (e.g., a request to subscribe to information + /// that does not simultaneously include configuration parameters + /// needed by the recipient); the associated error type SHOULD be + /// "modify". + NotAcceptable => "not-acceptable", + + /// The recipient or server does not allow any entity to perform the + /// action (e.g., sending to entities at a blacklisted domain); the + /// associated error type SHOULD be "cancel". + NotAllowed => "not-allowed", + + /// The sender needs to provide credentials before being allowed to + /// perform the action, or has provided improper credentials (the name + /// "not-authorized", which was borrowed from the "401 Unauthorized" + /// error of [HTTP], might lead the reader to think that this condition + /// relates to authorization, but instead it is typically used in + /// relation to authentication); the associated error type SHOULD be + /// "auth". + NotAuthorized => "not-authorized", + + /// The entity has violated some local service policy (e.g., a message + /// contains words that are prohibited by the service) and the server + /// MAY choose to specify the policy in the element or in an + /// application-specific condition element; the associated error type + /// SHOULD be "modify" or "wait" depending on the policy being + /// violated. + PolicyViolation => "policy-violation", + + /// The intended recipient is temporarily unavailable, undergoing + /// maintenance, etc.; the associated error type SHOULD be "wait". + RecipientUnavailable => "recipient-unavailable", + + /// The recipient or server is redirecting requests for this + /// information to another entity, typically in a temporary fashion (as + /// opposed to the error condition, which is used for permanent + /// addressing failures); the associated error type SHOULD be "modify" + /// and the error stanza SHOULD contain the alternate address in the + /// XML character data of the element (which MUST be a URI + /// or IRI with which the sender can communicate, typically an XMPP IRI + /// as specified in [XMPP‑URI]). + Redirect => "redirect", + + /// The requesting entity is not authorized to access the requested + /// service because prior registration is necessary (examples of prior + /// registration include members-only rooms in XMPP multi-user chat + /// [XEP‑0045] and gateways to non-XMPP instant messaging services, + /// which traditionally required registration in order to use the + /// gateway [XEP‑0100]); the associated error type SHOULD be "auth". + RegistrationRequired => "registration-required", + + /// A remote server or service specified as part or all of the JID of + /// the intended recipient does not exist or cannot be resolved (e.g., + /// there is no _xmpp-server._tcp DNS SRV record, the A or AAAA + /// fallback resolution fails, or A/AAAA lookups succeed but there is + /// no response on the IANA-registered port 5269); the associated error + /// type SHOULD be "cancel". + RemoteServerNotFound => "remote-server-not-found", + + /// A remote server or service specified as part or all of the JID of + /// the intended recipient (or needed to fulfill a request) was + /// resolved but communications could not be established within a + /// reasonable amount of time (e.g., an XML stream cannot be + /// established at the resolved IP address and port, or an XML stream + /// can be established but stream negotiation fails because of problems + /// with TLS, SASL, Server Dialback, etc.); the associated error type + /// SHOULD be "wait" (unless the error is of a more permanent nature, + /// e.g., the remote server is found but it cannot be authenticated or + /// it violates security policies). + RemoteServerTimeout => "remote-server-timeout", + + /// The server or recipient is busy or lacks the system resources + /// necessary to service the request; the associated error type SHOULD + /// be "wait". + ResourceConstraint => "resource-constraint", + + /// The server or recipient does not currently provide the requested + /// service; the associated error type SHOULD be "cancel". + ServiceUnavailable => "service-unavailable", + + /// The requesting entity is not authorized to access the requested + /// service because a prior subscription is necessary (examples of + /// prior subscription include authorization to receive presence + /// information as defined in [XMPP‑IM] and opt-in data feeds for XMPP + /// publish-subscribe as defined in [XEP‑0060]); the associated error + /// type SHOULD be "auth". + SubscriptionRequired => "subscription-required", + + /// The error condition is not one of those defined by the other + /// conditions in this list; any error type can be associated with this + /// condition, and it SHOULD NOT be used except in conjunction with an + /// application-specific condition. + UndefinedCondition => "undefined-condition", + + /// The recipient or server understood the request but was not + /// expecting it at this time (e.g., the request was out of order); the + /// associated error type SHOULD be "wait" or "modify". + UnexpectedRequest => "unexpected-request", + } +); + +type Lang = String; + +/// The representation of a stanza error. +#[derive(Debug, Clone)] +pub struct StanzaError { + /// The type of this error. + pub type_: ErrorType, + + /// The JID of the entity who set this error. + pub by: Option, + + /// One of the defined conditions for this error to happen. + pub defined_condition: DefinedCondition, + + /// Human-readable description of this error. + pub texts: BTreeMap, + + /// A protocol-specific extension for this error. + pub other: Option, +} + +impl MessagePayload for StanzaError {} +impl PresencePayload for StanzaError {} + +impl StanzaError { + /// Create a new `` with the according content. + pub fn new(type_: ErrorType, defined_condition: DefinedCondition, lang: L, text: T) -> StanzaError + where L: Into, + T: Into, + { + StanzaError { + type_, + by: None, + defined_condition, + texts: { + let mut map = BTreeMap::new(); + map.insert(lang.into(), text.into()); + map + }, + other: None, + } + } +} + +impl TryFrom for StanzaError { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "error", DEFAULT_NS); + check_no_unknown_attributes!(elem, "error", ["type", "by"]); + + let mut stanza_error = StanzaError { + type_: get_attr!(elem, "type", Required), + by: get_attr!(elem, "by", Option), + defined_condition: DefinedCondition::UndefinedCondition, + texts: BTreeMap::new(), + other: None, + }; + let mut defined_condition = None; + + for child in elem.children() { + if child.is("text", ns::XMPP_STANZAS) { + check_no_children!(child, "text"); + check_no_unknown_attributes!(child, "text", ["xml:lang"]); + let lang = get_attr!(elem, "xml:lang", Default); + if stanza_error.texts.insert(lang, child.text()).is_some() { + return Err(Error::ParseError( + "Text element present twice for the same xml:lang.", + )); + } + } else if child.has_ns(ns::XMPP_STANZAS) { + if defined_condition.is_some() { + return Err(Error::ParseError( + "Error must not have more than one defined-condition.", + )); + } + check_no_children!(child, "defined-condition"); + check_no_attributes!(child, "defined-condition"); + let condition = DefinedCondition::try_from(child.clone())?; + defined_condition = Some(condition); + } else { + if stanza_error.other.is_some() { + return Err(Error::ParseError( + "Error must not have more than one other element.", + )); + } + stanza_error.other = Some(child.clone()); + } + } + stanza_error.defined_condition = + defined_condition.ok_or(Error::ParseError("Error must have a defined-condition."))?; + + Ok(stanza_error) + } +} + +impl From for Element { + fn from(err: StanzaError) -> Element { + Element::builder("error") + .ns(ns::DEFAULT_NS) + .attr("type", err.type_) + .attr("by", err.by) + .append(err.defined_condition) + .append_all( + err.texts.into_iter().map(|(lang, text)| { + Element::builder("text") + .ns(ns::XMPP_STANZAS) + .attr("xml:lang", lang) + .append(text) + }) + ) + .append_all(err.other) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(ErrorType, 1); + assert_size!(DefinedCondition, 1); + assert_size!(StanzaError, 108); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(ErrorType, 1); + assert_size!(DefinedCondition, 1); + assert_size!(StanzaError, 216); + } + + #[test] + fn test_simple() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "".parse().unwrap(); + let error = StanzaError::try_from(elem).unwrap(); + assert_eq!(error.type_, ErrorType::Cancel); + assert_eq!( + error.defined_condition, + DefinedCondition::UndefinedCondition + ); + } + + #[test] + fn test_invalid_type() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "".parse().unwrap(); + let error = StanzaError::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'type' missing."); + + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let error = StanzaError::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'type' attribute."); + } + + #[test] + fn test_invalid_condition() { + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let error = StanzaError::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Error must have a defined-condition."); + } +} diff --git a/xmpp-parsers/src/stanza_id.rs b/xmpp-parsers/src/stanza_id.rs new file mode 100644 index 0000000000000000000000000000000000000000..1ba48f85da115c69d93a790b021b0eee6fd1e418 --- /dev/null +++ b/xmpp-parsers/src/stanza_id.rs @@ -0,0 +1,124 @@ +// 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 crate::message::MessagePayload; +use jid::Jid; + +generate_element!( + /// Gives the identifier a service has stamped on this stanza, often in + /// order to identify it inside of [an archive](../mam/index.html). + StanzaId, "stanza-id", SID, + attributes: [ + /// The id associated to this stanza by another entity. + id: Required = "id", + + /// The entity who stamped this stanza-id. + by: Required = "by", + ] +); + +impl MessagePayload for StanzaId {} + +generate_element!( + /// A hack for MUC before version 1.31 to track a message which may have + /// its 'id' attribute changed. + OriginId, "origin-id", SID, + attributes: [ + /// The id this client set for this stanza. + id: Required = "id", + ] +); + +impl MessagePayload for OriginId {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::str::FromStr; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(StanzaId, 52); + assert_size!(OriginId, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(StanzaId, 104); + assert_size!(OriginId, 24); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let stanza_id = StanzaId::try_from(elem).unwrap(); + assert_eq!(stanza_id.id, String::from("coucou")); + assert_eq!(stanza_id.by, Jid::from_str("coucou@coucou").unwrap()); + + let elem: Element = "" + .parse() + .unwrap(); + let origin_id = OriginId::try_from(elem).unwrap(); + assert_eq!(origin_id.id, String::from("coucou")); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = StanzaId::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in stanza-id element."); + } + + #[test] + fn test_invalid_id() { + let elem: Element = "".parse().unwrap(); + let error = StanzaId::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'id' missing."); + } + + #[test] + fn test_invalid_by() { + let elem: Element = "" + .parse() + .unwrap(); + let error = StanzaId::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'by' missing."); + } + + #[test] + fn test_serialise() { + let elem: Element = "" + .parse() + .unwrap(); + let stanza_id = StanzaId { + id: String::from("coucou"), + by: Jid::from_str("coucou@coucou").unwrap(), + }; + let elem2 = stanza_id.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/stream.rs b/xmpp-parsers/src/stream.rs new file mode 100644 index 0000000000000000000000000000000000000000..c1b5e1ea6276aedb5523e79e48f345ce5c284ef7 --- /dev/null +++ b/xmpp-parsers/src/stream.rs @@ -0,0 +1,101 @@ +// Copyright (c) 2018 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 jid::BareJid; + +generate_element!( + /// The stream opening for client-server communications. + Stream, "stream", STREAM, + attributes: [ + /// The JID of the entity opening this stream. + from: Option = "from", + + /// The JID of the entity receiving this stream opening. + to: Option = "to", + + /// The id of the stream, used for authentication challenges. + id: Option = "id", + + /// The XMPP version used during this stream. + version: Option = "version", + + /// The default human language for all subsequent stanzas, which will + /// be transmitted to other entities for better localisation. + xml_lang: Option = "xml:lang", + ] +); + +impl Stream { + /// Creates a simple client→server `` element. + pub fn new(to: BareJid) -> Stream { + Stream { + from: None, + to: Some(to), + id: None, + version: Some(String::from("1.0")), + xml_lang: None, + } + } + + /// Sets the [@from](#structfield.from) attribute on this `` + /// element. + pub fn with_from(mut self, from: BareJid) -> Stream { + self.from = Some(from); + self + } + + /// Sets the [@id](#structfield.id) attribute on this `` + /// element. + pub fn with_id(mut self, id: String) -> Stream { + self.id = Some(id); + self + } + + /// Sets the [@xml:lang](#structfield.xml_lang) attribute on this + /// `` element. + pub fn with_lang(mut self, xml_lang: String) -> Stream { + self.xml_lang = Some(xml_lang); + self + } + + /// Checks whether the version matches the expected one. + pub fn is_version(&self, version: &str) -> bool { + match self.version { + None => false, + Some(ref self_version) => self_version == &String::from(version), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Stream, 84); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Stream, 168); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + let stream = Stream::try_from(elem).unwrap(); + assert_eq!(stream.from, Some(BareJid::domain("some-server.example"))); + assert_eq!(stream.to, None); + assert_eq!(stream.id, Some(String::from("abc"))); + assert_eq!(stream.version, Some(String::from("1.0"))); + assert_eq!(stream.xml_lang, Some(String::from("en"))); + } +} diff --git a/xmpp-parsers/src/time.rs b/xmpp-parsers/src/time.rs new file mode 100644 index 0000000000000000000000000000000000000000..363cd210890eb6de2f0427ad02e4058c17d56fe5 --- /dev/null +++ b/xmpp-parsers/src/time.rs @@ -0,0 +1,112 @@ +// Copyright (c) 2019 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 chrono::FixedOffset; +use crate::date::DateTime; +use crate::iq::{IqGetPayload, IqResultPayload}; +use crate::ns; +use crate::util::error::Error; +use crate::Element; +use std::convert::TryFrom; +use std::str::FromStr; + +generate_empty_element!( + /// An entity time query. + TimeQuery, "time", TIME +); + +impl IqGetPayload for TimeQuery {} + +/// An entity time result, containing an unique DateTime. +#[derive(Debug, Clone)] +pub struct TimeResult(pub DateTime); + +impl IqResultPayload for TimeResult {} + +impl TryFrom for TimeResult { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "time", TIME); + check_no_attributes!(elem, "time"); + + let mut tzo = None; + let mut utc = None; + + for child in elem.children() { + if child.is("tzo", ns::TIME) { + if tzo.is_some() { + return Err(Error::ParseError("More than one tzo element in time.")); + } + check_no_children!(child, "tzo"); + check_no_attributes!(child, "tzo"); + // TODO: Add a FromStr implementation to FixedOffset to avoid this hack. + let fake_date = String::from("2019-04-22T11:38:00") + &child.text(); + let date_time = DateTime::from_str(&fake_date)?; + tzo = Some(date_time.timezone()); + } else if child.is("utc", ns::TIME) { + if utc.is_some() { + return Err(Error::ParseError("More than one utc element in time.")); + } + check_no_children!(child, "utc"); + check_no_attributes!(child, "utc"); + let date_time = DateTime::from_str(&child.text())?; + if date_time.timezone() != FixedOffset::east(0) { + return Err(Error::ParseError("Non-UTC timezone for utc element.")); + } + utc = Some(date_time); + } else { + return Err(Error::ParseError( + "Unknown child in time element.", + )); + } + } + + let tzo = tzo.ok_or(Error::ParseError("Missing tzo child in time element."))?; + let utc = utc.ok_or(Error::ParseError("Missing utc child in time element."))?; + let date = utc.with_timezone(tzo); + + Ok(TimeResult(date)) + } +} + +impl From for Element { + fn from(time: TimeResult) -> Element { + Element::builder("time") + .ns(ns::TIME) + .append(Element::builder("tzo") + .append(format!("{}", time.0.timezone()))) + .append(Element::builder("utc") + .append(time.0.with_timezone(FixedOffset::east(0)).format("%FT%TZ"))) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // DateTime’s size doesn’t depend on the architecture. + #[test] + fn test_size() { + assert_size!(TimeQuery, 0); + assert_size!(TimeResult, 16); + } + + #[test] + fn parse_response() { + let elem: Element = + "" + .parse() + .unwrap(); + let elem1 = elem.clone(); + let time = TimeResult::try_from(elem).unwrap(); + assert_eq!(time.0.timezone(), FixedOffset::west(6 * 3600)); + assert_eq!(time.0, DateTime::from_str("2006-12-19T12:58:35-05:00").unwrap()); + let elem2 = Element::from(time); + assert_eq!(elem1, elem2); + } +} diff --git a/xmpp-parsers/src/tune.rs b/xmpp-parsers/src/tune.rs new file mode 100644 index 0000000000000000000000000000000000000000..ed35357d8bef5e9b4e7fdd7854e6de13bd516e1a --- /dev/null +++ b/xmpp-parsers/src/tune.rs @@ -0,0 +1,228 @@ +// Copyright (c) 2019 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 crate::util::error::Error; +use crate::pubsub::PubSubPayload; +use crate::ns; +use crate::Element; +use std::convert::TryFrom; + +generate_elem_id!( + /// The artist or performer of the song or piece. + Artist, "artist", TUNE +); + +generate_elem_id!( + /// The duration of the song or piece in seconds. + Length, "length", TUNE, + u16 +); + +generate_elem_id!( + /// The user's rating of the song or piece, from 1 (lowest) to 10 (highest). + Rating, "rating", TUNE, + u8 +); + +generate_elem_id!( + /// The collection (e.g., album) or other source (e.g., a band website that hosts streams or + /// audio files). + Source, "source", TUNE +); + +generate_elem_id!( + /// The title of the song or piece. + Title, "title", TUNE +); + +generate_elem_id!( + /// A unique identifier for the tune; e.g., the track number within a collection or the + /// specific URI for the object (e.g., a stream or audio file). + Track, "track", TUNE +); + +generate_elem_id!( + /// A URI or URL pointing to information about the song, collection, or artist. + Uri, "uri", TUNE +); + +/// Container for formatted text. +#[derive(Debug, Clone)] +pub struct Tune { + /// The artist or performer of the song or piece. + artist: Option, + + /// The duration of the song or piece in seconds. + length: Option, + + /// The user's rating of the song or piece, from 1 (lowest) to 10 (highest). + rating: Option, + + /// The collection (e.g., album) or other source (e.g., a band website that hosts streams or + /// audio files). + source: Option, + + /// The title of the song or piece. + title: Option, + + /// A unique identifier for the tune; e.g., the track number within a collection or the + /// specific URI for the object (e.g., a stream or audio file). + track: Option<Track>, + + /// A URI or URL pointing to information about the song, collection, or artist. + uri: Option<Uri>, +} + +impl PubSubPayload for Tune {} + +impl Tune { + fn new() -> Tune { + Tune { + artist: None, + length: None, + rating: None, + source: None, + title: None, + track: None, + uri: None, + } + } +} + +impl TryFrom<Element> for Tune { + type Error = Error; + + fn try_from(elem: Element) -> Result<Tune, Error> { + check_self!(elem, "tune", TUNE); + check_no_attributes!(elem, "tune"); + + let mut tune = Tune::new(); + for child in elem.children() { + if child.is("artist", ns::TUNE) { + if tune.artist.is_some() { + return Err(Error::ParseError("Tune can’t have more than one artist.")); + } + tune.artist = Some(Artist::try_from(child.clone())?); + } else if child.is("length", ns::TUNE) { + if tune.length.is_some() { + return Err(Error::ParseError("Tune can’t have more than one length.")); + } + tune.length = Some(Length::try_from(child.clone())?); + } else if child.is("rating", ns::TUNE) { + if tune.rating.is_some() { + return Err(Error::ParseError("Tune can’t have more than one rating.")); + } + tune.rating = Some(Rating::try_from(child.clone())?); + } else if child.is("source", ns::TUNE) { + if tune.source.is_some() { + return Err(Error::ParseError("Tune can’t have more than one source.")); + } + tune.source = Some(Source::try_from(child.clone())?); + } else if child.is("title", ns::TUNE) { + if tune.title.is_some() { + return Err(Error::ParseError("Tune can’t have more than one title.")); + } + tune.title = Some(Title::try_from(child.clone())?); + } else if child.is("track", ns::TUNE) { + if tune.track.is_some() { + return Err(Error::ParseError("Tune can’t have more than one track.")); + } + tune.track = Some(Track::try_from(child.clone())?); + } else if child.is("uri", ns::TUNE) { + if tune.uri.is_some() { + return Err(Error::ParseError("Tune can’t have more than one uri.")); + } + tune.uri = Some(Uri::try_from(child.clone())?); + } else { + return Err(Error::ParseError("Unknown element in User Tune.")); + } + } + + Ok(tune) + } +} + +impl From<Tune> for Element { + fn from(tune: Tune) -> Element { + Element::builder("tune") + .ns(ns::TUNE) + .append_all(tune.artist) + .append_all(tune.length) + .append_all(tune.rating) + .append_all(tune.source) + .append_all(tune.title) + .append_all(tune.track) + .append_all(tune.uri) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[cfg(target_pointer_width = "32")] + #[test] + #[ignore] + fn test_size() { + assert_size!(Tune, 64); + assert_size!(Artist, 12); + assert_size!(Length, 2); + assert_size!(Rating, 1); + assert_size!(Source, 12); + assert_size!(Title, 12); + assert_size!(Track, 12); + assert_size!(Uri, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Tune, 128); + assert_size!(Artist, 24); + assert_size!(Length, 2); + assert_size!(Rating, 1); + assert_size!(Source, 24); + assert_size!(Title, 24); + assert_size!(Track, 24); + assert_size!(Uri, 24); + } + + #[test] + fn empty() { + let elem: Element = "<tune xmlns='http://jabber.org/protocol/tune'/>" + .parse() + .unwrap(); + let elem2 = elem.clone(); + let tune = Tune::try_from(elem).unwrap(); + assert!(tune.artist.is_none()); + assert!(tune.length.is_none()); + assert!(tune.rating.is_none()); + assert!(tune.source.is_none()); + assert!(tune.title.is_none()); + assert!(tune.track.is_none()); + assert!(tune.uri.is_none()); + + let elem3 = tune.into(); + assert_eq!(elem2, elem3); + } + + #[test] + fn full() { + let elem: Element = "<tune xmlns='http://jabber.org/protocol/tune'><artist>Yes</artist><length>686</length><rating>8</rating><source>Yessongs</source><title>Heart of the Sunrise3http://www.yesworld.com/lyrics/Fragile.html#9" + .parse() + .unwrap(); + let tune = Tune::try_from(elem).unwrap(); + assert_eq!(tune.artist, Some(Artist::from_str("Yes").unwrap())); + assert_eq!(tune.length, Some(Length(686))); + assert_eq!(tune.rating, Some(Rating(8))); + assert_eq!(tune.source, Some(Source::from_str("Yessongs").unwrap())); + assert_eq!(tune.title, Some(Title::from_str("Heart of the Sunrise").unwrap())); + assert_eq!(tune.track, Some(Track::from_str("3").unwrap())); + assert_eq!(tune.uri, Some(Uri::from_str("http://www.yesworld.com/lyrics/Fragile.html#9").unwrap())); + } +} diff --git a/xmpp-parsers/src/util/compare_elements.rs b/xmpp-parsers/src/util/compare_elements.rs new file mode 100644 index 0000000000000000000000000000000000000000..7494be7bf2fd14a724bb0e683227b143840c8328 --- /dev/null +++ b/xmpp-parsers/src/util/compare_elements.rs @@ -0,0 +1,125 @@ +// Copyright (c) 2017 Astro +// +// 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 minidom::{Element, Node}; + +pub trait NamespaceAwareCompare { + /// Namespace-aware comparison for tests + fn compare_to(&self, other: &Self) -> bool; +} + +impl NamespaceAwareCompare for Node { + fn compare_to(&self, other: &Self) -> bool { + match (self, other) { + (&Node::Element(ref elem1), &Node::Element(ref elem2)) => { + Element::compare_to(elem1, elem2) + } + (&Node::Text(ref text1), &Node::Text(ref text2)) => text1 == text2, + _ => false, + } + } +} + +impl NamespaceAwareCompare for Element { + fn compare_to(&self, other: &Self) -> bool { + if self.name() == other.name() && self.ns() == other.ns() && self.attrs().eq(other.attrs()) + { + let child_elems = self.children().count(); + let text_is_whitespace = self + .texts() + .all(|text| text.chars().all(char::is_whitespace)); + if child_elems > 0 && text_is_whitespace { + // Ignore all the whitespace text nodes + self.children() + .zip(other.children()) + .all(|(node1, node2)| node1.compare_to(node2)) + } else { + // Compare with text nodes + self.nodes() + .zip(other.nodes()) + .all(|(node1, node2)| node1.compare_to(node2)) + } + } else { + false + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + + #[test] + fn simple() { + let elem1: Element = "x 3".parse().unwrap(); + let elem2: Element = "x 3".parse().unwrap(); + assert!(elem1.compare_to(&elem2)); + } + + #[test] + fn wrong_attr_name() { + let elem1: Element = "x 3".parse().unwrap(); + let elem2: Element = "x 3".parse().unwrap(); + assert!(!elem1.compare_to(&elem2)); + } + + #[test] + fn wrong_attr_value() { + let elem1: Element = "x 3".parse().unwrap(); + let elem2: Element = "x 3".parse().unwrap(); + assert!(!elem1.compare_to(&elem2)); + } + + #[test] + fn attr_order() { + let elem1: Element = "".parse().unwrap(); + let elem2: Element = "".parse().unwrap(); + assert!(elem1.compare_to(&elem2)); + } + + #[test] + fn wrong_texts() { + let elem1: Element = "foo".parse().unwrap(); + let elem2: Element = "bar".parse().unwrap(); + assert!(!elem1.compare_to(&elem2)); + } + + #[test] + fn children() { + let elem1: Element = "".parse().unwrap(); + let elem2: Element = "".parse().unwrap(); + assert!(elem1.compare_to(&elem2)); + } + + #[test] + fn wrong_children() { + let elem1: Element = "".parse().unwrap(); + let elem2: Element = "".parse().unwrap(); + assert!(!elem1.compare_to(&elem2)); + } + + #[test] + fn xmlns_wrong() { + let elem1: Element = "".parse().unwrap(); + let elem2: Element = "".parse().unwrap(); + assert!(!elem1.compare_to(&elem2)); + } + + #[test] + fn xmlns_other_prefix() { + let elem1: Element = "".parse().unwrap(); + let elem2: Element = "".parse().unwrap(); + assert!(elem1.compare_to(&elem2)); + } + + #[test] + fn xmlns_dup() { + let elem1: Element = "".parse().unwrap(); + let elem2: Element = "".parse().unwrap(); + assert!(elem1.compare_to(&elem2)); + } +} diff --git a/xmpp-parsers/src/util/error.rs b/xmpp-parsers/src/util/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..33b2a699a2dea2cf8cbde53cd6a66ec5fa388a83 --- /dev/null +++ b/xmpp-parsers/src/util/error.rs @@ -0,0 +1,105 @@ +// Copyright (c) 2017-2018 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::error::Error as StdError; +use std::fmt; + +/// Contains one of the potential errors triggered while parsing an +/// [Element](../struct.Element.html) into a specialised struct. +#[derive(Debug)] +pub enum Error { + /// The usual error when parsing something. + /// + /// TODO: use a structured error so the user can report it better, instead + /// of a freeform string. + ParseError(&'static str), + + /// Generated when some base64 content fails to decode, usually due to + /// extra characters. + Base64Error(base64::DecodeError), + + /// Generated when text which should be an integer fails to parse. + ParseIntError(std::num::ParseIntError), + + /// Generated when text which should be a string fails to parse. + ParseStringError(std::string::ParseError), + + /// Generated when text which should be an IP address (IPv4 or IPv6) fails + /// to parse. + ParseAddrError(std::net::AddrParseError), + + /// Generated when text which should be a [JID](../../jid/struct.Jid.html) + /// fails to parse. + JidParseError(jid::JidParseError), + + /// Generated when text which should be a + /// [DateTime](../date/struct.DateTime.html) fails to parse. + ChronoParseError(chrono::ParseError), +} + +impl StdError for Error { + fn cause(&self) -> Option<&dyn StdError> { + match self { + Error::ParseError(_) => None, + Error::Base64Error(e) => Some(e), + Error::ParseIntError(e) => Some(e), + Error::ParseStringError(e) => Some(e), + Error::ParseAddrError(e) => Some(e), + Error::JidParseError(e) => Some(e), + Error::ChronoParseError(e) => Some(e), + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::ParseError(s) => write!(fmt, "parse error: {}", s), + Error::Base64Error(e) => write!(fmt, "base64 error: {}", e), + Error::ParseIntError(e) => write!(fmt, "integer parsing error: {}", e), + Error::ParseStringError(e) => write!(fmt, "string parsing error: {}", e), + Error::ParseAddrError(e) => write!(fmt, "IP address parsing error: {}", e), + Error::JidParseError(e) => write!(fmt, "JID parsing error: {}", e), + Error::ChronoParseError(e) => write!(fmt, "time parsing error: {}", e), + } + } +} + +impl From for Error { + fn from(err: base64::DecodeError) -> Error { + Error::Base64Error(err) + } +} + +impl From for Error { + fn from(err: std::num::ParseIntError) -> Error { + Error::ParseIntError(err) + } +} + +impl From for Error { + fn from(err: std::string::ParseError) -> Error { + Error::ParseStringError(err) + } +} + +impl From for Error { + fn from(err: std::net::AddrParseError) -> Error { + Error::ParseAddrError(err) + } +} + +impl From for Error { + fn from(err: jid::JidParseError) -> Error { + Error::JidParseError(err) + } +} + +impl From for Error { + fn from(err: chrono::ParseError) -> Error { + Error::ChronoParseError(err) + } +} diff --git a/xmpp-parsers/src/util/helpers.rs b/xmpp-parsers/src/util/helpers.rs new file mode 100644 index 0000000000000000000000000000000000000000..7bf3feb1f2b59e453bd4dd58694aa9083e1d6f3e --- /dev/null +++ b/xmpp-parsers/src/util/helpers.rs @@ -0,0 +1,119 @@ +// 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 crate::util::error::Error; +use jid::Jid; +use std::str::FromStr; + +/// Codec for text content. +pub struct Text; + +impl Text { + pub fn decode(s: &str) -> Result { + Ok(s.to_owned()) + } + + pub fn encode(string: &str) -> Option { + Some(string.to_owned()) + } +} + +/// Codec for plain text content. +pub struct PlainText; + +impl PlainText { + pub fn decode(s: &str) -> Result, Error> { + Ok(match s { + "" => None, + text => Some(text.to_owned()), + }) + } + + pub fn encode(string: &Option) -> Option { + string.as_ref().map(ToOwned::to_owned) + } +} + +/// Codec for trimmed plain text content. +pub struct TrimmedPlainText; + +impl TrimmedPlainText { + pub fn decode(s: &str) -> Result { + Ok(match s.trim() { + "" => return Err(Error::ParseError("URI missing in uri.")), + text => text.to_owned(), + }) + } + + pub fn encode(string: &str) -> Option { + Some(string.to_owned()) + } +} + +/// Codec wrapping base64 encode/decode. +pub struct Base64; + +impl Base64 { + pub fn decode(s: &str) -> Result, Error> { + Ok(base64::decode(s)?) + } + + pub fn encode(b: &[u8]) -> Option { + Some(base64::encode(b)) + } +} + +/// Codec wrapping base64 encode/decode, while ignoring whitespace characters. +pub struct WhitespaceAwareBase64; + +impl WhitespaceAwareBase64 { + pub fn decode(s: &str) -> Result, Error> { + let s: String = s.chars().filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t').collect(); + Ok(base64::decode(&s)?) + } + + pub fn encode(b: &[u8]) -> Option { + Some(base64::encode(b)) + } +} + +/// Codec for colon-separated bytes of uppercase hexadecimal. +pub struct ColonSeparatedHex; + +impl ColonSeparatedHex { + pub fn decode(s: &str) -> Result, Error> { + let mut bytes = vec![]; + for i in 0..(1 + s.len()) / 3 { + let byte = u8::from_str_radix(&s[3 * i..3 * i + 2], 16)?; + if 3 * i + 2 < s.len() { + assert_eq!(&s[3 * i + 2..3 * i + 3], ":"); + } + bytes.push(byte); + } + Ok(bytes) + } + + pub fn encode(b: &[u8]) -> Option { + let mut bytes = vec![]; + for byte in b { + bytes.push(format!("{:02X}", byte)); + } + Some(bytes.join(":")) + } +} + +/// Codec for a JID. +pub struct JidCodec; + +impl JidCodec { + pub fn decode(s: &str) -> Result { + Ok(Jid::from_str(s)?) + } + + pub fn encode(jid: &Jid) -> Option { + Some(jid.to_string()) + } +} diff --git a/xmpp-parsers/src/util/macros.rs b/xmpp-parsers/src/util/macros.rs new file mode 100644 index 0000000000000000000000000000000000000000..d703e258325dbb0a03a67ee46a64f6712d29afe2 --- /dev/null +++ b/xmpp-parsers/src/util/macros.rs @@ -0,0 +1,808 @@ +// Copyright (c) 2017-2018 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/. + +macro_rules! get_attr { + ($elem:ident, $attr:tt, $type:tt) => { + get_attr!($elem, $attr, $type, value, value.parse()?) + }; + ($elem:ident, $attr:tt, OptionEmpty, $value:ident, $func:expr) => { + match $elem.attr($attr) { + Some("") => None, + Some($value) => Some($func), + None => None, + } + }; + ($elem:ident, $attr:tt, Option, $value:ident, $func:expr) => { + match $elem.attr($attr) { + Some($value) => Some($func), + None => None, + } + }; + ($elem:ident, $attr:tt, Required, $value:ident, $func:expr) => { + match $elem.attr($attr) { + Some($value) => $func, + None => { + return Err(crate::util::error::Error::ParseError(concat!( + "Required attribute '", + $attr, + "' missing." + ))); + } + } + }; + ($elem:ident, $attr:tt, RequiredNonEmpty, $value:ident, $func:expr) => { + match $elem.attr($attr) { + Some("") => { + return Err(crate::util::error::Error::ParseError(concat!( + "Required attribute '", + $attr, + "' must not be empty." + ))); + }, + Some($value) => $func, + None => { + return Err(crate::util::error::Error::ParseError(concat!( + "Required attribute '", + $attr, + "' missing." + ))); + } + } + }; + ($elem:ident, $attr:tt, Default, $value:ident, $func:expr) => { + match $elem.attr($attr) { + Some($value) => $func, + None => ::std::default::Default::default(), + } + }; +} + +macro_rules! generate_attribute { + ($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+,}) => ( + generate_attribute!($(#[$meta])* $elem, $name, {$($(#[$a_meta])* $a => $b),+}); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+,}, Default = $default:ident) => ( + generate_attribute!($(#[$meta])* $elem, $name, {$($(#[$a_meta])* $a => $b),+}, Default = $default); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+}) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq)] + pub enum $elem { + $( + $(#[$a_meta])* + $a + ),+ + } + impl ::std::str::FromStr for $elem { + type Err = crate::util::error::Error; + fn from_str(s: &str) -> Result<$elem, crate::util::error::Error> { + Ok(match s { + $($b => $elem::$a),+, + _ => return Err(crate::util::error::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))), + }) + } + } + impl std::fmt::Display for $elem { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!(fmt, "{}", match self { + $($elem::$a => $b),+ + }) + } + } + impl ::minidom::IntoAttributeValue for $elem { + fn into_attribute_value(self) -> Option { + Some(String::from(match self { + $($elem::$a => $b),+ + })) + } + } + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+}, Default = $default:ident) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq)] + pub enum $elem { + $( + $(#[$a_meta])* + $a + ),+ + } + impl ::std::str::FromStr for $elem { + type Err = crate::util::error::Error; + fn from_str(s: &str) -> Result<$elem, crate::util::error::Error> { + Ok(match s { + $($b => $elem::$a),+, + _ => return Err(crate::util::error::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))), + }) + } + } + impl ::minidom::IntoAttributeValue for $elem { + #[allow(unreachable_patterns)] + fn into_attribute_value(self) -> Option { + Some(String::from(match self { + $elem::$default => return None, + $($elem::$a => $b),+ + })) + } + } + impl ::std::default::Default for $elem { + fn default() -> $elem { + $elem::$default + } + } + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, ($(#[$meta_symbol:meta])* $symbol:ident => $value:tt)) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq)] + pub enum $elem { + $(#[$meta_symbol])* + $symbol, + /// Value when absent. + None, + } + impl ::std::str::FromStr for $elem { + type Err = crate::util::error::Error; + fn from_str(s: &str) -> Result { + Ok(match s { + $value => $elem::$symbol, + _ => return Err(crate::util::error::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))), + }) + } + } + impl ::minidom::IntoAttributeValue for $elem { + fn into_attribute_value(self) -> Option { + match self { + $elem::$symbol => Some(String::from($value)), + $elem::None => None + } + } + } + impl ::std::default::Default for $elem { + fn default() -> $elem { + $elem::None + } + } + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, bool) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq)] + pub enum $elem { + /// True value, represented by either 'true' or '1'. + True, + /// False value, represented by either 'false' or '0'. + False, + } + impl ::std::str::FromStr for $elem { + type Err = crate::util::error::Error; + fn from_str(s: &str) -> Result { + Ok(match s { + "true" | "1" => $elem::True, + "false" | "0" => $elem::False, + _ => return Err(crate::util::error::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))), + }) + } + } + impl ::minidom::IntoAttributeValue for $elem { + fn into_attribute_value(self) -> Option { + match self { + $elem::True => Some(String::from("true")), + $elem::False => None + } + } + } + impl ::std::default::Default for $elem { + fn default() -> $elem { + $elem::False + } + } + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $type:tt, Default = $default:expr) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq)] + pub struct $elem(pub $type); + impl ::std::str::FromStr for $elem { + type Err = crate::util::error::Error; + fn from_str(s: &str) -> Result { + Ok($elem($type::from_str(s)?)) + } + } + impl ::minidom::IntoAttributeValue for $elem { + fn into_attribute_value(self) -> Option { + match self { + $elem($default) => None, + $elem(value) => Some(format!("{}", value)), + } + } + } + impl ::std::default::Default for $elem { + fn default() -> $elem { + $elem($default) + } + } + ); +} + +macro_rules! generate_element_enum { + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+,}) => ( + generate_element_enum!($(#[$meta])* $elem, $name, $ns, {$($(#[$enum_meta])* $enum => $enum_name),+}); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+}) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq)] + pub enum $elem { + $( + $(#[$enum_meta])* + $enum + ),+ + } + impl ::std::convert::TryFrom for $elem { + type Error = crate::util::error::Error; + fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> { + check_ns_only!(elem, $name, $ns); + check_no_children!(elem, $name); + check_no_attributes!(elem, $name); + Ok(match elem.name() { + $($enum_name => $elem::$enum,)+ + _ => return Err(crate::util::error::Error::ParseError(concat!("This is not a ", $name, " element."))), + }) + } + } + impl From<$elem> for crate::Element { + fn from(elem: $elem) -> crate::Element { + crate::Element::builder( + match elem { + $($elem::$enum => $enum_name,)+ + } + ) + .ns(crate::ns::$ns) + .build() + } + } + impl From<$elem> for ::minidom::Node { + fn from(elem: $elem) -> ::minidom::Node { + ::minidom::Node::Element(elem.into()) + } + } + ); +} + +macro_rules! generate_attribute_enum { + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, $attr:tt, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+,}) => ( + generate_attribute_enum!($(#[$meta])* $elem, $name, $ns, $attr, {$($(#[$enum_meta])* $enum => $enum_name),+}); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, $attr:tt, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+}) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq)] + pub enum $elem { + $( + $(#[$enum_meta])* + $enum + ),+ + } + impl ::std::convert::TryFrom for $elem { + type Error = crate::util::error::Error; + fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> { + check_ns_only!(elem, $name, $ns); + check_no_children!(elem, $name); + check_no_unknown_attributes!(elem, $name, [$attr]); + Ok(match get_attr!(elem, $attr, Required) { + $($enum_name => $elem::$enum,)+ + _ => return Err(crate::util::error::Error::ParseError(concat!("Invalid ", $name, " ", $attr, " value."))), + }) + } + } + impl From<$elem> for crate::Element { + fn from(elem: $elem) -> crate::Element { + crate::Element::builder($name) + .ns(crate::ns::$ns) + .attr($attr, match elem { + $($elem::$enum => $enum_name,)+ + }) + .build() + } + } + impl From<$elem> for ::minidom::Node { + fn from(elem: $elem) -> ::minidom::Node { + ::minidom::Node::Element(elem.into()) + } + } + ); +} + +macro_rules! check_self { + ($elem:ident, $name:tt, $ns:ident) => { + check_self!($elem, $name, $ns, $name); + }; + ($elem:ident, $name:tt, $ns:ident, $pretty_name:tt) => { + if !$elem.is($name, crate::ns::$ns) { + return Err(crate::util::error::Error::ParseError(concat!( + "This is not a ", + $pretty_name, + " element." + ))); + } + }; +} + +macro_rules! check_ns_only { + ($elem:ident, $name:tt, $ns:ident) => { + if !$elem.has_ns(crate::ns::$ns) { + return Err(crate::util::error::Error::ParseError(concat!( + "This is not a ", + $name, + " element." + ))); + } + }; +} + +macro_rules! check_no_children { + ($elem:ident, $name:tt) => { + #[cfg(not(feature = "disable-validation"))] + for _ in $elem.children() { + return Err(crate::util::error::Error::ParseError(concat!( + "Unknown child in ", + $name, + " element." + ))); + } + }; +} + +macro_rules! check_no_attributes { + ($elem:ident, $name:tt) => { + #[cfg(not(feature = "disable-validation"))] + for _ in $elem.attrs() { + return Err(crate::util::error::Error::ParseError(concat!( + "Unknown attribute in ", + $name, + " element." + ))); + } + }; +} + +macro_rules! check_no_unknown_attributes { + ($elem:ident, $name:tt, [$($attr:tt),*]) => ( + #[cfg(not(feature = "disable-validation"))] + for (_attr, _) in $elem.attrs() { + $( + if _attr == $attr { + continue; + } + )* + return Err(crate::util::error::Error::ParseError(concat!("Unknown attribute in ", $name, " element."))); + } + ); +} + +macro_rules! generate_empty_element { + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident) => ( + $(#[$meta])* + #[derive(Debug, Clone)] + pub struct $elem; + + impl ::std::convert::TryFrom for $elem { + type Error = crate::util::error::Error; + + fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> { + check_self!(elem, $name, $ns); + check_no_children!(elem, $name); + check_no_attributes!(elem, $name); + Ok($elem) + } + } + + impl From<$elem> for crate::Element { + fn from(_: $elem) -> crate::Element { + crate::Element::builder($name) + .ns(crate::ns::$ns) + .build() + } + } + + impl From<$elem> for ::minidom::Node { + fn from(elem: $elem) -> ::minidom::Node { + ::minidom::Node::Element(elem.into()) + } + } + ); +} + +macro_rules! generate_id { + ($(#[$meta:meta])* $elem:ident) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct $elem(pub String); + impl ::std::str::FromStr for $elem { + type Err = crate::util::error::Error; + fn from_str(s: &str) -> Result<$elem, crate::util::error::Error> { + // TODO: add a way to parse that differently when needed. + Ok($elem(String::from(s))) + } + } + impl ::minidom::IntoAttributeValue for $elem { + fn into_attribute_value(self) -> Option { + Some(self.0) + } + } + ); +} + +macro_rules! generate_elem_id { + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident) => ( + generate_elem_id!($(#[$meta])* $elem, $name, $ns, String); + impl ::std::str::FromStr for $elem { + type Err = crate::util::error::Error; + fn from_str(s: &str) -> Result<$elem, crate::util::error::Error> { + // TODO: add a way to parse that differently when needed. + Ok($elem(String::from(s))) + } + } + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, $type:ty) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct $elem(pub $type); + impl ::std::convert::TryFrom for $elem { + type Error = crate::util::error::Error; + fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> { + check_self!(elem, $name, $ns); + check_no_children!(elem, $name); + check_no_attributes!(elem, $name); + // TODO: add a way to parse that differently when needed. + Ok($elem(elem.text().parse()?)) + } + } + impl From<$elem> for crate::Element { + fn from(elem: $elem) -> crate::Element { + crate::Element::builder($name) + .ns(crate::ns::$ns) + .append(elem.0.to_string()) + .build() + } + } + + impl From<$elem> for ::minidom::Node { + fn from(elem: $elem) -> ::minidom::Node { + ::minidom::Node::Element(elem.into()) + } + } + ); +} + +macro_rules! decl_attr { + (OptionEmpty, $type:ty) => ( + Option<$type> + ); + (Option, $type:ty) => ( + Option<$type> + ); + (Required, $type:ty) => ( + $type + ); + (RequiredNonEmpty, $type:ty) => ( + $type + ); + (Default, $type:ty) => ( + $type + ); +} + +macro_rules! start_decl { + (Vec, $type:ty) => ( + Vec<$type> + ); + (Option, $type:ty) => ( + Option<$type> + ); + (Required, $type:ty) => ( + $type + ); + (Present, $type:ty) => ( + bool + ); +} + +macro_rules! start_parse_elem { + ($temp:ident: Vec) => { + let mut $temp = Vec::new(); + }; + ($temp:ident: Option) => { + let mut $temp = None; + }; + ($temp:ident: Required) => { + let mut $temp = None; + }; + ($temp:ident: Present) => { + let mut $temp = false; + }; +} + +macro_rules! do_parse { + ($elem:ident, Element) => { + $elem.clone() + }; + ($elem:ident, String) => { + $elem.text() + }; + ($elem:ident, $constructor:ident) => { + $constructor::try_from($elem.clone())? + }; +} + +macro_rules! do_parse_elem { + ($temp:ident: Vec = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => { + $temp.push(do_parse!($elem, $constructor)); + }; + ($temp:ident: Option = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => { + if $temp.is_some() { + return Err(crate::util::error::Error::ParseError(concat!( + "Element ", + $parent_name, + " must not have more than one ", + $name, + " child." + ))); + } + $temp = Some(do_parse!($elem, $constructor)); + }; + ($temp:ident: Required = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => { + if $temp.is_some() { + return Err(crate::util::error::Error::ParseError(concat!( + "Element ", + $parent_name, + " must not have more than one ", + $name, + " child." + ))); + } + $temp = Some(do_parse!($elem, $constructor)); + }; + ($temp:ident: Present = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => { + if $temp { + return Err(crate::util::error::Error::ParseError(concat!( + "Element ", + $parent_name, + " must not have more than one ", + $name, + " child." + ))); + } + $temp = true; + }; +} + +macro_rules! finish_parse_elem { + ($temp:ident: Vec = $name:tt, $parent_name:tt) => { + $temp + }; + ($temp:ident: Option = $name:tt, $parent_name:tt) => { + $temp + }; + ($temp:ident: Required = $name:tt, $parent_name:tt) => { + $temp.ok_or(crate::util::error::Error::ParseError(concat!( + "Missing child ", + $name, + " in ", + $parent_name, + " element." + )))? + }; + ($temp:ident: Present = $name:tt, $parent_name:tt) => { + $temp + }; +} + +macro_rules! generate_serialiser { + ($builder:ident, $parent:ident, $elem:ident, Required, String, ($name:tt, $ns:ident)) => { + $builder.append( + crate::Element::builder($name) + .ns(crate::ns::$ns) + .append(::minidom::Node::Text($parent.$elem)) + ) + }; + ($builder:ident, $parent:ident, $elem:ident, Option, String, ($name:tt, $ns:ident)) => { + $builder.append_all($parent.$elem.map(|elem| { + crate::Element::builder($name) + .ns(crate::ns::$ns) + .append(::minidom::Node::Text(elem)) + }) + ) + }; + ($builder:ident, $parent:ident, $elem:ident, Option, $constructor:ident, ($name:tt, *)) => { + $builder.append_all($parent.$elem.map(|elem| { + crate::Element::builder($name) + .ns(elem.get_ns()) + .append(::minidom::Node::Element(crate::Element::from(elem))) + }) + ) + }; + ($builder:ident, $parent:ident, $elem:ident, Option, $constructor:ident, ($name:tt, $ns:ident)) => { + $builder.append_all($parent.$elem.map(|elem| { + crate::Element::builder($name) + .ns(crate::ns::$ns) + .append(::minidom::Node::Element(crate::Element::from(elem))) + }) + ) + }; + ($builder:ident, $parent:ident, $elem:ident, Vec, $constructor:ident, ($name:tt, $ns:ident)) => { + $builder.append_all($parent.$elem.into_iter()) + }; + ($builder:ident, $parent:ident, $elem:ident, Present, $constructor:ident, ($name:tt, $ns:ident)) => { + $builder.append(::minidom::Node::Element(crate::Element::builder($name).ns(crate::ns::$ns).build())) + }; + ($builder:ident, $parent:ident, $elem:ident, $_:ident, $constructor:ident, ($name:tt, $ns:ident)) => { + $builder.append(::minidom::Node::Element(crate::Element::from($parent.$elem))) + }; +} + +macro_rules! generate_child_test { + ($child:ident, $name:tt, *) => { + true + }; + ($child:ident, $name:tt, $ns:tt) => { + $child.is($name, crate::ns::$ns) + }; +} + +macro_rules! generate_element { + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),+,]) => ( + generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: []); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),+]) => ( + generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: []); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),*]) => ( + generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [], children: [$($(#[$child_meta])* $child_ident: $coucou<$child_type> = ($child_name, $child_ns) => $child_constructor),*]); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),*,], children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),*]) => ( + generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: [$($(#[$child_meta])* $child_ident: $coucou<$child_type> = ($child_name, $child_ns) => $child_constructor),*]); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >)) => ( + generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [], children: [], text: ($(#[$text_meta])* $text_ident: $codec<$text_type>)); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),+], text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >)) => ( + generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: [], text: ($(#[$text_meta])* $text_ident: $codec<$text_type>)); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),*], children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),*] $(, text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >))*) => ( + $(#[$meta])* + #[derive(Debug, Clone)] + pub struct $elem { + $( + $(#[$attr_meta])* + pub $attr: decl_attr!($attr_action, $attr_type), + )* + $( + $(#[$child_meta])* + pub $child_ident: start_decl!($coucou, $child_type), + )* + $( + $(#[$text_meta])* + pub $text_ident: $text_type, + )* + } + + impl ::std::convert::TryFrom for $elem { + type Error = crate::util::error::Error; + + fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> { + check_self!(elem, $name, $ns); + check_no_unknown_attributes!(elem, $name, [$($attr_name),*]); + $( + start_parse_elem!($child_ident: $coucou); + )* + for _child in elem.children() { + $( + if generate_child_test!(_child, $child_name, $child_ns) { + do_parse_elem!($child_ident: $coucou = $child_constructor => _child, $child_name, $name); + continue; + } + )* + return Err(crate::util::error::Error::ParseError(concat!("Unknown child in ", $name, " element."))); + } + Ok($elem { + $( + $attr: get_attr!(elem, $attr_name, $attr_action), + )* + $( + $child_ident: finish_parse_elem!($child_ident: $coucou = $child_name, $name), + )* + $( + $text_ident: $codec::decode(&elem.text())?, + )* + }) + } + } + + impl From<$elem> for crate::Element { + fn from(elem: $elem) -> crate::Element { + let mut builder = crate::Element::builder($name) + .ns(crate::ns::$ns); + $( + builder = builder.attr($attr_name, elem.$attr); + )* + $( + builder = generate_serialiser!(builder, elem, $child_ident, $coucou, $child_constructor, ($child_name, $child_ns)); + )* + $( + builder = builder.append_all($codec::encode(&elem.$text_ident).map(::minidom::Node::Text).into_iter()); + )* + + builder.build() + } + } + + impl From<$elem> for ::minidom::Node { + fn from(elem: $elem) -> ::minidom::Node { + ::minidom::Node::Element(elem.into()) + } + } + ); +} + +#[cfg(test)] +macro_rules! assert_size ( + ($t:ty, $sz:expr) => ( + assert_eq!(::std::mem::size_of::<$t>(), $sz); + ); +); + +// TODO: move that to src/pubsub/mod.rs, once we figure out how to use macros from there. +macro_rules! impl_pubsub_item { + ($item:ident, $ns:ident) => { + impl ::std::convert::TryFrom for $item { + type Error = Error; + + fn try_from(elem: crate::Element) -> Result<$item, Error> { + check_self!(elem, "item", $ns); + check_no_unknown_attributes!(elem, "item", ["id", "publisher"]); + let mut payloads = elem.children().cloned().collect::>(); + let payload = payloads.pop(); + if !payloads.is_empty() { + return Err(Error::ParseError( + "More than a single payload in item element.", + )); + } + Ok($item(crate::pubsub::Item { + id: get_attr!(elem, "id", Option), + publisher: get_attr!(elem, "publisher", Option), + payload, + })) + } + } + + impl From<$item> for crate::Element { + fn from(item: $item) -> crate::Element { + crate::Element::builder("item") + .ns(ns::$ns) + .attr("id", item.0.id) + .attr("publisher", item.0.publisher) + .append_all(item.0.payload) + .build() + } + } + + impl From<$item> for ::minidom::Node { + fn from(item: $item) -> ::minidom::Node { + ::minidom::Node::Element(item.into()) + } + } + + impl ::std::ops::Deref for $item { + type Target = crate::pubsub::Item; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl ::std::ops::DerefMut for $item { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + } +} diff --git a/xmpp-parsers/src/util/mod.rs b/xmpp-parsers/src/util/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..2a595f98dcf136ad651002cee79194cd64c48733 --- /dev/null +++ b/xmpp-parsers/src/util/mod.rs @@ -0,0 +1,19 @@ +// Copyright (c) 2019 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/. + +/// Error type returned by every parser on failure. +pub mod error; + +/// Various helpers. +pub(crate) mod helpers; + +/// Helper macros to parse and serialise more easily. +#[macro_use] +mod macros; + +#[cfg(test)] +/// Namespace-aware comparison for tests +pub(crate) mod compare_elements; diff --git a/xmpp-parsers/src/version.rs b/xmpp-parsers/src/version.rs new file mode 100644 index 0000000000000000000000000000000000000000..028ce968a2bc70e7a5bf168366958d04109b0061 --- /dev/null +++ b/xmpp-parsers/src/version.rs @@ -0,0 +1,89 @@ +// 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 crate::iq::{IqGetPayload, IqResultPayload}; + +generate_empty_element!( + /// Represents a query for the software version a remote entity is using. + /// + /// It should only be used in an ``, as it can only + /// represent the request, and not a result. + VersionQuery, + "query", + VERSION +); + +impl IqGetPayload for VersionQuery {} + +generate_element!( + /// Represents the answer about the software version we are using. + /// + /// It should only be used in an ``, as it can only + /// represent the result, and not a request. + VersionResult, "query", VERSION, + children: [ + /// The name of this client. + name: Required = ("name", VERSION) => String, + + /// The version of this client. + version: Required = ("version", VERSION) => String, + + /// The OS this client is running on. + os: Option = ("os", VERSION) => String + ] +); + +impl IqResultPayload for VersionResult {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::compare_elements::NamespaceAwareCompare; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(VersionQuery, 0); + assert_size!(VersionResult, 36); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(VersionQuery, 0); + assert_size!(VersionResult, 72); + } + + #[test] + fn simple() { + let elem: Element = + "xmpp-rs0.3.0" + .parse() + .unwrap(); + let version = VersionResult::try_from(elem).unwrap(); + assert_eq!(version.name, String::from("xmpp-rs")); + assert_eq!(version.version, String::from("0.3.0")); + assert_eq!(version.os, None); + } + + #[test] + fn serialisation() { + let version = VersionResult { + name: String::from("xmpp-rs"), + version: String::from("0.3.0"), + os: None, + }; + let elem1 = Element::from(version); + let elem2: Element = + "xmpp-rs0.3.0" + .parse() + .unwrap(); + println!("{:?}", elem1); + assert!(elem1.compare_to(&elem2)); + } +} diff --git a/xmpp-parsers/src/websocket.rs b/xmpp-parsers/src/websocket.rs new file mode 100644 index 0000000000000000000000000000000000000000..fbc1fb2b7aa983210195e1f4d439b0efb2332020 --- /dev/null +++ b/xmpp-parsers/src/websocket.rs @@ -0,0 +1,102 @@ +// Copyright (c) 2018 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 jid::BareJid; + +generate_element!( + /// The stream opening for WebSocket. + Open, "open", WEBSOCKET, + attributes: [ + /// The JID of the entity opening this stream. + from: Option = "from", + + /// The JID of the entity receiving this stream opening. + to: Option = "to", + + /// The id of the stream, used for authentication challenges. + id: Option = "id", + + /// The XMPP version used during this stream. + version: Option = "version", + + /// The default human language for all subsequent stanzas, which will + /// be transmitted to other entities for better localisation. + xml_lang: Option = "xml:lang", + ] +); + +impl Open { + /// Creates a simple client→server `` element. + pub fn new(to: BareJid) -> Open { + Open { + from: None, + to: Some(to), + id: None, + version: Some(String::from("1.0")), + xml_lang: None, + } + } + + /// Sets the [@from](#structfield.from) attribute on this `` + /// element. + pub fn with_from(mut self, from: BareJid) -> Open { + self.from = Some(from); + self + } + + /// Sets the [@id](#structfield.id) attribute on this `` element. + pub fn with_id(mut self, id: String) -> Open { + self.id = Some(id); + self + } + + /// Sets the [@xml:lang](#structfield.xml_lang) attribute on this `` + /// element. + pub fn with_lang(mut self, xml_lang: String) -> Open { + self.xml_lang = Some(xml_lang); + self + } + + /// Checks whether the version matches the expected one. + pub fn is_version(&self, version: &str) -> bool { + match self.version { + None => false, + Some(ref self_version) => self_version == &String::from(version), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Open, 84); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Open, 168); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let open = Open::try_from(elem).unwrap(); + assert_eq!(open.from, None); + assert_eq!(open.to, None); + assert_eq!(open.id, None); + assert_eq!(open.version, None); + assert_eq!(open.xml_lang, None); + } +} diff --git a/xmpp-parsers/src/xhtml.rs b/xmpp-parsers/src/xhtml.rs new file mode 100644 index 0000000000000000000000000000000000000000..080fb4a2e4e0d604f4efed8342fe1a034d11df45 --- /dev/null +++ b/xmpp-parsers/src/xhtml.rs @@ -0,0 +1,515 @@ +// Copyright (c) 2019 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 crate::util::error::Error; +use crate::message::MessagePayload; +use crate::ns; +use minidom::{Element, Node}; +use std::convert::TryFrom; +use std::collections::HashMap; + +// TODO: Use a proper lang type. +type Lang = String; + +/// Container for formatted text. +#[derive(Debug, Clone)] +pub struct XhtmlIm { + /// Map of language to body element. + bodies: HashMap, +} + +impl XhtmlIm { + /// Serialise formatted text to HTML. + pub fn to_html(self) -> String { + let mut html = Vec::new(); + // TODO: use the best language instead. + for (lang, body) in self.bodies { + if lang.is_empty() { + assert!(body.xml_lang.is_none()); + } else { + assert_eq!(Some(lang), body.xml_lang); + } + for tag in body.children { + html.push(tag.to_html()); + } + break; + } + html.concat() + } + + /// Removes all unknown elements. + fn flatten(self) -> XhtmlIm { + let mut bodies = HashMap::new(); + for (lang, body) in self.bodies { + let children = body.children.into_iter().fold(vec![], |mut acc, child| { + match child { + Child::Tag(Tag::Unknown(children)) => acc.extend(children), + any => acc.push(any), + } + acc + }); + let body = Body { + children, + ..body + }; + bodies.insert(lang, body); + } + XhtmlIm { + bodies, + } + } +} + +impl MessagePayload for XhtmlIm {} + +impl TryFrom for XhtmlIm { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "html", XHTML_IM); + check_no_attributes!(elem, "html"); + + let mut bodies = HashMap::new(); + for child in elem.children() { + if child.is("body", ns::XHTML) { + let child = child.clone(); + let lang = match child.attr("xml:lang") { + Some(lang) => lang, + None => "", + }.to_string(); + let body = Body::try_from(child)?; + match bodies.insert(lang, body) { + None => (), + Some(_) => return Err(Error::ParseError("Two identical language bodies found in XHTML-IM.")) + } + } else { + return Err(Error::ParseError("Unknown element in XHTML-IM.")); + } + } + + Ok(XhtmlIm { bodies }.flatten()) + } +} + +impl From for Element { + fn from(wrapper: XhtmlIm) -> Element { + Element::builder("html") + .ns(ns::XHTML_IM) + .append_all(wrapper.bodies.into_iter().map(|(lang, body)| { + if lang.is_empty() { + assert!(body.xml_lang.is_none()); + } else { + assert_eq!(Some(lang), body.xml_lang); + } + Element::from(body) + })) + .build() + } +} + +#[derive(Debug, Clone)] +enum Child { + Tag(Tag), + Text(String), +} + +impl Child { + fn to_html(self) -> String { + match self { + Child::Tag(tag) => tag.to_html(), + Child::Text(text) => text, + } + } +} + +#[derive(Debug, Clone)] +struct Property { + key: String, + value: String, +} + +type Css = Vec; + +fn get_style_string(style: Css) -> Option { + let mut result = vec![]; + for Property { key, value } in style { + result.push(format!("{}: {}", key, value)); + } + if result.is_empty() { + return None; + } + Some(result.join("; ")) +} + +#[derive(Debug, Clone)] +struct Body { + style: Css, + xml_lang: Option, + children: Vec, +} + +impl TryFrom for Body { + type Error = Error; + + fn try_from(elem: Element) -> Result { + let mut children = vec![]; + for child in elem.nodes() { + match child { + Node::Element(child) => children.push(Child::Tag(Tag::try_from(child.clone())?)), + Node::Text(text) => children.push(Child::Text(text.clone())), + Node::Comment(_) => unimplemented!() // XXX: remove! + } + } + + Ok(Body { style: parse_css(elem.attr("style")), xml_lang: elem.attr("xml:lang").map(|xml_lang| xml_lang.to_string()), children }) + } +} + +impl From for Element { + fn from(body: Body) -> Element { + Element::builder("body") + .ns(ns::XHTML) + .attr("style", get_style_string(body.style)) + .attr("xml:lang", body.xml_lang) + .append_all(children_to_nodes(body.children)) + .build() + } +} + +#[derive(Debug, Clone)] +enum Tag { + A { href: Option, style: Css, type_: Option, children: Vec }, + Blockquote { style: Css, children: Vec }, + Br, + Cite { style: Css, children: Vec }, + Em { children: Vec }, + Img { src: Option, alt: Option }, // TODO: height, width, style + Li { style: Css, children: Vec }, + Ol { style: Css, children: Vec }, + P { style: Css, children: Vec }, + Span { style: Css, children: Vec }, + Strong { children: Vec }, + Ul { style: Css, children: Vec }, + Unknown(Vec), +} + +impl Tag { + fn to_html(self) -> String { + match self { + Tag::A { href, style, type_, children } => { + let href = write_attr(href, "href"); + let style = write_attr(get_style_string(style), "style"); + let type_ = write_attr(type_, "type"); + format!("{}", href, style, type_, children_to_html(children)) + }, + Tag::Blockquote { style, children } => { + let style = write_attr(get_style_string(style), "style"); + format!("{}", style, children_to_html(children)) + }, + Tag::Br => String::from("
"), + Tag::Cite { style, children } => { + let style = write_attr(get_style_string(style), "style"); + format!("{}", style, children_to_html(children)) + }, + Tag::Em { children } => format!("{}", children_to_html(children)), + Tag::Img { src, alt } => { + let src = write_attr(src, "src"); + let alt = write_attr(alt, "alt"); + format!("", src, alt) + } + Tag::Li { style, children } => { + let style = write_attr(get_style_string(style), "style"); + format!("{}", style, children_to_html(children)) + } + Tag::Ol { style, children } => { + let style = write_attr(get_style_string(style), "style"); + format!("{}", style, children_to_html(children)) + } + Tag::P { style, children } => { + let style = write_attr(get_style_string(style), "style"); + format!("{}

", style, children_to_html(children)) + } + Tag::Span { style, children } => { + let style = write_attr(get_style_string(style), "style"); + format!("{}", style, children_to_html(children)) + } + Tag::Strong { children } => format!("{}", children_to_html(children)), + Tag::Ul { style, children } => { + let style = write_attr(get_style_string(style), "style"); + format!("{}", style, children_to_html(children)) + } + Tag::Unknown(_) => panic!("No unknown element should be present in XHTML-IM after parsing."), + } + } +} + +impl TryFrom for Tag { + type Error = Error; + + fn try_from(elem: Element) -> Result { + let mut children = vec![]; + for child in elem.nodes() { + match child { + Node::Element(child) => children.push(Child::Tag(Tag::try_from(child.clone())?)), + Node::Text(text) => children.push(Child::Text(text.clone())), + Node::Comment(_) => unimplemented!() // XXX: remove! + } + } + + Ok(match elem.name() { + "a" => Tag::A { href: elem.attr("href").map(|href| href.to_string()), style: parse_css(elem.attr("style")), type_: elem.attr("type").map(|type_| type_.to_string()), children }, + "blockquote" => Tag::Blockquote { style: parse_css(elem.attr("style")), children }, + "br" => Tag::Br, + "cite" => Tag::Cite { style: parse_css(elem.attr("style")), children }, + "em" => Tag::Em { children }, + "img" => Tag::Img { src: elem.attr("src").map(|src| src.to_string()), alt: elem.attr("alt").map(|alt| alt.to_string()) }, + "li" => Tag::Li { style: parse_css(elem.attr("style")), children }, + "ol" => Tag::Ol { style: parse_css(elem.attr("style")), children }, + "p" => Tag::P { style: parse_css(elem.attr("style")), children }, + "span" => Tag::Span { style: parse_css(elem.attr("style")), children }, + "strong" => Tag::Strong { children }, + "ul" => Tag::Ul { style: parse_css(elem.attr("style")), children }, + _ => Tag::Unknown(children), + }) + } +} + +impl From for Element { + fn from(tag: Tag) -> Element { + let (name, attrs, children) = match tag { + Tag::A { href, style, type_, children } => ("a", { + let mut attrs = vec![]; + if let Some(href) = href { + attrs.push(("href", href)); + } + if let Some(style) = get_style_string(style) { + attrs.push(("style", style)); + } + if let Some(type_) = type_ { + attrs.push(("type", type_)); + } + attrs + }, children), + Tag::Blockquote { style, children } => ("blockquote", match get_style_string(style) { + Some(style) => vec![("style", style)], + None => vec![], + }, children), + Tag::Br => ("br", vec![], vec![]), + Tag::Cite { style, children } => ("cite", match get_style_string(style) { + Some(style) => vec![("style", style)], + None => vec![], + }, children), + Tag::Em { children } => ("em", vec![], children), + Tag::Img { src, alt } => { + let mut attrs = vec![]; + if let Some(src) = src { + attrs.push(("src", src)); + } + if let Some(alt) = alt { + attrs.push(("alt", alt)); + } + ("img", attrs, vec![]) + }, + Tag::Li { style, children } => ("li", match get_style_string(style) { + Some(style) => vec![("style", style)], + None => vec![], + }, children), + Tag::Ol { style, children } => ("ol", match get_style_string(style) { + Some(style) => vec![("style", style)], + None => vec![], + }, children), + Tag::P { style, children } => ("p", match get_style_string(style) { + Some(style) => vec![("style", style)], + None => vec![], + }, children), + Tag::Span { style, children } => ("span", match get_style_string(style) { + Some(style) => vec![("style", style)], + None => vec![], + }, children), + Tag::Strong { children } => ("strong", vec![], children), + Tag::Ul { style, children } => ("ul", match get_style_string(style) { + Some(style) => vec![("style", style)], + None => vec![], + }, children), + Tag::Unknown(_) => panic!("No unknown element should be present in XHTML-IM after parsing."), + }; + let mut builder = Element::builder(name) + .ns(ns::XHTML) + .append_all(children_to_nodes(children)); + for (key, value) in attrs { + builder = builder.attr(key, value); + } + builder.build() + } +} + +fn children_to_nodes(children: Vec) -> impl IntoIterator { + children.into_iter().map(|child| match child { + Child::Tag(tag) => Node::Element(Element::from(tag)), + Child::Text(text) => Node::Text(text), + }) +} + +fn children_to_html(children: Vec) -> String { + children.into_iter().map(|child| child.to_html()).collect::>().concat() +} + +fn write_attr(attr: Option, name: &str) -> String { + match attr { + Some(attr) => format!(" {}='{}'", name, attr), + None => String::new(), + } +} + +fn parse_css(style: Option<&str>) -> Css { + let mut properties = vec![]; + if let Some(style) = style { + // TODO: make that parser a bit more resilient to things. + for part in style.split(";") { + let mut part = part.splitn(2, ":").map(|a| a.to_string()).collect::>(); + let key = part.pop().unwrap(); + let value = part.pop().unwrap(); + properties.push(Property { key, value }); + } + } + properties +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(target_pointer_width = "32")] + #[test] + #[ignore] + fn test_size() { + assert_size!(XhtmlIm, 0); + assert_size!(Child, 0); + assert_size!(Tag, 0); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(XhtmlIm, 56); + assert_size!(Child, 112); + assert_size!(Tag, 104); + } + + #[test] + fn test_empty() { + let elem: Element = "" + .parse() + .unwrap(); + let xhtml = XhtmlIm::try_from(elem).unwrap(); + assert_eq!(xhtml.bodies.len(), 0); + + let elem: Element = "" + .parse() + .unwrap(); + let xhtml = XhtmlIm::try_from(elem).unwrap(); + assert_eq!(xhtml.bodies.len(), 1); + + let elem: Element = "" + .parse() + .unwrap(); + let xhtml = XhtmlIm::try_from(elem).unwrap(); + assert_eq!(xhtml.bodies.len(), 2); + } + + #[test] + fn invalid_two_same_langs() { + let elem: Element = "" + .parse() + .unwrap(); + let error = XhtmlIm::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Two identical language bodies found in XHTML-IM."); + } + + #[test] + fn test_tag() { + let elem: Element = "" + .parse() + .unwrap(); + let body = Body::try_from(elem).unwrap(); + assert_eq!(body.children.len(), 0); + + let elem: Element = "

Hello world!

" + .parse() + .unwrap(); + let mut body = Body::try_from(elem).unwrap(); + assert_eq!(body.style.len(), 0); + assert_eq!(body.xml_lang, None); + assert_eq!(body.children.len(), 1); + let p = match body.children.pop() { + Some(Child::Tag(tag)) => tag, + _ => panic!(), + }; + let mut children = match p { + Tag::P { style, children } => { + assert_eq!(style.len(), 0); + assert_eq!(children.len(), 1); + children + }, + _ => panic!(), + }; + let text = match children.pop() { + Some(Child::Text(text)) => text, + _ => panic!(), + }; + assert_eq!(text, "Hello world!"); + } + + #[test] + fn test_unknown_element() { + let elem: Element = "Hello world!" + .parse() + .unwrap(); + let parsed = XhtmlIm::try_from(elem).unwrap(); + let parsed2 = parsed.clone(); + let html = parsed.to_html(); + assert_eq!(html, "Hello world!"); + + let elem = Element::from(parsed2); + assert_eq!(String::from(&elem), "Hello world!"); + } + + #[test] + fn test_generate_html() { + let elem: Element = "

Hello world!

" + .parse() + .unwrap(); + let xhtml_im = XhtmlIm::try_from(elem).unwrap(); + let html = xhtml_im.to_html(); + assert_eq!(html, "

Hello world!

"); + + let elem: Element = "

Hello world!

" + .parse() + .unwrap(); + let xhtml_im = XhtmlIm::try_from(elem).unwrap(); + let html = xhtml_im.to_html(); + assert_eq!(html, "

Hello world!

"); + } + + #[test] + fn generate_tree() { + let world = "world".to_string(); + + Body { style: vec![], xml_lang: Some("en".to_string()), children: vec![ + Child::Tag(Tag::P { style: vec![], children: vec![ + Child::Text("Hello ".to_string()), + Child::Tag(Tag::Strong { children: vec![ + Child::Text(world), + ] }), + Child::Text("!".to_string()), + ] }), + ] }; + } +} diff --git a/.gitlab-ci.yml b/xmpp-rs/.gitlab-ci.yml similarity index 100% rename from .gitlab-ci.yml rename to xmpp-rs/.gitlab-ci.yml diff --git a/xmpp-rs/Cargo.toml b/xmpp-rs/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..f14c16ec84242ea1d4527013d1ca74348ee0857e --- /dev/null +++ b/xmpp-rs/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "xmpp" +version = "0.3.0" +authors = [ + "Emmanuel Gil Peyrot ", + "Maxime “pep” Buquet ", +] +description = "High-level XMPP library" +homepage = "https://gitlab.com/linkmauve/xmpp-rs" +repository = "https://gitlab.com/linkmauve/xmpp-rs" +keywords = ["xmpp", "jabber", "chat", "messaging", "bot"] +categories = ["network-programming"] +license = "MPL-2.0" +edition = "2018" + +[dependencies] +tokio-xmpp = "1.0.1" +xmpp-parsers = "0.16" +futures = "0.1" +tokio = "0.1" +log = "0.4" diff --git a/ChangeLog b/xmpp-rs/ChangeLog similarity index 100% rename from ChangeLog rename to xmpp-rs/ChangeLog diff --git a/xmpp-rs/LICENSE b/xmpp-rs/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..14e2f777f6c395e7e04ab4aa306bbcc4b0c1120e --- /dev/null +++ b/xmpp-rs/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + 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/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/xmpp-rs/README.md similarity index 100% rename from README.md rename to xmpp-rs/README.md diff --git a/examples/hello_bot.rs b/xmpp-rs/examples/hello_bot.rs similarity index 100% rename from examples/hello_bot.rs rename to xmpp-rs/examples/hello_bot.rs diff --git a/src/lib.rs b/xmpp-rs/src/lib.rs similarity index 99% rename from src/lib.rs rename to xmpp-rs/src/lib.rs index 486d632649533c8ddb8c1035b1ca6bad9efa0e91..4c96db27c966aab663111933d7a2a7be4fa9730e 100644 --- a/src/lib.rs +++ b/xmpp-rs/src/lib.rs @@ -201,7 +201,7 @@ impl ClientBuilder<'_> { }; match event { - TokioXmppEvent::Online => { + TokioXmppEvent::Online(_) => { let presence = ClientBuilder::make_initial_presence(&disco, &node).into(); let packet = Packet::Stanza(presence); sender_tx.unbounded_send(packet) diff --git a/src/pubsub/avatar.rs b/xmpp-rs/src/pubsub/avatar.rs similarity index 100% rename from src/pubsub/avatar.rs rename to xmpp-rs/src/pubsub/avatar.rs diff --git a/src/pubsub/mod.rs b/xmpp-rs/src/pubsub/mod.rs similarity index 100% rename from src/pubsub/mod.rs rename to xmpp-rs/src/pubsub/mod.rs