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
 69generate_element!(
 70    Content, "content", JINGLE,
 71    attributes: [
 72        creator: Creator = "creator" => required,
 73        disposition: Disposition = "disposition" => default,
 74        name: ContentId = "name" => required,
 75        senders: Senders = "senders" => default
 76    ],
 77    children: [
 78        description: Option<Element> = ("description", JINGLE) => Element,
 79        transport: Option<Element> = ("transport", JINGLE) => Element,
 80        security: Option<Element> = ("security", JINGLE) => Element
 81    ]
 82);
 83
 84impl Content {
 85    pub fn new(creator: Creator, name: ContentId) -> Content {
 86        Content {
 87            creator,
 88            name,
 89            disposition: Disposition::Session,
 90            senders: Senders::Both,
 91            description: None,
 92            transport: None,
 93            security: None,
 94        }
 95    }
 96
 97    pub fn with_disposition(mut self, disposition: Disposition) -> Content {
 98        self.disposition = disposition;
 99        self
100    }
101
102    pub fn with_senders(mut self, senders: Senders) -> Content {
103        self.senders = senders;
104        self
105    }
106
107    pub fn with_description(mut self, description: Element) -> Content {
108        self.description = Some(description);
109        self
110    }
111
112    pub fn with_transport(mut self, transport: Element) -> Content {
113        self.transport = Some(transport);
114        self
115    }
116
117    pub fn with_security(mut self, security: Element) -> Content {
118        self.security = Some(security);
119        self
120    }
121}
122
123#[derive(Debug, Clone, PartialEq)]
124pub enum Reason {
125    AlternativeSession, //(String),
126    Busy,
127    Cancel,
128    ConnectivityError,
129    Decline,
130    Expired,
131    FailedApplication,
132    FailedTransport,
133    GeneralError,
134    Gone,
135    IncompatibleParameters,
136    MediaError,
137    SecurityError,
138    Success,
139    Timeout,
140    UnsupportedApplications,
141    UnsupportedTransports,
142}
143
144impl FromStr for Reason {
145    type Err = Error;
146
147    fn from_str(s: &str) -> Result<Reason, Error> {
148        Ok(match s {
149            "alternative-session" => Reason::AlternativeSession,
150            "busy" => Reason::Busy,
151            "cancel" => Reason::Cancel,
152            "connectivity-error" => Reason::ConnectivityError,
153            "decline" => Reason::Decline,
154            "expired" => Reason::Expired,
155            "failed-application" => Reason::FailedApplication,
156            "failed-transport" => Reason::FailedTransport,
157            "general-error" => Reason::GeneralError,
158            "gone" => Reason::Gone,
159            "incompatible-parameters" => Reason::IncompatibleParameters,
160            "media-error" => Reason::MediaError,
161            "security-error" => Reason::SecurityError,
162            "success" => Reason::Success,
163            "timeout" => Reason::Timeout,
164            "unsupported-applications" => Reason::UnsupportedApplications,
165            "unsupported-transports" => Reason::UnsupportedTransports,
166
167            _ => return Err(Error::ParseError("Unknown reason.")),
168        })
169    }
170}
171
172impl From<Reason> for Element {
173    fn from(reason: Reason) -> Element {
174        Element::builder(match reason {
175            Reason::AlternativeSession => "alternative-session",
176            Reason::Busy => "busy",
177            Reason::Cancel => "cancel",
178            Reason::ConnectivityError => "connectivity-error",
179            Reason::Decline => "decline",
180            Reason::Expired => "expired",
181            Reason::FailedApplication => "failed-application",
182            Reason::FailedTransport => "failed-transport",
183            Reason::GeneralError => "general-error",
184            Reason::Gone => "gone",
185            Reason::IncompatibleParameters => "incompatible-parameters",
186            Reason::MediaError => "media-error",
187            Reason::SecurityError => "security-error",
188            Reason::Success => "success",
189            Reason::Timeout => "timeout",
190            Reason::UnsupportedApplications => "unsupported-applications",
191            Reason::UnsupportedTransports => "unsupported-transports",
192        }).build()
193    }
194}
195
196#[derive(Debug, Clone)]
197pub struct ReasonElement {
198    pub reason: Reason,
199    pub text: Option<String>,
200}
201
202impl TryFrom<Element> for ReasonElement {
203    type Err = Error;
204
205    fn try_from(elem: Element) -> Result<ReasonElement, Error> {
206        check_self!(elem, "reason", JINGLE);
207        let mut reason = None;
208        let mut text = None;
209        for child in elem.children() {
210            if !child.has_ns(ns::JINGLE) {
211                return Err(Error::ParseError("Reason contains a foreign element."));
212            }
213            match child.name() {
214                "text" => {
215                    if text.is_some() {
216                        return Err(Error::ParseError("Reason must not have more than one text."));
217                    }
218                    text = Some(child.text());
219                },
220                name => {
221                    if reason.is_some() {
222                        return Err(Error::ParseError("Reason must not have more than one reason."));
223                    }
224                    reason = Some(name.parse()?);
225                },
226            }
227        }
228        let reason = reason.ok_or(Error::ParseError("Reason doesn’t contain a valid reason."))?;
229        Ok(ReasonElement {
230            reason: reason,
231            text: text,
232        })
233    }
234}
235
236impl From<ReasonElement> for Element {
237    fn from(reason: ReasonElement) -> Element {
238        Element::builder("reason")
239                .append(Element::from(reason.reason))
240                .append(reason.text)
241                .build()
242    }
243}
244
245generate_id!(SessionId);
246
247#[derive(Debug, Clone)]
248pub struct Jingle {
249    pub action: Action,
250    pub initiator: Option<Jid>,
251    pub responder: Option<Jid>,
252    pub sid: SessionId,
253    pub contents: Vec<Content>,
254    pub reason: Option<ReasonElement>,
255    pub other: Vec<Element>,
256}
257
258impl IqSetPayload for Jingle {}
259
260impl Jingle {
261    pub fn new(action: Action, sid: SessionId) -> Jingle {
262        Jingle {
263            action: action,
264            sid: sid,
265            initiator: None,
266            responder: None,
267            contents: Vec::new(),
268            reason: None,
269            other: Vec::new(),
270        }
271    }
272
273    pub fn with_initiator(mut self, initiator: Jid) -> Jingle {
274        self.initiator = Some(initiator);
275        self
276    }
277
278    pub fn with_responder(mut self, responder: Jid) -> Jingle {
279        self.responder = Some(responder);
280        self
281    }
282
283    pub fn add_content(mut self, content: Content) -> Jingle {
284        self.contents.push(content);
285        self
286    }
287
288    pub fn set_reason(mut self, content: Content) -> Jingle {
289        self.contents.push(content);
290        self
291    }
292}
293
294impl TryFrom<Element> for Jingle {
295    type Err = Error;
296
297    fn try_from(root: Element) -> Result<Jingle, Error> {
298        check_self!(root, "jingle", JINGLE, "Jingle");
299        check_no_unknown_attributes!(root, "Jingle", ["action", "initiator", "responder", "sid"]);
300
301        let mut jingle = Jingle {
302            action: get_attr!(root, "action", required),
303            initiator: get_attr!(root, "initiator", optional),
304            responder: get_attr!(root, "responder", optional),
305            sid: get_attr!(root, "sid", required),
306            contents: vec!(),
307            reason: None,
308            other: vec!(),
309        };
310
311        for child in root.children().cloned() {
312            if child.is("content", ns::JINGLE) {
313                let content = Content::try_from(child)?;
314                jingle.contents.push(content);
315            } else if child.is("reason", ns::JINGLE) {
316                if jingle.reason.is_some() {
317                    return Err(Error::ParseError("Jingle must not have more than one reason."));
318                }
319                let reason = ReasonElement::try_from(child)?;
320                jingle.reason = Some(reason);
321            } else {
322                jingle.other.push(child);
323            }
324        }
325
326        Ok(jingle)
327    }
328}
329
330impl From<Jingle> for Element {
331    fn from(jingle: Jingle) -> Element {
332        Element::builder("jingle")
333                .ns(ns::JINGLE)
334                .attr("action", jingle.action)
335                .attr("initiator", jingle.initiator)
336                .attr("responder", jingle.responder)
337                .attr("sid", jingle.sid)
338                .append(jingle.contents)
339                .append(jingle.reason)
340                .build()
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_simple() {
350        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'/>".parse().unwrap();
351        let jingle = Jingle::try_from(elem).unwrap();
352        assert_eq!(jingle.action, Action::SessionInitiate);
353        assert_eq!(jingle.sid, SessionId(String::from("coucou")));
354    }
355
356    #[test]
357    fn test_invalid_jingle() {
358        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1'/>".parse().unwrap();
359        let error = Jingle::try_from(elem).unwrap_err();
360        let message = match error {
361            Error::ParseError(string) => string,
362            _ => panic!(),
363        };
364        assert_eq!(message, "Required attribute 'action' missing.");
365
366        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-info'/>".parse().unwrap();
367        let error = Jingle::try_from(elem).unwrap_err();
368        let message = match error {
369            Error::ParseError(string) => string,
370            _ => panic!(),
371        };
372        assert_eq!(message, "Required attribute 'sid' missing.");
373
374        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='coucou' sid='coucou'/>".parse().unwrap();
375        let error = Jingle::try_from(elem).unwrap_err();
376        let message = match error {
377            Error::ParseError(string) => string,
378            _ => panic!(),
379        };
380        assert_eq!(message, "Unknown value for 'action' attribute.");
381    }
382
383    #[test]
384    fn test_content() {
385        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();
386        let jingle = Jingle::try_from(elem).unwrap();
387        assert_eq!(jingle.contents[0].creator, Creator::Initiator);
388        assert_eq!(jingle.contents[0].name, ContentId(String::from("coucou")));
389        assert_eq!(jingle.contents[0].senders, Senders::Both);
390        assert_eq!(jingle.contents[0].disposition, Disposition::Session);
391
392        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();
393        let jingle = Jingle::try_from(elem).unwrap();
394        assert_eq!(jingle.contents[0].senders, Senders::Both);
395
396        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();
397        let jingle = Jingle::try_from(elem).unwrap();
398        assert_eq!(jingle.contents[0].disposition, Disposition::EarlySession);
399    }
400
401    #[test]
402    fn test_invalid_content() {
403        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content/></jingle>".parse().unwrap();
404        let error = Jingle::try_from(elem).unwrap_err();
405        let message = match error {
406            Error::ParseError(string) => string,
407            _ => panic!(),
408        };
409        assert_eq!(message, "Required attribute 'creator' missing.");
410
411        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator'/></jingle>".parse().unwrap();
412        let error = Jingle::try_from(elem).unwrap_err();
413        let message = match error {
414            Error::ParseError(string) => string,
415            _ => panic!(),
416        };
417        assert_eq!(message, "Required attribute 'name' missing.");
418
419        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='coucou' name='coucou'/></jingle>".parse().unwrap();
420        let error = Jingle::try_from(elem).unwrap_err();
421        let message = match error {
422            Error::ParseError(string) => string,
423            _ => panic!(),
424        };
425        assert_eq!(message, "Unknown value for 'creator' attribute.");
426
427        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders='coucou'/></jingle>".parse().unwrap();
428        let error = Jingle::try_from(elem).unwrap_err();
429        let message = match error {
430            Error::ParseError(string) => string,
431            _ => panic!(),
432        };
433        assert_eq!(message, "Unknown value for 'senders' attribute.");
434
435        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders=''/></jingle>".parse().unwrap();
436        let error = Jingle::try_from(elem).unwrap_err();
437        let message = match error {
438            Error::ParseError(string) => string,
439            _ => panic!(),
440        };
441        assert_eq!(message, "Unknown value for 'senders' attribute.");
442    }
443
444    #[test]
445    fn test_reason() {
446        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/></reason></jingle>".parse().unwrap();
447        let jingle = Jingle::try_from(elem).unwrap();
448        let reason = jingle.reason.unwrap();
449        assert_eq!(reason.reason, Reason::Success);
450        assert_eq!(reason.text, None);
451
452        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/><text>coucou</text></reason></jingle>".parse().unwrap();
453        let jingle = Jingle::try_from(elem).unwrap();
454        let reason = jingle.reason.unwrap();
455        assert_eq!(reason.reason, Reason::Success);
456        assert_eq!(reason.text, Some(String::from("coucou")));
457    }
458
459    #[test]
460    fn test_invalid_reason() {
461        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason/></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, "Reason doesn’t contain a valid reason.");
468
469        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><a/></reason></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 reason.");
476
477        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();
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, "Reason contains a foreign element.");
484
485        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/></reason><reason/></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, "Jingle must not have more than one reason.");
492
493        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/><text/><text/></reason></jingle>".parse().unwrap();
494        let error = Jingle::try_from(elem).unwrap_err();
495        let message = match error {
496            Error::ParseError(string) => string,
497            _ => panic!(),
498        };
499        assert_eq!(message, "Reason must not have more than one text.");
500    }
501}