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