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