1use std::collections::HashMap;
26use std::time::{Duration, Instant};
27
28use rtsp_types::{Message, Method, Request, StatusCode, Version, headers};
29
30use crate::auth::{Authenticator, Credentials, RequestContext};
31use crate::error::{Error, MessageKind, Result};
32use crate::interleaved::{self, MAGIC};
33use crate::state::{SessionState, client_next_state};
34use crate::transport::Transport;
35
36type Body = Vec<u8>;
38
39const DEFAULT_SESSION_TIMEOUT_SECS: u64 = 60;
41
42const KEEPALIVE_FRACTION_DEN: u32 = 2;
45
46pub const MAX_SESSION_WARNINGS: usize = 16;
49
50pub const MIN_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
54
55const MAX_INBOUND_BYTES: usize = 1024 * 1024;
62
63#[derive(Debug, Clone)]
65struct Pending {
66 method: Method,
67 uri: String,
68 request: Request<Body>,
70 auth_retried: bool,
72}
73
74#[non_exhaustive]
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct RtpInfoEntry {
78 pub uri: String,
80 pub seq: Option<u16>,
82 pub rtptime: Option<u32>,
84}
85
86#[non_exhaustive]
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum ClientEvent {
90 Response {
92 cseq: u32,
94 method: Method,
96 status: StatusCode,
98 body: Vec<u8>,
100 },
101 AuthRetry {
104 method: Method,
106 cseq: u32,
108 request: Vec<u8>,
110 },
111 MediaData {
113 channel: u8,
115 data: Vec<u8>,
117 },
118}
119
120#[must_use = "an unchecked outcome silently ignores a terminal protocol error"]
125#[derive(Debug)]
126pub struct HandleOutcome {
127 pub events: Vec<ClientEvent>,
129 pub error: Option<Error>,
131}
132
133impl HandleOutcome {
134 pub fn into_result(self) -> Result<Vec<ClientEvent>> {
138 match self.error {
139 Some(e) => Err(e),
140 None => Ok(self.events),
141 }
142 }
143}
144
145#[derive(Debug)]
147pub struct ClientSession {
148 state: SessionState,
149 next_cseq: u32,
150 session_id: Option<String>,
151 session_timeout: Option<u64>,
152 credentials: Option<Credentials>,
153 authenticator: Option<Authenticator>,
154 negotiated_transport: Option<Transport>,
155 pending: HashMap<u32, Pending>,
156 inbound: Vec<u8>,
159 user_agent: String,
160 last_activity: Option<Instant>,
162 last_uri: Option<String>,
164 session_warnings: Vec<String>,
166 session_warning_total: usize,
168 scanned: usize,
170 skip_until: usize,
172 last_challenge: Option<String>,
174 content_base: Option<String>,
176 redirect_location: Option<String>,
178 rtp_info: Vec<RtpInfoEntry>,
179}
180
181impl Default for ClientSession {
182 fn default() -> Self {
183 Self::new()
184 }
185}
186
187impl ClientSession {
188 pub fn new() -> Self {
191 ClientSession {
192 state: SessionState::Init,
193 next_cseq: 1,
194 session_id: None,
195 session_timeout: None,
196 credentials: None,
197 authenticator: None,
198 negotiated_transport: None,
199 pending: HashMap::new(),
200 inbound: Vec::new(),
201 user_agent: "rtsp-runtime".to_string(),
202 last_activity: None,
203 last_uri: None,
204 session_warnings: Vec::new(),
205 session_warning_total: 0,
206 scanned: 0,
207 skip_until: 0,
208 last_challenge: None,
209 content_base: None,
210 redirect_location: None,
211 rtp_info: Vec::new(),
212 }
213 }
214
215 pub fn with_credentials(mut self, credentials: Credentials) -> Self {
217 self.credentials = Some(credentials);
218 self
219 }
220
221 pub fn last_challenge(&self) -> Option<&str> {
225 self.last_challenge.as_deref()
226 }
227
228 pub fn authenticate_with(
235 &mut self,
236 credentials: Credentials,
237 challenge: Option<&str>,
238 ) -> Result<()> {
239 let line = challenge
240 .map(str::to_string)
241 .or_else(|| self.last_challenge.clone());
242 self.authenticator = match line {
243 Some(line) => Some(Authenticator::from_challenge(&line, credentials.clone())?),
244 None => None,
245 };
246 self.credentials = Some(credentials);
247 Ok(())
248 }
249
250 pub fn with_user_agent(mut self, ua: impl Into<String>) -> Self {
252 self.user_agent = ua.into();
253 self
254 }
255
256 pub fn state(&self) -> SessionState {
258 self.state
259 }
260
261 pub fn peek_next_cseq(&self) -> u32 {
267 self.next_cseq
268 }
269
270 fn record_session_warning(&mut self, w: String) {
271 self.session_warning_total += 1;
272 if self.session_warnings.len() >= MAX_SESSION_WARNINGS {
273 self.session_warnings.remove(0);
274 }
275 self.session_warnings.push(w);
276 }
277
278 #[cfg(test)]
281 pub(crate) fn session_warning_count(&self) -> usize {
282 self.session_warning_total
283 }
284
285 pub fn session_warnings(&self) -> &[String] {
288 &self.session_warnings
289 }
290
291 pub fn has_buffered_input(&self) -> bool {
295 !self.inbound.is_empty()
296 }
297
298 pub fn mark_activity(&mut self, now: Instant) {
303 self.last_activity = Some(now);
304 }
305
306 pub fn poll_timeout(&self) -> Option<Instant> {
311 self.session_id.as_ref()?;
312 let timeout =
313 Duration::from_secs(self.session_timeout.unwrap_or(DEFAULT_SESSION_TIMEOUT_SECS));
314 self.last_activity?
316 .checked_add((timeout / KEEPALIVE_FRACTION_DEN).max(MIN_KEEPALIVE_INTERVAL))
317 }
318
319 pub fn handle_timeout(&mut self, now: Instant) -> Result<Option<Vec<u8>>> {
323 match self.poll_timeout() {
324 Some(deadline) if now >= deadline => {}
325 _ => return Ok(None),
326 }
327 let Some(uri) = self.last_uri.clone() else {
328 return Ok(None);
329 };
330 let bytes = self.get_parameter(&uri, b"")?;
331 self.last_activity = Some(now);
332 Ok(Some(bytes))
333 }
334
335 pub fn session_id(&self) -> Option<&str> {
337 self.session_id.as_deref()
338 }
339
340 pub fn session_timeout(&self) -> Option<u64> {
342 self.session_timeout
343 }
344
345 pub fn negotiated_transport(&self) -> Option<&Transport> {
347 self.negotiated_transport.as_ref()
348 }
349
350 pub fn take_rtp_info(&mut self) -> Vec<RtpInfoEntry> {
353 core::mem::take(&mut self.rtp_info)
354 }
355
356 pub fn take_negotiated_transport(&mut self) -> Option<Transport> {
359 self.negotiated_transport.take()
360 }
361
362 pub fn content_base(&self) -> Option<&str> {
366 self.content_base.as_deref()
367 }
368
369 pub fn redirect_location(&self) -> Option<&str> {
373 self.redirect_location.as_deref()
374 }
375
376 pub fn options(&mut self, uri: &str) -> Result<Vec<u8>> {
380 self.build_request(Method::Options, uri, None, &[])
381 }
382
383 pub fn describe(&mut self, uri: &str) -> Result<Vec<u8>> {
385 self.build_request(
386 Method::Describe,
387 uri,
388 None,
389 &[(headers::ACCEPT, "application/sdp".to_string())],
390 )
391 }
392
393 pub fn setup(&mut self, uri: &str, transport: &Transport) -> Result<Vec<u8>> {
395 self.build_request(
396 Method::Setup,
397 uri,
398 None,
399 &[(headers::TRANSPORT, transport.to_header_value()?)],
400 )
401 }
402
403 pub fn play(&mut self, uri: &str) -> Result<Vec<u8>> {
405 self.build_request(Method::Play, uri, None, &[])
406 }
407
408 pub fn pause(&mut self, uri: &str) -> Result<Vec<u8>> {
410 self.build_request(Method::Pause, uri, None, &[])
411 }
412
413 pub fn teardown(&mut self, uri: &str) -> Result<Vec<u8>> {
415 self.build_request(Method::Teardown, uri, None, &[])
416 }
417
418 pub fn announce(&mut self, uri: &str, sdp: &str) -> Result<Vec<u8>> {
420 self.build_request_with_body(
421 Method::Announce,
422 uri,
423 sdp.as_bytes(),
424 &[(headers::CONTENT_TYPE, "application/sdp".to_string())],
425 )
426 }
427
428 pub fn record(&mut self, uri: &str) -> Result<Vec<u8>> {
430 self.build_request(Method::Record, uri, None, &[])
431 }
432
433 pub fn get_parameter(&mut self, uri: &str, body: &[u8]) -> Result<Vec<u8>> {
436 self.build_request_with_body(Method::GetParameter, uri, body, &[])
437 }
438
439 fn build_request(
440 &mut self,
441 method: Method,
442 uri: &str,
443 _range: Option<&str>,
444 extra: &[(headers::HeaderName, String)],
445 ) -> Result<Vec<u8>> {
446 self.build_request_with_body(method, uri, &[], extra)
447 }
448
449 fn build_request_with_body(
450 &mut self,
451 method: Method,
452 uri: &str,
453 body: &[u8],
454 extra: &[(headers::HeaderName, String)],
455 ) -> Result<Vec<u8>> {
456 client_next_state(self.state, &method)?;
458
459 let cseq = self.next_cseq;
460 let request = self.assemble(method.clone(), uri, cseq, body, extra)?;
461 let bytes = serialize(&Message::from(request.clone()))?;
462 self.next_cseq += 1;
463 self.last_uri = Some(uri.to_string());
464 self.pending.insert(
465 cseq,
466 Pending {
467 method,
468 uri: uri.to_string(),
469 request,
470 auth_retried: false,
471 },
472 );
473 Ok(bytes)
474 }
475
476 fn assemble(
479 &mut self,
480 method: Method,
481 uri: &str,
482 cseq: u32,
483 body: &[u8],
484 extra: &[(headers::HeaderName, String)],
485 ) -> Result<Request<Body>> {
486 let url = rtsp_types::Url::parse(uri)
487 .map_err(|e| Error::TransportParse(format!("invalid request URI {uri:?}: {e}")))?;
488 let mut builder = Request::builder(method.clone(), Version::V1_0)
489 .request_uri(url)
490 .header(headers::CSEQ, cseq.to_string())
491 .header(headers::USER_AGENT, self.user_agent.clone());
492 if let Some(sid) = &self.session_id {
493 builder = builder.header(headers::SESSION, sid.clone());
494 }
495 for (name, value) in extra {
496 builder = builder.header(name.clone(), value.clone());
497 }
498 if let Some(auth) = &mut self.authenticator {
499 let ctx = RequestContext::new(<&str>::from(&method), uri);
500 let value = auth.authorization(&ctx)?;
501 builder = builder.header(headers::AUTHORIZATION, value);
502 }
503 let request = if body.is_empty() {
504 builder.build(Vec::new())
505 } else {
506 builder.build(body.to_vec())
507 };
508 Ok(request)
509 }
510
511 pub fn handle_data(&mut self, data: &[u8]) -> HandleOutcome {
526 let mut events = Vec::new();
527 let error = self.handle_data_into(data, &mut events).err();
528 HandleOutcome { events, error }
529 }
530
531 pub fn handle_data_strict(&mut self, data: &[u8]) -> Result<Vec<ClientEvent>> {
538 self.handle_data(data).into_result()
539 }
540
541 fn handle_data_into(&mut self, data: &[u8], events: &mut Vec<ClientEvent>) -> Result<()> {
542 self.inbound.extend_from_slice(data);
543 if self.inbound.len() > MAX_INBOUND_BYTES {
544 return Err(Error::MessageParse {
545 what: MessageKind::Response,
546 source: format!(
547 "inbound buffer of {} bytes exceeds the {MAX_INBOUND_BYTES}-byte maximum \
548 (unterminated header or an unfulfilled Content-Length)",
549 self.inbound.len()
550 )
551 .into(),
552 });
553 }
554 let mut pos = 0;
559 let result = self.decode_from(&mut pos, events);
560 self.inbound.drain(..pos);
561 result
562 }
563
564 fn decode_from(&mut self, pos: &mut usize, events: &mut Vec<ClientEvent>) -> Result<()> {
567 loop {
568 let rest = &self.inbound[*pos..];
569 if rest.is_empty() {
570 break;
571 }
572 if rest[0] == MAGIC {
573 match interleaved::InterleavedFrame::parse(rest)? {
575 Some((frame, consumed)) => {
576 events.push(ClientEvent::MediaData {
577 channel: frame.channel,
578 data: frame.payload,
579 });
580 *pos += consumed;
581 }
582 None => break, }
584 continue;
585 }
586
587 if rest.len() < self.skip_until {
590 break;
591 }
592 if crate::framing::head_end(rest, self.scanned)?.is_none() {
593 self.scanned = rest.len();
594 break;
595 }
596 match Message::<Body>::parse(rest) {
597 Ok((message, consumed)) => {
598 *pos += consumed;
599 self.scanned = 0;
600 self.skip_until = 0;
601 self.process_message(message, events)?;
602 }
603 Err(rtsp_types::ParseError::Incomplete(needed)) => {
604 if let Some(more) = needed {
605 self.skip_until = rest.len().saturating_add(more.get());
606 }
607 break;
608 }
609 Err(rtsp_types::ParseError::Error) => {
610 return Err(Error::MessageParse {
611 what: MessageKind::Response,
612 source: Box::new(rtsp_types::ParseError::Error),
613 });
614 }
615 }
616 }
617 Ok(())
618 }
619
620 fn process_message(
621 &mut self,
622 message: Message<Body>,
623 events: &mut Vec<ClientEvent>,
624 ) -> Result<()> {
625 match message {
626 Message::Response(response) => {
627 let Some(cseq) = header_value(response.header(&headers::CSEQ))
634 .and_then(|s| s.trim().parse::<u32>().ok())
635 else {
636 return Ok(());
637 };
638 let status = response.status();
639
640 if let Some(challenge) = header_value(response.header(&headers::WWW_AUTHENTICATE))
641 .or_else(|| header_value(response.header(&headers::PROXY_AUTHENTICATE)))
642 {
643 self.last_challenge = Some(challenge.to_string());
644 }
645
646 if status == StatusCode::Unauthorized
648 && let Some(retry) = self.try_auth_retry(cseq, &response)?
649 {
650 events.push(retry);
651 return Ok(());
652 }
653
654 let Some(pending) = self.pending.remove(&cseq) else {
658 return Ok(());
659 };
660
661 if let Some(session_hdr) = header_value(response.header(&headers::SESSION)) {
665 match crate::session_header::SessionHeader::parse_with_warnings(session_hdr) {
666 Ok((parsed, warnings)) => {
667 for w in warnings {
668 self.record_session_warning(w);
669 }
670 self.session_id = Some(parsed.id);
671 if let Some(t) = parsed.timeout {
672 self.session_timeout = Some(t.as_secs());
673 }
674 }
675 Err(e) => {
676 let w = format!("unusable Session header {session_hdr:?}: {e}");
677 self.record_session_warning(w);
678 }
679 }
680 }
681 if pending.method == Method::Setup
683 && let Some(t) = header_value(response.header(&headers::TRANSPORT))
684 {
685 self.negotiated_transport = Some(Transport::parse(t)?);
686 }
687
688 if pending.method == Method::Play && status.is_success() {
689 self.rtp_info = response
690 .typed_header::<headers::RtpInfos>()
691 .ok()
692 .flatten()
693 .and_then(|infos| infos.try_into_v1().ok())
694 .map(|infos| match infos {
695 headers::RtpInfos::V1(list) => list
696 .into_iter()
697 .map(|i| RtpInfoEntry {
698 uri: i.uri.to_string(),
699 seq: i.seq,
700 rtptime: i.rtptime,
701 })
702 .collect(),
703 _ => Vec::new(),
704 })
705 .unwrap_or_default();
706 }
707
708 self.redirect_location = status
709 .is_redirection()
710 .then(|| header_value(response.header(&headers::LOCATION)))
711 .flatten()
712 .map(|v| v.trim().to_string());
713
714 if pending.method == Method::Describe && status.is_success() {
715 self.content_base = header_value(response.header(&headers::CONTENT_BASE))
716 .or_else(|| header_value(response.header(&headers::CONTENT_LOCATION)))
717 .map(|v| v.trim().to_string());
718 }
719
720 if status.is_success() {
722 self.state = client_next_state(self.state, &pending.method)?;
723 if pending.method == Method::Teardown {
725 self.session_id = None;
726 self.session_timeout = None;
727 self.authenticator = None;
728 }
729 } else if status.is_redirection() {
730 self.state = SessionState::Init;
731 }
732 events.push(ClientEvent::Response {
735 cseq,
736 method: pending.method,
737 status,
738 body: response.into_body(),
739 });
740 Ok(())
741 }
742 Message::Data(data) => {
743 events.push(ClientEvent::MediaData {
744 channel: data.channel_id(),
745 data: data.into_body(),
746 });
747 Ok(())
748 }
749 Message::Request(_) => {
750 Ok(())
753 }
754 }
755 }
756
757 fn try_auth_retry(
761 &mut self,
762 cseq: u32,
763 response: &rtsp_types::Response<Body>,
764 ) -> Result<Option<ClientEvent>> {
765 let creds = match &self.credentials {
766 Some(c) => c.clone(),
767 None => return Ok(None),
768 };
769 let (method, uri, already) = match self.pending.get(&cseq) {
771 Some(p) => (p.method.clone(), p.uri.clone(), p.auth_retried),
772 None => return Ok(None),
773 };
774
775 let challenge = header_value(response.header(&headers::WWW_AUTHENTICATE))
776 .ok_or(Error::MissingAuthChallenge)?;
777 let stale = crate::headers_util::challenge_is_stale(challenge);
778
779 self.authenticator = Some(Authenticator::from_challenge(challenge, creds)?);
786 if already && !stale {
792 return Ok(None);
793 }
794
795 let (body, extra) = self.replay_extra(cseq);
801 self.pending.remove(&cseq);
802 let new_cseq = self.next_cseq;
803 let request = self.assemble(method.clone(), &uri, new_cseq, &body, &extra)?;
804 let bytes = serialize(&Message::from(request.clone()))?;
805 self.next_cseq += 1;
806 self.pending.insert(
807 new_cseq,
808 Pending {
809 method: method.clone(),
810 uri,
811 request,
812 auth_retried: true,
813 },
814 );
815 Ok(Some(ClientEvent::AuthRetry {
816 method,
817 cseq: new_cseq,
818 request: bytes,
819 }))
820 }
821
822 fn replay_extra(&self, old_cseq: u32) -> (Vec<u8>, Vec<(headers::HeaderName, String)>) {
828 let Some(p) = self.pending.get(&old_cseq) else {
829 return (Vec::new(), Vec::new());
830 };
831 let hop_by_hop = [
832 headers::CSEQ,
833 headers::SESSION,
834 headers::AUTHORIZATION,
835 headers::USER_AGENT,
836 headers::CONTENT_LENGTH,
837 ];
838 let extra = p
839 .request
840 .headers()
841 .filter(|(name, _)| !hop_by_hop.iter().any(|h| h == *name))
842 .map(|(name, value)| (name.clone(), value.as_str().to_string()))
843 .collect();
844 (p.request.body().clone(), extra)
845 }
846}
847
848fn header_value(h: Option<&headers::HeaderValue>) -> Option<&str> {
850 h.map(|v| v.as_str())
851}
852
853fn serialize(message: &Message<Body>) -> Result<Vec<u8>> {
855 let mut out = Vec::new();
856 message.write(&mut out).map_err(Error::MessageWrite)?;
857 Ok(out)
858}
859
860#[cfg(test)]
861mod tests {
862 use super::*;
863
864 #[test]
865 fn play_in_init_bites() {
866 let mut c = ClientSession::new();
867 assert!(matches!(
868 c.play("rtsp://h/s"),
869 Err(crate::error::Error::MethodNotValidInState { .. })
870 ));
871 }
872
873 #[test]
874 fn setup_allowed_in_init() {
875 let mut c = ClientSession::new();
876 let t = Transport::single(crate::transport::TransportSpec::rtp_avp_tcp_interleaved(
877 0, 1,
878 ));
879 assert!(c.setup("rtsp://h/s", &t).is_ok());
880 }
881
882 #[test]
883 fn cseq_increments() {
884 let mut c = ClientSession::new();
885 let a = c.options("rtsp://h/s").unwrap();
886 let b = c.describe("rtsp://h/s").unwrap();
887 assert!(String::from_utf8_lossy(&a).contains("CSeq: 1"));
888 assert!(String::from_utf8_lossy(&b).contains("CSeq: 2"));
889 }
890
891 #[test]
892 fn announce_emits_request_with_sdp_content_type() {
893 let mut c = ClientSession::new();
894 let sdp = "v=0\r\no=- 0 0 IN IP4 0.0.0.0\r\ns=Test\r\n";
895 let bytes = c.announce("rtsp://h/s", sdp).unwrap();
896 let s = String::from_utf8_lossy(&bytes);
897 assert!(s.contains("ANNOUNCE rtsp://h/s"));
898 assert!(s.contains("Content-Type: application/sdp"));
899 assert!(s.contains(sdp));
900 }
901
902 #[test]
903 fn redirect_location_is_taken_from_a_3xx_and_cleared_by_the_next_response() {
904 let mut c = ClientSession::new();
905 c.describe("rtsp://h/s").unwrap();
906 assert_eq!(c.redirect_location(), None);
907 let _ = c.handle_data(b"RTSP/1.0 302 Found\r\nCSeq: 1\r\nLocation: rtsp://other/s\r\n\r\n");
908 assert_eq!(c.redirect_location(), Some("rtsp://other/s"));
909 c.describe("rtsp://other/s").unwrap();
910 let _ = c.handle_data(b"RTSP/1.0 200 OK\r\nCSeq: 2\r\n\r\n");
911 assert_eq!(c.redirect_location(), None);
912 }
913
914 fn wire_401(challenge: &str) -> Vec<u8> {
915 format!("RTSP/1.0 401 Unauthorized\r\nCSeq: 1\r\nWWW-Authenticate: {challenge}\r\n\r\n")
916 .into_bytes()
917 }
918
919 #[test]
920 fn challenge_is_recorded_without_credentials() {
921 let mut c = ClientSession::new();
922 c.describe("rtsp://h/s").unwrap();
923 assert_eq!(c.last_challenge(), None);
924 let events = c
925 .handle_data_strict(&wire_401("Basic realm=\"cam\""))
926 .unwrap();
927 assert!(matches!(
928 events.as_slice(),
929 [ClientEvent::Response {
930 status: StatusCode::Unauthorized,
931 ..
932 }]
933 ));
934 assert_eq!(c.last_challenge(), Some("Basic realm=\"cam\""));
935 }
936
937 #[test]
938 fn authenticate_with_makes_next_request_authorized() {
939 let mut c = ClientSession::new();
940 c.describe("rtsp://h/s").unwrap();
941 c.handle_data_strict(&wire_401("Basic realm=\"cam\""))
942 .unwrap();
943 let before = c.describe("rtsp://h/s").unwrap();
944 assert!(!String::from_utf8_lossy(&before).contains("Authorization"));
945 c.authenticate_with(Credentials::new("u", "p"), None)
946 .unwrap();
947 let after = String::from_utf8_lossy(&c.describe("rtsp://h/s").unwrap()).into_owned();
948 assert!(after.contains("Authorization: Basic dTpw"), "got: {after}");
949 }
950
951 #[test]
952 fn authenticate_with_explicit_challenge_overrides_recorded_one() {
953 let mut c = ClientSession::new();
954 c.authenticate_with(Credentials::new("u", "p"), Some("Basic realm=\"x\""))
955 .unwrap();
956 let req = String::from_utf8_lossy(&c.options("rtsp://h/s").unwrap()).into_owned();
957 assert!(req.contains("Authorization: Basic dTpw"), "got: {req}");
958 }
959
960 #[test]
961 fn a_401_without_a_challenge_cannot_be_answered() {
962 let mut c = ClientSession::new();
963 c.authenticate_with(Credentials::new("u", "p"), Some("Basic realm=\"x\""))
964 .unwrap();
965 c.describe("rtsp://h/s").unwrap();
966 let err = c
967 .handle_data_strict(b"RTSP/1.0 401 Unauthorized\r\nCSeq: 1\r\n\r\n")
968 .unwrap_err();
969 assert!(matches!(err, Error::MissingAuthChallenge), "{err:?}");
970 }
971
972 #[test]
973 fn a_broadcast_auth_failure_keeps_its_source() {
974 let err = Error::from(broadcast_auth::Error::InvalidBearerToken);
975 assert!(
976 matches!(err, Error::Auth(broadcast_auth::Error::InvalidBearerToken)),
977 "{err:?}"
978 );
979 }
980
981 fn setup_request(c: &mut ClientSession) {
982 let t = Transport::single(crate::transport::TransportSpec::rtp_avp_tcp_interleaved(
983 0, 1,
984 ));
985 c.setup("rtsp://h/s/trackID=1", &t).unwrap();
986 }
987
988 fn feed(c: &mut ClientSession, text: &str) {
989 c.handle_data_strict(text.replace('\n', "\r\n").as_bytes())
990 .unwrap();
991 }
992
993 #[test]
994 fn content_base_is_taken_from_the_describe_response() {
995 let mut c = ClientSession::new();
996 assert_eq!(c.content_base(), None);
997 c.describe("rtsp://h/s").unwrap();
998 feed(
999 &mut c,
1000 "RTSP/1.0 200 OK\nCSeq: 1\nContent-Base: rtsp://h/s/\nContent-Location: rtsp://h/loc/\n\n",
1001 );
1002 assert_eq!(c.content_base(), Some("rtsp://h/s/"));
1003 c.describe("rtsp://h/s").unwrap();
1004 feed(
1005 &mut c,
1006 "RTSP/1.0 200 OK\nCSeq: 2\nContent-Location: rtsp://h/loc/\n\n",
1007 );
1008 assert_eq!(c.content_base(), Some("rtsp://h/loc/"));
1009 c.describe("rtsp://h/s").unwrap();
1010 feed(&mut c, "RTSP/1.0 200 OK\nCSeq: 3\n\n");
1011 assert_eq!(c.content_base(), None);
1012 }
1013
1014 #[test]
1015 fn take_negotiated_transport_only_returns_the_latest_setup_reply() {
1016 let mut c = ClientSession::new();
1017 setup_request(&mut c);
1018 feed(
1019 &mut c,
1020 "RTSP/1.0 200 OK\nCSeq: 1\nSession: A1\nTransport: RTP/AVP/TCP;unicast;interleaved=0-1\n\n",
1021 );
1022 let first = c
1023 .take_negotiated_transport()
1024 .expect("first reply carries one");
1025 assert_eq!(first.first().and_then(|s| s.interleaved), Some((0, 1)));
1026 assert!(c.take_negotiated_transport().is_none(), "taken once");
1027 setup_request(&mut c);
1028 feed(&mut c, "RTSP/1.0 200 OK\nCSeq: 2\nSession: A1\n\n");
1029 assert!(
1030 c.take_negotiated_transport().is_none(),
1031 "a reply without Transport negotiates nothing"
1032 );
1033 }
1034
1035 #[test]
1036 fn take_rtp_info_returns_the_play_response_entries_once() {
1037 let mut c = ClientSession::new();
1038 setup_request(&mut c);
1039 feed(
1040 &mut c,
1041 "RTSP/1.0 200 OK\nCSeq: 1\nSession: A1\nTransport: RTP/AVP/TCP;unicast;interleaved=0-1\n\n",
1042 );
1043 c.play("rtsp://h/s").unwrap();
1044 feed(
1045 &mut c,
1046 "RTSP/1.0 200 OK\nCSeq: 2\nSession: A1\nRTP-Info: url=rtsp://h/s/t1;seq=7;rtptime=100,url=rtsp://h/s/t2;seq=9;rtptime=4294967295\n\n",
1047 );
1048 let info = c.take_rtp_info();
1049 assert_eq!(info.len(), 2);
1050 assert_eq!(info[0].uri, "rtsp://h/s/t1");
1051 assert_eq!(info[0].seq, Some(7));
1052 assert_eq!(info[0].rtptime, Some(100));
1053 assert_eq!(info[1].rtptime, Some(u32::MAX));
1054 assert!(c.take_rtp_info().is_empty(), "taken once");
1055 }
1056
1057 #[test]
1063 fn unknown_cseq_is_skipped_without_discarding_earlier_events() {
1064 fn wire(s: &str) -> Vec<u8> {
1065 s.replace('\n', "\r\n").into_bytes()
1066 }
1067 let mut c = ClientSession::new();
1068 c.options("rtsp://h/s").unwrap(); c.describe("rtsp://h/s").unwrap(); let mut batch = wire("RTSP/1.0 200 OK\nCSeq: 1\n\n");
1075 batch.extend(wire("RTSP/1.0 200 OK\nCSeq: 99\n\n"));
1076 batch.extend(wire("RTSP/1.0 200 OK\nCSeq: 2\n\n"));
1077
1078 let events = c
1079 .handle_data_strict(&batch)
1080 .expect("an unmatched CSeq must not fail the whole batch");
1081 let cseqs: Vec<u32> = events
1082 .into_iter()
1083 .map(|e| match e {
1084 ClientEvent::Response { cseq, .. } => cseq,
1085 other => panic!("unexpected event: {other:?}"),
1086 })
1087 .collect();
1088 assert_eq!(
1089 cseqs,
1090 vec![1, 2],
1091 "expected both real responses despite the unmatched CSeq 99"
1092 );
1093 }
1094
1095 #[test]
1099 fn many_interleaved_frames_in_one_read_decode_in_order_and_keep_the_tail() {
1100 const FRAMES: usize = 600;
1101 const PAYLOAD: usize = 800;
1102 let mut wire = Vec::new();
1103 for i in 0..FRAMES {
1104 let payload = vec![(i % 251) as u8; PAYLOAD];
1105 wire.extend(
1106 interleaved::InterleavedFrame::slice_to_bytes((i % 2) as u8, &payload).unwrap(),
1107 );
1108 }
1109 let split = wire.len() - 100; let mut c = ClientSession::new();
1111 let first = c.handle_data(&wire[..split]);
1112 assert!(first.error.is_none());
1113 assert_eq!(first.events.len(), FRAMES - 1);
1114 let second = c.handle_data(&wire[split..]);
1115 assert!(second.error.is_none());
1116 assert_eq!(second.events.len(), 1);
1117 for (i, e) in first.events.iter().chain(second.events.iter()).enumerate() {
1118 match e {
1119 ClientEvent::MediaData { channel, data } => {
1120 assert_eq!(*channel as usize, i % 2);
1121 assert_eq!(data.len(), PAYLOAD);
1122 assert!(data.iter().all(|b| *b == (i % 251) as u8));
1123 }
1124 other => panic!("unexpected {other:?}"),
1125 }
1126 }
1127 assert!(!c.has_buffered_input());
1128 }
1129
1130 #[test]
1134 fn inbound_buffer_is_capped_against_an_unterminated_header() {
1135 let mut c = ClientSession::new();
1136 c.options("rtsp://h/s").unwrap();
1137 let chunk = vec![b'A'; 64 * 1024];
1138 let mut last = Ok(Vec::new());
1139 for _ in 0..(MAX_INBOUND_BYTES / chunk.len() + 2) {
1140 last = c.handle_data_strict(&chunk);
1141 if last.is_err() {
1142 break;
1143 }
1144 }
1145 assert!(
1146 matches!(last, Err(crate::error::Error::MessageParse { .. })),
1147 "expected the oversized inbound buffer to be rejected, not grown forever"
1148 );
1149 }
1150
1151 #[test]
1152 fn record_fails_from_init_state() {
1153 let mut c = ClientSession::new();
1154 assert!(matches!(
1155 c.record("rtsp://h/s"),
1156 Err(crate::error::Error::MethodNotValidInState { .. })
1157 ));
1158 }
1159
1160 #[test]
1161 fn record_transitions_to_recording_state() {
1162 fn wire(s: &str) -> Vec<u8> {
1164 s.replace('\n', "\r\n").into_bytes()
1165 }
1166 let mut c = ClientSession::new();
1167 let t = Transport::single(crate::transport::TransportSpec::rtp_avp_tcp_interleaved(
1169 0, 1,
1170 ));
1171 c.setup("rtsp://h/s", &t).unwrap();
1172 c.handle_data_strict(&wire(
1173 "RTSP/1.0 200 OK\nCSeq: 1\nSession: 42\nTransport: RTP/AVP/TCP;interleaved=0-1\n\n",
1174 ))
1175 .unwrap();
1176 assert_eq!(c.state(), SessionState::Ready);
1177
1178 c.record("rtsp://h/s").unwrap();
1180 assert_eq!(c.state(), SessionState::Ready);
1181 c.handle_data_strict(&wire("RTSP/1.0 200 OK\nCSeq: 2\nSession: 42\n\n"))
1183 .unwrap();
1184 assert_eq!(c.state(), SessionState::Recording);
1185 }
1186
1187 #[test]
1192 fn auth_retry_replays_original_body_and_content_type() {
1193 fn wire(s: &str) -> Vec<u8> {
1194 s.replace('\n', "\r\n").into_bytes()
1195 }
1196 let mut c = ClientSession::new().with_credentials(Credentials::new("admin", "12345"));
1197 let sdp = "v=0\r\no=- 0 0 IN IP4 0.0.0.0\r\ns=Test\r\n";
1198 let first = c.announce("rtsp://h/s", sdp).unwrap();
1199 assert!(String::from_utf8_lossy(&first).contains("CSeq: 1"));
1200
1201 let events = c
1202 .handle_data_strict(&wire(
1203 "RTSP/1.0 401 Unauthorized\nCSeq: 1\nWWW-Authenticate: Digest realm=\"cam\",nonce=\"abc123\",qop=\"auth\"\n\n",
1204 ))
1205 .unwrap();
1206 let retry = events
1207 .into_iter()
1208 .find_map(|e| match e {
1209 ClientEvent::AuthRetry { request, .. } => Some(request),
1210 _ => None,
1211 })
1212 .expect("expected an AuthRetry event");
1213 let retry = String::from_utf8_lossy(&retry);
1214
1215 assert!(retry.contains("ANNOUNCE rtsp://h/s"), "{retry}");
1216 assert!(retry.contains("Authorization: Digest "), "{retry}");
1217 assert!(
1218 retry.contains("Content-Type: application/sdp"),
1219 "Content-Type dropped on auth retry: {retry}"
1220 );
1221 assert!(
1222 retry.contains(sdp),
1223 "SDP body dropped on auth retry: {retry}"
1224 );
1225 let expected_len = format!("Content-Length: {}", sdp.len());
1226 assert!(
1227 retry.contains(&expected_len),
1228 "expected {expected_len:?} in: {retry}"
1229 );
1230 }
1231
1232 #[test]
1241 fn nonce_rotation_without_stale_is_picked_up_on_the_first_retry() {
1242 fn wire(s: &str) -> Vec<u8> {
1243 s.replace('\n', "\r\n").into_bytes()
1244 }
1245 let mut c = ClientSession::new().with_credentials(Credentials::new("admin", "12345"));
1246
1247 c.options("rtsp://h/s").unwrap(); let events = c
1250 .handle_data_strict(&wire(
1251 "RTSP/1.0 401 Unauthorized\nCSeq: 1\nWWW-Authenticate: Digest realm=\"cam\",nonce=\"nonce-1\",qop=\"auth\"\n\n",
1252 ))
1253 .unwrap();
1254 assert!(
1255 events
1256 .iter()
1257 .any(|e| matches!(e, ClientEvent::AuthRetry { cseq: 2, .. })),
1258 "{events:?}"
1259 );
1260 c.handle_data_strict(&wire("RTSP/1.0 200 OK\nCSeq: 2\n\n"))
1261 .unwrap();
1262
1263 c.options("rtsp://h/s").unwrap(); let events = c
1267 .handle_data_strict(&wire(
1268 "RTSP/1.0 401 Unauthorized\nCSeq: 3\nWWW-Authenticate: Digest realm=\"cam\",nonce=\"nonce-2\",qop=\"auth\"\n\n",
1269 ))
1270 .unwrap();
1271 let retry = events
1272 .into_iter()
1273 .find_map(|e| match e {
1274 ClientEvent::AuthRetry {
1275 cseq: 4, request, ..
1276 } => Some(request),
1277 _ => None,
1278 })
1279 .expect("expected a retry (CSeq 4), not a give-up");
1280 let retry = String::from_utf8_lossy(&retry);
1281 assert!(
1282 retry.contains("nonce=\"nonce-2\""),
1283 "retry signed with the stale nonce instead of the rotated one: {retry}"
1284 );
1285 }
1286
1287 fn established(timeout: Option<u64>) -> ClientSession {
1288 let mut c = ClientSession::new();
1289 let _ = c
1290 .setup(
1291 "rtsp://h/s",
1292 &Transport::single(crate::transport::TransportSpec::rtp_avp_tcp_interleaved(
1293 0, 1,
1294 )),
1295 )
1296 .unwrap();
1297 let sess = match timeout {
1298 Some(t) => format!("abc;timeout={t}"),
1299 None => "abc".into(),
1300 };
1301 let resp = format!(
1302 "RTSP/1.0 200 OK\r\nCSeq: 1\r\nSession: {sess}\r\nTransport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n\r\n"
1303 );
1304 c.handle_data_strict(resp.as_bytes()).unwrap();
1305 c
1306 }
1307
1308 #[test]
1309 fn the_keepalive_interval_is_floored_and_fires_at_most_once_per_interval() {
1310 let t0 = Instant::now();
1312 let mut c = established(Some(1));
1313 c.mark_activity(t0);
1314 assert_eq!(c.poll_timeout(), Some(t0 + MIN_KEEPALIVE_INTERVAL));
1315 assert!(
1316 c.handle_timeout(t0 + Duration::from_millis(500))
1317 .unwrap()
1318 .is_none()
1319 );
1320 let mut sent = 0;
1322 let tick = t0 + MIN_KEEPALIVE_INTERVAL;
1323 for _ in 0..1000 {
1324 if c.handle_timeout(tick).unwrap().is_some() {
1325 sent += 1;
1326 }
1327 }
1328 assert_eq!(sent, 1, "keepalive must not busy-loop");
1329 }
1330
1331 #[test]
1332 fn a_zero_timeout_is_a_warning_and_the_default_not_a_busy_loop() {
1333 let t0 = Instant::now();
1334 let mut c = ClientSession::new();
1335 let _ = c
1336 .setup(
1337 "rtsp://h/s",
1338 &Transport::single(crate::transport::TransportSpec::rtp_avp_tcp_interleaved(
1339 0, 1,
1340 )),
1341 )
1342 .unwrap();
1343 c.handle_data_strict(
1344 b"RTSP/1.0 200 OK\r\nCSeq: 1\r\nSession: abc;timeout=0\r\nTransport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n\r\n",
1345 )
1346 .unwrap();
1347 assert_eq!(c.session_warnings().len(), 1, "{:?}", c.session_warnings());
1348 assert!(c.session_warnings()[0].contains("timeout"));
1349 c.mark_activity(t0);
1350 assert_eq!(c.poll_timeout(), Some(t0 + Duration::from_secs(30)));
1351 }
1352
1353 #[test]
1356 fn odd_received_session_ids_are_echoed_verbatim() {
1357 for (hdr, id) in [
1358 ("\"weird\"", "\"weird\""),
1359 ("a b;timeout=30", "a b"),
1360 ("abc,def", "abc,def"),
1361 ("ab\"c", "ab\"c"),
1362 ] {
1363 let mut c = ClientSession::new();
1364 let _ = c
1365 .setup(
1366 "rtsp://h/s",
1367 &Transport::single(crate::transport::TransportSpec::rtp_avp_tcp_interleaved(
1368 0, 1,
1369 )),
1370 )
1371 .unwrap();
1372 let resp = format!(
1373 "RTSP/1.0 200 OK\r\nCSeq: 1\r\nSession: {hdr}\r\nTransport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n\r\n"
1374 );
1375 c.handle_data_strict(resp.as_bytes()).unwrap();
1376 assert_eq!(c.session_id(), Some(id), "{hdr}");
1377 let play = String::from_utf8(c.play("rtsp://h/s").unwrap()).unwrap();
1378 assert!(
1379 play.contains(&format!("Session: {id}\r\n")),
1380 "{hdr}: {play}"
1381 );
1382 }
1383 }
1384
1385 #[test]
1386 fn session_warnings_are_capped_but_counted() {
1387 let mut c = ClientSession::new();
1388 for i in 1..=(MAX_SESSION_WARNINGS as u32 + 10) {
1389 let _ = c.options("rtsp://h/s").unwrap();
1390 c.handle_data_strict(
1391 format!("RTSP/1.0 200 OK\r\nCSeq: {i}\r\nSession: abc;timeout=x\r\n\r\n")
1392 .as_bytes(),
1393 )
1394 .unwrap();
1395 }
1396 assert_eq!(c.session_warnings().len(), MAX_SESSION_WARNINGS);
1397 assert_eq!(c.session_warning_count(), MAX_SESSION_WARNINGS + 10);
1398 }
1399
1400 #[test]
1401 fn an_unusable_session_header_is_observable_not_silent() {
1402 let mut c = ClientSession::new();
1403 let _ = c.options("rtsp://h/s").unwrap();
1404 c.handle_data_strict(b"RTSP/1.0 200 OK\r\nCSeq: 1\r\nSession: ;timeout=5\r\n\r\n")
1405 .unwrap();
1406 assert!(c.session_id().is_none());
1407 assert_eq!(c.session_warnings().len(), 1);
1408 }
1409
1410 #[test]
1411 fn keepalive_is_due_at_half_the_session_timeout() {
1412 let t0 = Instant::now();
1413 let mut c = established(Some(20));
1414 assert_eq!(c.poll_timeout(), None, "no activity recorded yet");
1415 c.mark_activity(t0);
1416 assert_eq!(c.poll_timeout(), Some(t0 + Duration::from_secs(10)));
1417 assert!(
1418 c.handle_timeout(t0 + Duration::from_secs(9))
1419 .unwrap()
1420 .is_none()
1421 );
1422 let req = c
1423 .handle_timeout(t0 + Duration::from_secs(10))
1424 .unwrap()
1425 .expect("keepalive due");
1426 let text = String::from_utf8(req).unwrap();
1427 assert!(
1428 text.starts_with("GET_PARAMETER rtsp://h/s RTSP/1.0"),
1429 "{text}"
1430 );
1431 assert!(text.contains("Session: abc"), "{text}");
1432 assert_eq!(
1434 c.poll_timeout(),
1435 Some(t0 + Duration::from_secs(10) + Duration::from_secs(10))
1436 );
1437 }
1438
1439 #[test]
1440 fn keepalive_uses_the_rfc_default_timeout_when_none_was_declared() {
1441 let t0 = Instant::now();
1442 let mut c = established(None);
1443 c.mark_activity(t0);
1444 assert_eq!(c.poll_timeout(), Some(t0 + Duration::from_secs(30))); }
1446
1447 #[test]
1448 fn no_keepalive_before_setup_or_after_teardown() {
1449 let t0 = Instant::now();
1450 let mut c = ClientSession::new();
1451 c.mark_activity(t0);
1452 assert_eq!(c.poll_timeout(), None);
1453 assert!(
1454 c.handle_timeout(t0 + Duration::from_secs(3600))
1455 .unwrap()
1456 .is_none()
1457 );
1458 }
1459
1460 #[test]
1461 fn head_cap_holds_for_a_70_kib_run_of_a_and_for_9000_header_lines() {
1462 let mut c = ClientSession::new();
1463 assert!(matches!(
1464 c.handle_data_strict(&vec![b'A'; 70 * 1024]),
1465 Err(Error::MessageParse { .. })
1466 ));
1467 let mut c = ClientSession::new();
1468 let mut junk = b"RTSP/1.0 200 OK\r\n".to_vec();
1469 junk.extend(b"X-A: b\r\n".repeat(9000));
1470 assert!(matches!(
1471 c.handle_data_strict(&junk),
1472 Err(Error::MessageParse { .. })
1473 ));
1474 }
1475
1476 #[test]
1477 fn a_response_fed_one_byte_at_a_time_is_still_decoded() {
1478 let mut c = ClientSession::new();
1479 let _ = c.options("rtsp://h/s").unwrap();
1480 let resp = b"RTSP/1.0 200 OK\r\nCSeq: 1\r\nPublic: OPTIONS\r\n\r\n";
1481 let mut got = 0;
1482 for b in resp {
1483 got += c.handle_data_strict(&[*b]).unwrap().len();
1484 }
1485 assert_eq!(got, 1);
1486 }
1487
1488 #[test]
1489 fn an_unterminated_header_over_the_head_cap_is_rejected() {
1490 let mut c = ClientSession::new();
1491 let mut junk = b"RTSP/1.0 200 OK\r\nX-Pad: ".to_vec();
1492 junk.extend(std::iter::repeat_n(b'a', 70 * 1024)); let err = c
1494 .handle_data_strict(&junk)
1495 .expect_err("header phase is capped at 64 KiB");
1496 assert!(matches!(err, Error::MessageParse { .. }), "{err:?}");
1497 }
1498
1499 #[test]
1500 fn stale_quoted_rechallenge_refreshes_nonce_after_a_prior_retry() {
1501 fn wire(s: &str) -> Vec<u8> {
1502 s.replace('\n', "\r\n").into_bytes()
1503 }
1504 let mut c = ClientSession::new().with_credentials(Credentials::new("admin", "12345"));
1505 c.options("rtsp://h/s").unwrap(); c.handle_data_strict(&wire(
1509 "RTSP/1.0 401 Unauthorized\nCSeq: 1\nWWW-Authenticate: Digest realm=\"cam\",nonce=\"nonce-1\",qop=\"auth\"\n\n",
1510 )).unwrap();
1511
1512 let events = c
1515 .handle_data_strict(&wire(
1516 "RTSP/1.0 401 Unauthorized\nCSeq: 2\nWWW-Authenticate: Digest realm=\"cam\",nonce=\"nonce-2\",qop=\"auth\",stale=\"True\"\n\n",
1517 ))
1518 .unwrap();
1519 let retry = events
1520 .into_iter()
1521 .find_map(|e| match e {
1522 ClientEvent::AuthRetry {
1523 cseq: 3, request, ..
1524 } => Some(request),
1525 _ => None,
1526 })
1527 .expect("a quoted stale=true re-challenge must trigger a retry");
1528 let retry = String::from_utf8_lossy(&retry);
1529 assert!(
1530 retry.contains("nonce=\"nonce-2\""),
1531 "quoted stale=\"True\" was not detected: {retry}"
1532 );
1533 }
1534
1535 #[test]
1539 fn client_session_debug_does_not_leak_embedded_credentials_secret() {
1540 let c = ClientSession::new()
1541 .with_credentials(Credentials::new("admin", "extremely-secret-password"));
1542 let debug = format!("{c:?}");
1543 assert!(
1544 !debug.contains("extremely-secret-password"),
1545 "leaked via ClientSession Debug: {debug}"
1546 );
1547 assert!(debug.contains("***"), "expected redaction marker: {debug}");
1548 }
1549}