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