From 54a78807fe45b6b96c077068316e3c6e70dfe490 Mon Sep 17 00:00:00 2001 From: Konstantin Baltruschat Date: Fri, 28 Jun 2024 22:29:38 +0200 Subject: [PATCH] sdp-types: add srtp crypto attribute --- crates/sdp-types/src/attributes/crypto.rs | 610 ++++++++++++++++++++ crates/sdp-types/src/attributes/mod.rs | 2 + crates/sdp-types/src/lib.rs | 3 +- crates/sdp-types/src/session_description.rs | 163 +++--- 4 files changed, 704 insertions(+), 74 deletions(-) create mode 100644 crates/sdp-types/src/attributes/crypto.rs diff --git a/crates/sdp-types/src/attributes/crypto.rs b/crates/sdp-types/src/attributes/crypto.rs new file mode 100644 index 0000000..e2da73e --- /dev/null +++ b/crates/sdp-types/src/attributes/crypto.rs @@ -0,0 +1,610 @@ +use bytes::Bytes; +use bytesstr::BytesStr; +use internal::{ws, IResult}; +use nom::branch::alt; +use nom::bytes::complete::{tag, take_while1}; +use nom::character::complete::{char, digit1}; +use nom::combinator::{map, map_res, not, opt, peek}; +use nom::error::context; +use nom::multi::{separated_list0, separated_list1}; +use nom::sequence::{preceded, separated_pair, terminated, tuple}; +use std::fmt; + +/// Crypto attribte (for SRTP only) (`a=crypto`) +/// +/// [RFC4568](https://www.rfc-editor.org/rfc/rfc4568) +#[derive(Debug, Clone)] +pub struct SrtpCrypto { + /// Unique identifier in a media description + pub tag: u32, + + /// Crypto suite describing the encryption and authentication algorithm to use + pub suite: SrtpSuite, + + /// One or more keys to use + pub keys: Vec, + + /// Additional SRTP params + pub params: Vec, +} + +impl SrtpCrypto { + pub fn parse<'i>(src: &Bytes, i: &'i str) -> IResult<&'i str, Self> { + context( + "parsing srtp-crypto attribute", + map( + ws(( + // tag + number, + // suite + SrtpSuite::parse(src), + // keying material + parse_srtp_key_params(src), + // session params + separated_list0( + take_while1(char::is_whitespace), + SrtpSessionParam::parse(src), + ), + )), + |(tag, suite, keys, params)| Self { + tag, + suite, + keys, + params, + }, + ), + )(i) + } +} + +impl fmt::Display for SrtpCrypto { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.tag, self.suite)?; + + if !self.keys.is_empty() { + write!(f, " ")?; + } + + let mut keys = self.keys.iter().peekable(); + + while let Some(key) = keys.next() { + write!(f, "inline:{key}")?; + + if keys.peek().is_some() { + write!(f, ";")?; + } + } + + for param in &self.params { + write!(f, " {param}")?; + } + + Ok(()) + } +} + +macro_rules! suite { + ($($suite:ident),* $(,)?) => { + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + #[allow(non_camel_case_types)] + pub enum SrtpSuite { + $($suite,)* + Ext(BytesStr), + } + + impl SrtpSuite { + pub fn parse(src: &Bytes) -> impl Fn(&str) -> IResult<&str, Self> + '_ { + move |i| { + context( + "parsing srtp suite", + alt(( + $( + map(tag(stringify!($suite)), |_| Self::$suite), + )* + map(take_while1(is_alphanumeric_or_underscore), move |suite| { + Self::Ext(BytesStr::from_parse(src, suite)) + }), + )), + )(i) + } + } + + pub fn as_str(&self) -> &str { + match self { + $(Self::$suite => stringify!($suite),)* + Self::Ext(ext) => ext, + } + } + } + + impl fmt::Display for SrtpSuite { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } + } + }; +} + +suite! { + AES_CM_128_HMAC_SHA1_80, + AES_CM_128_HMAC_SHA1_32, + F8_128_HMAC_SHA1_80, + SEED_CTR_128_HMAC_SHA1_80, + SEED_128_CCM_80, + SEED_128_GCM_96, + AES_192_CM_HMAC_SHA1_80, + AES_192_CM_HMAC_SHA1_32, + AES_256_CM_HMAC_SHA1_80, + AES_256_CM_HMAC_SHA1_32, + AEAD_AES_128_GCM, + AEAD_AES_256_GCM, +} + +/// Parameters for an SRTP sessions +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SrtpSessionParam { + /// The SRTP Key Derivation Rate is the rate at which a pseudo-random function is applied to a master key + Kdr(u32), + /// SRTP messages are not encrypted + UnencryptedSrtp, + /// SRTCP messages are not encrypted + UnencryptedSrtcp, + /// SRTP messages are not authenticated + UnauthenticatedSrtp, + //// Use forward error correction for the RTP packets + FecOrder(SrtpFecOrder), + /// Use separate master key(s) for a Forward Error Correction (FEC) stream + FecKey(Vec), + /// Window Size Hint + WindowSizeHint(u32), + /// Unknown parameter + Ext(BytesStr), +} + +/// Order of forward error correction (FEC) relative to SRTP services +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SrtpFecOrder { + /// FEC is applied before SRTP processing by the sender + /// FEC is applied after SRTP processing by the receiver + FecSrtp, + + /// FEC is applied after SRTP processing by the sender + /// FEC is applied before SRTP processing by the receiver + SrtpFec, +} + +impl SrtpSessionParam { + pub fn parse(src: &Bytes) -> impl Fn(&str) -> IResult<&str, Self> + '_ { + move |i| { + context( + "parsing srtp-session-param", + alt(( + map(preceded(tag("KDR="), number), Self::Kdr), + map(tag("UNENCRYPTED_SRTP"), |_| Self::UnencryptedSrtp), + map(tag("UNENCRYPTED_SRTCP"), |_| Self::UnencryptedSrtcp), + map(tag("UNAUTHENTICATED_SRTP"), |_| Self::UnauthenticatedSrtp), + preceded( + tag("FEC_ORDER="), + alt(( + map(tag("FEC_SRTP"), |_| Self::FecOrder(SrtpFecOrder::FecSrtp)), + map(tag("SRTP_FEC"), |_| Self::FecOrder(SrtpFecOrder::SrtpFec)), + )), + ), + map(preceded(tag("FEC_KEY="), parse_srtp_key_params(src)), Self::FecKey), + map(preceded(tag("WSH="), number), Self::WindowSizeHint), + map( + preceded(peek(not(char('-'))), take_while1(is_visible_char)), + |ext| Self::Ext(BytesStr::from_parse(src, ext)), + ), + )), + )(i) + } + } +} + +impl fmt::Display for SrtpSessionParam { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SrtpSessionParam::Kdr(v) => write!(f, "KDR={v}"), + SrtpSessionParam::UnencryptedSrtp => write!(f, "UNENCRYPTED_SRTP"), + SrtpSessionParam::UnencryptedSrtcp => write!(f, "UNENCRYPTED_SRTCP"), + SrtpSessionParam::UnauthenticatedSrtp => write!(f, "UNAUTHENTICATED_SRTP"), + SrtpSessionParam::FecOrder(order) => { + let order = match order { + SrtpFecOrder::FecSrtp => "FEC_SRTP", + SrtpFecOrder::SrtpFec => "SRTP_FEC", + }; + + write!(f, "FEC_ORDER={order}") + } + SrtpSessionParam::FecKey(keys) => { + if keys.is_empty() { + return Ok(()); + } + + write!(f, "FEC_KEY=")?; + + let mut keys = keys.iter().peekable(); + + while let Some(key) = keys.next() { + write!(f, "inline:{key}")?; + + if keys.peek().is_some() { + write!(f, ";")?; + } + } + + Ok(()) + } + SrtpSessionParam::WindowSizeHint(v) => write!(f, "WSH={v}"), + SrtpSessionParam::Ext(ext) => write!(f, "{ext}"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SrtpKeyingMaterial { + /// Concatenated master key and salt, base64 encoded + pub key_and_salt: BytesStr, + + /// Master key lifetime (max number of SRTP/SRTCP packets using this master key) + pub lifetime: Option, + + /// Master key index and length of the MKI field in SRTP packets + pub mki: Option<(u32, u32)>, +} + +impl SrtpKeyingMaterial { + pub fn parse(src: &Bytes) -> impl Fn(&str) -> IResult<&str, Self> + '_ { + move |i| { + context( + "parsing keying material", + map( + tuple(( + // key and salt + take_while1(is_base64_char), + // lifetime + opt(map( + terminated( + preceded(char('|'), tuple((opt(tag("2^")), number))), + // Do not parse the mki here by mistake + peek(not(char(':'))), + ), + |(exp, n)| { + if exp.is_some() { + 2u32.pow(n) + } else { + n + } + }, + )), + // mki + opt(preceded( + char('|'), + separated_pair(number, char(':'), number), + )), + )), + |(key_and_salt, lifetime, mki)| Self { + key_and_salt: BytesStr::from_parse(src, key_and_salt), + lifetime, + mki, + }, + ), + )(i) + } + } +} + +impl fmt::Display for SrtpKeyingMaterial { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.key_and_salt)?; + + if let Some(lifetime) = self.lifetime { + if lifetime.is_power_of_two() { + write!(f, "|2^{}", lifetime.trailing_zeros())?; + } else { + write!(f, "|{lifetime}")?; + } + } + + if let Some((mki, mki_length)) = self.mki { + write!(f, "|{mki}:{mki_length}")?; + } + + Ok(()) + } +} + +fn parse_srtp_key_params( + src: &Bytes, +) -> impl FnMut(&str) -> IResult<&str, Vec> + '_ { + move |i| { + separated_list1( + char(';'), + preceded(tag("inline:"), SrtpKeyingMaterial::parse(src)), + )(i) + } +} + +fn is_alphanumeric_or_underscore(c: char) -> bool { + matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_') +} + +fn is_visible_char(c: char) -> bool { + matches!(c, '\u{21}'..='\u{7E}') +} + +fn is_base64_char(c: char) -> bool { + c.is_ascii_alphanumeric() || matches!(c, '+' | '/' | '=') +} + +fn number(i: &str) -> IResult<&str, u32> { + context("parsing number", map_res(digit1, str::parse))(i) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn srtp_session_param_kdr() { + let i = BytesStr::from_static("KDR=5"); + let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap(); + + assert!(rem.is_empty(), "rem is not empty: {rem:?}"); + + let SrtpSessionParam::Kdr(5) = param else { + panic!("expected Kdr got {param:?}") + }; + + assert_eq!(param.to_string(), "KDR=5"); + } + + #[test] + fn srtp_session_param_unencrypted_srtp() { + let i = BytesStr::from_static("UNENCRYPTED_SRTP"); + let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap(); + + assert!(rem.is_empty(), "rem is not empty: {rem:?}"); + + let SrtpSessionParam::UnencryptedSrtp = param else { + panic!("expected UnencryptedSrtp got {param:?}") + }; + + assert_eq!(param.to_string(), "UNENCRYPTED_SRTP"); + } + + #[test] + fn srtp_session_param_unencrypted_srtcp() { + let i = BytesStr::from_static("UNENCRYPTED_SRTCP"); + let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap(); + + assert!(rem.is_empty(), "rem is not empty: {rem:?}"); + + let SrtpSessionParam::UnencryptedSrtcp = param else { + panic!("expected UnencryptedSrtcp got {param:?}") + }; + + assert_eq!(param.to_string(), "UNENCRYPTED_SRTCP"); + } + + #[test] + fn srtp_session_param_unauthenticated_srtp() { + let i = BytesStr::from_static("UNAUTHENTICATED_SRTP"); + let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap(); + + assert!(rem.is_empty(), "rem is not empty: {rem:?}"); + + let SrtpSessionParam::UnauthenticatedSrtp = param else { + panic!("expected UnauthenticatedSrtp got {param:?}") + }; + + assert_eq!(param.to_string(), "UNAUTHENTICATED_SRTP"); + } + + #[test] + fn srtp_session_param_fec_order1() { + let i = BytesStr::from_static("FEC_ORDER=SRTP_FEC"); + let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap(); + + assert!(rem.is_empty(), "rem is not empty: {rem:?}"); + + let SrtpSessionParam::FecOrder(SrtpFecOrder::SrtpFec) = param else { + panic!("expected FecOrder(SrtpFecOrder::SrtpFec) got {param:?}") + }; + + assert_eq!(param.to_string(), "FEC_ORDER=SRTP_FEC"); + } + + #[test] + fn srtp_session_param_fec_order2() { + let i = BytesStr::from_static("FEC_ORDER=FEC_SRTP"); + let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap(); + + assert!(rem.is_empty(), "rem is not empty: {rem:?}"); + + let SrtpSessionParam::FecOrder(SrtpFecOrder::FecSrtp) = param else { + panic!("expected FecOrder(SrtpFecOrder::FecSrtp) got {param:?}") + }; + + assert_eq!(param.to_string(), "FEC_ORDER=FEC_SRTP"); + } + + #[test] + fn srtp_session_param_fec_key1() { + let i = BytesStr::from_static( + "FEC_KEY=inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:4", + ); + let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap(); + + assert!(rem.is_empty(), "rem is not empty: {rem:?}"); + + let SrtpSessionParam::FecKey(key) = ¶m else { + panic!("expected FecKey(..) got {param:?}") + }; + + assert_eq!(key.len(), 1); + + assert_eq!( + key[0].key_and_salt, + "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj" + ); + assert_eq!(key[0].lifetime, Some(1048576)); + assert_eq!(key[0].mki, Some((1, 4))); + + assert_eq!( + param.to_string(), + "FEC_KEY=inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:4" + ); + } + + #[test] + fn srtp_session_param_fec_key2() { + let i = BytesStr::from_static( + "FEC_KEY=inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:4;inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^14|1:2", + ); + let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap(); + + assert!(rem.is_empty(), "rem is not empty: {rem:?}"); + + let SrtpSessionParam::FecKey(key) = ¶m else { + panic!("expected FecKey(..) got {param:?}") + }; + + assert_eq!(key.len(), 2); + + assert_eq!( + key[0].key_and_salt, + "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj" + ); + assert_eq!(key[0].lifetime, Some(1048576)); + assert_eq!(key[0].mki, Some((1, 4))); + + assert_eq!( + key[1].key_and_salt, + "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj" + ); + assert_eq!(key[1].lifetime, Some(16384)); + assert_eq!(key[1].mki, Some((1, 2))); + + assert_eq!( + param.to_string(), + "FEC_KEY=inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:4;inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^14|1:2" + ); + } + + #[test] + fn srtp_session_param_window_size_hint() { + let i = BytesStr::from_static("WSH=5"); + let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap(); + + assert!(rem.is_empty(), "rem is not empty: {rem:?}"); + + let SrtpSessionParam::WindowSizeHint(5) = param else { + panic!("expected WindowSizeHint got {param:?}") + }; + + assert_eq!(param.to_string(), "WSH=5"); + } + + #[test] + fn keying_material_missing_lifetime() { + let i = BytesStr::from_static("d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|1:4"); + let (rem, key) = SrtpKeyingMaterial::parse(i.as_ref())(&i).unwrap(); + + assert!(rem.is_empty(), "rem is not empty: {rem:?}"); + + assert_eq!(key.key_and_salt, "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj"); + assert_eq!(key.lifetime, None); + assert_eq!(key.mki, Some((1, 4))); + + assert_eq!( + key.to_string(), + "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|1:4" + ); + } + + #[test] + fn keying_material_missing_mki() { + let i = BytesStr::from_static("d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^10"); + let (rem, key) = SrtpKeyingMaterial::parse(i.as_ref())(&i).unwrap(); + + assert!(rem.is_empty(), "rem is not empty: {rem:?}"); + + assert_eq!(key.key_and_salt, "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj"); + assert_eq!(key.lifetime, Some(1024)); + assert_eq!(key.mki, None); + + assert_eq!( + key.to_string(), + "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^10" + ); + } + + #[test] + fn keying_material_only_key_and_salt() { + let i = BytesStr::from_static("d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj"); + let (rem, key) = SrtpKeyingMaterial::parse(i.as_ref())(&i).unwrap(); + + assert!(rem.is_empty(), "rem is not empty: {rem:?}"); + + assert_eq!(key.key_and_salt, "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj"); + assert_eq!(key.lifetime, None); + assert_eq!(key.mki, None); + + assert_eq!(key.to_string(), "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj"); + } + + #[test] + fn parse_everything() { + let i = BytesStr::from_static( + "\ +1 AES_CM_128_HMAC_SHA1_80 inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:4;inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^14|1:2 \ +KDR=100 \ +UNENCRYPTED_SRTP \ +UNENCRYPTED_SRTCP \ +UNAUTHENTICATED_SRTP \ +FEC_ORDER=FEC_SRTP \ +FEC_ORDER=SRTP_FEC \ +FEC_KEY=inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|1:4 \ +WSH=123", + ); + let (rem, c) = SrtpCrypto::parse(i.as_ref(), &i).unwrap(); + assert!(rem.is_empty(), "rem is not empty: {rem:?}"); + + assert_eq!(c.tag, 1); + assert_eq!(c.suite, SrtpSuite::AES_CM_128_HMAC_SHA1_80); + + assert_eq!(c.keys.len(), 2); + + assert_eq!( + c.keys[0].key_and_salt, + "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj" + ); + assert_eq!(c.keys[0].lifetime, Some(1048576)); + assert_eq!(c.keys[0].mki, Some((1, 4))); + + assert_eq!( + c.keys[1].key_and_salt, + "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj" + ); + assert_eq!(c.keys[1].lifetime, Some(16384)); + assert_eq!(c.keys[1].mki, Some((1, 2))); + + assert_eq!(c.params[0], SrtpSessionParam::Kdr(100)); + assert_eq!(c.params[1], SrtpSessionParam::UnencryptedSrtp); + assert_eq!(c.params[2], SrtpSessionParam::UnencryptedSrtcp); + assert_eq!(c.params[3], SrtpSessionParam::UnauthenticatedSrtp); + assert_eq!( + c.params[4], + SrtpSessionParam::FecOrder(SrtpFecOrder::FecSrtp) + ); + assert_eq!( + c.params[5], + SrtpSessionParam::FecOrder(SrtpFecOrder::SrtpFec) + ); + assert_eq!(c.params[7], SrtpSessionParam::WindowSizeHint(123)); + + assert_eq!(c.to_string(), "1 AES_CM_128_HMAC_SHA1_80 inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:4;inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^14|1:2 KDR=100 UNENCRYPTED_SRTP UNENCRYPTED_SRTCP UNAUTHENTICATED_SRTP FEC_ORDER=FEC_SRTP FEC_ORDER=SRTP_FEC FEC_KEY=inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|1:4 WSH=123"); + } +} diff --git a/crates/sdp-types/src/attributes/mod.rs b/crates/sdp-types/src/attributes/mod.rs index 984fa7d..c9685df 100644 --- a/crates/sdp-types/src/attributes/mod.rs +++ b/crates/sdp-types/src/attributes/mod.rs @@ -3,6 +3,7 @@ use bytesstr::BytesStr; use std::fmt; mod candidate; +mod crypto; mod direction; mod fmtp; mod ice; @@ -10,6 +11,7 @@ mod rtcp; mod rtpmap; pub use candidate::{IceCandidate, InvalidCandidateParamError, UntaggedAddress}; +pub use crypto::{SrtpCrypto, SrtpFecOrder, SrtpKeyingMaterial, SrtpSessionParam, SrtpSuite}; pub use direction::Direction; pub use fmtp::Fmtp; pub use ice::{IceOptions, IcePassword, IceUsernameFragment}; diff --git a/crates/sdp-types/src/lib.rs b/crates/sdp-types/src/lib.rs index 816cdf9..7d84224 100644 --- a/crates/sdp-types/src/lib.rs +++ b/crates/sdp-types/src/lib.rs @@ -17,7 +17,8 @@ mod time; pub use attributes::{ Direction, Fmtp, IceCandidate, IceOptions, IcePassword, IceUsernameFragment, - InvalidCandidateParamError, Rtcp, RtpMap, UnknownAttribute, UntaggedAddress, + InvalidCandidateParamError, Rtcp, RtpMap, SrtpCrypto, SrtpFecOrder, SrtpKeyingMaterial, + SrtpSessionParam, SrtpSuite, UnknownAttribute, UntaggedAddress, }; pub use bandwidth::Bandwidth; pub use connection::Connection; diff --git a/crates/sdp-types/src/session_description.rs b/crates/sdp-types/src/session_description.rs index d260f44..f573ddd 100644 --- a/crates/sdp-types/src/session_description.rs +++ b/crates/sdp-types/src/session_description.rs @@ -5,7 +5,7 @@ use crate::time::Time; use crate::{bandwidth::Bandwidth, Rtcp}; use crate::{ Direction, Fmtp, IceCandidate, IceOptions, IcePassword, IceUsernameFragment, RtpMap, - UnknownAttribute, + SrtpCrypto, UnknownAttribute, }; use bytesstr::BytesStr; use internal::{verbose_error_to_owned, Finish}; @@ -69,6 +69,9 @@ pub struct MediaDescription { /// ICE a=end-of-candidates attribute pub ice_end_of_candidates: bool, + /// Crypto attributes + pub crypto: Vec, + /// Additional attributes pub attributes: Vec, } @@ -107,6 +110,10 @@ impl fmt::Display for MediaDescription { write!(f, "{}\r\n", pwd)?; } + for crypto in &self.crypto { + write!(f, "a=cryto:{crypto}\r\n")?; + } + for attr in &self.attributes { write!(f, "{}\r\n", attr)?; } @@ -174,6 +181,52 @@ impl SessionDescription { } } +impl fmt::Display for SessionDescription { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "\ +v=0\r\n\ +{}\r\n\ +s={}\r\n\ +", + self.origin, self.name + )?; + + if let Some(conn) = &self.connection { + write!(f, "{conn}\r\n")?; + } + + for bw in &self.bandwidth { + write!(f, "{bw}\r\n")?; + } + + write!(f, "{}\r\n{}", self.time, self.ice_options)?; + + if self.ice_lite { + f.write_str("a=ice-lite\r\n")?; + } + + if let Some(ufrag) = &self.ice_ufrag { + write!(f, "{ufrag}\r\n")?; + } + + if let Some(pwd) = &self.ice_pwd { + write!(f, "{pwd}\r\n")?; + } + + for attr in &self.attributes { + write!(f, "{attr}\r\n")?; + } + + for media_description in &self.media_descriptions { + write!(f, "{media_description}")?; + } + + Ok(()) + } +} + #[derive(Default)] struct Parser { name: Option, @@ -218,8 +271,8 @@ impl Parser { [b'c', b'=', ..] => { let (_, c) = Connection::parse(src.as_ref(), line).finish()?; - if let Some(media_scope) = self.media_descriptions.last_mut() { - media_scope.connection = Some(c); + if let Some(media_description) = self.media_descriptions.last_mut() { + media_description.connection = Some(c); } else { self.connection = Some(c); } @@ -227,17 +280,17 @@ impl Parser { [b'b', b'=', ..] => { let (_, b) = Bandwidth::parse(src.as_ref(), line).finish()?; - if let Some(media_scope) = self.media_descriptions.last_mut() { - media_scope.bandwidth.push(b); + if let Some(media_description) = self.media_descriptions.last_mut() { + media_description.bandwidth.push(b); } else { self.bandwidth.push(b); } } [b'm', b'=', ..] => { - let (_, desc) = Media::parse(src.as_ref(), line).finish()?; + let (_, media) = Media::parse(src.as_ref(), line).finish()?; self.media_descriptions.push(MediaDescription { - media: desc, + media, // inherit session direction direction: self.direction, connection: None, @@ -249,6 +302,7 @@ impl Parser { ice_pwd: None, ice_candidates: vec![], ice_end_of_candidates: false, + crypto: vec![], attributes: vec![], }); } @@ -284,8 +338,8 @@ impl Parser { "rtpmap" => { let (_, rtpmap) = RtpMap::parse(src.as_ref(), line).finish()?; - if let Some(media_scope) = self.media_descriptions.last_mut() { - media_scope.rtpmaps.push(rtpmap); + if let Some(media_description) = self.media_descriptions.last_mut() { + media_description.rtpmaps.push(rtpmap); } // TODO error here ? @@ -293,8 +347,8 @@ impl Parser { "fmtp" => { let (_, fmtp) = Fmtp::parse(src.as_ref(), line).finish()?; - if let Some(media_scope) = self.media_descriptions.last_mut() { - media_scope.fmtps.push(fmtp); + if let Some(media_description) = self.media_descriptions.last_mut() { + media_description.fmtps.push(fmtp); } // TODO error here ? @@ -302,8 +356,8 @@ impl Parser { "rtcp" => { let (_, rtcp) = Rtcp::parse(src.as_ref(), line).finish()?; - if let Some(media_scope) = self.media_descriptions.last_mut() { - media_scope.rtcp_attr = Some(rtcp); + if let Some(media_description) = self.media_descriptions.last_mut() { + media_description.rtcp_attr = Some(rtcp); } // TODO error here? @@ -318,8 +372,8 @@ impl Parser { "ice-ufrag" => { let (_, ufrag) = IceUsernameFragment::parse(src.as_ref(), value).finish()?; - if let Some(media_scope) = self.media_descriptions.last_mut() { - media_scope.ice_ufrag = Some(ufrag); + if let Some(media_description) = self.media_descriptions.last_mut() { + media_description.ice_ufrag = Some(ufrag); } else { self.ice_ufrag = Some(ufrag); } @@ -327,8 +381,8 @@ impl Parser { "ice-pwd" => { let (_, pwd) = IcePassword::parse(src.as_ref(), value).finish()?; - if let Some(media_scope) = self.media_descriptions.last_mut() { - media_scope.ice_pwd = Some(pwd); + if let Some(media_description) = self.media_descriptions.last_mut() { + media_description.ice_pwd = Some(pwd); } else { self.ice_pwd = Some(pwd); } @@ -336,8 +390,17 @@ impl Parser { "candidate" => { let (_, candidate) = IceCandidate::parse(src.as_ref(), line).finish()?; - if let Some(media_scope) = self.media_descriptions.last_mut() { - media_scope.ice_candidates.push(candidate); + if let Some(media_description) = self.media_descriptions.last_mut() { + media_description.ice_candidates.push(candidate); + } + + // TODO error here? + } + "crypto" => { + let (_, crypto) = SrtpCrypto::parse(src.as_ref(), value).finish()?; + + if let Some(media_description) = self.media_descriptions.last_mut() { + media_description.crypto.push(crypto); } // TODO error here? @@ -348,8 +411,8 @@ impl Parser { value: Some(src.slice_ref(value)), }; - if let Some(media_scope) = self.media_descriptions.last_mut() { - media_scope.attributes.push(attr); + if let Some(media_description) = self.media_descriptions.last_mut() { + media_description.attributes.push(attr); } else { self.attributes.push(attr); } @@ -360,8 +423,8 @@ impl Parser { } fn parse_attribute_without_value(&mut self, src: &BytesStr, line: &str) { - let direction = if let Some(media_scope) = self.media_descriptions.last_mut() { - &mut media_scope.direction + let direction = if let Some(media_description) = self.media_descriptions.last_mut() { + &mut media_description.direction } else { &mut self.direction }; @@ -372,8 +435,8 @@ impl Parser { "sendonly" => *direction = Direction::SendOnly, "inactive" => *direction = Direction::Inactive, "end-of-candidates" => { - if let Some(media_scope) = self.media_descriptions.last_mut() { - media_scope.ice_end_of_candidates = true; + if let Some(media_description) = self.media_descriptions.last_mut() { + media_description.ice_end_of_candidates = true; } // TODO error here? @@ -384,8 +447,8 @@ impl Parser { value: None, }; - if let Some(media_scope) = self.media_descriptions.last_mut() { - media_scope.attributes.push(attr); + if let Some(media_description) = self.media_descriptions.last_mut() { + media_description.attributes.push(attr); } else { self.attributes.push(attr); } @@ -412,49 +475,3 @@ impl Parser { }) } } - -impl fmt::Display for SessionDescription { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "\ -v=0\r\n\ -{}\r\n\ -s={}\r\n\ -", - self.origin, self.name - )?; - - if let Some(conn) = &self.connection { - write!(f, "{conn}\r\n")?; - } - - for bw in &self.bandwidth { - write!(f, "{bw}\r\n")?; - } - - write!(f, "{}\r\n{}", self.time, self.ice_options)?; - - if self.ice_lite { - f.write_str("a=ice-lite\r\n")?; - } - - if let Some(ufrag) = &self.ice_ufrag { - write!(f, "{ufrag}\r\n")?; - } - - if let Some(pwd) = &self.ice_pwd { - write!(f, "{pwd}\r\n")?; - } - - for attr in &self.attributes { - write!(f, "{attr}\r\n")?; - } - - for media_scope in &self.media_descriptions { - write!(f, "{media_scope}")?; - } - - Ok(()) - } -}