// 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/. #![allow(missing_docs)] use try_from::TryFrom; use std::str::FromStr; use minidom::Element; use jid::Jid; use error::Error; use ns; use iq::IqSetPayload; 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 ); generate_element!( Content, "content", JINGLE, attributes: [ /// Who created this content. creator: Creator = "creator" => required, /// How the content definition is to be interpreted by the recipient. disposition: Disposition = "disposition" => default, /// A per-session unique identifier for this content. name: ContentId = "name" => required, /// Who can send data for this content. senders: Senders = "senders" => default ], children: [ description: Option = ("description", JINGLE) => Element, transport: Option = ("transport", JINGLE) => Element, security: Option = ("security", JINGLE) => Element ] ); impl Content { pub fn new(creator: Creator, name: ContentId) -> Content { Content { creator, name, disposition: Disposition::Session, senders: Senders::Both, description: None, transport: None, security: None, } } pub fn with_disposition(mut self, disposition: Disposition) -> Content { self.disposition = disposition; self } pub fn with_senders(mut self, senders: Senders) -> Content { self.senders = senders; self } pub fn with_description(mut self, description: Element) -> Content { self.description = Some(description); self } pub fn with_transport(mut self, transport: Element) -> Content { self.transport = Some(transport); self } pub fn with_security(mut self, security: Element) -> Content { self.security = Some(security); self } } #[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", }).build() } } #[derive(Debug, Clone)] pub struct ReasonElement { pub reason: Reason, pub text: Option, } impl TryFrom for ReasonElement { type Err = Error; fn try_from(elem: Element) -> Result { check_self!(elem, "reason", JINGLE); let mut reason = None; let mut text = None; for child in elem.children() { if !child.has_ns(ns::JINGLE) { return Err(Error::ParseError("Reason contains a foreign element.")); } match child.name() { "text" => { if text.is_some() { return Err(Error::ParseError("Reason must not have more than one text.")); } text = Some(child.text()); }, name => { if reason.is_some() { return Err(Error::ParseError("Reason must not have more than one reason.")); } reason = Some(name.parse()?); }, } } let reason = reason.ok_or(Error::ParseError("Reason doesn’t contain a valid reason."))?; Ok(ReasonElement { reason: reason, text: text, }) } } impl From for Element { fn from(reason: ReasonElement) -> Element { Element::builder("reason") .append(Element::from(reason.reason)) .append(reason.text) .build() } } generate_id!( /// Unique identifier for a session between two JIDs. SessionId ); #[derive(Debug, Clone)] pub struct Jingle { pub action: Action, pub initiator: Option, pub responder: Option, pub sid: SessionId, pub contents: Vec, pub reason: Option, pub other: Vec, } impl IqSetPayload for Jingle {} impl Jingle { pub fn new(action: Action, sid: SessionId) -> Jingle { Jingle { action: action, sid: sid, initiator: None, responder: None, contents: Vec::new(), reason: None, other: Vec::new(), } } pub fn with_initiator(mut self, initiator: Jid) -> Jingle { self.initiator = Some(initiator); self } pub fn with_responder(mut self, responder: Jid) -> Jingle { self.responder = Some(responder); self } pub fn add_content(mut self, content: Content) -> Jingle { self.contents.push(content); self } pub fn set_reason(mut self, content: Content) -> Jingle { self.contents.push(content); self } } impl TryFrom for Jingle { type Err = 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", optional), responder: get_attr!(root, "responder", optional), 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(jingle.contents) .append(jingle.reason) .build() } } #[cfg(test)] mod tests { use super::*; #[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.text, None); 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.text, 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, "Reason must not have more than one text."); } }