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
7#![allow(missing_docs)]
8
9use try_from::TryFrom;
10use std::str::FromStr;
11
12use minidom::Element;
13use jid::Jid;
14
15use error::Error;
16use ns;
17use iq::IqSetPayload;
18
19generate_attribute!(Action, "action", {
20 ContentAccept => "content-accept",
21 ContentAdd => "content-add",
22 ContentModify => "content-modify",
23 ContentReject => "content-reject",
24 ContentRemove => "content-remove",
25 DescriptionInfo => "description-info",
26 SecurityInfo => "security-info",
27 SessionAccept => "session-accept",
28 SessionInfo => "session-info",
29 SessionInitiate => "session-initiate",
30 SessionTerminate => "session-terminate",
31 TransportAccept => "transport-accept",
32 TransportInfo => "transport-info",
33 TransportReject => "transport-reject",
34 TransportReplace => "transport-replace",
35});
36
37generate_attribute!(Creator, "creator", {
38 Initiator => "initiator",
39 Responder => "responder",
40});
41
42generate_attribute!(Senders, "senders", {
43 Both => "both",
44 Initiator => "initiator",
45 None => "none",
46 Responder => "responder",
47}, Default = Both);
48
49// From https://www.iana.org/assignments/cont-disp/cont-disp.xhtml
50generate_attribute!(Disposition, "disposition", {
51 Inline => "inline",
52 Attachment => "attachment",
53 FormData => "form-data",
54 Signal => "signal",
55 Alert => "alert",
56 Icon => "icon",
57 Render => "render",
58 RecipientListHistory => "recipient-list-history",
59 Session => "session",
60 Aib => "aib",
61 EarlySession => "early-session",
62 RecipientList => "recipient-list",
63 Notification => "notification",
64 ByReference => "by-reference",
65 InfoPackage => "info-package",
66 RecordingSession => "recording-session",
67}, Default = Session);
68
69generate_id!(ContentId);
70
71generate_element!(
72 Content, "content", JINGLE,
73 attributes: [
74 creator: Creator = "creator" => required,
75 disposition: Disposition = "disposition" => default,
76 name: ContentId = "name" => required,
77 senders: Senders = "senders" => default
78 ],
79 children: [
80 description: Option<Element> = ("description", JINGLE) => Element,
81 transport: Option<Element> = ("transport", JINGLE) => Element,
82 security: Option<Element> = ("security", JINGLE) => Element
83 ]
84);
85
86impl Content {
87 pub fn new(creator: Creator, name: ContentId) -> Content {
88 Content {
89 creator,
90 name,
91 disposition: Disposition::Session,
92 senders: Senders::Both,
93 description: None,
94 transport: None,
95 security: None,
96 }
97 }
98
99 pub fn with_disposition(mut self, disposition: Disposition) -> Content {
100 self.disposition = disposition;
101 self
102 }
103
104 pub fn with_senders(mut self, senders: Senders) -> Content {
105 self.senders = senders;
106 self
107 }
108
109 pub fn with_description(mut self, description: Element) -> Content {
110 self.description = Some(description);
111 self
112 }
113
114 pub fn with_transport(mut self, transport: Element) -> Content {
115 self.transport = Some(transport);
116 self
117 }
118
119 pub fn with_security(mut self, security: Element) -> Content {
120 self.security = Some(security);
121 self
122 }
123}
124
125#[derive(Debug, Clone, PartialEq)]
126pub enum Reason {
127 AlternativeSession, //(String),
128 Busy,
129 Cancel,
130 ConnectivityError,
131 Decline,
132 Expired,
133 FailedApplication,
134 FailedTransport,
135 GeneralError,
136 Gone,
137 IncompatibleParameters,
138 MediaError,
139 SecurityError,
140 Success,
141 Timeout,
142 UnsupportedApplications,
143 UnsupportedTransports,
144}
145
146impl FromStr for Reason {
147 type Err = Error;
148
149 fn from_str(s: &str) -> Result<Reason, Error> {
150 Ok(match s {
151 "alternative-session" => Reason::AlternativeSession,
152 "busy" => Reason::Busy,
153 "cancel" => Reason::Cancel,
154 "connectivity-error" => Reason::ConnectivityError,
155 "decline" => Reason::Decline,
156 "expired" => Reason::Expired,
157 "failed-application" => Reason::FailedApplication,
158 "failed-transport" => Reason::FailedTransport,
159 "general-error" => Reason::GeneralError,
160 "gone" => Reason::Gone,
161 "incompatible-parameters" => Reason::IncompatibleParameters,
162 "media-error" => Reason::MediaError,
163 "security-error" => Reason::SecurityError,
164 "success" => Reason::Success,
165 "timeout" => Reason::Timeout,
166 "unsupported-applications" => Reason::UnsupportedApplications,
167 "unsupported-transports" => Reason::UnsupportedTransports,
168
169 _ => return Err(Error::ParseError("Unknown reason.")),
170 })
171 }
172}
173
174impl From<Reason> for Element {
175 fn from(reason: Reason) -> Element {
176 Element::builder(match reason {
177 Reason::AlternativeSession => "alternative-session",
178 Reason::Busy => "busy",
179 Reason::Cancel => "cancel",
180 Reason::ConnectivityError => "connectivity-error",
181 Reason::Decline => "decline",
182 Reason::Expired => "expired",
183 Reason::FailedApplication => "failed-application",
184 Reason::FailedTransport => "failed-transport",
185 Reason::GeneralError => "general-error",
186 Reason::Gone => "gone",
187 Reason::IncompatibleParameters => "incompatible-parameters",
188 Reason::MediaError => "media-error",
189 Reason::SecurityError => "security-error",
190 Reason::Success => "success",
191 Reason::Timeout => "timeout",
192 Reason::UnsupportedApplications => "unsupported-applications",
193 Reason::UnsupportedTransports => "unsupported-transports",
194 }).build()
195 }
196}
197
198#[derive(Debug, Clone)]
199pub struct ReasonElement {
200 pub reason: Reason,
201 pub text: Option<String>,
202}
203
204impl TryFrom<Element> for ReasonElement {
205 type Err = Error;
206
207 fn try_from(elem: Element) -> Result<ReasonElement, Error> {
208 check_self!(elem, "reason", JINGLE);
209 let mut reason = None;
210 let mut text = None;
211 for child in elem.children() {
212 if !child.has_ns(ns::JINGLE) {
213 return Err(Error::ParseError("Reason contains a foreign element."));
214 }
215 match child.name() {
216 "text" => {
217 if text.is_some() {
218 return Err(Error::ParseError("Reason must not have more than one text."));
219 }
220 text = Some(child.text());
221 },
222 name => {
223 if reason.is_some() {
224 return Err(Error::ParseError("Reason must not have more than one reason."));
225 }
226 reason = Some(name.parse()?);
227 },
228 }
229 }
230 let reason = reason.ok_or(Error::ParseError("Reason doesn’t contain a valid reason."))?;
231 Ok(ReasonElement {
232 reason: reason,
233 text: text,
234 })
235 }
236}
237
238impl From<ReasonElement> for Element {
239 fn from(reason: ReasonElement) -> Element {
240 Element::builder("reason")
241 .append(Element::from(reason.reason))
242 .append(reason.text)
243 .build()
244 }
245}
246
247generate_id!(SessionId);
248
249#[derive(Debug, Clone)]
250pub struct Jingle {
251 pub action: Action,
252 pub initiator: Option<Jid>,
253 pub responder: Option<Jid>,
254 pub sid: SessionId,
255 pub contents: Vec<Content>,
256 pub reason: Option<ReasonElement>,
257 pub other: Vec<Element>,
258}
259
260impl IqSetPayload for Jingle {}
261
262impl Jingle {
263 pub fn new(action: Action, sid: SessionId) -> Jingle {
264 Jingle {
265 action: action,
266 sid: sid,
267 initiator: None,
268 responder: None,
269 contents: Vec::new(),
270 reason: None,
271 other: Vec::new(),
272 }
273 }
274
275 pub fn with_initiator(mut self, initiator: Jid) -> Jingle {
276 self.initiator = Some(initiator);
277 self
278 }
279
280 pub fn with_responder(mut self, responder: Jid) -> Jingle {
281 self.responder = Some(responder);
282 self
283 }
284
285 pub fn add_content(mut self, content: Content) -> Jingle {
286 self.contents.push(content);
287 self
288 }
289
290 pub fn set_reason(mut self, content: Content) -> Jingle {
291 self.contents.push(content);
292 self
293 }
294}
295
296impl TryFrom<Element> for Jingle {
297 type Err = Error;
298
299 fn try_from(root: Element) -> Result<Jingle, Error> {
300 check_self!(root, "jingle", JINGLE, "Jingle");
301 check_no_unknown_attributes!(root, "Jingle", ["action", "initiator", "responder", "sid"]);
302
303 let mut jingle = Jingle {
304 action: get_attr!(root, "action", required),
305 initiator: get_attr!(root, "initiator", optional),
306 responder: get_attr!(root, "responder", optional),
307 sid: get_attr!(root, "sid", required),
308 contents: vec!(),
309 reason: None,
310 other: vec!(),
311 };
312
313 for child in root.children().cloned() {
314 if child.is("content", ns::JINGLE) {
315 let content = Content::try_from(child)?;
316 jingle.contents.push(content);
317 } else if child.is("reason", ns::JINGLE) {
318 if jingle.reason.is_some() {
319 return Err(Error::ParseError("Jingle must not have more than one reason."));
320 }
321 let reason = ReasonElement::try_from(child)?;
322 jingle.reason = Some(reason);
323 } else {
324 jingle.other.push(child);
325 }
326 }
327
328 Ok(jingle)
329 }
330}
331
332impl From<Jingle> for Element {
333 fn from(jingle: Jingle) -> Element {
334 Element::builder("jingle")
335 .ns(ns::JINGLE)
336 .attr("action", jingle.action)
337 .attr("initiator", jingle.initiator)
338 .attr("responder", jingle.responder)
339 .attr("sid", jingle.sid)
340 .append(jingle.contents)
341 .append(jingle.reason)
342 .build()
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 #[test]
351 fn test_simple() {
352 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'/>".parse().unwrap();
353 let jingle = Jingle::try_from(elem).unwrap();
354 assert_eq!(jingle.action, Action::SessionInitiate);
355 assert_eq!(jingle.sid, SessionId(String::from("coucou")));
356 }
357
358 #[test]
359 fn test_invalid_jingle() {
360 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1'/>".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 'action' missing.");
367
368 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-info'/>".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 'sid' missing.");
375
376 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='coucou' sid='coucou'/>".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 'action' attribute.");
383 }
384
385 #[test]
386 fn test_content() {
387 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();
388 let jingle = Jingle::try_from(elem).unwrap();
389 assert_eq!(jingle.contents[0].creator, Creator::Initiator);
390 assert_eq!(jingle.contents[0].name, ContentId(String::from("coucou")));
391 assert_eq!(jingle.contents[0].senders, Senders::Both);
392 assert_eq!(jingle.contents[0].disposition, Disposition::Session);
393
394 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();
395 let jingle = Jingle::try_from(elem).unwrap();
396 assert_eq!(jingle.contents[0].senders, Senders::Both);
397
398 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();
399 let jingle = Jingle::try_from(elem).unwrap();
400 assert_eq!(jingle.contents[0].disposition, Disposition::EarlySession);
401 }
402
403 #[test]
404 fn test_invalid_content() {
405 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content/></jingle>".parse().unwrap();
406 let error = Jingle::try_from(elem).unwrap_err();
407 let message = match error {
408 Error::ParseError(string) => string,
409 _ => panic!(),
410 };
411 assert_eq!(message, "Required attribute 'creator' missing.");
412
413 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator'/></jingle>".parse().unwrap();
414 let error = Jingle::try_from(elem).unwrap_err();
415 let message = match error {
416 Error::ParseError(string) => string,
417 _ => panic!(),
418 };
419 assert_eq!(message, "Required attribute 'name' missing.");
420
421 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='coucou' name='coucou'/></jingle>".parse().unwrap();
422 let error = Jingle::try_from(elem).unwrap_err();
423 let message = match error {
424 Error::ParseError(string) => string,
425 _ => panic!(),
426 };
427 assert_eq!(message, "Unknown value for 'creator' attribute.");
428
429 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders='coucou'/></jingle>".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, "Unknown value for 'senders' attribute.");
436
437 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders=''/></jingle>".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, "Unknown value for 'senders' attribute.");
444 }
445
446 #[test]
447 fn test_reason() {
448 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/></reason></jingle>".parse().unwrap();
449 let jingle = Jingle::try_from(elem).unwrap();
450 let reason = jingle.reason.unwrap();
451 assert_eq!(reason.reason, Reason::Success);
452 assert_eq!(reason.text, None);
453
454 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/><text>coucou</text></reason></jingle>".parse().unwrap();
455 let jingle = Jingle::try_from(elem).unwrap();
456 let reason = jingle.reason.unwrap();
457 assert_eq!(reason.reason, Reason::Success);
458 assert_eq!(reason.text, Some(String::from("coucou")));
459 }
460
461 #[test]
462 fn test_invalid_reason() {
463 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason/></jingle>".parse().unwrap();
464 let error = Jingle::try_from(elem).unwrap_err();
465 let message = match error {
466 Error::ParseError(string) => string,
467 _ => panic!(),
468 };
469 assert_eq!(message, "Reason doesn’t contain a valid reason.");
470
471 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><a/></reason></jingle>".parse().unwrap();
472 let error = Jingle::try_from(elem).unwrap_err();
473 let message = match error {
474 Error::ParseError(string) => string,
475 _ => panic!(),
476 };
477 assert_eq!(message, "Unknown reason.");
478
479 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();
480 let error = Jingle::try_from(elem).unwrap_err();
481 let message = match error {
482 Error::ParseError(string) => string,
483 _ => panic!(),
484 };
485 assert_eq!(message, "Reason contains a foreign element.");
486
487 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/></reason><reason/></jingle>".parse().unwrap();
488 let error = Jingle::try_from(elem).unwrap_err();
489 let message = match error {
490 Error::ParseError(string) => string,
491 _ => panic!(),
492 };
493 assert_eq!(message, "Jingle must not have more than one reason.");
494
495 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/><text/><text/></reason></jingle>".parse().unwrap();
496 let error = Jingle::try_from(elem).unwrap_err();
497 let message = match error {
498 Error::ParseError(string) => string,
499 _ => panic!(),
500 };
501 assert_eq!(message, "Reason must not have more than one text.");
502 }
503}