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