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