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