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#[must_use = "an unchecked outcome silently ignores a terminal protocol error"]
113#[derive(Debug)]
114pub struct HandleOutcome {
115 pub events: Vec<ClientEvent>,
117 pub error: Option<Error>,
119}
120
121impl HandleOutcome {
122 pub fn into_result(self) -> Result<Vec<ClientEvent>> {
126 match self.error {
127 Some(e) => Err(e),
128 None => Ok(self.events),
129 }
130 }
131}
132
133#[derive(Debug)]
135pub struct ClientSession {
136 state: SessionState,
137 next_cseq: u32,
138 session_id: Option<String>,
139 session_timeout: Option<u64>,
140 credentials: Option<Credentials>,
141 authenticator: Option<Authenticator>,
142 negotiated_transport: Option<Transport>,
143 pending: HashMap<u32, Pending>,
144 inbound: Vec<u8>,
147 user_agent: String,
148 last_activity: Option<Instant>,
150 last_uri: Option<String>,
152 session_warnings: Vec<String>,
154 session_warning_total: usize,
156 scanned: usize,
158 skip_until: usize,
160}
161
162impl Default for ClientSession {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168impl ClientSession {
169 pub fn new() -> Self {
172 ClientSession {
173 state: SessionState::Init,
174 next_cseq: 1,
175 session_id: None,
176 session_timeout: None,
177 credentials: None,
178 authenticator: None,
179 negotiated_transport: None,
180 pending: HashMap::new(),
181 inbound: Vec::new(),
182 user_agent: "rtsp-runtime".to_string(),
183 last_activity: None,
184 last_uri: None,
185 session_warnings: Vec::new(),
186 session_warning_total: 0,
187 scanned: 0,
188 skip_until: 0,
189 }
190 }
191
192 pub fn with_credentials(mut self, credentials: Credentials) -> Self {
194 self.credentials = Some(credentials);
195 self
196 }
197
198 pub fn with_user_agent(mut self, ua: impl Into<String>) -> Self {
200 self.user_agent = ua.into();
201 self
202 }
203
204 pub fn state(&self) -> SessionState {
206 self.state
207 }
208
209 pub fn peek_next_cseq(&self) -> u32 {
215 self.next_cseq
216 }
217
218 fn record_session_warning(&mut self, w: String) {
219 self.session_warning_total += 1;
220 if self.session_warnings.len() >= MAX_SESSION_WARNINGS {
221 self.session_warnings.remove(0);
222 }
223 self.session_warnings.push(w);
224 }
225
226 pub fn session_warning_count(&self) -> usize {
229 self.session_warning_total
230 }
231
232 pub fn session_warnings(&self) -> &[String] {
236 &self.session_warnings
237 }
238
239 pub fn has_buffered_input(&self) -> bool {
243 !self.inbound.is_empty()
244 }
245
246 pub fn mark_activity(&mut self, now: Instant) {
251 self.last_activity = Some(now);
252 }
253
254 pub fn poll_timeout(&self) -> Option<Instant> {
259 self.session_id.as_ref()?;
260 let timeout =
261 Duration::from_secs(self.session_timeout.unwrap_or(DEFAULT_SESSION_TIMEOUT_SECS));
262 self.last_activity?
264 .checked_add((timeout / KEEPALIVE_FRACTION_DEN).max(MIN_KEEPALIVE_INTERVAL))
265 }
266
267 pub fn handle_timeout(&mut self, now: Instant) -> Result<Option<Vec<u8>>> {
271 match self.poll_timeout() {
272 Some(deadline) if now >= deadline => {}
273 _ => return Ok(None),
274 }
275 let Some(uri) = self.last_uri.clone() else {
276 return Ok(None);
277 };
278 let bytes = self.get_parameter(&uri, b"")?;
279 self.last_activity = Some(now);
280 Ok(Some(bytes))
281 }
282
283 pub fn session_id(&self) -> Option<&str> {
285 self.session_id.as_deref()
286 }
287
288 pub fn session_timeout(&self) -> Option<u64> {
290 self.session_timeout
291 }
292
293 pub fn negotiated_transport(&self) -> Option<&Transport> {
295 self.negotiated_transport.as_ref()
296 }
297
298 pub fn options(&mut self, uri: &str) -> Result<Vec<u8>> {
302 self.build_request(Method::Options, uri, None, &[])
303 }
304
305 pub fn describe(&mut self, uri: &str) -> Result<Vec<u8>> {
307 self.build_request(
308 Method::Describe,
309 uri,
310 None,
311 &[(headers::ACCEPT, "application/sdp".to_string())],
312 )
313 }
314
315 pub fn setup(&mut self, uri: &str, transport: &Transport) -> Result<Vec<u8>> {
317 self.build_request(
318 Method::Setup,
319 uri,
320 None,
321 &[(headers::TRANSPORT, transport.to_header_value()?)],
322 )
323 }
324
325 pub fn play(&mut self, uri: &str) -> Result<Vec<u8>> {
327 self.build_request(Method::Play, uri, None, &[])
328 }
329
330 pub fn pause(&mut self, uri: &str) -> Result<Vec<u8>> {
332 self.build_request(Method::Pause, uri, None, &[])
333 }
334
335 pub fn teardown(&mut self, uri: &str) -> Result<Vec<u8>> {
337 self.build_request(Method::Teardown, uri, None, &[])
338 }
339
340 pub fn announce(&mut self, uri: &str, sdp: &str) -> Result<Vec<u8>> {
342 self.build_request_with_body(
343 Method::Announce,
344 uri,
345 sdp.as_bytes(),
346 &[(headers::CONTENT_TYPE, "application/sdp".to_string())],
347 )
348 }
349
350 pub fn record(&mut self, uri: &str) -> Result<Vec<u8>> {
352 self.build_request(Method::Record, uri, None, &[])
353 }
354
355 pub fn get_parameter(&mut self, uri: &str, body: &[u8]) -> Result<Vec<u8>> {
358 self.build_request_with_body(Method::GetParameter, uri, body, &[])
359 }
360
361 fn build_request(
362 &mut self,
363 method: Method,
364 uri: &str,
365 _range: Option<&str>,
366 extra: &[(headers::HeaderName, String)],
367 ) -> Result<Vec<u8>> {
368 self.build_request_with_body(method, uri, &[], extra)
369 }
370
371 fn build_request_with_body(
372 &mut self,
373 method: Method,
374 uri: &str,
375 body: &[u8],
376 extra: &[(headers::HeaderName, String)],
377 ) -> Result<Vec<u8>> {
378 client_next_state(self.state, &method)?;
380
381 let cseq = self.next_cseq;
382 let request = self.assemble(method.clone(), uri, cseq, body, extra)?;
383 let bytes = serialize(&Message::from(request.clone()))?;
384 self.next_cseq += 1;
385 self.last_uri = Some(uri.to_string());
386 self.pending.insert(
387 cseq,
388 Pending {
389 method,
390 uri: uri.to_string(),
391 request,
392 auth_retried: false,
393 },
394 );
395 Ok(bytes)
396 }
397
398 fn assemble(
401 &mut self,
402 method: Method,
403 uri: &str,
404 cseq: u32,
405 body: &[u8],
406 extra: &[(headers::HeaderName, String)],
407 ) -> Result<Request<Body>> {
408 let url = rtsp_types::Url::parse(uri)
409 .map_err(|e| Error::TransportParse(format!("invalid request URI {uri:?}: {e}")))?;
410 let mut builder = Request::builder(method.clone(), Version::V1_0)
411 .request_uri(url)
412 .header(headers::CSEQ, cseq.to_string())
413 .header(headers::USER_AGENT, self.user_agent.clone());
414 if let Some(sid) = &self.session_id {
415 builder = builder.header(headers::SESSION, sid.clone());
416 }
417 for (name, value) in extra {
418 builder = builder.header(name.clone(), value.clone());
419 }
420 if let Some(auth) = &mut self.authenticator {
421 let ctx = RequestContext::new(<&str>::from(&method), uri);
422 let value = auth.authorization(&ctx)?;
423 builder = builder.header(headers::AUTHORIZATION, value);
424 }
425 let request = if body.is_empty() {
426 builder.build(Vec::new())
427 } else {
428 builder.build(body.to_vec())
429 };
430 Ok(request)
431 }
432
433 pub fn handle_data(&mut self, data: &[u8]) -> HandleOutcome {
448 let mut events = Vec::new();
449 let error = self.handle_data_into(data, &mut events).err();
450 HandleOutcome { events, error }
451 }
452
453 pub fn handle_data_strict(&mut self, data: &[u8]) -> Result<Vec<ClientEvent>> {
460 self.handle_data(data).into_result()
461 }
462
463 fn handle_data_into(&mut self, data: &[u8], events: &mut Vec<ClientEvent>) -> Result<()> {
464 self.inbound.extend_from_slice(data);
465 if self.inbound.len() > MAX_INBOUND_BYTES {
466 return Err(Error::MessageParse(format!(
467 "inbound buffer of {} bytes exceeds the {MAX_INBOUND_BYTES}-byte maximum \
468 (unterminated header or an unfulfilled Content-Length)",
469 self.inbound.len()
470 )));
471 }
472 let mut pos = 0;
477 let result = self.decode_from(&mut pos, events);
478 self.inbound.drain(..pos);
479 result
480 }
481
482 fn decode_from(&mut self, pos: &mut usize, events: &mut Vec<ClientEvent>) -> Result<()> {
485 loop {
486 let rest = &self.inbound[*pos..];
487 if rest.is_empty() {
488 break;
489 }
490 if rest[0] == MAGIC {
491 match interleaved::InterleavedFrame::parse(rest)? {
493 Some((frame, consumed)) => {
494 events.push(ClientEvent::MediaData {
495 channel: frame.channel,
496 data: frame.payload,
497 });
498 *pos += consumed;
499 }
500 None => break, }
502 continue;
503 }
504
505 if rest.len() < self.skip_until {
508 break;
509 }
510 if crate::framing::head_end(rest, self.scanned)?.is_none() {
511 self.scanned = rest.len();
512 break;
513 }
514 match Message::<Body>::parse(rest) {
515 Ok((message, consumed)) => {
516 *pos += consumed;
517 self.scanned = 0;
518 self.skip_until = 0;
519 self.process_message(message, events)?;
520 }
521 Err(rtsp_types::ParseError::Incomplete(needed)) => {
522 if let Some(more) = needed {
523 self.skip_until = rest.len().saturating_add(more.get());
524 }
525 break;
526 }
527 Err(rtsp_types::ParseError::Error) => {
528 return Err(Error::MessageParse("malformed RTSP message".into()));
529 }
530 }
531 }
532 Ok(())
533 }
534
535 fn process_message(
536 &mut self,
537 message: Message<Body>,
538 events: &mut Vec<ClientEvent>,
539 ) -> Result<()> {
540 match message {
541 Message::Response(response) => {
542 let Some(cseq) = header_value(response.header(&headers::CSEQ))
549 .and_then(|s| s.trim().parse::<u32>().ok())
550 else {
551 return Ok(());
552 };
553 let status = response.status();
554
555 if status == StatusCode::Unauthorized
557 && let Some(retry) = self.try_auth_retry(cseq, &response)?
558 {
559 events.push(retry);
560 return Ok(());
561 }
562
563 let Some(pending) = self.pending.remove(&cseq) else {
567 return Ok(());
568 };
569
570 if let Some(session_hdr) = header_value(response.header(&headers::SESSION)) {
574 match crate::session_header::SessionHeader::parse_with_warnings(session_hdr) {
575 Ok((parsed, warnings)) => {
576 for w in warnings {
577 log::warn!("RTSP Session header {session_hdr:?}: {w}");
578 self.record_session_warning(w);
579 }
580 self.session_id = Some(parsed.id);
581 if let Some(t) = parsed.timeout {
582 self.session_timeout = Some(t.as_secs());
583 }
584 }
585 Err(e) => {
586 let w = format!("unusable Session header {session_hdr:?}: {e}");
587 log::warn!("RTSP {w}");
588 self.record_session_warning(w);
589 }
590 }
591 }
592 if pending.method == Method::Setup
594 && let Some(t) = header_value(response.header(&headers::TRANSPORT))
595 {
596 self.negotiated_transport = Some(Transport::parse(t)?);
597 }
598
599 if status.is_success() {
601 self.state = client_next_state(self.state, &pending.method)?;
602 if pending.method == Method::Teardown {
604 self.session_id = None;
605 self.session_timeout = None;
606 self.authenticator = None;
607 }
608 } else if status.is_redirection() {
609 self.state = SessionState::Init;
610 }
611 events.push(ClientEvent::Response {
614 cseq,
615 method: pending.method,
616 status,
617 body: response.into_body(),
618 });
619 Ok(())
620 }
621 Message::Data(data) => {
622 events.push(ClientEvent::MediaData {
623 channel: data.channel_id(),
624 data: data.into_body(),
625 });
626 Ok(())
627 }
628 Message::Request(_) => {
629 Ok(())
632 }
633 }
634 }
635
636 fn try_auth_retry(
640 &mut self,
641 cseq: u32,
642 response: &rtsp_types::Response<Body>,
643 ) -> Result<Option<ClientEvent>> {
644 let creds = match &self.credentials {
645 Some(c) => c.clone(),
646 None => return Ok(None),
647 };
648 let (method, uri, already) = match self.pending.get(&cseq) {
650 Some(p) => (p.method.clone(), p.uri.clone(), p.auth_retried),
651 None => return Ok(None),
652 };
653
654 let challenge = header_value(response.header(&headers::WWW_AUTHENTICATE))
655 .ok_or_else(|| Error::Auth("401 without WWW-Authenticate".into()))?;
656 let stale = crate::headers_util::challenge_is_stale(challenge);
657
658 self.authenticator = Some(Authenticator::from_challenge(challenge, creds)?);
665 if already && !stale {
671 return Ok(None);
672 }
673
674 let (body, extra) = self.replay_extra(cseq);
680 self.pending.remove(&cseq);
681 let new_cseq = self.next_cseq;
682 let request = self.assemble(method.clone(), &uri, new_cseq, &body, &extra)?;
683 let bytes = serialize(&Message::from(request.clone()))?;
684 self.next_cseq += 1;
685 self.pending.insert(
686 new_cseq,
687 Pending {
688 method: method.clone(),
689 uri,
690 request,
691 auth_retried: true,
692 },
693 );
694 Ok(Some(ClientEvent::AuthRetry {
695 method,
696 cseq: new_cseq,
697 request: bytes,
698 }))
699 }
700
701 fn replay_extra(&self, old_cseq: u32) -> (Vec<u8>, Vec<(headers::HeaderName, String)>) {
707 let Some(p) = self.pending.get(&old_cseq) else {
708 return (Vec::new(), Vec::new());
709 };
710 let hop_by_hop = [
711 headers::CSEQ,
712 headers::SESSION,
713 headers::AUTHORIZATION,
714 headers::USER_AGENT,
715 headers::CONTENT_LENGTH,
716 ];
717 let extra = p
718 .request
719 .headers()
720 .filter(|(name, _)| !hop_by_hop.iter().any(|h| h == *name))
721 .map(|(name, value)| (name.clone(), value.as_str().to_string()))
722 .collect();
723 (p.request.body().clone(), extra)
724 }
725}
726
727fn header_value(h: Option<&headers::HeaderValue>) -> Option<&str> {
729 h.map(|v| v.as_str())
730}
731
732fn serialize(message: &Message<Body>) -> Result<Vec<u8>> {
734 let mut out = Vec::new();
735 message
736 .write(&mut out)
737 .map_err(|e| Error::MessageWrite(e.to_string()))?;
738 Ok(out)
739}
740
741#[cfg(test)]
742mod tests {
743 use super::*;
744
745 #[test]
746 fn play_in_init_bites() {
747 let mut c = ClientSession::new();
748 assert!(c.play("rtsp://h/s").is_err());
749 }
750
751 #[test]
752 fn setup_allowed_in_init() {
753 let mut c = ClientSession::new();
754 let t = Transport::single(crate::transport::TransportSpec::rtp_avp_tcp_interleaved(
755 0, 1,
756 ));
757 assert!(c.setup("rtsp://h/s", &t).is_ok());
758 }
759
760 #[test]
761 fn cseq_increments() {
762 let mut c = ClientSession::new();
763 let a = c.options("rtsp://h/s").unwrap();
764 let b = c.describe("rtsp://h/s").unwrap();
765 assert!(String::from_utf8_lossy(&a).contains("CSeq: 1"));
766 assert!(String::from_utf8_lossy(&b).contains("CSeq: 2"));
767 }
768
769 #[test]
770 fn announce_emits_request_with_sdp_content_type() {
771 let mut c = ClientSession::new();
772 let sdp = "v=0\r\no=- 0 0 IN IP4 0.0.0.0\r\ns=Test\r\n";
773 let bytes = c.announce("rtsp://h/s", sdp).unwrap();
774 let s = String::from_utf8_lossy(&bytes);
775 assert!(s.contains("ANNOUNCE rtsp://h/s"));
776 assert!(s.contains("Content-Type: application/sdp"));
777 assert!(s.contains(sdp));
778 }
779
780 #[test]
786 fn unknown_cseq_is_skipped_without_discarding_earlier_events() {
787 fn wire(s: &str) -> Vec<u8> {
788 s.replace('\n', "\r\n").into_bytes()
789 }
790 let mut c = ClientSession::new();
791 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");
798 batch.extend(wire("RTSP/1.0 200 OK\nCSeq: 99\n\n"));
799 batch.extend(wire("RTSP/1.0 200 OK\nCSeq: 2\n\n"));
800
801 let events = c
802 .handle_data_strict(&batch)
803 .expect("an unmatched CSeq must not fail the whole batch");
804 let cseqs: Vec<u32> = events
805 .into_iter()
806 .map(|e| match e {
807 ClientEvent::Response { cseq, .. } => cseq,
808 other => panic!("unexpected event: {other:?}"),
809 })
810 .collect();
811 assert_eq!(
812 cseqs,
813 vec![1, 2],
814 "expected both real responses despite the unmatched CSeq 99"
815 );
816 }
817
818 #[test]
822 fn many_interleaved_frames_in_one_read_decode_in_order_and_keep_the_tail() {
823 const FRAMES: usize = 600;
824 const PAYLOAD: usize = 800;
825 let mut wire = Vec::new();
826 for i in 0..FRAMES {
827 let payload = vec![(i % 251) as u8; PAYLOAD];
828 wire.extend(
829 interleaved::InterleavedFrame::slice_to_bytes((i % 2) as u8, &payload).unwrap(),
830 );
831 }
832 let split = wire.len() - 100; let mut c = ClientSession::new();
834 let first = c.handle_data(&wire[..split]);
835 assert!(first.error.is_none());
836 assert_eq!(first.events.len(), FRAMES - 1);
837 let second = c.handle_data(&wire[split..]);
838 assert!(second.error.is_none());
839 assert_eq!(second.events.len(), 1);
840 for (i, e) in first.events.iter().chain(second.events.iter()).enumerate() {
841 match e {
842 ClientEvent::MediaData { channel, data } => {
843 assert_eq!(*channel as usize, i % 2);
844 assert_eq!(data.len(), PAYLOAD);
845 assert!(data.iter().all(|b| *b == (i % 251) as u8));
846 }
847 other => panic!("unexpected {other:?}"),
848 }
849 }
850 assert!(!c.has_buffered_input());
851 }
852
853 #[test]
857 fn inbound_buffer_is_capped_against_an_unterminated_header() {
858 let mut c = ClientSession::new();
859 c.options("rtsp://h/s").unwrap();
860 let chunk = vec![b'A'; 64 * 1024];
861 let mut last = Ok(Vec::new());
862 for _ in 0..(MAX_INBOUND_BYTES / chunk.len() + 2) {
863 last = c.handle_data_strict(&chunk);
864 if last.is_err() {
865 break;
866 }
867 }
868 assert!(
869 last.is_err(),
870 "expected the oversized inbound buffer to be rejected, not grown forever"
871 );
872 }
873
874 #[test]
875 fn record_fails_from_init_state() {
876 let mut c = ClientSession::new();
877 assert!(c.record("rtsp://h/s").is_err());
878 }
879
880 #[test]
881 fn record_transitions_to_recording_state() {
882 fn wire(s: &str) -> Vec<u8> {
884 s.replace('\n', "\r\n").into_bytes()
885 }
886 let mut c = ClientSession::new();
887 let t = Transport::single(crate::transport::TransportSpec::rtp_avp_tcp_interleaved(
889 0, 1,
890 ));
891 c.setup("rtsp://h/s", &t).unwrap();
892 c.handle_data_strict(&wire(
893 "RTSP/1.0 200 OK\nCSeq: 1\nSession: 42\nTransport: RTP/AVP/TCP;interleaved=0-1\n\n",
894 ))
895 .unwrap();
896 assert_eq!(c.state(), SessionState::Ready);
897
898 c.record("rtsp://h/s").unwrap();
900 assert_eq!(c.state(), SessionState::Ready);
901 c.handle_data_strict(&wire("RTSP/1.0 200 OK\nCSeq: 2\nSession: 42\n\n"))
903 .unwrap();
904 assert_eq!(c.state(), SessionState::Recording);
905 }
906
907 #[test]
912 fn auth_retry_replays_original_body_and_content_type() {
913 fn wire(s: &str) -> Vec<u8> {
914 s.replace('\n', "\r\n").into_bytes()
915 }
916 let mut c = ClientSession::new().with_credentials(Credentials::new("admin", "12345"));
917 let sdp = "v=0\r\no=- 0 0 IN IP4 0.0.0.0\r\ns=Test\r\n";
918 let first = c.announce("rtsp://h/s", sdp).unwrap();
919 assert!(String::from_utf8_lossy(&first).contains("CSeq: 1"));
920
921 let events = c
922 .handle_data_strict(&wire(
923 "RTSP/1.0 401 Unauthorized\nCSeq: 1\nWWW-Authenticate: Digest realm=\"cam\",nonce=\"abc123\",qop=\"auth\"\n\n",
924 ))
925 .unwrap();
926 let retry = events
927 .into_iter()
928 .find_map(|e| match e {
929 ClientEvent::AuthRetry { request, .. } => Some(request),
930 _ => None,
931 })
932 .expect("expected an AuthRetry event");
933 let retry = String::from_utf8_lossy(&retry);
934
935 assert!(retry.contains("ANNOUNCE rtsp://h/s"), "{retry}");
936 assert!(retry.contains("Authorization: Digest "), "{retry}");
937 assert!(
938 retry.contains("Content-Type: application/sdp"),
939 "Content-Type dropped on auth retry: {retry}"
940 );
941 assert!(
942 retry.contains(sdp),
943 "SDP body dropped on auth retry: {retry}"
944 );
945 let expected_len = format!("Content-Length: {}", sdp.len());
946 assert!(
947 retry.contains(&expected_len),
948 "expected {expected_len:?} in: {retry}"
949 );
950 }
951
952 #[test]
961 fn nonce_rotation_without_stale_is_picked_up_on_the_first_retry() {
962 fn wire(s: &str) -> Vec<u8> {
963 s.replace('\n', "\r\n").into_bytes()
964 }
965 let mut c = ClientSession::new().with_credentials(Credentials::new("admin", "12345"));
966
967 c.options("rtsp://h/s").unwrap(); let events = c
970 .handle_data_strict(&wire(
971 "RTSP/1.0 401 Unauthorized\nCSeq: 1\nWWW-Authenticate: Digest realm=\"cam\",nonce=\"nonce-1\",qop=\"auth\"\n\n",
972 ))
973 .unwrap();
974 assert!(
975 events
976 .iter()
977 .any(|e| matches!(e, ClientEvent::AuthRetry { cseq: 2, .. })),
978 "{events:?}"
979 );
980 c.handle_data_strict(&wire("RTSP/1.0 200 OK\nCSeq: 2\n\n"))
981 .unwrap();
982
983 c.options("rtsp://h/s").unwrap(); let events = c
987 .handle_data_strict(&wire(
988 "RTSP/1.0 401 Unauthorized\nCSeq: 3\nWWW-Authenticate: Digest realm=\"cam\",nonce=\"nonce-2\",qop=\"auth\"\n\n",
989 ))
990 .unwrap();
991 let retry = events
992 .into_iter()
993 .find_map(|e| match e {
994 ClientEvent::AuthRetry {
995 cseq: 4, request, ..
996 } => Some(request),
997 _ => None,
998 })
999 .expect("expected a retry (CSeq 4), not a give-up");
1000 let retry = String::from_utf8_lossy(&retry);
1001 assert!(
1002 retry.contains("nonce=\"nonce-2\""),
1003 "retry signed with the stale nonce instead of the rotated one: {retry}"
1004 );
1005 }
1006
1007 fn established(timeout: Option<u64>) -> ClientSession {
1008 let mut c = ClientSession::new();
1009 let _ = c
1010 .setup(
1011 "rtsp://h/s",
1012 &Transport::single(crate::transport::TransportSpec::rtp_avp_tcp_interleaved(
1013 0, 1,
1014 )),
1015 )
1016 .unwrap();
1017 let sess = match timeout {
1018 Some(t) => format!("abc;timeout={t}"),
1019 None => "abc".into(),
1020 };
1021 let resp = format!(
1022 "RTSP/1.0 200 OK\r\nCSeq: 1\r\nSession: {sess}\r\nTransport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n\r\n"
1023 );
1024 c.handle_data_strict(resp.as_bytes()).unwrap();
1025 c
1026 }
1027
1028 #[test]
1029 fn the_keepalive_interval_is_floored_and_fires_at_most_once_per_interval() {
1030 let t0 = Instant::now();
1032 let mut c = established(Some(1));
1033 c.mark_activity(t0);
1034 assert_eq!(c.poll_timeout(), Some(t0 + MIN_KEEPALIVE_INTERVAL));
1035 assert!(
1036 c.handle_timeout(t0 + Duration::from_millis(500))
1037 .unwrap()
1038 .is_none()
1039 );
1040 let mut sent = 0;
1042 let tick = t0 + MIN_KEEPALIVE_INTERVAL;
1043 for _ in 0..1000 {
1044 if c.handle_timeout(tick).unwrap().is_some() {
1045 sent += 1;
1046 }
1047 }
1048 assert_eq!(sent, 1, "keepalive must not busy-loop");
1049 }
1050
1051 #[test]
1052 fn a_zero_timeout_is_a_warning_and_the_default_not_a_busy_loop() {
1053 let t0 = Instant::now();
1054 let mut c = ClientSession::new();
1055 let _ = c
1056 .setup(
1057 "rtsp://h/s",
1058 &Transport::single(crate::transport::TransportSpec::rtp_avp_tcp_interleaved(
1059 0, 1,
1060 )),
1061 )
1062 .unwrap();
1063 c.handle_data_strict(
1064 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",
1065 )
1066 .unwrap();
1067 assert_eq!(c.session_warnings().len(), 1, "{:?}", c.session_warnings());
1068 assert!(c.session_warnings()[0].contains("timeout"));
1069 c.mark_activity(t0);
1070 assert_eq!(c.poll_timeout(), Some(t0 + Duration::from_secs(30)));
1071 }
1072
1073 #[test]
1076 fn odd_received_session_ids_are_echoed_verbatim() {
1077 for (hdr, id) in [
1078 ("\"weird\"", "\"weird\""),
1079 ("a b;timeout=30", "a b"),
1080 ("abc,def", "abc,def"),
1081 ("ab\"c", "ab\"c"),
1082 ] {
1083 let mut c = ClientSession::new();
1084 let _ = c
1085 .setup(
1086 "rtsp://h/s",
1087 &Transport::single(crate::transport::TransportSpec::rtp_avp_tcp_interleaved(
1088 0, 1,
1089 )),
1090 )
1091 .unwrap();
1092 let resp = format!(
1093 "RTSP/1.0 200 OK\r\nCSeq: 1\r\nSession: {hdr}\r\nTransport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n\r\n"
1094 );
1095 c.handle_data_strict(resp.as_bytes()).unwrap();
1096 assert_eq!(c.session_id(), Some(id), "{hdr}");
1097 let play = String::from_utf8(c.play("rtsp://h/s").unwrap()).unwrap();
1098 assert!(
1099 play.contains(&format!("Session: {id}\r\n")),
1100 "{hdr}: {play}"
1101 );
1102 }
1103 }
1104
1105 #[test]
1106 fn session_warnings_are_capped_but_counted() {
1107 let mut c = ClientSession::new();
1108 for i in 1..=(MAX_SESSION_WARNINGS as u32 + 10) {
1109 let _ = c.options("rtsp://h/s").unwrap();
1110 c.handle_data_strict(
1111 format!("RTSP/1.0 200 OK\r\nCSeq: {i}\r\nSession: abc;timeout=x\r\n\r\n")
1112 .as_bytes(),
1113 )
1114 .unwrap();
1115 }
1116 assert_eq!(c.session_warnings().len(), MAX_SESSION_WARNINGS);
1117 assert_eq!(c.session_warning_count(), MAX_SESSION_WARNINGS + 10);
1118 }
1119
1120 #[test]
1121 fn an_unusable_session_header_is_observable_not_silent() {
1122 let mut c = ClientSession::new();
1123 let _ = c.options("rtsp://h/s").unwrap();
1124 c.handle_data_strict(b"RTSP/1.0 200 OK\r\nCSeq: 1\r\nSession: ;timeout=5\r\n\r\n")
1125 .unwrap();
1126 assert!(c.session_id().is_none());
1127 assert_eq!(c.session_warnings().len(), 1);
1128 }
1129
1130 #[test]
1131 fn keepalive_is_due_at_half_the_session_timeout() {
1132 let t0 = Instant::now();
1133 let mut c = established(Some(20));
1134 assert_eq!(c.poll_timeout(), None, "no activity recorded yet");
1135 c.mark_activity(t0);
1136 assert_eq!(c.poll_timeout(), Some(t0 + Duration::from_secs(10)));
1137 assert!(
1138 c.handle_timeout(t0 + Duration::from_secs(9))
1139 .unwrap()
1140 .is_none()
1141 );
1142 let req = c
1143 .handle_timeout(t0 + Duration::from_secs(10))
1144 .unwrap()
1145 .expect("keepalive due");
1146 let text = String::from_utf8(req).unwrap();
1147 assert!(
1148 text.starts_with("GET_PARAMETER rtsp://h/s RTSP/1.0"),
1149 "{text}"
1150 );
1151 assert!(text.contains("Session: abc"), "{text}");
1152 assert_eq!(
1154 c.poll_timeout(),
1155 Some(t0 + Duration::from_secs(10) + Duration::from_secs(10))
1156 );
1157 }
1158
1159 #[test]
1160 fn keepalive_uses_the_rfc_default_timeout_when_none_was_declared() {
1161 let t0 = Instant::now();
1162 let mut c = established(None);
1163 c.mark_activity(t0);
1164 assert_eq!(c.poll_timeout(), Some(t0 + Duration::from_secs(30))); }
1166
1167 #[test]
1168 fn no_keepalive_before_setup_or_after_teardown() {
1169 let t0 = Instant::now();
1170 let mut c = ClientSession::new();
1171 c.mark_activity(t0);
1172 assert_eq!(c.poll_timeout(), None);
1173 assert!(
1174 c.handle_timeout(t0 + Duration::from_secs(3600))
1175 .unwrap()
1176 .is_none()
1177 );
1178 }
1179
1180 #[test]
1181 fn head_cap_holds_for_a_70_kib_run_of_a_and_for_9000_header_lines() {
1182 let mut c = ClientSession::new();
1183 assert!(matches!(
1184 c.handle_data_strict(&vec![b'A'; 70 * 1024]),
1185 Err(Error::MessageParse(_))
1186 ));
1187 let mut c = ClientSession::new();
1188 let mut junk = b"RTSP/1.0 200 OK\r\n".to_vec();
1189 junk.extend(b"X-A: b\r\n".repeat(9000));
1190 assert!(matches!(
1191 c.handle_data_strict(&junk),
1192 Err(Error::MessageParse(_))
1193 ));
1194 }
1195
1196 #[test]
1197 fn a_response_fed_one_byte_at_a_time_is_still_decoded() {
1198 let mut c = ClientSession::new();
1199 let _ = c.options("rtsp://h/s").unwrap();
1200 let resp = b"RTSP/1.0 200 OK\r\nCSeq: 1\r\nPublic: OPTIONS\r\n\r\n";
1201 let mut got = 0;
1202 for b in resp {
1203 got += c.handle_data_strict(&[*b]).unwrap().len();
1204 }
1205 assert_eq!(got, 1);
1206 }
1207
1208 #[test]
1209 fn an_unterminated_header_over_the_head_cap_is_rejected() {
1210 let mut c = ClientSession::new();
1211 let mut junk = b"RTSP/1.0 200 OK\r\nX-Pad: ".to_vec();
1212 junk.extend(std::iter::repeat_n(b'a', 70 * 1024)); let err = c
1214 .handle_data_strict(&junk)
1215 .expect_err("header phase is capped at 64 KiB");
1216 assert!(matches!(err, Error::MessageParse(_)), "{err:?}");
1217 }
1218
1219 #[test]
1220 fn stale_quoted_rechallenge_refreshes_nonce_after_a_prior_retry() {
1221 fn wire(s: &str) -> Vec<u8> {
1222 s.replace('\n', "\r\n").into_bytes()
1223 }
1224 let mut c = ClientSession::new().with_credentials(Credentials::new("admin", "12345"));
1225 c.options("rtsp://h/s").unwrap(); c.handle_data_strict(&wire(
1229 "RTSP/1.0 401 Unauthorized\nCSeq: 1\nWWW-Authenticate: Digest realm=\"cam\",nonce=\"nonce-1\",qop=\"auth\"\n\n",
1230 )).unwrap();
1231
1232 let events = c
1235 .handle_data_strict(&wire(
1236 "RTSP/1.0 401 Unauthorized\nCSeq: 2\nWWW-Authenticate: Digest realm=\"cam\",nonce=\"nonce-2\",qop=\"auth\",stale=\"True\"\n\n",
1237 ))
1238 .unwrap();
1239 let retry = events
1240 .into_iter()
1241 .find_map(|e| match e {
1242 ClientEvent::AuthRetry {
1243 cseq: 3, request, ..
1244 } => Some(request),
1245 _ => None,
1246 })
1247 .expect("a quoted stale=true re-challenge must trigger a retry");
1248 let retry = String::from_utf8_lossy(&retry);
1249 assert!(
1250 retry.contains("nonce=\"nonce-2\""),
1251 "quoted stale=\"True\" was not detected: {retry}"
1252 );
1253 }
1254
1255 #[test]
1259 fn client_session_debug_does_not_leak_embedded_credentials_secret() {
1260 let c = ClientSession::new()
1261 .with_credentials(Credentials::new("admin", "extremely-secret-password"));
1262 let debug = format!("{c:?}");
1263 assert!(
1264 !debug.contains("extremely-secret-password"),
1265 "leaked via ClientSession Debug: {debug}"
1266 );
1267 assert!(debug.contains("***"), "expected redaction marker: {debug}");
1268 }
1269}