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
  7#![allow(missing_docs)]
  8
  9use try_from::TryFrom;
 10use std::str::FromStr;
 11
 12use minidom::Element;
 13use jid::Jid;
 14
 15use error::Error;
 16use ns;
 17use iq::IqSetPayload;
 18
 19generate_attribute!(Action, "action", {
 20    ContentAccept => "content-accept",
 21    ContentAdd => "content-add",
 22    ContentModify => "content-modify",
 23    ContentReject => "content-reject",
 24    ContentRemove => "content-remove",
 25    DescriptionInfo => "description-info",
 26    SecurityInfo => "security-info",
 27    SessionAccept => "session-accept",
 28    SessionInfo => "session-info",
 29    SessionInitiate => "session-initiate",
 30    SessionTerminate => "session-terminate",
 31    TransportAccept => "transport-accept",
 32    TransportInfo => "transport-info",
 33    TransportReject => "transport-reject",
 34    TransportReplace => "transport-replace",
 35});
 36
 37generate_attribute!(Creator, "creator", {
 38    Initiator => "initiator",
 39    Responder => "responder",
 40});
 41
 42generate_attribute!(Senders, "senders", {
 43    Both => "both",
 44    Initiator => "initiator",
 45    None => "none",
 46    Responder => "responder",
 47}, Default = Both);
 48
 49// From https://www.iana.org/assignments/cont-disp/cont-disp.xhtml
 50generate_attribute!(Disposition, "disposition", {
 51    Inline => "inline",
 52    Attachment => "attachment",
 53    FormData => "form-data",
 54    Signal => "signal",
 55    Alert => "alert",
 56    Icon => "icon",
 57    Render => "render",
 58    RecipientListHistory => "recipient-list-history",
 59    Session => "session",
 60    Aib => "aib",
 61    EarlySession => "early-session",
 62    RecipientList => "recipient-list",
 63    Notification => "notification",
 64    ByReference => "by-reference",
 65    InfoPackage => "info-package",
 66    RecordingSession => "recording-session",
 67}, Default = Session);
 68
 69generate_id!(ContentId);
 70
 71generate_element!(
 72    Content, "content", JINGLE,
 73    attributes: [
 74        creator: Creator = "creator" => required,
 75        disposition: Disposition = "disposition" => default,
 76        name: ContentId = "name" => required,
 77        senders: Senders = "senders" => default
 78    ],
 79    children: [
 80        description: Option<Element> = ("description", JINGLE) => Element,
 81        transport: Option<Element> = ("transport", JINGLE) => Element,
 82        security: Option<Element> = ("security", JINGLE) => Element
 83    ]
 84);
 85
 86impl Content {
 87    pub fn new(creator: Creator, name: ContentId) -> Content {
 88        Content {
 89            creator,
 90            name,
 91            disposition: Disposition::Session,
 92            senders: Senders::Both,
 93            description: None,
 94            transport: None,
 95            security: None,
 96        }
 97    }
 98
 99    pub fn with_disposition(mut self, disposition: Disposition) -> Content {
100        self.disposition = disposition;
101        self
102    }
103
104    pub fn with_senders(mut self, senders: Senders) -> Content {
105        self.senders = senders;
106        self
107    }
108
109    pub fn with_description(mut self, description: Element) -> Content {
110        self.description = Some(description);
111        self
112    }
113
114    pub fn with_transport(mut self, transport: Element) -> Content {
115        self.transport = Some(transport);
116        self
117    }
118
119    pub fn with_security(mut self, security: Element) -> Content {
120        self.security = Some(security);
121        self
122    }
123}
124
125#[derive(Debug, Clone, PartialEq)]
126pub enum Reason {
127    AlternativeSession, //(String),
128    Busy,
129    Cancel,
130    ConnectivityError,
131    Decline,
132    Expired,
133    FailedApplication,
134    FailedTransport,
135    GeneralError,
136    Gone,
137    IncompatibleParameters,
138    MediaError,
139    SecurityError,
140    Success,
141    Timeout,
142    UnsupportedApplications,
143    UnsupportedTransports,
144}
145
146impl FromStr for Reason {
147    type Err = Error;
148
149    fn from_str(s: &str) -> Result<Reason, Error> {
150        Ok(match s {
151            "alternative-session" => Reason::AlternativeSession,
152            "busy" => Reason::Busy,
153            "cancel" => Reason::Cancel,
154            "connectivity-error" => Reason::ConnectivityError,
155            "decline" => Reason::Decline,
156            "expired" => Reason::Expired,
157            "failed-application" => Reason::FailedApplication,
158            "failed-transport" => Reason::FailedTransport,
159            "general-error" => Reason::GeneralError,
160            "gone" => Reason::Gone,
161            "incompatible-parameters" => Reason::IncompatibleParameters,
162            "media-error" => Reason::MediaError,
163            "security-error" => Reason::SecurityError,
164            "success" => Reason::Success,
165            "timeout" => Reason::Timeout,
166            "unsupported-applications" => Reason::UnsupportedApplications,
167            "unsupported-transports" => Reason::UnsupportedTransports,
168
169            _ => return Err(Error::ParseError("Unknown reason.")),
170        })
171    }
172}
173
174impl From<Reason> for Element {
175    fn from(reason: Reason) -> Element {
176        Element::builder(match reason {
177            Reason::AlternativeSession => "alternative-session",
178            Reason::Busy => "busy",
179            Reason::Cancel => "cancel",
180            Reason::ConnectivityError => "connectivity-error",
181            Reason::Decline => "decline",
182            Reason::Expired => "expired",
183            Reason::FailedApplication => "failed-application",
184            Reason::FailedTransport => "failed-transport",
185            Reason::GeneralError => "general-error",
186            Reason::Gone => "gone",
187            Reason::IncompatibleParameters => "incompatible-parameters",
188            Reason::MediaError => "media-error",
189            Reason::SecurityError => "security-error",
190            Reason::Success => "success",
191            Reason::Timeout => "timeout",
192            Reason::UnsupportedApplications => "unsupported-applications",
193            Reason::UnsupportedTransports => "unsupported-transports",
194        }).build()
195    }
196}
197
198#[derive(Debug, Clone)]
199pub struct ReasonElement {
200    pub reason: Reason,
201    pub text: Option<String>,
202}
203
204impl TryFrom<Element> for ReasonElement {
205    type Err = Error;
206
207    fn try_from(elem: Element) -> Result<ReasonElement, Error> {
208        check_self!(elem, "reason", JINGLE);
209        let mut reason = None;
210        let mut text = None;
211        for child in elem.children() {
212            if !child.has_ns(ns::JINGLE) {
213                return Err(Error::ParseError("Reason contains a foreign element."));
214            }
215            match child.name() {
216                "text" => {
217                    if text.is_some() {
218                        return Err(Error::ParseError("Reason must not have more than one text."));
219                    }
220                    text = Some(child.text());
221                },
222                name => {
223                    if reason.is_some() {
224                        return Err(Error::ParseError("Reason must not have more than one reason."));
225                    }
226                    reason = Some(name.parse()?);
227                },
228            }
229        }
230        let reason = reason.ok_or(Error::ParseError("Reason doesn’t contain a valid reason."))?;
231        Ok(ReasonElement {
232            reason: reason,
233            text: text,
234        })
235    }
236}
237
238impl From<ReasonElement> for Element {
239    fn from(reason: ReasonElement) -> Element {
240        Element::builder("reason")
241                .append(Element::from(reason.reason))
242                .append(reason.text)
243                .build()
244    }
245}
246
247generate_id!(SessionId);
248
249#[derive(Debug, Clone)]
250pub struct Jingle {
251    pub action: Action,
252    pub initiator: Option<Jid>,
253    pub responder: Option<Jid>,
254    pub sid: SessionId,
255    pub contents: Vec<Content>,
256    pub reason: Option<ReasonElement>,
257    pub other: Vec<Element>,
258}
259
260impl IqSetPayload for Jingle {}
261
262impl Jingle {
263    pub fn new(action: Action, sid: SessionId) -> Jingle {
264        Jingle {
265            action: action,
266            sid: sid,
267            initiator: None,
268            responder: None,
269            contents: Vec::new(),
270            reason: None,
271            other: Vec::new(),
272        }
273    }
274
275    pub fn with_initiator(mut self, initiator: Jid) -> Jingle {
276        self.initiator = Some(initiator);
277        self
278    }
279
280    pub fn with_responder(mut self, responder: Jid) -> Jingle {
281        self.responder = Some(responder);
282        self
283    }
284
285    pub fn add_content(mut self, content: Content) -> Jingle {
286        self.contents.push(content);
287        self
288    }
289
290    pub fn set_reason(mut self, content: Content) -> Jingle {
291        self.contents.push(content);
292        self
293    }
294}
295
296impl TryFrom<Element> for Jingle {
297    type Err = Error;
298
299    fn try_from(root: Element) -> Result<Jingle, Error> {
300        check_self!(root, "jingle", JINGLE, "Jingle");
301        check_no_unknown_attributes!(root, "Jingle", ["action", "initiator", "responder", "sid"]);
302
303        let mut jingle = Jingle {
304            action: get_attr!(root, "action", required),
305            initiator: get_attr!(root, "initiator", optional),
306            responder: get_attr!(root, "responder", optional),
307            sid: get_attr!(root, "sid", required),
308            contents: vec!(),
309            reason: None,
310            other: vec!(),
311        };
312
313        for child in root.children().cloned() {
314            if child.is("content", ns::JINGLE) {
315                let content = Content::try_from(child)?;
316                jingle.contents.push(content);
317            } else if child.is("reason", ns::JINGLE) {
318                if jingle.reason.is_some() {
319                    return Err(Error::ParseError("Jingle must not have more than one reason."));
320                }
321                let reason = ReasonElement::try_from(child)?;
322                jingle.reason = Some(reason);
323            } else {
324                jingle.other.push(child);
325            }
326        }
327
328        Ok(jingle)
329    }
330}
331
332impl From<Jingle> for Element {
333    fn from(jingle: Jingle) -> Element {
334        Element::builder("jingle")
335                .ns(ns::JINGLE)
336                .attr("action", jingle.action)
337                .attr("initiator", jingle.initiator)
338                .attr("responder", jingle.responder)
339                .attr("sid", jingle.sid)
340                .append(jingle.contents)
341                .append(jingle.reason)
342                .build()
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn test_simple() {
352        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'/>".parse().unwrap();
353        let jingle = Jingle::try_from(elem).unwrap();
354        assert_eq!(jingle.action, Action::SessionInitiate);
355        assert_eq!(jingle.sid, SessionId(String::from("coucou")));
356    }
357
358    #[test]
359    fn test_invalid_jingle() {
360        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1'/>".parse().unwrap();
361        let error = Jingle::try_from(elem).unwrap_err();
362        let message = match error {
363            Error::ParseError(string) => string,
364            _ => panic!(),
365        };
366        assert_eq!(message, "Required attribute 'action' missing.");
367
368        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-info'/>".parse().unwrap();
369        let error = Jingle::try_from(elem).unwrap_err();
370        let message = match error {
371            Error::ParseError(string) => string,
372            _ => panic!(),
373        };
374        assert_eq!(message, "Required attribute 'sid' missing.");
375
376        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='coucou' sid='coucou'/>".parse().unwrap();
377        let error = Jingle::try_from(elem).unwrap_err();
378        let message = match error {
379            Error::ParseError(string) => string,
380            _ => panic!(),
381        };
382        assert_eq!(message, "Unknown value for 'action' attribute.");
383    }
384
385    #[test]
386    fn test_content() {
387        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();
388        let jingle = Jingle::try_from(elem).unwrap();
389        assert_eq!(jingle.contents[0].creator, Creator::Initiator);
390        assert_eq!(jingle.contents[0].name, ContentId(String::from("coucou")));
391        assert_eq!(jingle.contents[0].senders, Senders::Both);
392        assert_eq!(jingle.contents[0].disposition, Disposition::Session);
393
394        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();
395        let jingle = Jingle::try_from(elem).unwrap();
396        assert_eq!(jingle.contents[0].senders, Senders::Both);
397
398        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();
399        let jingle = Jingle::try_from(elem).unwrap();
400        assert_eq!(jingle.contents[0].disposition, Disposition::EarlySession);
401    }
402
403    #[test]
404    fn test_invalid_content() {
405        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content/></jingle>".parse().unwrap();
406        let error = Jingle::try_from(elem).unwrap_err();
407        let message = match error {
408            Error::ParseError(string) => string,
409            _ => panic!(),
410        };
411        assert_eq!(message, "Required attribute 'creator' missing.");
412
413        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator'/></jingle>".parse().unwrap();
414        let error = Jingle::try_from(elem).unwrap_err();
415        let message = match error {
416            Error::ParseError(string) => string,
417            _ => panic!(),
418        };
419        assert_eq!(message, "Required attribute 'name' missing.");
420
421        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='coucou' name='coucou'/></jingle>".parse().unwrap();
422        let error = Jingle::try_from(elem).unwrap_err();
423        let message = match error {
424            Error::ParseError(string) => string,
425            _ => panic!(),
426        };
427        assert_eq!(message, "Unknown value for 'creator' attribute.");
428
429        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders='coucou'/></jingle>".parse().unwrap();
430        let error = Jingle::try_from(elem).unwrap_err();
431        let message = match error {
432            Error::ParseError(string) => string,
433            _ => panic!(),
434        };
435        assert_eq!(message, "Unknown value for 'senders' attribute.");
436
437        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders=''/></jingle>".parse().unwrap();
438        let error = Jingle::try_from(elem).unwrap_err();
439        let message = match error {
440            Error::ParseError(string) => string,
441            _ => panic!(),
442        };
443        assert_eq!(message, "Unknown value for 'senders' attribute.");
444    }
445
446    #[test]
447    fn test_reason() {
448        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/></reason></jingle>".parse().unwrap();
449        let jingle = Jingle::try_from(elem).unwrap();
450        let reason = jingle.reason.unwrap();
451        assert_eq!(reason.reason, Reason::Success);
452        assert_eq!(reason.text, None);
453
454        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/><text>coucou</text></reason></jingle>".parse().unwrap();
455        let jingle = Jingle::try_from(elem).unwrap();
456        let reason = jingle.reason.unwrap();
457        assert_eq!(reason.reason, Reason::Success);
458        assert_eq!(reason.text, Some(String::from("coucou")));
459    }
460
461    #[test]
462    fn test_invalid_reason() {
463        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason/></jingle>".parse().unwrap();
464        let error = Jingle::try_from(elem).unwrap_err();
465        let message = match error {
466            Error::ParseError(string) => string,
467            _ => panic!(),
468        };
469        assert_eq!(message, "Reason doesn’t contain a valid reason.");
470
471        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><a/></reason></jingle>".parse().unwrap();
472        let error = Jingle::try_from(elem).unwrap_err();
473        let message = match error {
474            Error::ParseError(string) => string,
475            _ => panic!(),
476        };
477        assert_eq!(message, "Unknown reason.");
478
479        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();
480        let error = Jingle::try_from(elem).unwrap_err();
481        let message = match error {
482            Error::ParseError(string) => string,
483            _ => panic!(),
484        };
485        assert_eq!(message, "Reason contains a foreign element.");
486
487        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/></reason><reason/></jingle>".parse().unwrap();
488        let error = Jingle::try_from(elem).unwrap_err();
489        let message = match error {
490            Error::ParseError(string) => string,
491            _ => panic!(),
492        };
493        assert_eq!(message, "Jingle must not have more than one reason.");
494
495        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/><text/><text/></reason></jingle>".parse().unwrap();
496        let error = Jingle::try_from(elem).unwrap_err();
497        let message = match error {
498            Error::ParseError(string) => string,
499            _ => panic!(),
500        };
501        assert_eq!(message, "Reason must not have more than one text.");
502    }
503}