#![cfg(feature = "tokio")]
use rtsp_runtime::client::ClientEvent;
use rtsp_runtime::server::ServerEvent;
use rtsp_runtime::{
AsyncRtspClient, AsyncRtspServer, ClientSession, Credentials, SessionState, StatusCode,
Transport, TransportSpec,
};
use tokio::net::{TcpListener, TcpStream};
const URI: &str = "rtsp://127.0.0.1/stream";
fn tcp_interleaved() -> Transport {
Transport::single(TransportSpec::rtp_avp_tcp_interleaved(0, 1))
}
#[tokio::test]
async fn plain_tcp_full_session() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (sock, _) = listener.accept().await.unwrap();
let mut srv = AsyncRtspServer::accept(sock);
let mut states = Vec::new();
for _ in 0..5 {
let events = srv.next_request().await.unwrap().expect("request");
states.push(srv.state());
assert!(
events
.iter()
.any(|e| matches!(e, ServerEvent::RequestAccepted { .. }))
);
}
states
});
let mut client = AsyncRtspClient::connect(addr).await.unwrap();
let ev = client.options(URI).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
let ev = client.describe(URI).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
let ev = client.setup(URI, &tcp_interleaved()).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
assert_eq!(client.state(), SessionState::Ready);
assert!(client.session_id().is_some());
let ev = client.play(URI).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
assert_eq!(client.state(), SessionState::Playing);
let ev = client.teardown(URI).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
assert_eq!(client.state(), SessionState::Init);
let server_states = server.await.unwrap();
assert_eq!(
server_states,
vec![
SessionState::Init, SessionState::Init, SessionState::Ready, SessionState::Playing, SessionState::Init, ]
);
}
#[tokio::test]
async fn interleaved_media_over_tcp() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let payload1: Vec<u8> = (0u8..32).collect();
let payload2: Vec<u8> = (100u8..140).collect();
let p1 = payload1.clone();
let p2 = payload2.clone();
let (done_tx, done_rx) = tokio::sync::oneshot::channel::<()>();
let server = tokio::spawn(async move {
let (sock, _) = listener.accept().await.unwrap();
let mut srv = AsyncRtspServer::accept(sock);
srv.next_request().await.unwrap().expect("SETUP");
srv.next_request().await.unwrap().expect("PLAY");
srv.send_interleaved(0, &p1).await.unwrap();
srv.send_interleaved(0, &p2).await.unwrap();
srv.stream_mut().flush().await.unwrap();
let _ = done_rx.await;
});
let mut client = AsyncRtspClient::connect(addr).await.unwrap();
client.setup(URI, &tcp_interleaved()).await.unwrap();
client.play(URI).await.unwrap();
let f1 = client.recv_interleaved().await.unwrap().expect("frame 1");
assert!(matches!(f1, ClientEvent::MediaData { channel: 0, ref data } if *data == payload1));
let f2 = client.recv_interleaved().await.unwrap().expect("frame 2");
assert!(matches!(f2, ClientEvent::MediaData { channel: 0, ref data } if *data == payload2));
let _ = done_tx.send(());
server.await.unwrap();
}
#[tokio::test]
async fn digest_auth_over_loopback() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut sock, _) = listener.accept().await.unwrap();
let req1 = read_one_request(&mut sock).await;
assert!(req1.contains("DESCRIBE"));
assert!(
!req1.contains("Authorization:"),
"first request must be unauthenticated"
);
let cseq1 = cseq_of(&req1);
let challenge = "Digest realm=\"IP Camera\", \
nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", qop=\"auth\", algorithm=MD5";
let resp401 = format!(
"RTSP/1.0 401 Unauthorized\r\nCSeq: {cseq1}\r\n\
WWW-Authenticate: {challenge}\r\n\r\n"
);
sock.write_all(resp401.as_bytes()).await.unwrap();
sock.flush().await.unwrap();
let req2 = read_one_request(&mut sock).await;
assert!(req2.contains("DESCRIBE"));
assert!(
req2.contains("Authorization: Digest "),
"retry must carry a Digest Authorization: {req2}"
);
assert!(req2.contains("response="));
assert!(req2.contains("nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\""));
let cseq2 = cseq_of(&req2);
let ok = format!("RTSP/1.0 200 OK\r\nCSeq: {cseq2}\r\n\r\n");
sock.write_all(ok.as_bytes()).await.unwrap();
sock.flush().await.unwrap();
cseq2
});
let session = ClientSession::new().with_credentials(Credentials::new("admin", "12345"));
let mut client = AsyncRtspClient::connect_with(addr, session).await.unwrap();
let ev = client.describe(URI).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
let cseq2 = server.await.unwrap();
assert!(cseq2 > 1, "retry used a fresh CSeq");
}
#[cfg(feature = "tls")]
#[tokio::test]
async fn tls_full_session_over_loopback() {
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
let cert_der = include_bytes!("fixtures/localhost-cert.der").to_vec();
let key_der = include_bytes!("fixtures/localhost-key.der").to_vec();
let server_config = {
let certs = vec![CertificateDer::from(cert_der.clone())];
let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_der));
rustls::ServerConfig::builder_with_provider(std::sync::Arc::new(
rustls::crypto::aws_lc_rs::default_provider(),
))
.with_safe_default_protocol_versions()
.expect("aws-lc-rs provider supports the safe default protocol versions")
.with_no_client_auth()
.with_single_cert(certs, key)
.expect("server config")
};
let client_config = {
let mut roots = rustls::RootCertStore::empty();
roots
.add(CertificateDer::from(cert_der))
.expect("add self-signed root");
rustls::ClientConfig::builder_with_provider(std::sync::Arc::new(
rustls::crypto::aws_lc_rs::default_provider(),
))
.with_safe_default_protocol_versions()
.expect("aws-lc-rs provider supports the safe default protocol versions")
.with_root_certificates(roots)
.with_no_client_auth()
};
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (tcp, _) = listener.accept().await.unwrap();
let mut srv = AsyncRtspServer::accept_tls(tcp, server_config)
.await
.expect("TLS handshake (server)");
let mut states = Vec::new();
for _ in 0..4 {
srv.next_request().await.unwrap().expect("request");
states.push(srv.state());
}
states
});
let mut client = AsyncRtspClient::connect_tls(addr, "localhost", client_config)
.await
.expect("TLS handshake (client)");
let ev = client.options(URI).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
let ev = client.setup(URI, &tcp_interleaved()).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
assert_eq!(client.state(), SessionState::Ready);
let ev = client.play(URI).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
assert_eq!(client.state(), SessionState::Playing);
let ev = client.teardown(URI).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
let states = server.await.unwrap();
assert_eq!(
states,
vec![
SessionState::Init, SessionState::Ready, SessionState::Playing, SessionState::Init, ]
);
}
#[tokio::test]
async fn stray_response_for_an_abandoned_request_does_not_win() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut sock, _) = listener.accept().await.unwrap();
let mut buf = Vec::new();
let mut chunk = [0u8; 4096];
loop {
if buf.windows(4).filter(|w| *w == b"\r\n\r\n").count() >= 2 {
break;
}
let n = sock.read(&mut chunk).await.unwrap();
assert!(n > 0, "peer closed before both requests arrived");
buf.extend_from_slice(&chunk[..n]);
}
let mut out = Vec::new();
out.extend_from_slice(b"RTSP/1.0 200 OK\r\nCSeq: 2\r\nContent-Length: 0\r\n\r\n");
out.extend_from_slice(b"RTSP/1.0 200 OK\r\nCSeq: 1\r\nContent-Length: 0\r\n\r\n");
sock.write_all(&out).await.unwrap();
});
let mut client = AsyncRtspClient::connect(addr).await.unwrap();
let _ = tokio::time::timeout(std::time::Duration::from_millis(50), client.options(URI)).await;
let ev = client.describe(URI).await.unwrap();
match ev {
ClientEvent::Response { cseq, .. } => assert_eq!(
cseq, 2,
"returned the stray CSeq-1 response instead of the awaited CSeq-2 one"
),
other => panic!("unexpected event: {other:?}"),
}
server.await.unwrap();
}
#[tokio::test]
async fn server_receives_interleaved_frame_during_play() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (sock, _) = listener.accept().await.unwrap();
let mut srv = AsyncRtspServer::accept(sock);
let setup_events = srv.next_request().await.unwrap().expect("SETUP");
assert!(
setup_events
.iter()
.any(|e| matches!(e, ServerEvent::SessionSetup { .. }))
);
let play_events = srv.next_request().await.unwrap().expect("PLAY");
assert!(
play_events
.iter()
.any(|e| matches!(e, ServerEvent::RequestAccepted { .. }))
);
srv.next_request()
.await
.unwrap()
.expect("interleaved frame")
});
let mut client = TcpStream::connect(addr).await.unwrap();
client
.write_all(
b"SETUP rtsp://127.0.0.1/stream RTSP/1.0\r\n\
CSeq: 1\r\n\
Transport: RTP/AVP/TCP;interleaved=0-1\r\n\r\n",
)
.await
.unwrap();
let mut buf = [0u8; 4096];
let n = client.read(&mut buf).await.unwrap();
let resp = String::from_utf8_lossy(&buf[..n]).to_string();
assert!(resp.contains("200"), "SETUP failed: {resp}");
let sid = resp
.lines()
.find_map(|l| l.strip_prefix("Session:"))
.expect("Session header")
.split(';')
.next()
.unwrap()
.trim()
.to_string();
client
.write_all(
format!("PLAY rtsp://127.0.0.1/stream RTSP/1.0\r\nCSeq: 2\r\nSession: {sid}\r\n\r\n")
.as_bytes(),
)
.await
.unwrap();
let n = client.read(&mut buf).await.unwrap();
assert!(String::from_utf8_lossy(&buf[..n]).contains("200"));
let rtcp_payload = vec![0xAB_u8; 24];
let mut frame = vec![0x24u8, 1];
frame.extend_from_slice(&(rtcp_payload.len() as u16).to_be_bytes());
frame.extend_from_slice(&rtcp_payload);
client.write_all(&frame).await.unwrap();
let events = server.await.unwrap();
assert!(
events.iter().any(
|e| matches!(e, ServerEvent::MediaData { channel: 1, data } if *data == rtcp_payload)
),
"expected a MediaData event for the interleaved RTCP frame: {events:?}"
);
}
#[tokio::test]
async fn server_read_buffer_is_capped_against_an_unterminated_header() {
use tokio::io::AsyncWriteExt;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (sock, _) = listener.accept().await.unwrap();
let mut srv = AsyncRtspServer::accept(sock);
srv.next_request().await
});
let client = TcpStream::connect(addr).await.unwrap();
let mut client = client;
let chunk = vec![b'A'; 64 * 1024];
for _ in 0..40 {
if client.write_all(&chunk).await.is_err() {
break;
}
}
let result = tokio::time::timeout(std::time::Duration::from_secs(2), server)
.await
.expect("server task must reject the oversized buffer instead of blocking forever")
.unwrap();
assert!(
result.is_err(),
"expected the oversized request buffer to be rejected, not grown forever"
);
drop(client);
}
#[tokio::test]
async fn pending_media_queue_is_capped_while_exchange_waits() {
const OVERFLOW: usize = 1024 + 50;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (sock, _) = listener.accept().await.unwrap();
let mut srv = AsyncRtspServer::accept(sock);
srv.next_request().await.unwrap().expect("SETUP");
srv.next_request().await.unwrap().expect("PLAY");
for i in 0..OVERFLOW {
let payload = (i as u32).to_be_bytes().to_vec();
srv.send_interleaved(0, &payload).await.unwrap();
}
srv.next_request().await.unwrap().expect("GET_PARAMETER");
});
let mut client = AsyncRtspClient::connect(addr).await.unwrap();
client.setup(URI, &tcp_interleaved()).await.unwrap();
client.play(URI).await.unwrap();
client.get_parameter(URI, &[]).await.unwrap();
let mut indices = Vec::new();
while indices.len() < 1024 {
match client.recv_interleaved().await.unwrap() {
Some(ClientEvent::MediaData { data, .. }) => {
indices.push(u32::from_be_bytes(data.try_into().unwrap()));
}
Some(other) => panic!("unexpected event: {other:?}"),
None => break,
}
}
assert_eq!(
indices.len(),
1024,
"expected exactly the capped number of frames, got {}",
indices.len()
);
assert_eq!(
indices[0],
(OVERFLOW - 1024) as u32,
"expected the oldest frames to have been dropped, not the newest"
);
assert_eq!(*indices.last().unwrap(), (OVERFLOW - 1) as u32);
server.await.unwrap();
}
use tokio::io::{AsyncReadExt, AsyncWriteExt};
async fn read_one_request(sock: &mut TcpStream) -> String {
let mut buf = Vec::new();
let mut chunk = [0u8; 1024];
loop {
if let Some(pos) = find_header_end(&buf) {
return String::from_utf8_lossy(&buf[..pos]).to_string();
}
let n = sock.read(&mut chunk).await.unwrap();
assert!(n > 0, "peer closed before a full request");
buf.extend_from_slice(&chunk[..n]);
}
}
fn find_header_end(buf: &[u8]) -> Option<usize> {
buf.windows(4).position(|w| w == b"\r\n\r\n").map(|p| p + 4)
}
fn cseq_of(request: &str) -> u32 {
for line in request.lines() {
if let Some(rest) = line.strip_prefix("CSeq:") {
return rest.trim().parse().unwrap();
}
}
panic!("no CSeq in request: {request}");
}