use rtsp_types as rtsp; use std::collections::HashMap; use super::types::RtspRequest; pub(crate) const OPTIONS_PUBLIC_CAPABILITIES: &str = "OPTIONS, DESCRIBE, SETUP, PLAY, GET_PARAMETER, SET_PARAMETER, TEARDOWN"; pub(crate) fn strip_interleaved_frames_prefix(buffer: &mut Vec) -> bool { if buffer.len() < 4 || buffer[0] != b'$' { return false; } let payload_len = u16::from_be_bytes([buffer[2], buffer[3]]) as usize; let frame_len = 4 + payload_len; if buffer.len() < frame_len { return false; } buffer.drain(0..frame_len); true } pub(crate) fn take_rtsp_request_from_buffer(buffer: &mut Vec) -> Option { let delimiter = b"\r\n\r\n"; let pos = find_bytes(buffer, delimiter)?; let req_end = pos + delimiter.len(); let req_bytes: Vec = buffer.drain(0..req_end).collect(); Some(String::from_utf8_lossy(&req_bytes).to_string()) } fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option { haystack .windows(needle.len()) .position(|window| window == needle) } pub(crate) fn parse_rtsp_request(raw: &str) -> Option { let (message, consumed): (rtsp::Message>, usize) = rtsp::Message::parse(raw.as_bytes()).ok()?; if consumed != raw.len() { return None; } let request = match message { rtsp::Message::Request(req) => req, _ => return None, }; let uri = request .request_uri() .map(|value| value.as_str().to_string()) .unwrap_or_default(); let mut headers = HashMap::new(); for (name, value) in request.headers() { headers.insert(name.to_string().to_ascii_lowercase(), value.to_string()); } Some(RtspRequest { method: request.method().clone(), uri, version: request.version(), headers, }) } pub(crate) fn parse_interleaved_channel(transport: &str) -> Option { let lower = transport.to_ascii_lowercase(); if let Some((_, v)) = lower.split_once("interleaved=") { let head = v.split(';').next().unwrap_or(v); let first = head.split('-').next().unwrap_or(head).trim(); return first.parse::().ok(); } None } pub(crate) fn is_tcp_transport_request(transport: &str) -> bool { transport .split(',') .map(str::trim) .map(str::to_ascii_lowercase) .any(|item| item.contains("rtp/avp/tcp") || item.contains("interleaved=")) } pub(crate) fn is_valid_rtsp_path(method: &rtsp::Method, uri: &str, configured_path: &str) -> bool { if matches!(method, rtsp::Method::Options) && uri.trim() == "*" { return true; } let normalized_cfg = configured_path.trim_matches('/'); if normalized_cfg.is_empty() { return false; } let request_path = extract_rtsp_path(uri); if request_path == normalized_cfg { return true; } if !matches!(method, rtsp::Method::Setup | rtsp::Method::Teardown) { return false; } let control_track_path = format!("{}/trackID=0", normalized_cfg); request_path == "trackID=0" || request_path == control_track_path } fn extract_rtsp_path(uri: &str) -> String { let raw_path = if let Some((_, remainder)) = uri.split_once("://") { match remainder.find('/') { Some(idx) => &remainder[idx..], None => "/", } } else { uri }; raw_path .split('?') .next() .unwrap_or(raw_path) .split('#') .next() .unwrap_or(raw_path) .trim_matches('/') .to_string() } #[cfg(test)] mod tests { use super::*; #[test] fn rtsp_path_matching_follows_sdp_control_rules() { assert!(is_valid_rtsp_path( &rtsp::Method::Describe, "rtsp://127.0.0.1/live", "live" )); assert!(is_valid_rtsp_path( &rtsp::Method::Describe, "rtsp://127.0.0.1/live/?token=1", "/live/" )); assert!(!is_valid_rtsp_path( &rtsp::Method::Describe, "rtsp://127.0.0.1/live2", "live" )); assert!(!is_valid_rtsp_path( &rtsp::Method::Describe, "rtsp://127.0.0.1/", "/" )); assert!(is_valid_rtsp_path( &rtsp::Method::Setup, "rtsp://127.0.0.1/live/trackID=0", "live" )); assert!(is_valid_rtsp_path( &rtsp::Method::Setup, "rtsp://127.0.0.1/trackID=0", "live" )); assert!(!is_valid_rtsp_path( &rtsp::Method::Describe, "rtsp://127.0.0.1/live/trackID=0", "live" )); assert!(is_valid_rtsp_path(&rtsp::Method::Options, "*", "live")); } #[test] fn transport_parsing_detects_tcp_interleaved_requests() { assert!(is_tcp_transport_request( "RTP/AVP/TCP;unicast;interleaved=0-1" )); assert!(is_tcp_transport_request("RTP/AVP;unicast;interleaved=2-3")); assert!(!is_tcp_transport_request( "RTP/AVP;unicast;client_port=8000-8001" )); } #[test] fn options_public_includes_standard_methods() { assert!(OPTIONS_PUBLIC_CAPABILITIES.contains("GET_PARAMETER")); assert!(OPTIONS_PUBLIC_CAPABILITIES.contains("TEARDOWN")); } }