1use std::net::IpAddr;
13
14use crate::session::{
15 Attribute, Connection, Direction, MediaDescription, Origin, SessionDescription, Timing,
16};
17
18#[derive(Debug, Clone)]
20pub struct Capabilities {
21 pub address: IpAddr,
23 pub audio_port: u16,
25 pub audio_formats: Vec<String>,
27 pub rtpmaps: Vec<(String, String)>,
29 pub direction: Direction,
31 pub session_id: u64,
33 pub session_version: u64,
35 pub crypto: Option<crate::crypto::Crypto>,
40 pub dtls: Option<crate::fingerprint::Fingerprint>,
46 pub rtcp_mux: bool,
48 pub dtls_setup: crate::fingerprint::SetupCapabilities,
50}
51
52impl Capabilities {
53 #[must_use]
55 pub fn g711(address: IpAddr, audio_port: u16) -> Self {
56 Self {
57 address,
58 audio_port,
59 audio_formats: vec!["0".to_owned(), "8".to_owned(), "101".to_owned()],
60 rtpmaps: vec![
61 ("0".to_owned(), "PCMU/8000".to_owned()),
62 ("8".to_owned(), "PCMA/8000".to_owned()),
63 ("101".to_owned(), "telephone-event/8000".to_owned()),
64 ],
65 direction: Direction::SendRecv,
66 session_id: 1,
67 session_version: 1,
68 crypto: None,
69 dtls: None,
70 rtcp_mux: false,
71 dtls_setup: crate::fingerprint::SetupCapabilities::both(),
72 }
73 }
74
75 #[must_use]
91 pub fn with_opus(address: IpAddr, audio_port: u16) -> Self {
92 Self {
93 address,
94 audio_port,
95 audio_formats: vec![
96 "111".to_owned(),
97 "0".to_owned(),
98 "8".to_owned(),
99 "101".to_owned(),
100 ],
101 rtpmaps: vec![
102 ("111".to_owned(), "opus/48000/2".to_owned()),
103 ("0".to_owned(), "PCMU/8000".to_owned()),
104 ("8".to_owned(), "PCMA/8000".to_owned()),
105 ("101".to_owned(), "telephone-event/8000".to_owned()),
106 ],
107 direction: Direction::SendRecv,
108 session_id: 1,
109 session_version: 1,
110 crypto: None,
111 dtls: None,
112 rtcp_mux: false,
113 dtls_setup: crate::fingerprint::SetupCapabilities::both(),
114 }
115 }
116
117 #[must_use]
124 pub fn with_srtp(mut self, secure_signalling: bool) -> Self {
125 self.crypto = crate::crypto::Crypto::offer(
126 1,
127 crate::crypto::Suite::AesCm128HmacSha1_80,
128 secure_signalling,
129 );
130 self
131 }
132
133 #[must_use]
147 pub fn with_dtls_srtp(mut self, fingerprint: crate::fingerprint::Fingerprint) -> Self {
148 self.dtls = Some(fingerprint);
149 self.crypto = None;
153 self
154 }
155
156 #[must_use]
158 pub fn with_rtcp_mux(mut self) -> Self {
159 self.rtcp_mux = true;
160 self
161 }
162
163 #[must_use]
165 pub fn with_dtls_setup_capabilities(
166 mut self,
167 setup: crate::fingerprint::SetupCapabilities,
168 ) -> Self {
169 self.dtls_setup = setup;
170 self
171 }
172
173 #[must_use]
179 pub fn protocol(&self) -> &'static str {
180 if self.dtls.is_some() {
181 "UDP/TLS/RTP/SAVP"
182 } else if self.crypto.is_some() {
183 "RTP/SAVP"
184 } else {
185 "RTP/AVP"
186 }
187 }
188
189 #[must_use]
191 pub fn dtls(&self) -> Option<&crate::fingerprint::Fingerprint> {
192 self.dtls.as_ref()
193 }
194
195 fn rtpmap_for(&self, format: &str) -> Option<&str> {
196 self.rtpmaps
197 .iter()
198 .find(|(payload, _)| payload == format)
199 .map(|(_, value)| value.as_str())
200 }
201}
202
203#[must_use]
207pub fn answer(offer: &SessionDescription, capabilities: &Capabilities) -> SessionDescription {
208 let mut media = Vec::with_capacity(offer.media.len());
209
210 for offered in &offer.media {
211 media.push(answer_stream(offer, offered, capabilities));
212 }
213
214 SessionDescription {
215 origin: Origin::new(
216 capabilities.address,
217 capabilities.session_id,
218 capabilities.session_version,
219 ),
220 session_name: "-".to_owned(),
221 connection: Some(Connection::new(capabilities.address)),
222 timing: vec![Timing::default()],
223 attributes: Vec::new(),
224 media,
225 other: Vec::new(),
226 }
227}
228
229fn answer_stream(
230 offer: &SessionDescription,
231 offered: &MediaDescription,
232 capabilities: &Capabilities,
233) -> MediaDescription {
234 if offered.is_rejected() {
237 return rejected(offered);
238 }
239
240 if offered.media != "audio" || capabilities.audio_port == 0 {
243 return rejected(offered);
244 }
245
246 let dtls_offer = offered.protocol.contains("TLS");
254 let secure_offer = offered.protocol.contains("SAVP");
255
256 let answering_dtls = match (dtls_offer, capabilities.dtls.as_ref()) {
257 (true, Some(ours)) if fingerprint_of(offer, offered).is_some() => Some(ours),
261 (true, _) => return rejected(offered),
262 (false, _) => None,
263 };
264
265 let answering_crypto = match (secure_offer && !dtls_offer, capabilities.crypto.as_ref()) {
270 (true, Some(ours)) => match offered.crypto().and_then(|theirs| ours.accepting(&theirs)) {
271 Some(accepted) => Some(accepted),
272 None => return rejected(offered),
273 },
274 (true, None) => return rejected(offered),
275 (false, _) => None,
278 };
279
280 let mux_agreed = capabilities.rtcp_mux && offered.rtcp_mux();
285 let common: Vec<String> = offered
286 .formats
287 .iter()
288 .filter(|format| {
289 !mux_agreed
290 || format
291 .parse::<u8>()
292 .map_or(true, |payload| !(64..=95).contains(&payload))
293 })
294 .filter(|format| supports(capabilities, offered, format))
295 .cloned()
296 .collect();
297
298 if common.is_empty() || common.iter().all(|f| is_telephone_event(offered, f)) {
301 return rejected(offered);
302 }
303
304 let mut attributes = Vec::new();
305 for format in &common {
306 let rtpmap = offered
309 .rtpmap(format)
310 .or_else(|| capabilities.rtpmap_for(format));
311 if let Some(rtpmap) = rtpmap {
312 attributes.push(Attribute::valued("rtpmap", format!("{format} {rtpmap}")));
313 }
314 if is_telephone_event(offered, format) {
318 attributes.push(Attribute::valued("fmtp", format!("{format} 0-15")));
319 }
320 }
321
322 let offered_direction = offered
327 .declared_direction()
328 .unwrap_or_else(|| offer.direction());
329 let direction = negotiate_direction(offered_direction, capabilities.direction);
330 attributes.push(Attribute::flag(direction.as_str()));
331
332 if mux_agreed {
335 attributes.push(Attribute::flag("rtcp-mux"));
336 }
337
338 if let Some(crypto) = &answering_crypto {
339 attributes.push(Attribute::valued("crypto", crypto.to_value()));
340 }
341
342 if let Some(fingerprint) = answering_dtls {
343 attributes.push(Attribute::valued("fingerprint", fingerprint.to_value()));
344 let offered_role = setup_of(offer, offered)
348 .unwrap_or(crate::fingerprint::Setup::ActPass);
351 let Ok(role) = capabilities.dtls_setup.answer_to(offered_role) else {
352 return rejected(offered);
353 };
354 attributes.push(Attribute::valued("setup", role.as_str().to_owned()));
355 }
356
357 MediaDescription {
358 media: offered.media.clone(),
359 port: capabilities.audio_port,
360 protocol: offered.protocol.clone(),
361 formats: common,
362 connection: None,
363 attributes,
364 other: Vec::new(),
365 }
366}
367
368#[must_use]
375pub fn fingerprint_of(
376 offer: &SessionDescription,
377 stream: &MediaDescription,
378) -> Option<crate::fingerprint::Fingerprint> {
379 stream.fingerprint().or_else(|| offer.fingerprint())
380}
381
382#[must_use]
388pub fn setup_of(
389 description: &SessionDescription,
390 stream: &MediaDescription,
391) -> Option<crate::fingerprint::Setup> {
392 stream.setup().or_else(|| {
393 description
394 .attributes
395 .iter()
396 .find(|attribute| attribute.name == "setup")
397 .and_then(|attribute| attribute.value.as_deref())
398 .and_then(crate::fingerprint::Setup::parse)
399 })
400}
401
402#[must_use]
407pub fn negotiate_direction(offered: Direction, wanted: Direction) -> Direction {
408 let allowed = offered.mirrored();
409 let sends = allowed.sends() && wanted.sends();
410 let receives = allowed.receives() && wanted.receives();
411 match (sends, receives) {
412 (true, true) => Direction::SendRecv,
413 (true, false) => Direction::SendOnly,
414 (false, true) => Direction::RecvOnly,
415 (false, false) => Direction::Inactive,
416 }
417}
418
419fn rejected(offered: &MediaDescription) -> MediaDescription {
420 MediaDescription {
421 media: offered.media.clone(),
422 port: 0,
423 protocol: offered.protocol.clone(),
424 formats: offered.formats.first().cloned().into_iter().collect(),
427 connection: None,
428 attributes: Vec::new(),
429 other: Vec::new(),
430 }
431}
432
433fn supports(capabilities: &Capabilities, offered: &MediaDescription, format: &str) -> bool {
445 if let Some(offered_map) = offered.rtpmap(format) {
446 return capabilities
447 .rtpmaps
448 .iter()
449 .any(|(_, mapping)| crate::rtpmap::same_format(offered_map, mapping));
450 }
451
452 let is_dynamic = format
453 .parse::<u8>()
454 .is_ok_and(|payload| (96..=127).contains(&payload));
455 if is_dynamic {
456 return false;
458 }
459 capabilities.audio_formats.iter().any(|f| f == format)
460}
461
462fn encoding_of(rtpmap: &str) -> &str {
463 rtpmap.split('/').next().unwrap_or(rtpmap)
464}
465
466fn is_telephone_event(offered: &MediaDescription, format: &str) -> bool {
467 offered
468 .rtpmap(format)
469 .is_some_and(|mapping| encoding_of(mapping).eq_ignore_ascii_case("telephone-event"))
470}
471
472#[cfg(test)]
473#[allow(
474 clippy::unwrap_used,
475 clippy::expect_used,
476 clippy::panic,
477 clippy::indexing_slicing
478)]
479mod tests {
480 use super::*;
481 use crate::parse::parse;
482
483 fn local() -> IpAddr {
484 "192.0.2.20".parse().expect("valid")
485 }
486
487 fn offer(body: &str) -> SessionDescription {
488 parse(body).expect("the offer parses")
489 }
490
491 const AUDIO_OFFER: &str = "v=0\r\n\
492 o=alice 1 1 IN IP4 192.0.2.10\r\n\
493 s=-\r\n\
494 c=IN IP4 192.0.2.10\r\n\
495 t=0 0\r\n\
496 m=audio 49170 RTP/AVP 0 8 101\r\n\
497 a=rtpmap:0 PCMU/8000\r\n\
498 a=rtpmap:8 PCMA/8000\r\n\
499 a=rtpmap:101 telephone-event/8000\r\n\
500 a=fmtp:101 0-15\r\n\
501 a=sendrecv\r\n";
502
503 #[test]
504 fn a_plain_audio_offer_is_answered_with_the_common_codecs() {
505 let answered = answer(&offer(AUDIO_OFFER), &Capabilities::g711(local(), 40000));
506 assert_eq!(answered.media.len(), 1);
507 let audio = &answered.media[0];
508 assert_eq!(audio.port, 40000);
509 assert_eq!(audio.formats, vec!["0", "8", "101"]);
510 assert_eq!(audio.rtpmap("0"), Some("PCMU/8000"));
511 assert_eq!(audio.direction(), Direction::SendRecv);
512 }
513
514 #[test]
518 fn an_answer_keeps_the_offers_media_order_and_rejects_with_port_zero() {
519 let offered = offer(
520 "v=0\r\n\
521 o=alice 1 1 IN IP4 192.0.2.10\r\n\
522 s=-\r\n\
523 c=IN IP4 192.0.2.10\r\n\
524 t=0 0\r\n\
525 m=video 49172 RTP/AVP 96\r\n\
526 a=rtpmap:96 H264/90000\r\n\
527 m=audio 49170 RTP/AVP 0\r\n\
528 a=rtpmap:0 PCMU/8000\r\n\
529 m=application 49174 udp wb\r\n",
530 );
531 let answered = answer(&offered, &Capabilities::g711(local(), 40000));
532
533 assert_eq!(answered.media.len(), 3, "one answer per offered stream");
534 assert_eq!(answered.media[0].media, "video");
535 assert_eq!(answered.media[0].port, 0, "video is declined");
536 assert_eq!(answered.media[1].media, "audio");
537 assert_eq!(answered.media[1].port, 40000, "audio is accepted, in place");
538 assert_eq!(answered.media[2].media, "application");
539 assert_eq!(answered.media[2].port, 0);
540 }
541
542 #[test]
545 fn the_codec_order_is_the_offerers_not_ours() {
546 let offered = offer(
547 "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
548 m=audio 49170 RTP/AVP 8 0\r\n\
549 a=rtpmap:8 PCMA/8000\r\n\
550 a=rtpmap:0 PCMU/8000\r\n",
551 );
552 let answered = answer(&offered, &Capabilities::g711(local(), 40000));
554 assert_eq!(
555 answered.media[0].formats,
556 vec!["8", "0"],
557 "the offerer asked for PCMA first, so PCMA comes first"
558 );
559 }
560
561 #[test]
562 fn a_codec_we_do_not_have_is_left_out_of_the_answer() {
563 let offered = offer(
564 "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
565 m=audio 49170 RTP/AVP 0 9\r\n\
566 a=rtpmap:0 PCMU/8000\r\n\
567 a=rtpmap:9 G722/8000\r\n",
568 );
569 let answered = answer(&offered, &Capabilities::g711(local(), 40000));
570 assert_eq!(answered.media[0].formats, vec!["0"], "G.722 is not ours");
571 }
572
573 #[test]
576 fn a_stream_with_no_common_codec_is_rejected() {
577 let offered = offer(
578 "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
579 m=audio 49170 RTP/AVP 9\r\n\
580 a=rtpmap:9 G722/8000\r\n",
581 );
582 let answered = answer(&offered, &Capabilities::g711(local(), 40000));
583 assert!(answered.media[0].is_rejected());
584 assert!(!answered.media[0].formats.is_empty(), "still well-formed");
585 }
586
587 #[test]
590 fn a_stream_offering_only_dtmf_is_rejected() {
591 let offered = offer(
592 "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
593 m=audio 49170 RTP/AVP 101\r\n\
594 a=rtpmap:101 telephone-event/8000\r\n",
595 );
596 assert!(answer(&offered, &Capabilities::g711(local(), 40000)).media[0].is_rejected());
597 }
598
599 #[test]
602 fn directions_are_mirrored_rather_than_copied() {
603 for (offered_direction, expected) in [
604 ("sendrecv", Direction::SendRecv),
605 ("sendonly", Direction::RecvOnly),
606 ("recvonly", Direction::SendOnly),
607 ("inactive", Direction::Inactive),
608 ] {
609 let offered = offer(&format!(
610 "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
611 m=audio 49170 RTP/AVP 0\r\n\
612 a=rtpmap:0 PCMU/8000\r\n\
613 a={offered_direction}\r\n"
614 ));
615 assert_eq!(
616 answer(&offered, &Capabilities::g711(local(), 40000)).media[0].direction(),
617 expected,
618 "offer of {offered_direction}"
619 );
620 }
621 }
622
623 #[test]
628 fn a_session_level_direction_governs_streams_without_their_own() {
629 let offered = offer(
630 "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
631 a=sendonly\r\n\
632 m=audio 49170 RTP/AVP 0\r\n\
633 a=rtpmap:0 PCMU/8000\r\n",
634 );
635 assert_eq!(
636 answer(&offered, &Capabilities::g711(local(), 40000)).media[0].direction(),
637 Direction::RecvOnly,
638 "the session-level sendonly is what this stream offered"
639 );
640 }
641
642 #[test]
645 fn a_media_level_direction_overrides_the_session_level_one() {
646 let offered = offer(
647 "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
648 a=sendonly\r\n\
649 m=audio 49170 RTP/AVP 0\r\n\
650 a=rtpmap:0 PCMU/8000\r\n\
651 a=sendrecv\r\n",
652 );
653 assert_eq!(
654 answer(&offered, &Capabilities::g711(local(), 40000)).media[0].direction(),
655 Direction::SendRecv
656 );
657 }
658
659 #[test]
662 fn the_answer_cannot_widen_what_was_offered() {
663 assert_eq!(
664 negotiate_direction(Direction::SendOnly, Direction::SendRecv),
665 Direction::RecvOnly
666 );
667 assert_eq!(
668 negotiate_direction(Direction::SendRecv, Direction::RecvOnly),
669 Direction::RecvOnly
670 );
671 assert_eq!(
672 negotiate_direction(Direction::Inactive, Direction::SendRecv),
673 Direction::Inactive
674 );
675 assert_eq!(
676 negotiate_direction(Direction::RecvOnly, Direction::RecvOnly),
677 Direction::Inactive,
678 "the offerer will only receive and so will we: nothing flows"
679 );
680 }
681
682 #[test]
685 fn dynamic_payload_types_are_matched_by_name_not_number() {
686 let mut capabilities = Capabilities::g711(local(), 40000);
687 capabilities.audio_formats.push("96".to_owned());
688 capabilities
689 .rtpmaps
690 .push(("96".to_owned(), "opus/48000/2".to_owned()));
691
692 let offered = offer(
694 "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
695 m=audio 49170 RTP/AVP 96 0\r\n\
696 a=rtpmap:96 SPEEX/8000\r\n\
697 a=rtpmap:0 PCMU/8000\r\n",
698 );
699 let answered = answer(&offered, &capabilities);
700 assert_eq!(
701 answered.media[0].formats,
702 vec!["0"],
703 "96 is Speex there and Opus here; the numbers agreeing means nothing"
704 );
705
706 let matching = offer(
708 "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
709 m=audio 49170 RTP/AVP 96 0\r\n\
710 a=rtpmap:96 opus/48000/2\r\n\
711 a=rtpmap:0 PCMU/8000\r\n",
712 );
713 assert_eq!(
714 answer(&matching, &capabilities).media[0].formats,
715 vec!["96", "0"]
716 );
717 }
718
719 #[test]
723 fn an_rtpmap_only_matches_when_the_clock_rate_agrees() {
724 let offered = offer(
725 "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
726 m=audio 49170 RTP/AVP 0 101\r\n\
727 a=rtpmap:0 PCMU/8000\r\n\
728 a=rtpmap:101 telephone-event/16000\r\n",
729 );
730 assert_eq!(
731 answer(&offered, &Capabilities::g711(local(), 40000)).media[0].formats,
732 vec!["0"],
733 "telephone-event at 16000 is not the 8000 we support"
734 );
735 }
736
737 #[test]
740 fn a_missing_channel_count_means_one_channel() {
741 let mut capabilities = Capabilities::g711(local(), 40000);
742 capabilities.audio_formats.push("96".to_owned());
743 capabilities
744 .rtpmaps
745 .push(("96".to_owned(), "opus/48000".to_owned()));
746
747 let explicit_one = offer(
748 "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
749 m=audio 49170 RTP/AVP 96\r\n\
750 a=rtpmap:96 opus/48000/1\r\n",
751 );
752 assert_eq!(
753 answer(&explicit_one, &capabilities).media[0].formats,
754 vec!["96"],
755 "opus/48000 and opus/48000/1 are the same format"
756 );
757
758 let stereo = offer(
759 "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
760 m=audio 49170 RTP/AVP 96 0\r\n\
761 a=rtpmap:96 opus/48000/2\r\n\
762 a=rtpmap:0 PCMU/8000\r\n",
763 );
764 assert_eq!(
765 answer(&stereo, &capabilities).media[0].formats,
766 vec!["0"],
767 "two channels are not the one we support"
768 );
769 }
770
771 #[test]
772 fn a_dynamic_payload_type_without_an_rtpmap_is_not_accepted() {
773 let offered = offer(
774 "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
775 m=audio 49170 RTP/AVP 96 0\r\n\
776 a=rtpmap:0 PCMU/8000\r\n",
777 );
778 assert_eq!(
779 answer(&offered, &Capabilities::g711(local(), 40000)).media[0].formats,
780 vec!["0"]
781 );
782 }
783
784 #[test]
788 fn a_static_payload_type_remapped_by_the_offer_is_not_taken_on_the_number() {
789 let offered = offer(
790 "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
791 m=audio 49170 RTP/AVP 8 0\r\n\
792 a=rtpmap:8 iLBC/8000\r\n\
793 a=rtpmap:0 PCMU/8000\r\n",
794 );
795 assert_eq!(
796 answer(&offered, &Capabilities::g711(local(), 40000)).media[0].formats,
797 vec!["0"],
798 "8 means iLBC in this offer, and iLBC is not ours"
799 );
800 }
801
802 #[test]
805 fn a_stream_the_offer_already_rejected_stays_rejected() {
806 let offered = offer(
807 "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
808 m=audio 0 RTP/AVP 0\r\n\
809 a=rtpmap:0 PCMU/8000\r\n",
810 );
811 assert!(answer(&offered, &Capabilities::g711(local(), 40000)).media[0].is_rejected());
812 }
813
814 #[test]
815 fn the_dtmf_fmtp_declares_the_events_this_side_receives() {
816 let answered = answer(&offer(AUDIO_OFFER), &Capabilities::g711(local(), 40000));
817 let fmtp = answered.media[0]
818 .attributes
819 .iter()
820 .find(|a| a.name == "fmtp")
821 .and_then(|a| a.value.clone())
822 .expect("an fmtp for DTMF");
823 assert_eq!(fmtp, "101 0-15");
824 }
825
826 #[test]
830 fn the_dtmf_fmtp_is_not_an_echo_of_the_offers() {
831 let offered = offer(
832 "v=0\r\no=a 1 1 IN IP4 192.0.2.10\r\ns=-\r\nc=IN IP4 192.0.2.10\r\nt=0 0\r\n\
833 m=audio 49170 RTP/AVP 0 101\r\n\
834 a=rtpmap:0 PCMU/8000\r\n\
835 a=rtpmap:101 telephone-event/8000\r\n\
836 a=fmtp:101 0-15,32-36\r\n",
837 );
838 let answered = answer(&offered, &Capabilities::g711(local(), 40000));
839 let fmtp = answered.media[0]
840 .attributes
841 .iter()
842 .find(|a| a.name == "fmtp")
843 .and_then(|a| a.value.clone())
844 .expect("an fmtp for DTMF");
845 assert_eq!(fmtp, "101 0-15", "32-36 are events we never handle");
846 }
847
848 #[test]
849 fn the_answer_advertises_our_address_and_port() {
850 let answered = answer(&offer(AUDIO_OFFER), &Capabilities::g711(local(), 40000));
851 assert_eq!(
852 answered.connection.expect("a connection").address.ip(),
853 Some(local())
854 );
855 assert_eq!(answered.origin.address.ip(), Some(local()));
856 assert_eq!(answered.media[0].port, 40000);
857 }
858
859 #[test]
860 fn an_answer_reparses_to_itself() {
861 let answered = answer(&offer(AUDIO_OFFER), &Capabilities::g711(local(), 40000));
862 let round_tripped = parse(&answered.to_string_sdp()).expect("the answer parses");
863 assert_eq!(answered, round_tripped);
864 }
865
866 fn our_fingerprint() -> crate::fingerprint::Fingerprint {
871 crate::fingerprint::Fingerprint::of(
872 b"our certificate",
873 crate::fingerprint::HashFunc::Sha256,
874 )
875 }
876
877 fn their_fingerprint() -> crate::fingerprint::Fingerprint {
878 crate::fingerprint::Fingerprint::of(
879 b"their certificate",
880 crate::fingerprint::HashFunc::Sha256,
881 )
882 }
883
884 fn dtls_offer(extra_media: &str, session_level: &str) -> SessionDescription {
885 offer(&format!(
886 "v=0\r\n\
887 o=- 1 1 IN IP4 192.0.2.10\r\n\
888 s=-\r\n\
889 c=IN IP4 192.0.2.10\r\n\
890 t=0 0\r\n\
891 {session_level}\
892 m=audio 49170 UDP/TLS/RTP/SAVP 0 8\r\n\
893 a=rtpmap:0 PCMU/8000\r\n\
894 a=rtpmap:8 PCMA/8000\r\n\
895 {extra_media}"
896 ))
897 }
898
899 #[test]
901 fn a_dtls_offer_is_answered_with_a_fingerprint_and_a_role() {
902 let offered = dtls_offer(
903 &format!(
904 "a=fingerprint:{}\r\na=setup:actpass\r\n",
905 their_fingerprint().to_value()
906 ),
907 "",
908 );
909 let answered = answer(
910 &offered,
911 &Capabilities::g711(local(), 40000).with_dtls_srtp(our_fingerprint()),
912 );
913 let audio = answered.media.first().expect("an audio stream");
914 assert_ne!(audio.port, 0, "the stream should not be rejected");
915 assert_eq!(
916 audio.protocol, "UDP/TLS/RTP/SAVP",
917 "the answer's protocol must match the offer's, or neither side knows how to key it"
918 );
919 assert_eq!(
920 audio.fingerprint(),
921 Some(our_fingerprint()),
922 "the answer carries *our* fingerprint, not an echo of theirs"
923 );
924 assert_eq!(
925 audio.setup(),
926 Some(crate::fingerprint::Setup::Active),
927 "RFC 5763 §5: the answerer takes `active`, so its `ClientHello` opens its own NAT"
928 );
929 assert!(
930 audio.crypto().is_none(),
931 "a DTLS stream must not also carry an SDES key"
932 );
933 }
934
935 #[test]
937 fn a_session_level_fingerprint_is_found() {
938 let offered = dtls_offer(
939 "a=setup:actpass\r\n",
940 &format!("a=fingerprint:{}\r\n", their_fingerprint().to_value()),
941 );
942 let stream = offered.media.first().expect("an audio stream");
943 assert!(stream.fingerprint().is_none(), "none on the m= line");
944 assert_eq!(
945 fingerprint_of(&offered, stream),
946 Some(their_fingerprint()),
947 "the session-level value applies to a stream that does not override it"
948 );
949 let answered = answer(
950 &offered,
951 &Capabilities::g711(local(), 40000).with_dtls_srtp(our_fingerprint()),
952 );
953 assert_ne!(
954 answered.media.first().expect("a stream").port,
955 0,
956 "an offer whose fingerprint is at session level is still answerable"
957 );
958 }
959
960 #[test]
962 fn a_media_level_fingerprint_wins_over_the_session_level_one() {
963 let other = crate::fingerprint::Fingerprint::of(
964 b"a third certificate",
965 crate::fingerprint::HashFunc::Sha256,
966 );
967 let offered = dtls_offer(
968 &format!(
969 "a=fingerprint:{}\r\na=setup:actpass\r\n",
970 their_fingerprint().to_value()
971 ),
972 &format!("a=fingerprint:{}\r\n", other.to_value()),
973 );
974 let stream = offered.media.first().expect("an audio stream");
975 assert_eq!(fingerprint_of(&offered, stream), Some(their_fingerprint()));
976 }
977
978 #[test]
982 fn a_dtls_offer_with_no_fingerprint_is_rejected() {
983 let offered = dtls_offer("a=setup:actpass\r\n", "");
984 let answered = answer(
985 &offered,
986 &Capabilities::g711(local(), 40000).with_dtls_srtp(our_fingerprint()),
987 );
988 assert_eq!(
989 answered.media.first().expect("a stream").port,
990 0,
991 "an unverifiable DTLS offer must be declined, not answered"
992 );
993 }
994
995 #[test]
998 fn a_dtls_offer_to_an_endpoint_without_dtls_is_rejected() {
999 let offered = dtls_offer(
1000 &format!(
1001 "a=fingerprint:{}\r\na=setup:actpass\r\n",
1002 their_fingerprint().to_value()
1003 ),
1004 "",
1005 );
1006 let answered = answer(&offered, &Capabilities::g711(local(), 40000));
1007 assert_eq!(
1008 answered.media.first().expect("a stream").port,
1009 0,
1010 "answering a DTLS offer in the clear would be a downgrade this side chose"
1011 );
1012 }
1013
1014 #[test]
1017 fn the_role_is_answered_rather_than_copied() {
1018 for (offered_role, expected) in [
1019 ("actpass", crate::fingerprint::Setup::Active),
1020 ("passive", crate::fingerprint::Setup::Active),
1021 ("active", crate::fingerprint::Setup::Passive),
1022 ] {
1023 let offered = dtls_offer(
1024 &format!(
1025 "a=fingerprint:{}\r\na=setup:{offered_role}\r\n",
1026 their_fingerprint().to_value()
1027 ),
1028 "",
1029 );
1030 let answered = answer(
1031 &offered,
1032 &Capabilities::g711(local(), 40000).with_dtls_srtp(our_fingerprint()),
1033 );
1034 assert_eq!(
1035 answered.media.first().expect("a stream").setup(),
1036 Some(expected),
1037 "offered {offered_role}"
1038 );
1039 }
1040 }
1041
1042 #[test]
1046 fn offering_dtls_srtp_clears_any_sdes_key() {
1047 let capabilities = Capabilities::g711(local(), 40000)
1048 .with_srtp(true)
1049 .with_dtls_srtp(our_fingerprint());
1050 assert!(capabilities.crypto.is_none());
1051 assert_eq!(capabilities.protocol(), "UDP/TLS/RTP/SAVP");
1052 }
1053
1054 #[test]
1057 fn a_plain_offer_is_not_upgraded_to_dtls() {
1058 let answered = answer(
1059 &offer(AUDIO_OFFER),
1060 &Capabilities::g711(local(), 40000).with_dtls_srtp(our_fingerprint()),
1061 );
1062 let audio = answered.media.first().expect("a stream");
1063 assert_ne!(audio.port, 0, "a plain offer is still answerable");
1064 assert_eq!(audio.protocol, "RTP/AVP");
1065 assert!(audio.fingerprint().is_none());
1066 }
1067}