use std::io::{self, Result};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use async_trait::async_trait;
use rustls::pki_types::ServerName;
use rustls::ClientConfig;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio_rustls::TlsConnector;
use crate::pinger::Pinger;
use crate::tls::default_client_config;
use crate::uri::{get_uri, URI};
use crate::util::with_timeout;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
const DEFAULT_PORT_PLAIN: u16 = 1935;
const DEFAULT_PORT_TLS: u16 = 443;
const RTMP_VERSION: u8 = 3;
const HANDSHAKE_PAYLOAD_LEN: usize = 1536;
pub struct RtmpPinger {
pub target: String,
pub timeout: Duration,
tls_config: Option<Arc<ClientConfig>>,
}
impl RtmpPinger {
pub fn new(target: impl Into<String>) -> Self {
Self {
target: target.into(),
timeout: DEFAULT_TIMEOUT,
tls_config: None,
}
}
pub fn with_timeout(mut self, t: Duration) -> Self {
self.timeout = t;
self
}
pub fn with_tls_config(mut self, config: Arc<ClientConfig>) -> Self {
self.tls_config = Some(config);
self
}
async fn ping_plain(&self, uri: &URI) -> Result<()> {
let endpoint = endpoint_for(uri, DEFAULT_PORT_PLAIN)?;
with_timeout(self.timeout, async move {
let mut stream = TcpStream::connect(&endpoint).await?;
run_handshake(&mut stream).await
})
.await
}
async fn ping_tls(&self, uri: &URI) -> Result<()> {
let endpoint = endpoint_for(uri, DEFAULT_PORT_TLS)?;
let server_name = ServerName::try_from(uri.domain.clone())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
let config = self
.tls_config
.clone()
.unwrap_or_else(default_client_config);
with_timeout(self.timeout, async move {
let tcp = TcpStream::connect(&endpoint).await?;
let connector = TlsConnector::from(config);
let mut stream = connector.connect(server_name, tcp).await?;
run_handshake(&mut stream).await
})
.await
}
}
#[async_trait]
impl Pinger for RtmpPinger {
async fn ping(&self) -> Result<()> {
let uri = get_uri(&self.target);
if uri.domain.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"RTMP target is missing a host",
));
}
let scheme = uri.scheme.to_ascii_lowercase();
match scheme.as_str() {
"" | "rtmp" => self.ping_plain(&uri).await,
"rtmps" => self.ping_tls(&uri).await,
other => Err(io::Error::other(format!(
"scheme '{other}' is not supported by RtmpPinger (use rtmp:// or rtmps://)"
))),
}
}
}
fn endpoint_for(uri: &URI, default_port: u16) -> Result<String> {
let port = if uri.port > 0 {
uri.port as u16
} else {
default_port
};
Ok(format!("{}:{}", uri.domain, port))
}
async fn run_handshake<S>(stream: &mut S) -> Result<()>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let c1 = build_c1();
let mut c0_c1 = Vec::with_capacity(1 + HANDSHAKE_PAYLOAD_LEN);
c0_c1.push(RTMP_VERSION);
c0_c1.extend_from_slice(&c1);
stream.write_all(&c0_c1).await?;
let mut s0 = [0u8; 1];
stream.read_exact(&mut s0).await?;
if s0[0] != RTMP_VERSION {
return Err(io::Error::other(format!(
"RTMP S0 returned version {} (expected {RTMP_VERSION})",
s0[0]
)));
}
let mut s1 = [0u8; HANDSHAKE_PAYLOAD_LEN];
stream.read_exact(&mut s1).await?;
let mut s2 = [0u8; HANDSHAKE_PAYLOAD_LEN];
stream.read_exact(&mut s2).await?;
stream.write_all(&s1).await?;
Ok(())
}
fn build_c1() -> [u8; HANDSHAKE_PAYLOAD_LEN] {
let mut buf = [0u8; HANDSHAKE_PAYLOAD_LEN];
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
buf[0..4].copy_from_slice(&nanos.to_be_bytes());
buf[4..8].copy_from_slice(&[0; 4]);
let mut acc = nanos;
for chunk in buf[8..].chunks_mut(4) {
acc ^= acc.wrapping_shl(13);
acc ^= acc >> 17;
acc ^= acc.wrapping_shl(5);
let bytes = acc.to_be_bytes();
for (i, b) in chunk.iter_mut().enumerate() {
*b = bytes[i];
}
}
buf
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::duplex;
#[test]
fn c1_is_1536_bytes() {
let c1 = build_c1();
assert_eq!(c1.len(), HANDSHAKE_PAYLOAD_LEN);
assert_eq!(&c1[4..8], &[0u8; 4]);
}
#[tokio::test]
async fn handshake_round_trip() {
let (mut client, mut server) = duplex(8192);
let server_task = tokio::spawn(async move {
let mut c0 = [0u8; 1];
server.read_exact(&mut c0).await.unwrap();
assert_eq!(c0[0], RTMP_VERSION);
let mut c1 = [0u8; HANDSHAKE_PAYLOAD_LEN];
server.read_exact(&mut c1).await.unwrap();
let mut s0_s1_s2 = Vec::with_capacity(1 + HANDSHAKE_PAYLOAD_LEN * 2);
s0_s1_s2.push(RTMP_VERSION);
let s1 = [0xABu8; HANDSHAKE_PAYLOAD_LEN];
s0_s1_s2.extend_from_slice(&s1);
s0_s1_s2.extend_from_slice(&c1); server.write_all(&s0_s1_s2).await.unwrap();
let mut c2 = [0u8; HANDSHAKE_PAYLOAD_LEN];
server.read_exact(&mut c2).await.unwrap();
assert_eq!(c2, s1, "C2 must echo S1");
});
run_handshake(&mut client).await.unwrap();
server_task.await.unwrap();
}
#[tokio::test]
async fn handshake_rejects_wrong_s0_version() {
let (mut client, mut server) = duplex(8192);
tokio::spawn(async move {
let mut buf = [0u8; 1 + HANDSHAKE_PAYLOAD_LEN];
let _ = server.read_exact(&mut buf).await;
let mut bogus_s0_s1_s2 = vec![0xFFu8];
bogus_s0_s1_s2.extend_from_slice(&[0u8; HANDSHAKE_PAYLOAD_LEN]);
bogus_s0_s1_s2.extend_from_slice(&[0u8; HANDSHAKE_PAYLOAD_LEN]);
let _ = server.write_all(&bogus_s0_s1_s2).await;
});
let err = run_handshake(&mut client).await.unwrap_err();
assert!(err.to_string().contains("S0 returned version 255"));
}
#[tokio::test]
async fn handshake_rejects_eof() {
let (mut client, server) = duplex(8192);
drop(server);
assert!(run_handshake(&mut client).await.is_err());
}
}