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, 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 enum ClientEvent {
78 Response {
80 cseq: u32,
82 method: Method,
84 status: StatusCode,
86 body: Vec<u8>,
88 },
89 AuthRetry {
92 method: Method,
94 cseq: u32,
96 request: Vec<u8>,
98 },
99 MediaData {
101 channel: u8,
103 data: Vec<u8>,
105 },
106}
107
108#[derive(Debug)]
110pub struct ClientSession {
111 state: SessionState,
112 next_cseq: u32,
113 session_id: Option<String>,
114 session_timeout: Option<u64>,
115 credentials: Option<Credentials>,
116 authenticator: Option<Authenticator>,
117 negotiated_transport: Option<Transport>,
118 pending: HashMap<u32, Pending>,
119 inbound: Vec<u8>,
122 user_agent: String,
123 last_activity: Option<Instant>,
125 last_uri: Option<String>,
127 session_warnings: Vec<String>,
129 session_warning_total: usize,
131 scanned: usize,
133 skip_until: usize,
135}
136
137impl Default for ClientSession {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143impl ClientSession {
144 pub fn new() -> Self {
147 ClientSession {
148 state: SessionState::Init,
149 next_cseq: 1,
150 session_id: None,
151 session_timeout: None,
152 credentials: None,
153 authenticator: None,
154 negotiated_transport: None,
155 pending: HashMap::new(),
156 inbound: Vec::new(),
157 user_agent: "rtsp-runtime".to_string(),
158 last_activity: None,
159 last_uri: None,
160 session_warnings: Vec::new(),
161 session_warning_total: 0,
162 scanned: 0,
163 skip_until: 0,
164 }
165 }
166
167 pub fn with_credentials(mut self, credentials: Credentials) -> Self {
169 self.credentials = Some(credentials);
170 self
171 }
172
173 pub fn with_user_agent(mut self, ua: impl Into<String>) -> Self {
175 self.user_agent = ua.into();
176 self
177 }
178
179 pub fn state(&self) -> SessionState {
181 self.state
182 }
183
184 pub fn peek_next_cseq(&self) -> u32 {
190 self.next_cseq
191 }
192
193 fn record_session_warning(&mut self, w: String) {
194 self.session_warning_total += 1;
195 if self.session_warnings.len() >= MAX_SESSION_WARNINGS {
196 self.session_warnings.remove(0);
197 }
198 self.session_warnings.push(w);
199 }
200
201 pub fn session_warning_count(&self) -> usize {
204 self.session_warning_total
205 }
206
207 pub fn session_warnings(&self) -> &[String] {
211 &self.session_warnings
212 }
213
214 pub fn has_buffered_input(&self) -> bool {
218 !self.inbound.is_empty()
219 }
220
221 pub fn mark_activity(&mut self, now: Instant) {
226 self.last_activity = Some(now);
227 }
228
229 pub fn poll_timeout(&self) -> Option<Instant> {
234 self.session_id.as_ref()?;
235 let timeout =
236 Duration::from_secs(self.session_timeout.unwrap_or(DEFAULT_SESSION_TIMEOUT_SECS));
237 self.last_activity?
239 .checked_add((timeout / KEEPALIVE_FRACTION_DEN).max(MIN_KEEPALIVE_INTERVAL))
240 }
241
242 pub fn handle_timeout(&mut self, now: Instant) -> Result<Option<Vec<u8>>> {
246 match self.poll_timeout() {
247 Some(deadline) if now >= deadline => {}
248 _ => return Ok(None),
249 }
250 let Some(uri) = self.last_uri.clone() else {
251 return Ok(None);
252 };
253 let bytes = self.get_parameter(&uri, b"")?;
254 self.last_activity = Some(now);
255 Ok(Some(bytes))
256 }
257
258 pub fn session_id(&self) -> Option<&str> {
260 self.session_id.as_deref()
261 }
262
263 pub fn session_timeout(&self) -> Option<u64> {
265 self.session_timeout
266 }
267
268 pub fn negotiated_transport(&self) -> Option<&Transport> {
270 self.negotiated_transport.as_ref()
271 }
272
273 pub fn options(&mut self, uri: &str) -> Result<Vec<u8>> {
277 self.build_request(Method::Options, uri, None, &[])
278 }
279
280 pub fn describe(&mut self, uri: &str) -> Result<Vec<u8>> {
282 self.build_request(
283 Method::Describe,
284 uri,
285 None,
286 &[(headers::ACCEPT, "application/sdp".to_string())],
287 )
288 }
289
290 pub fn setup(&mut self, uri: &str, transport: &Transport) -> Result<Vec<u8>> {
292 self.build_request(
293 Method::Setup,
294 uri,
295 None,
296 &[(headers::TRANSPORT, transport.to_header_value()?)],
297 )
298 }
299
300 pub fn play(&mut self, uri: &str) -> Result<Vec<u8>> {
302 self.build_request(Method::Play, uri, None, &[])
303 }
304
305 pub fn pause(&mut self, uri: &str) -> Result<Vec<u8>> {
307 self.build_request(Method::Pause, uri, None, &[])
308 }
309
310 pub fn teardown(&mut self, uri: &str) -> Result<Vec<u8>> {
312 self.build_request(Method::Teardown, uri, None, &[])
313 }
314
315 pub fn announce(&mut self, uri: &str, sdp: &str) -> Result<Vec<u8>> {
317 self.build_request_with_body(
318 Method::Announce,
319 uri,
320 sdp.as_bytes(),
321 &[(headers::CONTENT_TYPE, "application/sdp".to_string())],
322 )
323 }
324
325 pub fn record(&mut self, uri: &str) -> Result<Vec<u8>> {
327 self.build_request(Method::Record, uri, None, &[])
328 }
329
330 pub fn get_parameter(&mut self, uri: &str, body: &[u8]) -> Result<Vec<u8>> {
333 self.build_request_with_body(Method::GetParameter, uri, body, &[])
334 }
335
336 fn build_request(
337 &mut self,
338 method: Method,
339 uri: &str,
340 _range: Option<&str>,
341 extra: &[(headers::HeaderName, String)],
342 ) -> Result<Vec<u8>> {
343 self.build_request_with_body(method, uri, &[], extra)
344 }
345
346 fn build_request_with_body(
347 &mut self,
348 method: Method,
349 uri: &str,
350 body: &[u8],
351 extra: &[(headers::HeaderName, String)],
352 ) -> Result<Vec<u8>> {
353 client_next_state(self.state, &method)?;
355
356 let cseq = self.next_cseq;
357 let request = self.assemble(method.clone(), uri, cseq, body, extra)?;
358 let bytes = serialize(&Message::from(request.clone()))?;
359 self.next_cseq += 1;
360 self.last_uri = Some(uri.to_string());
361 self.pending.insert(
362 cseq,
363 Pending {
364 method,
365 uri: uri.to_string(),
366 request,
367 auth_retried: false,
368 },
369 );
370 Ok(bytes)
371 }
372
373 fn assemble(
376 &mut self,
377 method: Method,
378 uri: &str,
379 cseq: u32,
380 body: &[u8],
381 extra: &[(headers::HeaderName, String)],
382 ) -> Result<Request<Body>> {
383 let url = rtsp_types::Url::parse(uri)
384 .map_err(|e| Error::TransportParse(format!("invalid request URI {uri:?}: {e}")))?;
385 let mut builder = Request::builder(method.clone(), Version::V1_0)
386 .request_uri(url)
387 .header(headers::CSEQ, cseq.to_string())
388 .header(headers::USER_AGENT, self.user_agent.clone());
389 if let Some(sid) = &self.session_id {
390 builder = builder.header(headers::SESSION, sid.clone());
391 }
392 for (name, value) in extra {
393 builder = builder.header(name.clone(), value.clone());
394 }
395 if let Some(auth) = &mut self.authenticator {
396 let ctx = RequestContext::new(<&str>::from(&method), uri);
397 let value = auth.authorization(&ctx)?;
398 builder = builder.header(headers::AUTHORIZATION, value);
399 }
400 let request = if body.is_empty() {
401 builder.build(Vec::new())
402 } else {
403 builder.build(body.to_vec())
404 };
405 Ok(request)
406 }
407
408 pub fn handle_data(&mut self, data: &[u8]) -> Result<Vec<ClientEvent>> {
413 self.inbound.extend_from_slice(data);
414 if self.inbound.len() > MAX_INBOUND_BYTES {
415 return Err(Error::MessageParse(format!(
416 "inbound buffer of {} bytes exceeds the {MAX_INBOUND_BYTES}-byte maximum \
417 (unterminated header or an unfulfilled Content-Length)",
418 self.inbound.len()
419 )));
420 }
421 let mut events = Vec::new();
422
423 loop {
424 if self.inbound.is_empty() {
425 break;
426 }
427 if self.inbound[0] == MAGIC {
428 match interleaved::InterleavedFrame::parse(&self.inbound)? {
430 Some((frame, consumed)) => {
431 events.push(ClientEvent::MediaData {
432 channel: frame.channel,
433 data: frame.payload,
434 });
435 self.inbound.drain(..consumed);
436 }
437 None => break, }
439 continue;
440 }
441
442 if self.inbound.len() < self.skip_until {
445 break;
446 }
447 if crate::framing::head_end(&self.inbound, self.scanned)?.is_none() {
448 self.scanned = self.inbound.len();
449 break;
450 }
451 match Message::<Body>::parse(&self.inbound) {
452 Ok((message, consumed)) => {
453 self.inbound.drain(..consumed);
454 self.scanned = 0;
455 self.skip_until = 0;
456 self.process_message(message, &mut events)?;
457 }
458 Err(rtsp_types::ParseError::Incomplete(needed)) => {
459 if let Some(more) = needed {
460 self.skip_until = self.inbound.len().saturating_add(more.get());
461 }
462 break;
463 }
464 Err(rtsp_types::ParseError::Error) => {
465 return Err(Error::MessageParse("malformed RTSP message".into()));
466 }
467 }
468 }
469 Ok(events)
470 }
471
472 fn process_message(
473 &mut self,
474 message: Message<Body>,
475 events: &mut Vec<ClientEvent>,
476 ) -> Result<()> {
477 match message {
478 Message::Response(response) => {
479 let Some(cseq) = header_value(response.header(&headers::CSEQ))
486 .and_then(|s| s.trim().parse::<u32>().ok())
487 else {
488 return Ok(());
489 };
490 let status = response.status();
491
492 if status == StatusCode::Unauthorized
494 && let Some(retry) = self.try_auth_retry(cseq, &response)?
495 {
496 events.push(retry);
497 return Ok(());
498 }
499
500 let Some(pending) = self.pending.remove(&cseq) else {
504 return Ok(());
505 };
506
507 if let Some(session_hdr) = header_value(response.header(&headers::SESSION)) {
511 match crate::session_header::SessionHeader::parse_with_warnings(session_hdr) {
512 Ok((parsed, warnings)) => {
513 for w in warnings {
514 log::warn!("RTSP Session header {session_hdr:?}: {w}");
515 self.record_session_warning(w);
516 }
517 self.session_id = Some(parsed.id);
518 if let Some(t) = parsed.timeout {
519 self.session_timeout = Some(t.as_secs());
520 }
521 }
522 Err(e) => {
523 let w = format!("unusable Session header {session_hdr:?}: {e}");
524 log::warn!("RTSP {w}");
525 self.record_session_warning(w);
526 }
527 }
528 }
529 if pending.method == Method::Setup
531 && let Some(t) = header_value(response.header(&headers::TRANSPORT))
532 {
533 self.negotiated_transport = Some(Transport::parse(t)?);
534 }
535
536 if status.is_success() {
538 self.state = client_next_state(self.state, &pending.method)?;
539 if pending.method == Method::Teardown {
541 self.session_id = None;
542 self.session_timeout = None;
543 self.authenticator = None;
544 }
545 } else if status.is_redirection() {
546 self.state = SessionState::Init;
547 }
548 events.push(ClientEvent::Response {
551 cseq,
552 method: pending.method,
553 status,
554 body: response.into_body(),
555 });
556 Ok(())
557 }
558 Message::Data(data) => {
559 events.push(ClientEvent::MediaData {
560 channel: data.channel_id(),
561 data: data.into_body(),
562 });
563 Ok(())
564 }
565 Message::Request(_) => {
566 Ok(())
569 }
570 }
571 }
572
573 fn try_auth_retry(
577 &mut self,
578 cseq: u32,
579 response: &rtsp_types::Response<Body>,
580 ) -> Result<Option<ClientEvent>> {
581 let creds = match &self.credentials {
582 Some(c) => c.clone(),
583 None => return Ok(None),
584 };
585 let (method, uri, already) = match self.pending.get(&cseq) {
587 Some(p) => (p.method.clone(), p.uri.clone(), p.auth_retried),
588 None => return Ok(None),
589 };
590
591 let challenge = header_value(response.header(&headers::WWW_AUTHENTICATE))
592 .ok_or_else(|| Error::Auth("401 without WWW-Authenticate".into()))?;
593 let stale = crate::headers_util::challenge_is_stale(challenge);
594
595 self.authenticator = Some(Authenticator::from_challenge(challenge, creds)?);
602 if already && !stale {
608 return Ok(None);
609 }
610
611 let (body, extra) = self.replay_extra(cseq);
617 self.pending.remove(&cseq);
618 let new_cseq = self.next_cseq;
619 let request = self.assemble(method.clone(), &uri, new_cseq, &body, &extra)?;
620 let bytes = serialize(&Message::from(request.clone()))?;
621 self.next_cseq += 1;
622 self.pending.insert(
623 new_cseq,
624 Pending {
625 method: method.clone(),
626 uri,
627 request,
628 auth_retried: true,
629 },
630 );
631 Ok(Some(ClientEvent::AuthRetry {
632 method,
633 cseq: new_cseq,
634 request: bytes,
635 }))
636 }
637
638 fn replay_extra(&self, old_cseq: u32) -> (Vec<u8>, Vec<(headers::HeaderName, String)>) {
644 let Some(p) = self.pending.get(&old_cseq) else {
645 return (Vec::new(), Vec::new());
646 };
647 let hop_by_hop = [
648 headers::CSEQ,
649 headers::SESSION,
650 headers::AUTHORIZATION,
651 headers::USER_AGENT,
652 headers::CONTENT_LENGTH,
653 ];
654 let extra = p
655 .request
656 .headers()
657 .filter(|(name, _)| !hop_by_hop.iter().any(|h| h == *name))
658 .map(|(name, value)| (name.clone(), value.as_str().to_string()))
659 .collect();
660 (p.request.body().clone(), extra)
661 }
662}
663
664fn header_value(h: Option<&headers::HeaderValue>) -> Option<&str> {
666 h.map(|v| v.as_str())
667}
668
669fn serialize(message: &Message<Body>) -> Result<Vec<u8>> {
671 let mut out = Vec::new();
672 message
673 .write(&mut out)
674 .map_err(|e| Error::MessageWrite(e.to_string()))?;
675 Ok(out)
676}
677
678#[cfg(test)]
679mod tests {
680 use super::*;
681
682 #[test]
683 fn play_in_init_bites() {
684 let mut c = ClientSession::new();
685 assert!(c.play("rtsp://h/s").is_err());
686 }
687
688 #[test]
689 fn setup_allowed_in_init() {
690 let mut c = ClientSession::new();
691 let t = Transport::single(crate::transport::TransportSpec::rtp_avp_tcp_interleaved(
692 0, 1,
693 ));
694 assert!(c.setup("rtsp://h/s", &t).is_ok());
695 }
696
697 #[test]
698 fn cseq_increments() {
699 let mut c = ClientSession::new();
700 let a = c.options("rtsp://h/s").unwrap();
701 let b = c.describe("rtsp://h/s").unwrap();
702 assert!(String::from_utf8_lossy(&a).contains("CSeq: 1"));
703 assert!(String::from_utf8_lossy(&b).contains("CSeq: 2"));
704 }
705
706 #[test]
707 fn announce_emits_request_with_sdp_content_type() {
708 let mut c = ClientSession::new();
709 let sdp = "v=0\r\no=- 0 0 IN IP4 0.0.0.0\r\ns=Test\r\n";
710 let bytes = c.announce("rtsp://h/s", sdp).unwrap();
711 let s = String::from_utf8_lossy(&bytes);
712 assert!(s.contains("ANNOUNCE rtsp://h/s"));
713 assert!(s.contains("Content-Type: application/sdp"));
714 assert!(s.contains(sdp));
715 }
716
717 #[test]
723 fn unknown_cseq_is_skipped_without_discarding_earlier_events() {
724 fn wire(s: &str) -> Vec<u8> {
725 s.replace('\n', "\r\n").into_bytes()
726 }
727 let mut c = ClientSession::new();
728 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");
735 batch.extend(wire("RTSP/1.0 200 OK\nCSeq: 99\n\n"));
736 batch.extend(wire("RTSP/1.0 200 OK\nCSeq: 2\n\n"));
737
738 let events = c
739 .handle_data(&batch)
740 .expect("an unmatched CSeq must not fail the whole batch");
741 let cseqs: Vec<u32> = events
742 .into_iter()
743 .map(|e| match e {
744 ClientEvent::Response { cseq, .. } => cseq,
745 other => panic!("unexpected event: {other:?}"),
746 })
747 .collect();
748 assert_eq!(
749 cseqs,
750 vec![1, 2],
751 "expected both real responses despite the unmatched CSeq 99"
752 );
753 }
754
755 #[test]
759 fn inbound_buffer_is_capped_against_an_unterminated_header() {
760 let mut c = ClientSession::new();
761 c.options("rtsp://h/s").unwrap();
762 let chunk = vec![b'A'; 64 * 1024];
763 let mut last = Ok(Vec::new());
764 for _ in 0..(MAX_INBOUND_BYTES / chunk.len() + 2) {
765 last = c.handle_data(&chunk);
766 if last.is_err() {
767 break;
768 }
769 }
770 assert!(
771 last.is_err(),
772 "expected the oversized inbound buffer to be rejected, not grown forever"
773 );
774 }
775
776 #[test]
777 fn record_fails_from_init_state() {
778 let mut c = ClientSession::new();
779 assert!(c.record("rtsp://h/s").is_err());
780 }
781
782 #[test]
783 fn record_transitions_to_recording_state() {
784 fn wire(s: &str) -> Vec<u8> {
786 s.replace('\n', "\r\n").into_bytes()
787 }
788 let mut c = ClientSession::new();
789 let t = Transport::single(crate::transport::TransportSpec::rtp_avp_tcp_interleaved(
791 0, 1,
792 ));
793 c.setup("rtsp://h/s", &t).unwrap();
794 c.handle_data(&wire(
795 "RTSP/1.0 200 OK\nCSeq: 1\nSession: 42\nTransport: RTP/AVP/TCP;interleaved=0-1\n\n",
796 ))
797 .unwrap();
798 assert_eq!(c.state(), SessionState::Ready);
799
800 c.record("rtsp://h/s").unwrap();
802 assert_eq!(c.state(), SessionState::Ready);
803 c.handle_data(&wire("RTSP/1.0 200 OK\nCSeq: 2\nSession: 42\n\n"))
805 .unwrap();
806 assert_eq!(c.state(), SessionState::Recording);
807 }
808
809 #[test]
814 fn auth_retry_replays_original_body_and_content_type() {
815 fn wire(s: &str) -> Vec<u8> {
816 s.replace('\n', "\r\n").into_bytes()
817 }
818 let mut c = ClientSession::new().with_credentials(Credentials::new("admin", "12345"));
819 let sdp = "v=0\r\no=- 0 0 IN IP4 0.0.0.0\r\ns=Test\r\n";
820 let first = c.announce("rtsp://h/s", sdp).unwrap();
821 assert!(String::from_utf8_lossy(&first).contains("CSeq: 1"));
822
823 let events = c
824 .handle_data(&wire(
825 "RTSP/1.0 401 Unauthorized\nCSeq: 1\nWWW-Authenticate: Digest realm=\"cam\",nonce=\"abc123\",qop=\"auth\"\n\n",
826 ))
827 .unwrap();
828 let retry = events
829 .into_iter()
830 .find_map(|e| match e {
831 ClientEvent::AuthRetry { request, .. } => Some(request),
832 _ => None,
833 })
834 .expect("expected an AuthRetry event");
835 let retry = String::from_utf8_lossy(&retry);
836
837 assert!(retry.contains("ANNOUNCE rtsp://h/s"), "{retry}");
838 assert!(retry.contains("Authorization: Digest "), "{retry}");
839 assert!(
840 retry.contains("Content-Type: application/sdp"),
841 "Content-Type dropped on auth retry: {retry}"
842 );
843 assert!(
844 retry.contains(sdp),
845 "SDP body dropped on auth retry: {retry}"
846 );
847 let expected_len = format!("Content-Length: {}", sdp.len());
848 assert!(
849 retry.contains(&expected_len),
850 "expected {expected_len:?} in: {retry}"
851 );
852 }
853
854 #[test]
863 fn nonce_rotation_without_stale_is_picked_up_on_the_first_retry() {
864 fn wire(s: &str) -> Vec<u8> {
865 s.replace('\n', "\r\n").into_bytes()
866 }
867 let mut c = ClientSession::new().with_credentials(Credentials::new("admin", "12345"));
868
869 c.options("rtsp://h/s").unwrap(); let events = c
872 .handle_data(&wire(
873 "RTSP/1.0 401 Unauthorized\nCSeq: 1\nWWW-Authenticate: Digest realm=\"cam\",nonce=\"nonce-1\",qop=\"auth\"\n\n",
874 ))
875 .unwrap();
876 assert!(
877 events
878 .iter()
879 .any(|e| matches!(e, ClientEvent::AuthRetry { cseq: 2, .. })),
880 "{events:?}"
881 );
882 c.handle_data(&wire("RTSP/1.0 200 OK\nCSeq: 2\n\n"))
883 .unwrap();
884
885 c.options("rtsp://h/s").unwrap(); let events = c
889 .handle_data(&wire(
890 "RTSP/1.0 401 Unauthorized\nCSeq: 3\nWWW-Authenticate: Digest realm=\"cam\",nonce=\"nonce-2\",qop=\"auth\"\n\n",
891 ))
892 .unwrap();
893 let retry = events
894 .into_iter()
895 .find_map(|e| match e {
896 ClientEvent::AuthRetry {
897 cseq: 4, request, ..
898 } => Some(request),
899 _ => None,
900 })
901 .expect("expected a retry (CSeq 4), not a give-up");
902 let retry = String::from_utf8_lossy(&retry);
903 assert!(
904 retry.contains("nonce=\"nonce-2\""),
905 "retry signed with the stale nonce instead of the rotated one: {retry}"
906 );
907 }
908
909 fn established(timeout: Option<u64>) -> ClientSession {
910 let mut c = ClientSession::new();
911 let _ = c
912 .setup(
913 "rtsp://h/s",
914 &Transport::single(crate::transport::TransportSpec::rtp_avp_tcp_interleaved(
915 0, 1,
916 )),
917 )
918 .unwrap();
919 let sess = match timeout {
920 Some(t) => format!("abc;timeout={t}"),
921 None => "abc".into(),
922 };
923 let resp = format!(
924 "RTSP/1.0 200 OK\r\nCSeq: 1\r\nSession: {sess}\r\nTransport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n\r\n"
925 );
926 c.handle_data(resp.as_bytes()).unwrap();
927 c
928 }
929
930 #[test]
931 fn the_keepalive_interval_is_floored_and_fires_at_most_once_per_interval() {
932 let t0 = Instant::now();
934 let mut c = established(Some(1));
935 c.mark_activity(t0);
936 assert_eq!(c.poll_timeout(), Some(t0 + MIN_KEEPALIVE_INTERVAL));
937 assert!(
938 c.handle_timeout(t0 + Duration::from_millis(500))
939 .unwrap()
940 .is_none()
941 );
942 let mut sent = 0;
944 let tick = t0 + MIN_KEEPALIVE_INTERVAL;
945 for _ in 0..1000 {
946 if c.handle_timeout(tick).unwrap().is_some() {
947 sent += 1;
948 }
949 }
950 assert_eq!(sent, 1, "keepalive must not busy-loop");
951 }
952
953 #[test]
954 fn a_zero_timeout_is_a_warning_and_the_default_not_a_busy_loop() {
955 let t0 = Instant::now();
956 let mut c = ClientSession::new();
957 let _ = c
958 .setup(
959 "rtsp://h/s",
960 &Transport::single(crate::transport::TransportSpec::rtp_avp_tcp_interleaved(
961 0, 1,
962 )),
963 )
964 .unwrap();
965 c.handle_data(
966 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",
967 )
968 .unwrap();
969 assert_eq!(c.session_warnings().len(), 1, "{:?}", c.session_warnings());
970 assert!(c.session_warnings()[0].contains("timeout"));
971 c.mark_activity(t0);
972 assert_eq!(c.poll_timeout(), Some(t0 + Duration::from_secs(30)));
973 }
974
975 #[test]
978 fn odd_received_session_ids_are_echoed_verbatim() {
979 for (hdr, id) in [
980 ("\"weird\"", "\"weird\""),
981 ("a b;timeout=30", "a b"),
982 ("abc,def", "abc,def"),
983 ("ab\"c", "ab\"c"),
984 ] {
985 let mut c = ClientSession::new();
986 let _ = c
987 .setup(
988 "rtsp://h/s",
989 &Transport::single(crate::transport::TransportSpec::rtp_avp_tcp_interleaved(
990 0, 1,
991 )),
992 )
993 .unwrap();
994 let resp = format!(
995 "RTSP/1.0 200 OK\r\nCSeq: 1\r\nSession: {hdr}\r\nTransport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n\r\n"
996 );
997 c.handle_data(resp.as_bytes()).unwrap();
998 assert_eq!(c.session_id(), Some(id), "{hdr}");
999 let play = String::from_utf8(c.play("rtsp://h/s").unwrap()).unwrap();
1000 assert!(
1001 play.contains(&format!("Session: {id}\r\n")),
1002 "{hdr}: {play}"
1003 );
1004 }
1005 }
1006
1007 #[test]
1008 fn session_warnings_are_capped_but_counted() {
1009 let mut c = ClientSession::new();
1010 for i in 1..=(MAX_SESSION_WARNINGS as u32 + 10) {
1011 let _ = c.options("rtsp://h/s").unwrap();
1012 c.handle_data(
1013 format!("RTSP/1.0 200 OK\r\nCSeq: {i}\r\nSession: abc;timeout=x\r\n\r\n")
1014 .as_bytes(),
1015 )
1016 .unwrap();
1017 }
1018 assert_eq!(c.session_warnings().len(), MAX_SESSION_WARNINGS);
1019 assert_eq!(c.session_warning_count(), MAX_SESSION_WARNINGS + 10);
1020 }
1021
1022 #[test]
1023 fn an_unusable_session_header_is_observable_not_silent() {
1024 let mut c = ClientSession::new();
1025 let _ = c.options("rtsp://h/s").unwrap();
1026 c.handle_data(b"RTSP/1.0 200 OK\r\nCSeq: 1\r\nSession: ;timeout=5\r\n\r\n")
1027 .unwrap();
1028 assert!(c.session_id().is_none());
1029 assert_eq!(c.session_warnings().len(), 1);
1030 }
1031
1032 #[test]
1033 fn keepalive_is_due_at_half_the_session_timeout() {
1034 let t0 = Instant::now();
1035 let mut c = established(Some(20));
1036 assert_eq!(c.poll_timeout(), None, "no activity recorded yet");
1037 c.mark_activity(t0);
1038 assert_eq!(c.poll_timeout(), Some(t0 + Duration::from_secs(10)));
1039 assert!(
1040 c.handle_timeout(t0 + Duration::from_secs(9))
1041 .unwrap()
1042 .is_none()
1043 );
1044 let req = c
1045 .handle_timeout(t0 + Duration::from_secs(10))
1046 .unwrap()
1047 .expect("keepalive due");
1048 let text = String::from_utf8(req).unwrap();
1049 assert!(
1050 text.starts_with("GET_PARAMETER rtsp://h/s RTSP/1.0"),
1051 "{text}"
1052 );
1053 assert!(text.contains("Session: abc"), "{text}");
1054 assert_eq!(
1056 c.poll_timeout(),
1057 Some(t0 + Duration::from_secs(10) + Duration::from_secs(10))
1058 );
1059 }
1060
1061 #[test]
1062 fn keepalive_uses_the_rfc_default_timeout_when_none_was_declared() {
1063 let t0 = Instant::now();
1064 let mut c = established(None);
1065 c.mark_activity(t0);
1066 assert_eq!(c.poll_timeout(), Some(t0 + Duration::from_secs(30))); }
1068
1069 #[test]
1070 fn no_keepalive_before_setup_or_after_teardown() {
1071 let t0 = Instant::now();
1072 let mut c = ClientSession::new();
1073 c.mark_activity(t0);
1074 assert_eq!(c.poll_timeout(), None);
1075 assert!(
1076 c.handle_timeout(t0 + Duration::from_secs(3600))
1077 .unwrap()
1078 .is_none()
1079 );
1080 }
1081
1082 #[test]
1083 fn head_cap_holds_for_a_70_kib_run_of_a_and_for_9000_header_lines() {
1084 let mut c = ClientSession::new();
1085 assert!(matches!(
1086 c.handle_data(&vec![b'A'; 70 * 1024]),
1087 Err(Error::MessageParse(_))
1088 ));
1089 let mut c = ClientSession::new();
1090 let mut junk = b"RTSP/1.0 200 OK\r\n".to_vec();
1091 junk.extend(b"X-A: b\r\n".repeat(9000));
1092 assert!(matches!(c.handle_data(&junk), Err(Error::MessageParse(_))));
1093 }
1094
1095 #[test]
1096 fn a_response_fed_one_byte_at_a_time_is_still_decoded() {
1097 let mut c = ClientSession::new();
1098 let _ = c.options("rtsp://h/s").unwrap();
1099 let resp = b"RTSP/1.0 200 OK\r\nCSeq: 1\r\nPublic: OPTIONS\r\n\r\n";
1100 let mut got = 0;
1101 for b in resp {
1102 got += c.handle_data(&[*b]).unwrap().len();
1103 }
1104 assert_eq!(got, 1);
1105 }
1106
1107 #[test]
1108 fn an_unterminated_header_over_the_head_cap_is_rejected() {
1109 let mut c = ClientSession::new();
1110 let mut junk = b"RTSP/1.0 200 OK\r\nX-Pad: ".to_vec();
1111 junk.extend(std::iter::repeat_n(b'a', 70 * 1024)); let err = c
1113 .handle_data(&junk)
1114 .expect_err("header phase is capped at 64 KiB");
1115 assert!(matches!(err, Error::MessageParse(_)), "{err:?}");
1116 }
1117
1118 #[test]
1119 fn stale_quoted_rechallenge_refreshes_nonce_after_a_prior_retry() {
1120 fn wire(s: &str) -> Vec<u8> {
1121 s.replace('\n', "\r\n").into_bytes()
1122 }
1123 let mut c = ClientSession::new().with_credentials(Credentials::new("admin", "12345"));
1124 c.options("rtsp://h/s").unwrap(); c.handle_data(&wire(
1128 "RTSP/1.0 401 Unauthorized\nCSeq: 1\nWWW-Authenticate: Digest realm=\"cam\",nonce=\"nonce-1\",qop=\"auth\"\n\n",
1129 )).unwrap();
1130
1131 let events = c
1134 .handle_data(&wire(
1135 "RTSP/1.0 401 Unauthorized\nCSeq: 2\nWWW-Authenticate: Digest realm=\"cam\",nonce=\"nonce-2\",qop=\"auth\",stale=\"True\"\n\n",
1136 ))
1137 .unwrap();
1138 let retry = events
1139 .into_iter()
1140 .find_map(|e| match e {
1141 ClientEvent::AuthRetry {
1142 cseq: 3, request, ..
1143 } => Some(request),
1144 _ => None,
1145 })
1146 .expect("a quoted stale=true re-challenge must trigger a retry");
1147 let retry = String::from_utf8_lossy(&retry);
1148 assert!(
1149 retry.contains("nonce=\"nonce-2\""),
1150 "quoted stale=\"True\" was not detected: {retry}"
1151 );
1152 }
1153
1154 #[test]
1158 fn client_session_debug_does_not_leak_embedded_credentials_secret() {
1159 let c = ClientSession::new()
1160 .with_credentials(Credentials::new("admin", "extremely-secret-password"));
1161 let debug = format!("{c:?}");
1162 assert!(
1163 !debug.contains("extremely-secret-password"),
1164 "leaked via ClientSession Debug: {debug}"
1165 );
1166 assert!(debug.contains("***"), "expected redaction marker: {debug}");
1167 }
1168}