From 9da488f9095a29e514ad1c6eea469beb57c85b90 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Wed, 19 Apr 2017 02:27:42 +0100 Subject: [PATCH] Add a Jingle parser. --- src/jingle.rs | 491 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/ns.rs | 1 + 3 files changed, 493 insertions(+) create mode 100644 src/jingle.rs diff --git a/src/jingle.rs b/src/jingle.rs new file mode 100644 index 00000000..e732c6b4 --- /dev/null +++ b/src/jingle.rs @@ -0,0 +1,491 @@ +extern crate minidom; + +use std::str::FromStr; + +use minidom::Element; + +use error::Error; +use ns::{JINGLE_NS}; + +#[derive(Debug, PartialEq)] +pub enum Action { + ContentAccept, + ContentAdd, + ContentModify, + ContentReject, + ContentRemove, + DescriptionInfo, + SecurityInfo, + SessionAccept, + SessionInfo, + SessionInitiate, + SessionTerminate, + TransportAccept, + TransportInfo, + TransportReject, + TransportReplace, +} + +impl FromStr for Action { + type Err = Error; + + fn from_str(s: &str) -> Result { + if s == "content-accept" { + Ok(Action::ContentAccept) + } else if s == "content-add" { + Ok(Action::ContentAdd) + } else if s == "content-modify" { + Ok(Action::ContentModify) + } else if s == "content-reject" { + Ok(Action::ContentReject) + } else if s == "content-remove" { + Ok(Action::ContentRemove) + } else if s == "description-info" { + Ok(Action::DescriptionInfo) + } else if s == "security-info" { + Ok(Action::SecurityInfo) + } else if s == "session-accept" { + Ok(Action::SessionAccept) + } else if s == "session-info" { + Ok(Action::SessionInfo) + } else if s == "session-initiate" { + Ok(Action::SessionInitiate) + } else if s == "session-terminate" { + Ok(Action::SessionTerminate) + } else if s == "transport-accept" { + Ok(Action::TransportAccept) + } else if s == "transport-info" { + Ok(Action::TransportInfo) + } else if s == "transport-reject" { + Ok(Action::TransportReject) + } else if s == "transport-replace" { + Ok(Action::TransportReplace) + } else { + Err(Error::ParseError("Unknown action.")) + } + } +} + +// TODO: use a real JID type. +type Jid = String; + +#[derive(Debug, PartialEq)] +pub enum Creator { + Initiator, + Responder, +} + +impl FromStr for Creator { + type Err = Error; + + fn from_str(s: &str) -> Result { + if s == "initiator" { + Ok(Creator::Initiator) + } else if s == "responder" { + Ok(Creator::Responder) + } else { + Err(Error::ParseError("Unknown creator.")) + } + } +} + +#[derive(Debug, PartialEq)] +pub enum Senders { + Both, + Initiator, + None_, + Responder, +} + +impl FromStr for Senders { + type Err = Error; + + fn from_str(s: &str) -> Result { + if s == "both" { + Ok(Senders::Both) + } else if s == "initiator" { + Ok(Senders::Initiator) + } else if s == "none" { + Ok(Senders::None_) + } else if s == "responder" { + Ok(Senders::Responder) + } else { + Err(Error::ParseError("Unknown senders.")) + } + } +} + +#[derive(Debug)] +pub struct Content { + pub creator: Creator, + pub disposition: String, + pub name: String, + pub senders: Senders, + pub description: String, + pub transport: String, + pub security: Option, +} + +#[derive(Debug, PartialEq)] +pub enum Reason { + AlternativeSession, //(String), + Busy, + Cancel, + ConnectivityError, + Decline, + Expired, + FailedApplication, + FailedTransport, + GeneralError, + Gone, + IncompatibleParameters, + MediaError, + SecurityError, + Success, + Timeout, + UnsupportedApplications, + UnsupportedTransports, +} + +impl FromStr for Reason { + type Err = Error; + + fn from_str(s: &str) -> Result { + if s == "alternative-session" { + Ok(Reason::AlternativeSession) + } else if s == "busy" { + Ok(Reason::Busy) + } else if s == "cancel" { + Ok(Reason::Cancel) + } else if s == "connectivity-error" { + Ok(Reason::ConnectivityError) + } else if s == "decline" { + Ok(Reason::Decline) + } else if s == "expired" { + Ok(Reason::Expired) + } else if s == "failed-application" { + Ok(Reason::FailedApplication) + } else if s == "failed-transport" { + Ok(Reason::FailedTransport) + } else if s == "general-error" { + Ok(Reason::GeneralError) + } else if s == "gone" { + Ok(Reason::Gone) + } else if s == "incompatible-parameters" { + Ok(Reason::IncompatibleParameters) + } else if s == "media-error" { + Ok(Reason::MediaError) + } else if s == "security-error" { + Ok(Reason::SecurityError) + } else if s == "success" { + Ok(Reason::Success) + } else if s == "timeout" { + Ok(Reason::Timeout) + } else if s == "unsupported-applications" { + Ok(Reason::UnsupportedApplications) + } else if s == "unsupported-transports" { + Ok(Reason::UnsupportedTransports) + } else { + Err(Error::ParseError("Unknown reason.")) + } + } +} + +#[derive(Debug)] +pub struct ReasonElement { + pub reason: Reason, + pub text: Option, +} + +#[derive(Debug)] +pub struct Jingle { + pub action: Action, + pub initiator: Option, + pub responder: Option, + pub sid: String, + pub contents: Vec, + pub reason: Option, + //pub other: Vec, +} + +pub fn parse_jingle(root: &Element) -> Result { + assert!(root.is("jingle", JINGLE_NS)); + let mut contents: Vec = vec!(); + + let action = root.attr("action") + .ok_or(Error::ParseError("Jingle must have an 'action' attribute."))? + .parse()?; + let initiator = root.attr("initiator") + .and_then(|initiator| initiator.parse().ok()); + let responder = root.attr("responder") + .and_then(|responder| responder.parse().ok()); + let sid = root.attr("sid") + .ok_or(Error::ParseError("Jingle must have a 'sid' attribute."))?; + let mut reason_element = None; + + for child in root.children() { + if child.is("content", JINGLE_NS) { + let creator = child.attr("creator") + .ok_or(Error::ParseError("Content must have a 'creator' attribute."))? + .parse()?; + let disposition = child.attr("disposition") + .unwrap_or("session"); + let name = child.attr("name") + .ok_or(Error::ParseError("Content must have a 'name' attribute."))?; + let senders = child.attr("senders") + .unwrap_or("both") + .parse()?; + let mut description = None; + let mut transport = None; + let mut security = None; + for stuff in child.children() { + if stuff.name() == "description" { + if description.is_some() { + return Err(Error::ParseError("Content must not have more than one description.")); + } + description = Some(stuff.ns().ok_or(Error::ParseError("Description without a namespace."))?); + } else if stuff.name() == "transport" { + if transport.is_some() { + return Err(Error::ParseError("Content must not have more than one transport.")); + } + transport = Some(stuff.ns().ok_or(Error::ParseError("Transport without a namespace."))?); + } else if stuff.name() == "security" { + if security.is_some() { + return Err(Error::ParseError("Content must not have more than one security.")); + } + security = stuff.ns().and_then(|ns| ns.parse().ok()); + } + } + if description.is_none() { + return Err(Error::ParseError("Content must have one description.")); + } + if transport.is_none() { + return Err(Error::ParseError("Content must have one transport.")); + } + let description = description.unwrap().to_owned(); + let transport = transport.unwrap().to_owned(); + contents.push(Content { + creator: creator, + disposition: disposition.to_owned(), + name: name.to_owned(), + senders: senders, + description: description, + transport: transport, + security: security.to_owned(), + }); + } else if child.is("reason", JINGLE_NS) { + if reason_element.is_some() { + return Err(Error::ParseError("Jingle must not have more than one reason.")); + } + let mut reason = None; + let mut text = None; + for stuff in child.children() { + if stuff.ns() != Some(JINGLE_NS) { + return Err(Error::ParseError("Reason contains a foreign element.")); + } + let name = stuff.name(); + if name == "text" { + text = Some(stuff.text()); + } else { + reason = Some(name.parse()?); + } + } + if reason.is_none() { + return Err(Error::ParseError("Reason doesn’t contain a valid reason.")); + } + reason_element = Some(ReasonElement { + reason: reason.unwrap(), + text: text, + }); + } else { + return Err(Error::ParseError("Unknown element in jingle.")); + } + } + + return Ok(Jingle { + action: action, + initiator: initiator, + responder: responder, + sid: sid.to_owned(), + contents: contents, + reason: reason_element, + }); +} + +#[cfg(test)] +mod tests { + use minidom::Element; + use error::Error; + use jingle; + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + let jingle = jingle::parse_jingle(&elem).unwrap(); + assert_eq!(jingle.action, jingle::Action::SessionInitiate); + assert_eq!(jingle.sid, "coucou"); + } + + #[test] + fn test_invalid_jingle() { + let elem: Element = "".parse().unwrap(); + let error = jingle::parse_jingle(&elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Jingle must have an 'action' attribute."); + + let elem: Element = "".parse().unwrap(); + let error = jingle::parse_jingle(&elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Jingle must have a 'sid' attribute."); + + let elem: Element = "".parse().unwrap(); + let error = jingle::parse_jingle(&elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown action."); + + let elem: Element = "".parse().unwrap(); + let error = jingle::parse_jingle(&elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown element in jingle."); + } + + #[test] + fn test_content() { + let elem: Element = "".parse().unwrap(); + let jingle = jingle::parse_jingle(&elem).unwrap(); + assert_eq!(jingle.contents[0].creator, jingle::Creator::Initiator); + assert_eq!(jingle.contents[0].name, "coucou"); + assert_eq!(jingle.contents[0].senders, jingle::Senders::Both); + assert_eq!(jingle.contents[0].disposition, "session"); + println!("{:#?}", jingle); + + let elem: Element = "".parse().unwrap(); + let jingle = jingle::parse_jingle(&elem).unwrap(); + assert_eq!(jingle.contents[0].senders, jingle::Senders::Both); + + let elem: Element = "".parse().unwrap(); + let jingle = jingle::parse_jingle(&elem).unwrap(); + assert_eq!(jingle.contents[0].disposition, "early-session"); + } + + #[test] + fn test_invalid_content() { + let elem: Element = "".parse().unwrap(); + let error = jingle::parse_jingle(&elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Content must have a 'creator' attribute."); + + let elem: Element = "".parse().unwrap(); + let error = jingle::parse_jingle(&elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Content must have a 'name' attribute."); + + let elem: Element = "".parse().unwrap(); + let error = jingle::parse_jingle(&elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown creator."); + + let elem: Element = "".parse().unwrap(); + let error = jingle::parse_jingle(&elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown senders."); + + let elem: Element = "".parse().unwrap(); + let error = jingle::parse_jingle(&elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown senders."); + + let elem: Element = "".parse().unwrap(); + let error = jingle::parse_jingle(&elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Content must have one description."); + + let elem: Element = "".parse().unwrap(); + let error = jingle::parse_jingle(&elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Content must have one transport."); + } + + #[test] + fn test_reason() { + let elem: Element = "".parse().unwrap(); + let jingle = jingle::parse_jingle(&elem).unwrap(); + let reason = jingle.reason.unwrap(); + assert_eq!(reason.reason, jingle::Reason::Success); + assert_eq!(reason.text, None); + + let elem: Element = "coucou".parse().unwrap(); + let jingle = jingle::parse_jingle(&elem).unwrap(); + let reason = jingle.reason.unwrap(); + assert_eq!(reason.reason, jingle::Reason::Success); + assert_eq!(reason.text, Some(String::from("coucou"))); + } + + #[test] + fn test_invalid_reason() { + let elem: Element = "".parse().unwrap(); + let error = jingle::parse_jingle(&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::parse_jingle(&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::parse_jingle(&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::parse_jingle(&elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Jingle must not have more than one reason."); + } +} diff --git a/src/lib.rs b/src/lib.rs index a6aa1901..56d17f47 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,3 +7,4 @@ pub mod disco; pub mod data_forms; pub mod media_element; pub mod ecaps2; +pub mod jingle; diff --git a/src/ns.rs b/src/ns.rs index 36bd127c..55da2eb8 100644 --- a/src/ns.rs +++ b/src/ns.rs @@ -1,3 +1,4 @@ pub const DISCO_INFO_NS: &'static str = "http://jabber.org/protocol/disco#info"; pub const DATA_FORMS_NS: &'static str = "jabber:x:data"; pub const MEDIA_ELEMENT_NS: &'static str = "urn:xmpp:media-element"; +pub const JINGLE_NS: &'static str = "urn:xmpp:jingle:1";