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