use crate::errors::{Result, SessionError};
use rvoip_sip_core::types::sdp::{ParsedAttribute, SdpSession};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetupRole {
Active,
Passive,
Actpass,
Holdconn,
}
impl SetupRole {
pub fn parse(value: &str) -> Result<Self> {
match value.trim().to_lowercase().as_str() {
"active" => Ok(SetupRole::Active),
"passive" => Ok(SetupRole::Passive),
"actpass" => Ok(SetupRole::Actpass),
"holdconn" => Ok(SetupRole::Holdconn),
other => Err(SessionError::SDPNegotiationFailed(format!(
"Unknown a=setup role: {}",
other
))),
}
}
pub fn as_str(self) -> &'static str {
match self {
SetupRole::Active => "active",
SetupRole::Passive => "passive",
SetupRole::Actpass => "actpass",
SetupRole::Holdconn => "holdconn",
}
}
pub fn complementary(self) -> Self {
match self {
SetupRole::Actpass => SetupRole::Active,
SetupRole::Active => SetupRole::Passive,
SetupRole::Passive => SetupRole::Active,
SetupRole::Holdconn => SetupRole::Holdconn,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DtlsOffer {
pub hash_function: String,
pub fingerprint: String,
pub setup_role: SetupRole,
}
pub fn detect_dtls_offer(sdp: &SdpSession) -> Option<DtlsOffer> {
let audio = sdp.media_descriptions.iter().find(|m| m.media == "audio")?;
let mut fingerprint = None;
let mut setup = None;
for attr in audio
.generic_attributes
.iter()
.chain(sdp.generic_attributes.iter())
{
match attr {
ParsedAttribute::Fingerprint(hash, fp) if fingerprint.is_none() => {
fingerprint = Some((hash.clone(), fp.clone()));
}
ParsedAttribute::Setup(role) if setup.is_none() => {
setup = SetupRole::parse(role).ok();
}
_ => {}
}
}
match (fingerprint, setup) {
(Some((hash_function, fp)), Some(setup_role)) => Some(DtlsOffer {
hash_function,
fingerprint: fp,
setup_role,
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use rvoip_sip_core::sdp::SdpBuilder;
use std::str::FromStr;
fn dtls_audio_offer(hash: &str, fp: &str, setup: &str) -> SdpSession {
let sdp = SdpBuilder::new("Session")
.origin("-", "1", "0", "IN", "IP4", "127.0.0.1")
.connection("IN", "IP4", "127.0.0.1")
.time("0", "0")
.media_audio(16000, "RTP/SAVP")
.formats(&["0"])
.rtpmap("0", "PCMU/8000")
.attribute("fingerprint", Some(format!("{} {}", hash, fp)))
.attribute("setup", Some(setup.to_string()))
.attribute("sendrecv", None::<String>)
.done()
.build()
.expect("offer builds")
.to_string();
SdpSession::from_str(&sdp).expect("offer parses")
}
#[test]
fn rfc_8842_complementary_role_matrix() {
assert_eq!(SetupRole::Actpass.complementary(), SetupRole::Active);
assert_eq!(SetupRole::Active.complementary(), SetupRole::Passive);
assert_eq!(SetupRole::Passive.complementary(), SetupRole::Active);
assert_eq!(SetupRole::Holdconn.complementary(), SetupRole::Holdconn);
}
#[test]
fn parse_round_trip_preserves_role() {
for role in ["active", "passive", "actpass", "holdconn"] {
let parsed = SetupRole::parse(role).expect("legal role parses");
assert_eq!(parsed.as_str(), role);
}
}
#[test]
fn parse_rejects_unknown_role() {
assert!(SetupRole::parse("random-garbage").is_err());
assert!(SetupRole::parse("").is_err());
}
#[test]
fn parse_is_case_insensitive_and_trims_whitespace() {
assert_eq!(
SetupRole::parse(" ACTPASS ").expect("trim+casefold"),
SetupRole::Actpass
);
}
#[test]
fn detect_dtls_offer_returns_some_when_both_attributes_present() {
let offer = dtls_audio_offer(
"sha-256",
"AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89",
"actpass",
);
let detected = detect_dtls_offer(&offer).expect("detect");
assert_eq!(detected.hash_function, "sha-256");
assert!(detected.fingerprint.starts_with("AB:CD:EF"));
assert_eq!(detected.setup_role, SetupRole::Actpass);
}
#[test]
fn detect_dtls_offer_returns_none_for_plain_rtp_avp() {
let plain = SdpBuilder::new("Session")
.origin("-", "1", "0", "IN", "IP4", "127.0.0.1")
.connection("IN", "IP4", "127.0.0.1")
.time("0", "0")
.media_audio(16000, "RTP/AVP")
.formats(&["0"])
.rtpmap("0", "PCMU/8000")
.attribute("sendrecv", None::<String>)
.done()
.build()
.expect("plain offer builds")
.to_string();
let parsed = SdpSession::from_str(&plain).expect("offer parses");
assert!(detect_dtls_offer(&parsed).is_none());
}
#[test]
fn detect_dtls_offer_returns_none_when_only_fingerprint_present() {
let sdp = SdpBuilder::new("Session")
.origin("-", "1", "0", "IN", "IP4", "127.0.0.1")
.connection("IN", "IP4", "127.0.0.1")
.time("0", "0")
.media_audio(16000, "RTP/SAVP")
.formats(&["0"])
.rtpmap("0", "PCMU/8000")
.attribute("fingerprint", Some("sha-256 AB:CD:EF"))
.attribute("sendrecv", None::<String>)
.done()
.build()
.expect("partial offer builds")
.to_string();
let parsed = SdpSession::from_str(&sdp).expect("offer parses");
assert!(detect_dtls_offer(&parsed).is_none());
}
#[test]
fn unknown_setup_value_short_circuits_detection() {
let sdp = SdpBuilder::new("Session")
.origin("-", "1", "0", "IN", "IP4", "127.0.0.1")
.connection("IN", "IP4", "127.0.0.1")
.time("0", "0")
.media_audio(16000, "RTP/SAVP")
.formats(&["0"])
.rtpmap("0", "PCMU/8000")
.attribute("fingerprint", Some("sha-256 AB:CD:EF"))
.attribute("setup", Some("bogus"))
.attribute("sendrecv", None::<String>)
.done()
.build()
.expect("offer with bogus setup builds")
.to_string();
let parsed = SdpSession::from_str(&sdp).expect("offer parses");
assert!(detect_dtls_offer(&parsed).is_none());
}
}