jingle.rs

  1// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
  2//
  3// This Source Code Form is subject to the terms of the Mozilla Public
  4// License, v. 2.0. If a copy of the MPL was not distributed with this
  5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6
  7use try_from::TryFrom;
  8use std::str::FromStr;
  9
 10use minidom::Element;
 11use jid::Jid;
 12
 13use error::Error;
 14use ns;
 15use iq::IqSetPayload;
 16
 17generate_attribute!(
 18    /// The action attribute.
 19    Action, "action", {
 20        /// Accept a content-add action received from another party.
 21        ContentAccept => "content-accept",
 22
 23        /// Add one or more new content definitions to the session.
 24        ContentAdd => "content-add",
 25
 26        /// Change the directionality of media sending.
 27        ContentModify => "content-modify",
 28
 29        /// Reject a content-add action received from another party.
 30        ContentReject => "content-reject",
 31
 32        /// Remove one or more content definitions from the session.
 33        ContentRemove => "content-remove",
 34
 35        /// Exchange information about parameters for an application type.
 36        DescriptionInfo => "description-info",
 37
 38        /// Exchange information about security preconditions.
 39        SecurityInfo => "security-info",
 40
 41        /// Definitively accept a session negotiation.
 42        SessionAccept => "session-accept",
 43
 44        /// Send session-level information, such as a ping or a ringing message.
 45        SessionInfo => "session-info",
 46
 47        /// Request negotiation of a new Jingle session.
 48        SessionInitiate => "session-initiate",
 49
 50        /// End an existing session.
 51        SessionTerminate => "session-terminate",
 52
 53        /// Accept a transport-replace action received from another party.
 54        TransportAccept => "transport-accept",
 55
 56        /// Exchange transport candidates.
 57        TransportInfo => "transport-info",
 58
 59        /// Reject a transport-replace action received from another party.
 60        TransportReject => "transport-reject",
 61
 62        /// Redefine a transport method or replace it with a different method.
 63        TransportReplace => "transport-replace",
 64    }
 65);
 66
 67generate_attribute!(
 68    /// Which party originally generated the content type.
 69    Creator, "creator", {
 70        /// This content was created by the initiator of this session.
 71        Initiator => "initiator",
 72
 73        /// This content was created by the responder of this session.
 74        Responder => "responder",
 75    }
 76);
 77
 78generate_attribute!(
 79    /// Which parties in the session will be generating content.
 80    Senders, "senders", {
 81        /// Both parties can send for this content.
 82        Both => "both",
 83
 84        /// Only the initiator can send for this content.
 85        Initiator => "initiator",
 86
 87        /// No one can send for this content.
 88        None => "none",
 89
 90        /// Only the responder can send for this content.
 91        Responder => "responder",
 92    }, Default = Both
 93);
 94
 95generate_attribute!(
 96    /// How the content definition is to be interpreted by the recipient. The
 97    /// meaning of this attribute matches the "Content-Disposition" header as
 98    /// defined in RFC 2183 and applied to SIP by RFC 3261.
 99    ///
100    /// Possible values are defined here:
101    /// https://www.iana.org/assignments/cont-disp/cont-disp.xhtml
102    Disposition, "disposition", {
103        /// Displayed automatically.
104        Inline => "inline",
105
106        /// User controlled display.
107        Attachment => "attachment",
108
109        /// Process as form response.
110        FormData => "form-data",
111
112        /// Tunneled content to be processed silently.
113        Signal => "signal",
114
115        /// The body is a custom ring tone to alert the user.
116        Alert => "alert",
117
118        /// The body is displayed as an icon to the user.
119        Icon => "icon",
120
121        /// The body should be displayed to the user.
122        Render => "render",
123
124        /// The body contains a list of URIs that indicates the recipients of
125        /// the request.
126        RecipientListHistory => "recipient-list-history",
127
128        /// The body describes a communications session, for example, an
129        /// RFC2327 SDP body.
130        Session => "session",
131
132        /// Authenticated Identity Body.
133        Aib => "aib",
134
135        /// The body describes an early communications session, for example,
136        /// and [RFC2327] SDP body.
137        EarlySession => "early-session",
138
139        /// The body includes a list of URIs to which URI-list services are to
140        /// be applied.
141        RecipientList => "recipient-list",
142
143        /// The payload of the message carrying this Content-Disposition header
144        /// field value is an Instant Message Disposition Notification as
145        /// requested in the corresponding Instant Message.
146        Notification => "notification",
147
148        /// The body needs to be handled according to a reference to the body
149        /// that is located in the same SIP message as the body.
150        ByReference => "by-reference",
151
152        /// The body contains information associated with an Info Package.
153        InfoPackage => "info-package",
154
155        /// The body describes either metadata about the RS or the reason for
156        /// the metadata snapshot request as determined by the MIME value
157        /// indicated in the Content-Type.
158        RecordingSession => "recording-session",
159    }, Default = Session
160);
161
162generate_id!(
163    /// An unique identifier in a session, referencing a
164    /// [struct.Content.html](Content element).
165    ContentId
166);
167
168generate_element!(
169    /// Describes a session’s content, there can be multiple content in one
170    /// session.
171    Content, "content", JINGLE,
172    attributes: [
173        /// Who created this content.
174        creator: Creator = "creator" => required,
175
176        /// How the content definition is to be interpreted by the recipient.
177        disposition: Disposition = "disposition" => default,
178
179        /// A per-session unique identifier for this content.
180        name: ContentId = "name" => required,
181
182        /// Who can send data for this content.
183        senders: Senders = "senders" => default
184    ],
185    children: [
186        /// What to send.
187        description: Option<Element> = ("description", JINGLE) => Element,
188
189        /// How to send it.
190        transport: Option<Element> = ("transport", JINGLE) => Element,
191
192        /// With which security.
193        security: Option<Element> = ("security", JINGLE) => Element
194    ]
195);
196
197impl Content {
198    /// Create a new content.
199    pub fn new(creator: Creator, name: ContentId) -> Content {
200        Content {
201            creator,
202            name,
203            disposition: Disposition::Session,
204            senders: Senders::Both,
205            description: None,
206            transport: None,
207            security: None,
208        }
209    }
210
211    /// Set how the content is to be interpreted by the recipient.
212    pub fn with_disposition(mut self, disposition: Disposition) -> Content {
213        self.disposition = disposition;
214        self
215    }
216
217    /// Specify who can send data for this content.
218    pub fn with_senders(mut self, senders: Senders) -> Content {
219        self.senders = senders;
220        self
221    }
222
223    /// Set the description of this content.
224    pub fn with_description(mut self, description: Element) -> Content {
225        self.description = Some(description);
226        self
227    }
228
229    /// Set the transport of this content.
230    pub fn with_transport(mut self, transport: Element) -> Content {
231        self.transport = Some(transport);
232        self
233    }
234
235    /// Set the security of this content.
236    pub fn with_security(mut self, security: Element) -> Content {
237        self.security = Some(security);
238        self
239    }
240}
241
242/// Lists the possible reasons to be included in a Jingle iq.
243#[derive(Debug, Clone, PartialEq)]
244pub enum Reason {
245    /// The party prefers to use an existing session with the peer rather than
246    /// initiate a new session; the Jingle session ID of the alternative
247    /// session SHOULD be provided as the XML character data of the <sid/>
248    /// child.
249    AlternativeSession, //(String),
250
251    /// The party is busy and cannot accept a session.
252    Busy,
253
254    /// The initiator wishes to formally cancel the session initiation request.
255    Cancel,
256
257    /// The action is related to connectivity problems.
258    ConnectivityError,
259
260    /// The party wishes to formally decline the session.
261    Decline,
262
263    /// The session length has exceeded a pre-defined time limit (e.g., a
264    /// meeting hosted at a conference service).
265    Expired,
266
267    /// The party has been unable to initialize processing related to the
268    /// application type.
269    FailedApplication,
270
271    /// The party has been unable to establish connectivity for the transport
272    /// method.
273    FailedTransport,
274
275    /// The action is related to a non-specific application error.
276    GeneralError,
277
278    /// The entity is going offline or is no longer available.
279    Gone,
280
281    /// The party supports the offered application type but does not support
282    /// the offered or negotiated parameters.
283    IncompatibleParameters,
284
285    /// The action is related to media processing problems.
286    MediaError,
287
288    /// The action is related to a violation of local security policies.
289    SecurityError,
290
291    /// The action is generated during the normal course of state management
292    /// and does not reflect any error.
293    Success,
294
295    /// A request has not been answered so the sender is timing out the
296    /// request.
297    Timeout,
298
299    /// The party supports none of the offered application types.
300    UnsupportedApplications,
301
302    /// The party supports none of the offered transport methods.
303    UnsupportedTransports,
304}
305
306impl FromStr for Reason {
307    type Err = Error;
308
309    fn from_str(s: &str) -> Result<Reason, Error> {
310        Ok(match s {
311            "alternative-session" => Reason::AlternativeSession,
312            "busy" => Reason::Busy,
313            "cancel" => Reason::Cancel,
314            "connectivity-error" => Reason::ConnectivityError,
315            "decline" => Reason::Decline,
316            "expired" => Reason::Expired,
317            "failed-application" => Reason::FailedApplication,
318            "failed-transport" => Reason::FailedTransport,
319            "general-error" => Reason::GeneralError,
320            "gone" => Reason::Gone,
321            "incompatible-parameters" => Reason::IncompatibleParameters,
322            "media-error" => Reason::MediaError,
323            "security-error" => Reason::SecurityError,
324            "success" => Reason::Success,
325            "timeout" => Reason::Timeout,
326            "unsupported-applications" => Reason::UnsupportedApplications,
327            "unsupported-transports" => Reason::UnsupportedTransports,
328
329            _ => return Err(Error::ParseError("Unknown reason.")),
330        })
331    }
332}
333
334impl From<Reason> for Element {
335    fn from(reason: Reason) -> Element {
336        Element::builder(match reason {
337            Reason::AlternativeSession => "alternative-session",
338            Reason::Busy => "busy",
339            Reason::Cancel => "cancel",
340            Reason::ConnectivityError => "connectivity-error",
341            Reason::Decline => "decline",
342            Reason::Expired => "expired",
343            Reason::FailedApplication => "failed-application",
344            Reason::FailedTransport => "failed-transport",
345            Reason::GeneralError => "general-error",
346            Reason::Gone => "gone",
347            Reason::IncompatibleParameters => "incompatible-parameters",
348            Reason::MediaError => "media-error",
349            Reason::SecurityError => "security-error",
350            Reason::Success => "success",
351            Reason::Timeout => "timeout",
352            Reason::UnsupportedApplications => "unsupported-applications",
353            Reason::UnsupportedTransports => "unsupported-transports",
354        }).build()
355    }
356}
357
358/// Informs the recipient of something.
359#[derive(Debug, Clone)]
360pub struct ReasonElement {
361    /// The list of possible reasons to be included in a Jingle iq.
362    pub reason: Reason,
363
364    /// A human-readable description of this reason.
365    pub text: Option<String>,
366}
367
368impl TryFrom<Element> for ReasonElement {
369    type Err = Error;
370
371    fn try_from(elem: Element) -> Result<ReasonElement, Error> {
372        check_self!(elem, "reason", JINGLE);
373        let mut reason = None;
374        let mut text = None;
375        for child in elem.children() {
376            if !child.has_ns(ns::JINGLE) {
377                return Err(Error::ParseError("Reason contains a foreign element."));
378            }
379            match child.name() {
380                "text" => {
381                    if text.is_some() {
382                        return Err(Error::ParseError("Reason must not have more than one text."));
383                    }
384                    text = Some(child.text());
385                },
386                name => {
387                    if reason.is_some() {
388                        return Err(Error::ParseError("Reason must not have more than one reason."));
389                    }
390                    reason = Some(name.parse()?);
391                },
392            }
393        }
394        let reason = reason.ok_or(Error::ParseError("Reason doesn’t contain a valid reason."))?;
395        Ok(ReasonElement {
396            reason: reason,
397            text: text,
398        })
399    }
400}
401
402impl From<ReasonElement> for Element {
403    fn from(reason: ReasonElement) -> Element {
404        Element::builder("reason")
405                .append(Element::from(reason.reason))
406                .append(reason.text)
407                .build()
408    }
409}
410
411generate_id!(
412    /// Unique identifier for a session between two JIDs.
413    SessionId
414);
415
416/// The main Jingle container, to be included in an iq stanza.
417#[derive(Debug, Clone)]
418pub struct Jingle {
419    /// The action to execute on both ends.
420    pub action: Action,
421
422    /// Who the initiator is.
423    pub initiator: Option<Jid>,
424
425    /// Who the responder is.
426    pub responder: Option<Jid>,
427
428    /// Unique session identifier between two entities.
429    pub sid: SessionId,
430
431    /// A list of contents to be negociated in this session.
432    pub contents: Vec<Content>,
433
434    /// An optional reason.
435    pub reason: Option<ReasonElement>,
436
437    /// Payloads to be included.
438    pub other: Vec<Element>,
439}
440
441impl IqSetPayload for Jingle {}
442
443impl Jingle {
444    /// Create a new Jingle element.
445    pub fn new(action: Action, sid: SessionId) -> Jingle {
446        Jingle {
447            action: action,
448            sid: sid,
449            initiator: None,
450            responder: None,
451            contents: Vec::new(),
452            reason: None,
453            other: Vec::new(),
454        }
455    }
456
457    /// Set the initiator’s JID.
458    pub fn with_initiator(mut self, initiator: Jid) -> Jingle {
459        self.initiator = Some(initiator);
460        self
461    }
462
463    /// Set the responder’s JID.
464    pub fn with_responder(mut self, responder: Jid) -> Jingle {
465        self.responder = Some(responder);
466        self
467    }
468
469    /// Add a content to this Jingle container.
470    pub fn add_content(mut self, content: Content) -> Jingle {
471        self.contents.push(content);
472        self
473    }
474
475    /// Set the reason in this Jingle container.
476    pub fn set_reason(mut self, content: Content) -> Jingle {
477        self.contents.push(content);
478        self
479    }
480}
481
482impl TryFrom<Element> for Jingle {
483    type Err = Error;
484
485    fn try_from(root: Element) -> Result<Jingle, Error> {
486        check_self!(root, "jingle", JINGLE, "Jingle");
487        check_no_unknown_attributes!(root, "Jingle", ["action", "initiator", "responder", "sid"]);
488
489        let mut jingle = Jingle {
490            action: get_attr!(root, "action", required),
491            initiator: get_attr!(root, "initiator", optional),
492            responder: get_attr!(root, "responder", optional),
493            sid: get_attr!(root, "sid", required),
494            contents: vec!(),
495            reason: None,
496            other: vec!(),
497        };
498
499        for child in root.children().cloned() {
500            if child.is("content", ns::JINGLE) {
501                let content = Content::try_from(child)?;
502                jingle.contents.push(content);
503            } else if child.is("reason", ns::JINGLE) {
504                if jingle.reason.is_some() {
505                    return Err(Error::ParseError("Jingle must not have more than one reason."));
506                }
507                let reason = ReasonElement::try_from(child)?;
508                jingle.reason = Some(reason);
509            } else {
510                jingle.other.push(child);
511            }
512        }
513
514        Ok(jingle)
515    }
516}
517
518impl From<Jingle> for Element {
519    fn from(jingle: Jingle) -> Element {
520        Element::builder("jingle")
521                .ns(ns::JINGLE)
522                .attr("action", jingle.action)
523                .attr("initiator", jingle.initiator)
524                .attr("responder", jingle.responder)
525                .attr("sid", jingle.sid)
526                .append(jingle.contents)
527                .append(jingle.reason)
528                .build()
529    }
530}
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535
536    #[test]
537    fn test_size() {
538        assert_size!(Action, 1);
539        assert_size!(Creator, 1);
540        assert_size!(Senders, 1);
541        assert_size!(Disposition, 1);
542        assert_size!(ContentId, 24);
543        assert_size!(Content, 344);
544        assert_size!(Reason, 1);
545        assert_size!(ReasonElement, 32);
546        assert_size!(SessionId, 24);
547        assert_size!(Jingle, 256);
548    }
549
550    #[test]
551    fn test_simple() {
552        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'/>".parse().unwrap();
553        let jingle = Jingle::try_from(elem).unwrap();
554        assert_eq!(jingle.action, Action::SessionInitiate);
555        assert_eq!(jingle.sid, SessionId(String::from("coucou")));
556    }
557
558    #[test]
559    fn test_invalid_jingle() {
560        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1'/>".parse().unwrap();
561        let error = Jingle::try_from(elem).unwrap_err();
562        let message = match error {
563            Error::ParseError(string) => string,
564            _ => panic!(),
565        };
566        assert_eq!(message, "Required attribute 'action' missing.");
567
568        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-info'/>".parse().unwrap();
569        let error = Jingle::try_from(elem).unwrap_err();
570        let message = match error {
571            Error::ParseError(string) => string,
572            _ => panic!(),
573        };
574        assert_eq!(message, "Required attribute 'sid' missing.");
575
576        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='coucou' sid='coucou'/>".parse().unwrap();
577        let error = Jingle::try_from(elem).unwrap_err();
578        let message = match error {
579            Error::ParseError(string) => string,
580            _ => panic!(),
581        };
582        assert_eq!(message, "Unknown value for 'action' attribute.");
583    }
584
585    #[test]
586    fn test_content() {
587        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou'><description/><transport/></content></jingle>".parse().unwrap();
588        let jingle = Jingle::try_from(elem).unwrap();
589        assert_eq!(jingle.contents[0].creator, Creator::Initiator);
590        assert_eq!(jingle.contents[0].name, ContentId(String::from("coucou")));
591        assert_eq!(jingle.contents[0].senders, Senders::Both);
592        assert_eq!(jingle.contents[0].disposition, Disposition::Session);
593
594        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders='both'><description/><transport/></content></jingle>".parse().unwrap();
595        let jingle = Jingle::try_from(elem).unwrap();
596        assert_eq!(jingle.contents[0].senders, Senders::Both);
597
598        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' disposition='early-session'><description/><transport/></content></jingle>".parse().unwrap();
599        let jingle = Jingle::try_from(elem).unwrap();
600        assert_eq!(jingle.contents[0].disposition, Disposition::EarlySession);
601    }
602
603    #[test]
604    fn test_invalid_content() {
605        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content/></jingle>".parse().unwrap();
606        let error = Jingle::try_from(elem).unwrap_err();
607        let message = match error {
608            Error::ParseError(string) => string,
609            _ => panic!(),
610        };
611        assert_eq!(message, "Required attribute 'creator' missing.");
612
613        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator'/></jingle>".parse().unwrap();
614        let error = Jingle::try_from(elem).unwrap_err();
615        let message = match error {
616            Error::ParseError(string) => string,
617            _ => panic!(),
618        };
619        assert_eq!(message, "Required attribute 'name' missing.");
620
621        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='coucou' name='coucou'/></jingle>".parse().unwrap();
622        let error = Jingle::try_from(elem).unwrap_err();
623        let message = match error {
624            Error::ParseError(string) => string,
625            _ => panic!(),
626        };
627        assert_eq!(message, "Unknown value for 'creator' attribute.");
628
629        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders='coucou'/></jingle>".parse().unwrap();
630        let error = Jingle::try_from(elem).unwrap_err();
631        let message = match error {
632            Error::ParseError(string) => string,
633            _ => panic!(),
634        };
635        assert_eq!(message, "Unknown value for 'senders' attribute.");
636
637        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders=''/></jingle>".parse().unwrap();
638        let error = Jingle::try_from(elem).unwrap_err();
639        let message = match error {
640            Error::ParseError(string) => string,
641            _ => panic!(),
642        };
643        assert_eq!(message, "Unknown value for 'senders' attribute.");
644    }
645
646    #[test]
647    fn test_reason() {
648        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/></reason></jingle>".parse().unwrap();
649        let jingle = Jingle::try_from(elem).unwrap();
650        let reason = jingle.reason.unwrap();
651        assert_eq!(reason.reason, Reason::Success);
652        assert_eq!(reason.text, None);
653
654        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/><text>coucou</text></reason></jingle>".parse().unwrap();
655        let jingle = Jingle::try_from(elem).unwrap();
656        let reason = jingle.reason.unwrap();
657        assert_eq!(reason.reason, Reason::Success);
658        assert_eq!(reason.text, Some(String::from("coucou")));
659    }
660
661    #[test]
662    fn test_invalid_reason() {
663        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason/></jingle>".parse().unwrap();
664        let error = Jingle::try_from(elem).unwrap_err();
665        let message = match error {
666            Error::ParseError(string) => string,
667            _ => panic!(),
668        };
669        assert_eq!(message, "Reason doesn’t contain a valid reason.");
670
671        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><a/></reason></jingle>".parse().unwrap();
672        let error = Jingle::try_from(elem).unwrap_err();
673        let message = match error {
674            Error::ParseError(string) => string,
675            _ => panic!(),
676        };
677        assert_eq!(message, "Unknown reason.");
678
679        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><a xmlns='http://www.w3.org/1999/xhtml'/></reason></jingle>".parse().unwrap();
680        let error = Jingle::try_from(elem).unwrap_err();
681        let message = match error {
682            Error::ParseError(string) => string,
683            _ => panic!(),
684        };
685        assert_eq!(message, "Reason contains a foreign element.");
686
687        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/></reason><reason/></jingle>".parse().unwrap();
688        let error = Jingle::try_from(elem).unwrap_err();
689        let message = match error {
690            Error::ParseError(string) => string,
691            _ => panic!(),
692        };
693        assert_eq!(message, "Jingle must not have more than one reason.");
694
695        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/><text/><text/></reason></jingle>".parse().unwrap();
696        let error = Jingle::try_from(elem).unwrap_err();
697        let message = match error {
698            Error::ParseError(string) => string,
699            _ => panic!(),
700        };
701        assert_eq!(message, "Reason must not have more than one text.");
702    }
703}