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