Skip to main content

sip_header/
security.rs

1//! SIP Security mechanism parser (RFC 3329).
2//!
3//! Used by Security-Client, Security-Server, and Security-Verify headers.
4
5use std::fmt;
6
7/// A parsed security mechanism entry: `mechanism-name *(SEMI mech-params)`.
8#[derive(Debug, Clone, PartialEq, Eq)]
9#[non_exhaustive]
10pub struct SipSecurityMechanism {
11    mechanism: String,
12    params: Vec<(String, Option<String>)>,
13    quoted: Vec<bool>,
14}
15
16impl SipSecurityMechanism {
17    /// The mechanism name (e.g. `"digest"`, `"tls"`, `"ipsec-ike"`).
18    pub fn mechanism(&self) -> &str {
19        &self.mechanism
20    }
21
22    /// All parameters as `(key, optional_value)` pairs.
23    pub fn params(&self) -> &[(String, Option<String>)] {
24        &self.params
25    }
26
27    /// Look up a parameter by key (case-insensitive).
28    pub fn param(&self, key: &str) -> Option<Option<&str>> {
29        self.params
30            .iter()
31            .find(|(k, _)| k.eq_ignore_ascii_case(key))
32            .map(|(_, v)| v.as_deref())
33    }
34
35    /// The `q` preference value, if present.
36    pub fn q(&self) -> Option<&str> {
37        self.param("q")
38            .flatten()
39    }
40
41    /// The `d-alg` parameter, if present.
42    pub fn d_alg(&self) -> Option<&str> {
43        self.param("d-alg")
44            .flatten()
45    }
46
47    /// The `d-qop` parameter, if present.
48    pub fn d_qop(&self) -> Option<&str> {
49        self.param("d-qop")
50            .flatten()
51    }
52}
53
54impl fmt::Display for SipSecurityMechanism {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        write!(f, "{}", self.mechanism)?;
57        for ((key, value), quoted) in self
58            .params
59            .iter()
60            .zip(&self.quoted)
61        {
62            crate::write_param(f, key, value.as_deref(), *quoted)?;
63        }
64        Ok(())
65    }
66}
67
68/// Errors from parsing a security mechanism header value.
69#[derive(Debug, Clone, PartialEq, Eq)]
70#[non_exhaustive]
71pub enum SipSecurityError {
72    /// The input string was empty or whitespace-only.
73    Empty,
74    /// A mechanism entry could not be parsed.
75    InvalidFormat(String),
76}
77
78impl fmt::Display for SipSecurityError {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            Self::Empty => write!(f, "empty security mechanism value"),
82            Self::InvalidFormat(raw) => {
83                write!(f, "invalid security mechanism ({} bytes)", raw.len())
84            }
85        }
86    }
87}
88
89impl std::error::Error for SipSecurityError {}
90
91fn parse_mechanism(raw: &str) -> Result<SipSecurityMechanism, SipSecurityError> {
92    let raw = raw.trim();
93    if raw.is_empty() {
94        return Err(SipSecurityError::InvalidFormat(raw.to_string()));
95    }
96
97    let (mechanism_part, params_part) = match raw.split_once(';') {
98        Some((m, p)) => (m.trim(), Some(p)),
99        None => (raw, None),
100    };
101
102    if mechanism_part.is_empty() {
103        return Err(SipSecurityError::InvalidFormat(raw.to_string()));
104    }
105
106    let mechanism = mechanism_part.to_ascii_lowercase();
107    let (params, quoted) = crate::parse_params(params_part.unwrap_or(""))
108        .into_iter()
109        .map(|p| {
110            let (value, quoted) = p
111                .unquoted()
112                .map_or((None, false), |(v, q)| (Some(v), q));
113            (
114                (
115                    p.key
116                        .to_ascii_lowercase(),
117                    value,
118                ),
119                quoted,
120            )
121        })
122        .unzip();
123
124    Ok(SipSecurityMechanism {
125        mechanism,
126        params,
127        quoted,
128    })
129}
130
131/// Parsed security mechanism header value.
132#[derive(Debug, Clone, PartialEq, Eq)]
133#[non_exhaustive]
134pub struct SipSecurity(Vec<SipSecurityMechanism>);
135
136impl SipSecurity {
137    /// Parse a comma-separated security mechanism value.
138    pub fn parse(raw: &str) -> Result<Self, SipSecurityError> {
139        let raw = raw.trim();
140        if raw.is_empty() {
141            return Err(SipSecurityError::Empty);
142        }
143        Self::from_entries(crate::split_comma_entries(raw))
144    }
145
146    /// Build from entries a transport already split; each is one `sec-mechanism`.
147    pub fn from_entries<'a>(
148        entries: impl IntoIterator<Item = &'a str>,
149    ) -> Result<Self, SipSecurityError> {
150        let entries: Vec<_> = entries
151            .into_iter()
152            .map(parse_mechanism)
153            .collect::<Result<_, _>>()?;
154        if entries.is_empty() {
155            return Err(SipSecurityError::Empty);
156        }
157        Ok(Self(entries))
158    }
159
160    /// The parsed entries as a slice.
161    pub fn entries(&self) -> &[SipSecurityMechanism] {
162        &self.0
163    }
164
165    /// Consume self and return entries as a `Vec`.
166    pub fn into_entries(self) -> Vec<SipSecurityMechanism> {
167        self.0
168    }
169
170    /// Number of entries.
171    pub fn len(&self) -> usize {
172        self.0
173            .len()
174    }
175
176    /// Returns `true` if there are no entries.
177    pub fn is_empty(&self) -> bool {
178        self.0
179            .is_empty()
180    }
181}
182
183impl fmt::Display for SipSecurity {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        crate::fmt_joined(f, &self.0, ", ")
186    }
187}
188
189impl_from_str_via_parse!(SipSecurity, SipSecurityError);
190
191impl<'a> IntoIterator for &'a SipSecurity {
192    type Item = &'a SipSecurityMechanism;
193    type IntoIter = std::slice::Iter<'a, SipSecurityMechanism>;
194
195    fn into_iter(self) -> Self::IntoIter {
196        self.0
197            .iter()
198    }
199}
200
201impl IntoIterator for SipSecurity {
202    type Item = SipSecurityMechanism;
203    type IntoIter = std::vec::IntoIter<SipSecurityMechanism>;
204
205    fn into_iter(self) -> Self::IntoIter {
206        self.0
207            .into_iter()
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn single_mechanism() {
217        let sec = SipSecurity::parse("digest;d-qop=auth-int;q=0.1").unwrap();
218        assert_eq!(sec.len(), 1);
219        assert_eq!(sec.entries()[0].mechanism(), "digest");
220        assert_eq!(sec.entries()[0].d_qop(), Some("auth-int"));
221        assert_eq!(sec.entries()[0].q(), Some("0.1"));
222    }
223
224    #[test]
225    fn multiple_mechanisms() {
226        let sec = SipSecurity::parse("tls;q=0.2, digest;d-qop=auth;q=0.1").unwrap();
227        assert_eq!(sec.len(), 2);
228        assert_eq!(sec.entries()[0].mechanism(), "tls");
229        assert_eq!(sec.entries()[1].mechanism(), "digest");
230    }
231
232    #[test]
233    fn mechanism_no_params() {
234        let sec = SipSecurity::parse("tls").unwrap();
235        assert_eq!(sec.len(), 1);
236        assert_eq!(sec.entries()[0].mechanism(), "tls");
237        assert!(sec.entries()[0]
238            .params()
239            .is_empty());
240    }
241
242    #[test]
243    fn empty_input() {
244        assert!(matches!(
245            SipSecurity::parse(""),
246            Err(SipSecurityError::Empty)
247        ));
248    }
249
250    #[test]
251    fn display_roundtrip() {
252        let raw = "digest;d-qop=auth;q=0.1";
253        let sec = SipSecurity::parse(raw).unwrap();
254        assert_eq!(sec.to_string(), raw);
255    }
256
257    #[test]
258    fn from_str() {
259        let sec: SipSecurity = "tls;q=0.2"
260            .parse()
261            .unwrap();
262        assert_eq!(sec.len(), 1);
263    }
264
265    #[test]
266    fn d_alg_param() {
267        let sec = SipSecurity::parse("digest;d-alg=MD5;d-qop=auth").unwrap();
268        assert_eq!(sec.entries()[0].d_alg(), Some("MD5"));
269    }
270
271    #[test]
272    fn from_entries_matches_parse() {
273        let split = SipSecurity::from_entries(["tls;q=0.2", "digest;d-qop=auth;q=0.1"]).unwrap();
274        let joined = SipSecurity::parse("tls;q=0.2, digest;d-qop=auth;q=0.1").unwrap();
275        assert_eq!(split, joined);
276    }
277
278    #[test]
279    fn from_entries_bad_entry_is_error() {
280        assert!(matches!(
281            SipSecurity::from_entries(["tls", "   "]),
282            Err(SipSecurityError::InvalidFormat(_))
283        ));
284    }
285
286    #[test]
287    fn from_entries_empty_is_empty_error() {
288        assert!(matches!(
289            SipSecurity::from_entries(std::iter::empty::<&str>()),
290            Err(SipSecurityError::Empty)
291        ));
292    }
293}
294
295#[cfg(test)]
296mod param_tests {
297    use super::*;
298
299    #[test]
300    fn d_ver_stays_quoted_on_display() {
301        let input = r#"ipsec-3gpp;d-ver="0000000000000000000000000000abcd";q=0.1"#;
302        let sec = SipSecurity::parse(input).unwrap();
303        assert_eq!(
304            sec.entries()[0].param("d-ver"),
305            Some(Some("0000000000000000000000000000abcd"))
306        );
307        assert_eq!(sec.to_string(), input);
308    }
309
310    #[test]
311    fn quoted_value_keeps_semicolon() {
312        let sec = SipSecurity::parse(r#"digest;x="a;b";q=0.5"#).unwrap();
313        let mech = &sec.entries()[0];
314        assert_eq!(mech.param("x"), Some(Some("a;b")));
315        assert_eq!(mech.q(), Some("0.5"));
316    }
317
318    #[test]
319    fn quoted_pair_unescaped() {
320        let sec = SipSecurity::parse(r#"digest;x="a\"b""#).unwrap();
321        assert_eq!(sec.entries()[0].param("x"), Some(Some(r#"a"b"#)));
322        assert_eq!(sec.to_string(), r#"digest;x="a\"b""#);
323    }
324
325    #[test]
326    fn error_display_omits_input() {
327        let err = SipSecurity::parse(";secret-value").unwrap_err();
328        assert!(!err
329            .to_string()
330            .contains("secret-value"));
331    }
332}