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 std::convert::TryFrom;
  8use std::str::FromStr;
  9
 10use minidom::{Element, IntoElements, IntoAttributeValue, ElementEmitter};
 11use jid::Jid;
 12
 13use error::Error;
 14use ns;
 15
 16#[derive(Debug, Clone, PartialEq)]
 17pub enum Action {
 18    ContentAccept,
 19    ContentAdd,
 20    ContentModify,
 21    ContentReject,
 22    ContentRemove,
 23    DescriptionInfo,
 24    SecurityInfo,
 25    SessionAccept,
 26    SessionInfo,
 27    SessionInitiate,
 28    SessionTerminate,
 29    TransportAccept,
 30    TransportInfo,
 31    TransportReject,
 32    TransportReplace,
 33}
 34
 35impl FromStr for Action {
 36    type Err = Error;
 37
 38    fn from_str(s: &str) -> Result<Action, Error> {
 39        Ok(match s {
 40            "content-accept" => Action::ContentAccept,
 41            "content-add" => Action::ContentAdd,
 42            "content-modify" => Action::ContentModify,
 43            "content-reject" => Action::ContentReject,
 44            "content-remove" => Action::ContentRemove,
 45            "description-info" => Action::DescriptionInfo,
 46            "security-info" => Action::SecurityInfo,
 47            "session-accept" => Action::SessionAccept,
 48            "session-info" => Action::SessionInfo,
 49            "session-initiate" => Action::SessionInitiate,
 50            "session-terminate" => Action::SessionTerminate,
 51            "transport-accept" => Action::TransportAccept,
 52            "transport-info" => Action::TransportInfo,
 53            "transport-reject" => Action::TransportReject,
 54            "transport-replace" => Action::TransportReplace,
 55
 56            _ => return Err(Error::ParseError("Unknown action.")),
 57        })
 58    }
 59}
 60
 61impl IntoAttributeValue for Action {
 62    fn into_attribute_value(self) -> Option<String> {
 63        Some(String::from(match self {
 64            Action::ContentAccept => "content-accept",
 65            Action::ContentAdd => "content-add",
 66            Action::ContentModify => "content-modify",
 67            Action::ContentReject => "content-reject",
 68            Action::ContentRemove => "content-remove",
 69            Action::DescriptionInfo => "description-info",
 70            Action::SecurityInfo => "security-info",
 71            Action::SessionAccept => "session-accept",
 72            Action::SessionInfo => "session-info",
 73            Action::SessionInitiate => "session-initiate",
 74            Action::SessionTerminate => "session-terminate",
 75            Action::TransportAccept => "transport-accept",
 76            Action::TransportInfo => "transport-info",
 77            Action::TransportReject => "transport-reject",
 78            Action::TransportReplace => "transport-replace",
 79        }))
 80    }
 81}
 82
 83#[derive(Debug, Clone, PartialEq)]
 84pub enum Creator {
 85    Initiator,
 86    Responder,
 87}
 88
 89impl FromStr for Creator {
 90    type Err = Error;
 91
 92    fn from_str(s: &str) -> Result<Creator, Error> {
 93        Ok(match s {
 94            "initiator" => Creator::Initiator,
 95            "responder" => Creator::Responder,
 96
 97            _ => return Err(Error::ParseError("Unknown creator.")),
 98        })
 99    }
100}
101
102impl IntoAttributeValue for Creator {
103    fn into_attribute_value(self) -> Option<String> {
104        Some(String::from(match self {
105            Creator::Initiator => "initiator",
106            Creator::Responder => "responder",
107        }))
108    }
109}
110
111#[derive(Debug, Clone, PartialEq)]
112pub enum Senders {
113    Both,
114    Initiator,
115    None_,
116    Responder,
117}
118
119impl Default for Senders {
120    fn default() -> Senders {
121        Senders::Both
122    }
123}
124
125impl FromStr for Senders {
126    type Err = Error;
127
128    fn from_str(s: &str) -> Result<Senders, Error> {
129        Ok(match s {
130            "both" => Senders::Both,
131            "initiator" => Senders::Initiator,
132            "none" => Senders::None_,
133            "responder" => Senders::Responder,
134
135            _ => return Err(Error::ParseError("Unknown senders.")),
136        })
137    }
138}
139
140impl IntoAttributeValue for Senders {
141    fn into_attribute_value(self) -> Option<String> {
142        Some(String::from(match self {
143            Senders::Both => "both",
144            Senders::Initiator => "initiator",
145            Senders::None_ => "none",
146            Senders::Responder => "responder",
147        }))
148    }
149}
150
151#[derive(Debug, Clone)]
152pub struct Content {
153    pub creator: Creator,
154    pub disposition: String,
155    pub name: String,
156    pub senders: Senders,
157    pub description: Option<Element>,
158    pub transport: Option<Element>,
159    pub security: Option<Element>,
160}
161
162impl TryFrom<Element> for Content {
163    type Error = Error;
164
165    fn try_from(elem: Element) -> Result<Content, Error> {
166        if !elem.is("content", ns::JINGLE) {
167            return Err(Error::ParseError("This is not a content element."));
168        }
169
170        let mut content = Content {
171            creator: get_attr!(elem, "creator", required),
172            disposition: get_attr!(elem, "disposition", optional).unwrap_or(String::from("session")),
173            name: get_attr!(elem, "name", required),
174            senders: get_attr!(elem, "senders", default),
175            description: None,
176            transport: None,
177            security: None,
178        };
179        for child in elem.children() {
180            if child.name() == "description" {
181                if content.description.is_some() {
182                    return Err(Error::ParseError("Content must not have more than one description."));
183                }
184                content.description = Some(child.clone());
185            } else if child.name() == "transport" {
186                if content.transport.is_some() {
187                    return Err(Error::ParseError("Content must not have more than one transport."));
188                }
189                content.transport = Some(child.clone());
190            } else if child.name() == "security" {
191                if content.security.is_some() {
192                    return Err(Error::ParseError("Content must not have more than one security."));
193                }
194                content.security = Some(child.clone());
195            }
196        }
197        Ok(content)
198    }
199}
200
201impl Into<Element> for Content {
202    fn into(self) -> Element {
203        Element::builder("content")
204                .ns(ns::JINGLE)
205                .attr("creator", self.creator)
206                .attr("disposition", self.disposition)
207                .attr("name", self.name)
208                .attr("senders", self.senders)
209                .append(self.description)
210                .append(self.transport)
211                .append(self.security)
212                .build()
213    }
214}
215
216impl IntoElements for Content {
217    fn into_elements(self, emitter: &mut ElementEmitter) {
218        emitter.append_child(self.into());
219    }
220}
221
222#[derive(Debug, Clone, PartialEq)]
223pub enum Reason {
224    AlternativeSession, //(String),
225    Busy,
226    Cancel,
227    ConnectivityError,
228    Decline,
229    Expired,
230    FailedApplication,
231    FailedTransport,
232    GeneralError,
233    Gone,
234    IncompatibleParameters,
235    MediaError,
236    SecurityError,
237    Success,
238    Timeout,
239    UnsupportedApplications,
240    UnsupportedTransports,
241}
242
243impl FromStr for Reason {
244    type Err = Error;
245
246    fn from_str(s: &str) -> Result<Reason, Error> {
247        Ok(match s {
248            "alternative-session" => Reason::AlternativeSession,
249            "busy" => Reason::Busy,
250            "cancel" => Reason::Cancel,
251            "connectivity-error" => Reason::ConnectivityError,
252            "decline" => Reason::Decline,
253            "expired" => Reason::Expired,
254            "failed-application" => Reason::FailedApplication,
255            "failed-transport" => Reason::FailedTransport,
256            "general-error" => Reason::GeneralError,
257            "gone" => Reason::Gone,
258            "incompatible-parameters" => Reason::IncompatibleParameters,
259            "media-error" => Reason::MediaError,
260            "security-error" => Reason::SecurityError,
261            "success" => Reason::Success,
262            "timeout" => Reason::Timeout,
263            "unsupported-applications" => Reason::UnsupportedApplications,
264            "unsupported-transports" => Reason::UnsupportedTransports,
265
266            _ => return Err(Error::ParseError("Unknown reason.")),
267        })
268    }
269}
270
271impl Into<Element> for Reason {
272    fn into(self) -> Element {
273        Element::builder(match self {
274            Reason::AlternativeSession => "alternative-session",
275            Reason::Busy => "busy",
276            Reason::Cancel => "cancel",
277            Reason::ConnectivityError => "connectivity-error",
278            Reason::Decline => "decline",
279            Reason::Expired => "expired",
280            Reason::FailedApplication => "failed-application",
281            Reason::FailedTransport => "failed-transport",
282            Reason::GeneralError => "general-error",
283            Reason::Gone => "gone",
284            Reason::IncompatibleParameters => "incompatible-parameters",
285            Reason::MediaError => "media-error",
286            Reason::SecurityError => "security-error",
287            Reason::Success => "success",
288            Reason::Timeout => "timeout",
289            Reason::UnsupportedApplications => "unsupported-applications",
290            Reason::UnsupportedTransports => "unsupported-transports",
291        }).build()
292    }
293}
294
295#[derive(Debug, Clone)]
296pub struct ReasonElement {
297    pub reason: Reason,
298    pub text: Option<String>,
299}
300
301impl TryFrom<Element> for ReasonElement {
302    type Error = Error;
303
304    fn try_from(elem: Element) -> Result<ReasonElement, Error> {
305        if !elem.is("reason", ns::JINGLE) {
306            return Err(Error::ParseError("This is not a reason element."));
307        }
308        let mut reason = None;
309        let mut text = None;
310        for child in elem.children() {
311            if child.ns() != Some(ns::JINGLE) {
312                return Err(Error::ParseError("Reason contains a foreign element."));
313            }
314            match child.name() {
315                "text" => {
316                    if text.is_some() {
317                        return Err(Error::ParseError("Reason must not have more than one text."));
318                    }
319                    text = Some(child.text());
320                },
321                name => {
322                    if reason.is_some() {
323                        return Err(Error::ParseError("Reason must not have more than one reason."));
324                    }
325                    reason = Some(name.parse()?);
326                },
327            }
328        }
329        let reason = reason.ok_or(Error::ParseError("Reason doesn’t contain a valid reason."))?;
330        Ok(ReasonElement {
331            reason: reason,
332            text: text,
333        })
334    }
335}
336
337impl Into<Element> for ReasonElement {
338    fn into(self) -> Element {
339        let reason: Element = self.reason.into();
340        Element::builder("reason")
341                .append(reason)
342                .append(self.text)
343                .build()
344    }
345}
346
347impl IntoElements for ReasonElement {
348    fn into_elements(self, emitter: &mut ElementEmitter) {
349        emitter.append_child(self.into());
350    }
351}
352
353#[derive(Debug, Clone)]
354pub struct Jingle {
355    pub action: Action,
356    pub initiator: Option<Jid>,
357    pub responder: Option<Jid>,
358    pub sid: String,
359    pub contents: Vec<Content>,
360    pub reason: Option<ReasonElement>,
361    pub other: Vec<Element>,
362}
363
364impl TryFrom<Element> for Jingle {
365    type Error = Error;
366
367    fn try_from(root: Element) -> Result<Jingle, Error> {
368        if !root.is("jingle", ns::JINGLE) {
369            return Err(Error::ParseError("This is not a Jingle element."));
370        }
371
372        let mut jingle = Jingle {
373            action: get_attr!(root, "action", required),
374            initiator: get_attr!(root, "initiator", optional),
375            responder: get_attr!(root, "responder", optional),
376            sid: get_attr!(root, "sid", required),
377            contents: vec!(),
378            reason: None,
379            other: vec!(),
380        };
381
382        for child in root.children().cloned() {
383            if child.is("content", ns::JINGLE) {
384                let content = Content::try_from(child)?;
385                jingle.contents.push(content);
386            } else if child.is("reason", ns::JINGLE) {
387                if jingle.reason.is_some() {
388                    return Err(Error::ParseError("Jingle must not have more than one reason."));
389                }
390                let reason = ReasonElement::try_from(child)?;
391                jingle.reason = Some(reason);
392            } else {
393                jingle.other.push(child);
394            }
395        }
396
397        Ok(jingle)
398    }
399}
400
401impl Into<Element> for Jingle {
402    fn into(self) -> Element {
403        Element::builder("jingle")
404                .ns(ns::JINGLE)
405                .attr("action", self.action)
406                .attr("initiator", match self.initiator { Some(initiator) => Some(String::from(initiator)), None => None })
407                .attr("responder", match self.responder { Some(responder) => Some(String::from(responder)), None => None })
408                .attr("sid", self.sid)
409                .append(self.contents)
410                .append(self.reason)
411                .build()
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    #[test]
420    fn test_simple() {
421        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'/>".parse().unwrap();
422        let jingle = Jingle::try_from(elem).unwrap();
423        assert_eq!(jingle.action, Action::SessionInitiate);
424        assert_eq!(jingle.sid, "coucou");
425    }
426
427    #[test]
428    fn test_invalid_jingle() {
429        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1'/>".parse().unwrap();
430        let error = Jingle::try_from(elem).unwrap_err();
431        let message = match error {
432            Error::ParseError(string) => string,
433            _ => panic!(),
434        };
435        assert_eq!(message, "Required attribute 'action' missing.");
436
437        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-info'/>".parse().unwrap();
438        let error = Jingle::try_from(elem).unwrap_err();
439        let message = match error {
440            Error::ParseError(string) => string,
441            _ => panic!(),
442        };
443        assert_eq!(message, "Required attribute 'sid' missing.");
444
445        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='coucou' sid='coucou'/>".parse().unwrap();
446        let error = Jingle::try_from(elem).unwrap_err();
447        let message = match error {
448            Error::ParseError(string) => string,
449            _ => panic!(),
450        };
451        assert_eq!(message, "Unknown action.");
452    }
453
454    #[test]
455    fn test_content() {
456        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();
457        let jingle = Jingle::try_from(elem).unwrap();
458        assert_eq!(jingle.contents[0].creator, Creator::Initiator);
459        assert_eq!(jingle.contents[0].name, "coucou");
460        assert_eq!(jingle.contents[0].senders, Senders::Both);
461        assert_eq!(jingle.contents[0].disposition, "session");
462
463        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();
464        let jingle = Jingle::try_from(elem).unwrap();
465        assert_eq!(jingle.contents[0].senders, Senders::Both);
466
467        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();
468        let jingle = Jingle::try_from(elem).unwrap();
469        assert_eq!(jingle.contents[0].disposition, "early-session");
470    }
471
472    #[test]
473    fn test_invalid_content() {
474        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content/></jingle>".parse().unwrap();
475        let error = Jingle::try_from(elem).unwrap_err();
476        let message = match error {
477            Error::ParseError(string) => string,
478            _ => panic!(),
479        };
480        assert_eq!(message, "Required attribute 'creator' missing.");
481
482        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator'/></jingle>".parse().unwrap();
483        let error = Jingle::try_from(elem).unwrap_err();
484        let message = match error {
485            Error::ParseError(string) => string,
486            _ => panic!(),
487        };
488        assert_eq!(message, "Required attribute 'name' missing.");
489
490        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='coucou' name='coucou'/></jingle>".parse().unwrap();
491        let error = Jingle::try_from(elem).unwrap_err();
492        let message = match error {
493            Error::ParseError(string) => string,
494            _ => panic!(),
495        };
496        assert_eq!(message, "Unknown creator.");
497
498        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders='coucou'/></jingle>".parse().unwrap();
499        let error = Jingle::try_from(elem).unwrap_err();
500        let message = match error {
501            Error::ParseError(string) => string,
502            _ => panic!(),
503        };
504        assert_eq!(message, "Unknown senders.");
505
506        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders=''/></jingle>".parse().unwrap();
507        let error = Jingle::try_from(elem).unwrap_err();
508        let message = match error {
509            Error::ParseError(string) => string,
510            _ => panic!(),
511        };
512        assert_eq!(message, "Unknown senders.");
513    }
514
515    #[test]
516    fn test_reason() {
517        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/></reason></jingle>".parse().unwrap();
518        let jingle = Jingle::try_from(elem).unwrap();
519        let reason = jingle.reason.unwrap();
520        assert_eq!(reason.reason, Reason::Success);
521        assert_eq!(reason.text, None);
522
523        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/><text>coucou</text></reason></jingle>".parse().unwrap();
524        let jingle = Jingle::try_from(elem).unwrap();
525        let reason = jingle.reason.unwrap();
526        assert_eq!(reason.reason, Reason::Success);
527        assert_eq!(reason.text, Some(String::from("coucou")));
528    }
529
530    #[test]
531    fn test_invalid_reason() {
532        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason/></jingle>".parse().unwrap();
533        let error = Jingle::try_from(elem).unwrap_err();
534        let message = match error {
535            Error::ParseError(string) => string,
536            _ => panic!(),
537        };
538        assert_eq!(message, "Reason doesn’t contain a valid reason.");
539
540        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><a/></reason></jingle>".parse().unwrap();
541        let error = Jingle::try_from(elem).unwrap_err();
542        let message = match error {
543            Error::ParseError(string) => string,
544            _ => panic!(),
545        };
546        assert_eq!(message, "Unknown reason.");
547
548        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();
549        let error = Jingle::try_from(elem).unwrap_err();
550        let message = match error {
551            Error::ParseError(string) => string,
552            _ => panic!(),
553        };
554        assert_eq!(message, "Reason contains a foreign element.");
555
556        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/></reason><reason/></jingle>".parse().unwrap();
557        let error = Jingle::try_from(elem).unwrap_err();
558        let message = match error {
559            Error::ParseError(string) => string,
560            _ => panic!(),
561        };
562        assert_eq!(message, "Jingle must not have more than one reason.");
563
564        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/><text/><text/></reason></jingle>".parse().unwrap();
565        let error = Jingle::try_from(elem).unwrap_err();
566        let message = match error {
567            Error::ParseError(string) => string,
568            _ => panic!(),
569        };
570        assert_eq!(message, "Reason must not have more than one text.");
571    }
572}