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