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!(
18 /// The action attribute.
19 Action, "action", {
20 /// Accept a content-add action received from another party.
21 ContentAccept => "content-accept",
22
23 /// Add one or more new content definitions to the session.
24 ContentAdd => "content-add",
25
26 /// Change the directionality of media sending.
27 ContentModify => "content-modify",
28
29 /// Reject a content-add action received from another party.
30 ContentReject => "content-reject",
31
32 /// Remove one or more content definitions from the session.
33 ContentRemove => "content-remove",
34
35 /// Exchange information about parameters for an application type.
36 DescriptionInfo => "description-info",
37
38 /// Exchange information about security preconditions.
39 SecurityInfo => "security-info",
40
41 /// Definitively accept a session negotiation.
42 SessionAccept => "session-accept",
43
44 /// Send session-level information, such as a ping or a ringing message.
45 SessionInfo => "session-info",
46
47 /// Request negotiation of a new Jingle session.
48 SessionInitiate => "session-initiate",
49
50 /// End an existing session.
51 SessionTerminate => "session-terminate",
52
53 /// Accept a transport-replace action received from another party.
54 TransportAccept => "transport-accept",
55
56 /// Exchange transport candidates.
57 TransportInfo => "transport-info",
58
59 /// Reject a transport-replace action received from another party.
60 TransportReject => "transport-reject",
61
62 /// Redefine a transport method or replace it with a different method.
63 TransportReplace => "transport-replace",
64 }
65);
66
67generate_attribute!(
68 /// Which party originally generated the content type.
69 Creator, "creator", {
70 /// This content was created by the initiator of this session.
71 Initiator => "initiator",
72
73 /// This content was created by the responder of this session.
74 Responder => "responder",
75 }
76);
77
78generate_attribute!(
79 /// Which parties in the session will be generating content.
80 Senders, "senders", {
81 /// Both parties can send for this content.
82 Both => "both",
83
84 /// Only the initiator can send for this content.
85 Initiator => "initiator",
86
87 /// No one can send for this content.
88 None => "none",
89
90 /// Only the responder can send for this content.
91 Responder => "responder",
92 }, Default = Both
93);
94
95generate_attribute!(
96 /// How the content definition is to be interpreted by the recipient. The
97 /// meaning of this attribute matches the "Content-Disposition" header as
98 /// defined in RFC 2183 and applied to SIP by RFC 3261.
99 ///
100 /// Possible values are defined here:
101 /// https://www.iana.org/assignments/cont-disp/cont-disp.xhtml
102 Disposition, "disposition", {
103 /// Displayed automatically.
104 Inline => "inline",
105
106 /// User controlled display.
107 Attachment => "attachment",
108
109 /// Process as form response.
110 FormData => "form-data",
111
112 /// Tunneled content to be processed silently.
113 Signal => "signal",
114
115 /// The body is a custom ring tone to alert the user.
116 Alert => "alert",
117
118 /// The body is displayed as an icon to the user.
119 Icon => "icon",
120
121 /// The body should be displayed to the user.
122 Render => "render",
123
124 /// The body contains a list of URIs that indicates the recipients of
125 /// the request.
126 RecipientListHistory => "recipient-list-history",
127
128 /// The body describes a communications session, for example, an
129 /// RFC2327 SDP body.
130 Session => "session",
131
132 /// Authenticated Identity Body.
133 Aib => "aib",
134
135 /// The body describes an early communications session, for example,
136 /// and [RFC2327] SDP body.
137 EarlySession => "early-session",
138
139 /// The body includes a list of URIs to which URI-list services are to
140 /// be applied.
141 RecipientList => "recipient-list",
142
143 /// The payload of the message carrying this Content-Disposition header
144 /// field value is an Instant Message Disposition Notification as
145 /// requested in the corresponding Instant Message.
146 Notification => "notification",
147
148 /// The body needs to be handled according to a reference to the body
149 /// that is located in the same SIP message as the body.
150 ByReference => "by-reference",
151
152 /// The body contains information associated with an Info Package.
153 InfoPackage => "info-package",
154
155 /// The body describes either metadata about the RS or the reason for
156 /// the metadata snapshot request as determined by the MIME value
157 /// indicated in the Content-Type.
158 RecordingSession => "recording-session",
159 }, Default = Session
160);
161
162generate_id!(
163 /// An unique identifier in a session, referencing a
164 /// [struct.Content.html](Content element).
165 ContentId
166);
167
168generate_element!(
169 /// Describes a session’s content, there can be multiple content in one
170 /// session.
171 Content, "content", JINGLE,
172 attributes: [
173 /// Who created this content.
174 creator: Creator = "creator" => required,
175
176 /// How the content definition is to be interpreted by the recipient.
177 disposition: Disposition = "disposition" => default,
178
179 /// A per-session unique identifier for this content.
180 name: ContentId = "name" => required,
181
182 /// Who can send data for this content.
183 senders: Senders = "senders" => default
184 ],
185 children: [
186 /// What to send.
187 description: Option<Element> = ("description", JINGLE) => Element,
188
189 /// How to send it.
190 transport: Option<Element> = ("transport", JINGLE) => Element,
191
192 /// With which security.
193 security: Option<Element> = ("security", JINGLE) => Element
194 ]
195);
196
197impl Content {
198 /// Create a new content.
199 pub fn new(creator: Creator, name: ContentId) -> Content {
200 Content {
201 creator,
202 name,
203 disposition: Disposition::Session,
204 senders: Senders::Both,
205 description: None,
206 transport: None,
207 security: None,
208 }
209 }
210
211 /// Set how the content is to be interpreted by the recipient.
212 pub fn with_disposition(mut self, disposition: Disposition) -> Content {
213 self.disposition = disposition;
214 self
215 }
216
217 /// Specify who can send data for this content.
218 pub fn with_senders(mut self, senders: Senders) -> Content {
219 self.senders = senders;
220 self
221 }
222
223 /// Set the description of this content.
224 pub fn with_description(mut self, description: Element) -> Content {
225 self.description = Some(description);
226 self
227 }
228
229 /// Set the transport of this content.
230 pub fn with_transport(mut self, transport: Element) -> Content {
231 self.transport = Some(transport);
232 self
233 }
234
235 /// Set the security of this content.
236 pub fn with_security(mut self, security: Element) -> Content {
237 self.security = Some(security);
238 self
239 }
240}
241
242/// Lists the possible reasons to be included in a Jingle iq.
243#[derive(Debug, Clone, PartialEq)]
244pub enum Reason {
245 /// The party prefers to use an existing session with the peer rather than
246 /// initiate a new session; the Jingle session ID of the alternative
247 /// session SHOULD be provided as the XML character data of the <sid/>
248 /// child.
249 AlternativeSession, //(String),
250
251 /// The party is busy and cannot accept a session.
252 Busy,
253
254 /// The initiator wishes to formally cancel the session initiation request.
255 Cancel,
256
257 /// The action is related to connectivity problems.
258 ConnectivityError,
259
260 /// The party wishes to formally decline the session.
261 Decline,
262
263 /// The session length has exceeded a pre-defined time limit (e.g., a
264 /// meeting hosted at a conference service).
265 Expired,
266
267 /// The party has been unable to initialize processing related to the
268 /// application type.
269 FailedApplication,
270
271 /// The party has been unable to establish connectivity for the transport
272 /// method.
273 FailedTransport,
274
275 /// The action is related to a non-specific application error.
276 GeneralError,
277
278 /// The entity is going offline or is no longer available.
279 Gone,
280
281 /// The party supports the offered application type but does not support
282 /// the offered or negotiated parameters.
283 IncompatibleParameters,
284
285 /// The action is related to media processing problems.
286 MediaError,
287
288 /// The action is related to a violation of local security policies.
289 SecurityError,
290
291 /// The action is generated during the normal course of state management
292 /// and does not reflect any error.
293 Success,
294
295 /// A request has not been answered so the sender is timing out the
296 /// request.
297 Timeout,
298
299 /// The party supports none of the offered application types.
300 UnsupportedApplications,
301
302 /// The party supports none of the offered transport methods.
303 UnsupportedTransports,
304}
305
306impl FromStr for Reason {
307 type Err = Error;
308
309 fn from_str(s: &str) -> Result<Reason, Error> {
310 Ok(match s {
311 "alternative-session" => Reason::AlternativeSession,
312 "busy" => Reason::Busy,
313 "cancel" => Reason::Cancel,
314 "connectivity-error" => Reason::ConnectivityError,
315 "decline" => Reason::Decline,
316 "expired" => Reason::Expired,
317 "failed-application" => Reason::FailedApplication,
318 "failed-transport" => Reason::FailedTransport,
319 "general-error" => Reason::GeneralError,
320 "gone" => Reason::Gone,
321 "incompatible-parameters" => Reason::IncompatibleParameters,
322 "media-error" => Reason::MediaError,
323 "security-error" => Reason::SecurityError,
324 "success" => Reason::Success,
325 "timeout" => Reason::Timeout,
326 "unsupported-applications" => Reason::UnsupportedApplications,
327 "unsupported-transports" => Reason::UnsupportedTransports,
328
329 _ => return Err(Error::ParseError("Unknown reason.")),
330 })
331 }
332}
333
334impl From<Reason> for Element {
335 fn from(reason: Reason) -> Element {
336 Element::builder(match reason {
337 Reason::AlternativeSession => "alternative-session",
338 Reason::Busy => "busy",
339 Reason::Cancel => "cancel",
340 Reason::ConnectivityError => "connectivity-error",
341 Reason::Decline => "decline",
342 Reason::Expired => "expired",
343 Reason::FailedApplication => "failed-application",
344 Reason::FailedTransport => "failed-transport",
345 Reason::GeneralError => "general-error",
346 Reason::Gone => "gone",
347 Reason::IncompatibleParameters => "incompatible-parameters",
348 Reason::MediaError => "media-error",
349 Reason::SecurityError => "security-error",
350 Reason::Success => "success",
351 Reason::Timeout => "timeout",
352 Reason::UnsupportedApplications => "unsupported-applications",
353 Reason::UnsupportedTransports => "unsupported-transports",
354 }).build()
355 }
356}
357
358/// Informs the recipient of something.
359#[derive(Debug, Clone)]
360pub struct ReasonElement {
361 /// The list of possible reasons to be included in a Jingle iq.
362 pub reason: Reason,
363
364 /// A human-readable description of this reason.
365 pub text: Option<String>,
366}
367
368impl TryFrom<Element> for ReasonElement {
369 type Err = Error;
370
371 fn try_from(elem: Element) -> Result<ReasonElement, Error> {
372 check_self!(elem, "reason", JINGLE);
373 let mut reason = None;
374 let mut text = None;
375 for child in elem.children() {
376 if !child.has_ns(ns::JINGLE) {
377 return Err(Error::ParseError("Reason contains a foreign element."));
378 }
379 match child.name() {
380 "text" => {
381 if text.is_some() {
382 return Err(Error::ParseError("Reason must not have more than one text."));
383 }
384 text = Some(child.text());
385 },
386 name => {
387 if reason.is_some() {
388 return Err(Error::ParseError("Reason must not have more than one reason."));
389 }
390 reason = Some(name.parse()?);
391 },
392 }
393 }
394 let reason = reason.ok_or(Error::ParseError("Reason doesn’t contain a valid reason."))?;
395 Ok(ReasonElement {
396 reason: reason,
397 text: text,
398 })
399 }
400}
401
402impl From<ReasonElement> for Element {
403 fn from(reason: ReasonElement) -> Element {
404 Element::builder("reason")
405 .append(Element::from(reason.reason))
406 .append(reason.text)
407 .build()
408 }
409}
410
411generate_id!(
412 /// Unique identifier for a session between two JIDs.
413 SessionId
414);
415
416/// The main Jingle container, to be included in an iq stanza.
417#[derive(Debug, Clone)]
418pub struct Jingle {
419 /// The action to execute on both ends.
420 pub action: Action,
421
422 /// Who the initiator is.
423 pub initiator: Option<Jid>,
424
425 /// Who the responder is.
426 pub responder: Option<Jid>,
427
428 /// Unique session identifier between two entities.
429 pub sid: SessionId,
430
431 /// A list of contents to be negociated in this session.
432 pub contents: Vec<Content>,
433
434 /// An optional reason.
435 pub reason: Option<ReasonElement>,
436
437 /// Payloads to be included.
438 pub other: Vec<Element>,
439}
440
441impl IqSetPayload for Jingle {}
442
443impl Jingle {
444 /// Create a new Jingle element.
445 pub fn new(action: Action, sid: SessionId) -> Jingle {
446 Jingle {
447 action: action,
448 sid: sid,
449 initiator: None,
450 responder: None,
451 contents: Vec::new(),
452 reason: None,
453 other: Vec::new(),
454 }
455 }
456
457 /// Set the initiator’s JID.
458 pub fn with_initiator(mut self, initiator: Jid) -> Jingle {
459 self.initiator = Some(initiator);
460 self
461 }
462
463 /// Set the responder’s JID.
464 pub fn with_responder(mut self, responder: Jid) -> Jingle {
465 self.responder = Some(responder);
466 self
467 }
468
469 /// Add a content to this Jingle container.
470 pub fn add_content(mut self, content: Content) -> Jingle {
471 self.contents.push(content);
472 self
473 }
474
475 /// Set the reason in this Jingle container.
476 pub fn set_reason(mut self, content: Content) -> Jingle {
477 self.contents.push(content);
478 self
479 }
480}
481
482impl TryFrom<Element> for Jingle {
483 type Err = Error;
484
485 fn try_from(root: Element) -> Result<Jingle, Error> {
486 check_self!(root, "jingle", JINGLE, "Jingle");
487 check_no_unknown_attributes!(root, "Jingle", ["action", "initiator", "responder", "sid"]);
488
489 let mut jingle = Jingle {
490 action: get_attr!(root, "action", required),
491 initiator: get_attr!(root, "initiator", optional),
492 responder: get_attr!(root, "responder", optional),
493 sid: get_attr!(root, "sid", required),
494 contents: vec!(),
495 reason: None,
496 other: vec!(),
497 };
498
499 for child in root.children().cloned() {
500 if child.is("content", ns::JINGLE) {
501 let content = Content::try_from(child)?;
502 jingle.contents.push(content);
503 } else if child.is("reason", ns::JINGLE) {
504 if jingle.reason.is_some() {
505 return Err(Error::ParseError("Jingle must not have more than one reason."));
506 }
507 let reason = ReasonElement::try_from(child)?;
508 jingle.reason = Some(reason);
509 } else {
510 jingle.other.push(child);
511 }
512 }
513
514 Ok(jingle)
515 }
516}
517
518impl From<Jingle> for Element {
519 fn from(jingle: Jingle) -> Element {
520 Element::builder("jingle")
521 .ns(ns::JINGLE)
522 .attr("action", jingle.action)
523 .attr("initiator", jingle.initiator)
524 .attr("responder", jingle.responder)
525 .attr("sid", jingle.sid)
526 .append(jingle.contents)
527 .append(jingle.reason)
528 .build()
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 #[test]
537 fn test_size() {
538 assert_size!(Action, 1);
539 assert_size!(Creator, 1);
540 assert_size!(Senders, 1);
541 assert_size!(Disposition, 1);
542 assert_size!(ContentId, 24);
543 assert_size!(Content, 344);
544 assert_size!(Reason, 1);
545 assert_size!(ReasonElement, 32);
546 assert_size!(SessionId, 24);
547 assert_size!(Jingle, 256);
548 }
549
550 #[test]
551 fn test_simple() {
552 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'/>".parse().unwrap();
553 let jingle = Jingle::try_from(elem).unwrap();
554 assert_eq!(jingle.action, Action::SessionInitiate);
555 assert_eq!(jingle.sid, SessionId(String::from("coucou")));
556 }
557
558 #[test]
559 fn test_invalid_jingle() {
560 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1'/>".parse().unwrap();
561 let error = Jingle::try_from(elem).unwrap_err();
562 let message = match error {
563 Error::ParseError(string) => string,
564 _ => panic!(),
565 };
566 assert_eq!(message, "Required attribute 'action' missing.");
567
568 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-info'/>".parse().unwrap();
569 let error = Jingle::try_from(elem).unwrap_err();
570 let message = match error {
571 Error::ParseError(string) => string,
572 _ => panic!(),
573 };
574 assert_eq!(message, "Required attribute 'sid' missing.");
575
576 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='coucou' sid='coucou'/>".parse().unwrap();
577 let error = Jingle::try_from(elem).unwrap_err();
578 let message = match error {
579 Error::ParseError(string) => string,
580 _ => panic!(),
581 };
582 assert_eq!(message, "Unknown value for 'action' attribute.");
583 }
584
585 #[test]
586 fn test_content() {
587 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();
588 let jingle = Jingle::try_from(elem).unwrap();
589 assert_eq!(jingle.contents[0].creator, Creator::Initiator);
590 assert_eq!(jingle.contents[0].name, ContentId(String::from("coucou")));
591 assert_eq!(jingle.contents[0].senders, Senders::Both);
592 assert_eq!(jingle.contents[0].disposition, Disposition::Session);
593
594 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();
595 let jingle = Jingle::try_from(elem).unwrap();
596 assert_eq!(jingle.contents[0].senders, Senders::Both);
597
598 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();
599 let jingle = Jingle::try_from(elem).unwrap();
600 assert_eq!(jingle.contents[0].disposition, Disposition::EarlySession);
601 }
602
603 #[test]
604 fn test_invalid_content() {
605 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content/></jingle>".parse().unwrap();
606 let error = Jingle::try_from(elem).unwrap_err();
607 let message = match error {
608 Error::ParseError(string) => string,
609 _ => panic!(),
610 };
611 assert_eq!(message, "Required attribute 'creator' missing.");
612
613 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator'/></jingle>".parse().unwrap();
614 let error = Jingle::try_from(elem).unwrap_err();
615 let message = match error {
616 Error::ParseError(string) => string,
617 _ => panic!(),
618 };
619 assert_eq!(message, "Required attribute 'name' missing.");
620
621 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='coucou' name='coucou'/></jingle>".parse().unwrap();
622 let error = Jingle::try_from(elem).unwrap_err();
623 let message = match error {
624 Error::ParseError(string) => string,
625 _ => panic!(),
626 };
627 assert_eq!(message, "Unknown value for 'creator' attribute.");
628
629 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders='coucou'/></jingle>".parse().unwrap();
630 let error = Jingle::try_from(elem).unwrap_err();
631 let message = match error {
632 Error::ParseError(string) => string,
633 _ => panic!(),
634 };
635 assert_eq!(message, "Unknown value for 'senders' attribute.");
636
637 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders=''/></jingle>".parse().unwrap();
638 let error = Jingle::try_from(elem).unwrap_err();
639 let message = match error {
640 Error::ParseError(string) => string,
641 _ => panic!(),
642 };
643 assert_eq!(message, "Unknown value for 'senders' attribute.");
644 }
645
646 #[test]
647 fn test_reason() {
648 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/></reason></jingle>".parse().unwrap();
649 let jingle = Jingle::try_from(elem).unwrap();
650 let reason = jingle.reason.unwrap();
651 assert_eq!(reason.reason, Reason::Success);
652 assert_eq!(reason.text, None);
653
654 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/><text>coucou</text></reason></jingle>".parse().unwrap();
655 let jingle = Jingle::try_from(elem).unwrap();
656 let reason = jingle.reason.unwrap();
657 assert_eq!(reason.reason, Reason::Success);
658 assert_eq!(reason.text, Some(String::from("coucou")));
659 }
660
661 #[test]
662 fn test_invalid_reason() {
663 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason/></jingle>".parse().unwrap();
664 let error = Jingle::try_from(elem).unwrap_err();
665 let message = match error {
666 Error::ParseError(string) => string,
667 _ => panic!(),
668 };
669 assert_eq!(message, "Reason doesn’t contain a valid reason.");
670
671 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><a/></reason></jingle>".parse().unwrap();
672 let error = Jingle::try_from(elem).unwrap_err();
673 let message = match error {
674 Error::ParseError(string) => string,
675 _ => panic!(),
676 };
677 assert_eq!(message, "Unknown reason.");
678
679 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();
680 let error = Jingle::try_from(elem).unwrap_err();
681 let message = match error {
682 Error::ParseError(string) => string,
683 _ => panic!(),
684 };
685 assert_eq!(message, "Reason contains a foreign element.");
686
687 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/></reason><reason/></jingle>".parse().unwrap();
688 let error = Jingle::try_from(elem).unwrap_err();
689 let message = match error {
690 Error::ParseError(string) => string,
691 _ => panic!(),
692 };
693 assert_eq!(message, "Jingle must not have more than one reason.");
694
695 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/><text/><text/></reason></jingle>".parse().unwrap();
696 let error = Jingle::try_from(elem).unwrap_err();
697 let message = match error {
698 Error::ParseError(string) => string,
699 _ => panic!(),
700 };
701 assert_eq!(message, "Reason must not have more than one text.");
702 }
703}