Skip to main content

sipx_sip/headers/
misc.rs

1//! The remaining headers the core needs: `CSeq`, `Call-ID`, the scalars, `Content-Type`,
2//! `Date`, and the token-list headers.
3
4use bytes::Bytes;
5
6use crate::error::HeaderError;
7use crate::headers::grammar::{self, HeaderParam, is_token_char, parse_u64, skip_ws, trim};
8use crate::message::{Method, TypedHeader};
9use crate::name::HeaderName;
10
11/// The `CSeq` header (RFC 3261 §20.16): a sequence number and the method it belongs to.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct CSeq {
14    /// The sequence number.
15    pub sequence: u32,
16    /// The method, which must match the request line (RFC 4475 §3.1.2.17).
17    pub method: Method,
18}
19
20impl TypedHeader for CSeq {
21    const NAME: HeaderName = HeaderName::CSeq;
22
23    fn decode(value: &[u8]) -> Result<Self, HeaderError> {
24        let value = trim(value);
25        let space = value
26            .iter()
27            .position(|&b| matches!(b, b' ' | b'\t'))
28            .ok_or(HeaderError::Syntax { header: "CSeq" })?;
29        let digits = value.get(..space).unwrap_or(&[]);
30        let method_raw = trim(value.get(skip_ws(value, space)..).unwrap_or(&[]));
31
32        if method_raw.is_empty() || !method_raw.iter().all(|&b| is_token_char(b)) {
33            return Err(HeaderError::Syntax { header: "CSeq" });
34        }
35
36        // RFC 3261 §8.1.1.5 bounds the sequence number at 2^31-1, not 2^32-1, so that
37        // incrementing it cannot overflow a 32-bit counter. RFC 4475 §3.1.2.4 sends one above
38        // the limit and expects a 400.
39        let sequence = parse_u64(digits, "CSeq")?;
40        if sequence > u64::from(i32::MAX as u32) {
41            return Err(HeaderError::OutOfRange { header: "CSeq" });
42        }
43
44        Ok(Self {
45            sequence: u32::try_from(sequence)
46                .map_err(|_| HeaderError::OutOfRange { header: "CSeq" })?,
47            method: Method::parse(&Bytes::copy_from_slice(method_raw)),
48        })
49    }
50}
51
52/// The `Call-ID` header (RFC 3261 §20.8), an opaque identifier compared byte for byte.
53#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54pub struct CallId(pub Vec<u8>);
55
56impl TypedHeader for CallId {
57    const NAME: HeaderName = HeaderName::CallId;
58
59    fn decode(value: &[u8]) -> Result<Self, HeaderError> {
60        let value = trim(value);
61        if value.is_empty() {
62            return Err(HeaderError::Syntax { header: "Call-ID" });
63        }
64        Ok(Self(value.to_vec()))
65    }
66}
67
68/// The `Max-Forwards` header (RFC 3261 §20.22).
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct MaxForwards(pub u8);
71
72impl TypedHeader for MaxForwards {
73    const NAME: HeaderName = HeaderName::MaxForwards;
74
75    fn decode(value: &[u8]) -> Result<Self, HeaderError> {
76        let n = parse_u64(trim(value), "Max-Forwards")?;
77        u8::try_from(n)
78            .map(Self)
79            .map_err(|_| HeaderError::OutOfRange {
80                header: "Max-Forwards",
81            })
82    }
83}
84
85/// The `Expires` header (RFC 3261 §20.19), in seconds.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub struct Expires(pub u32);
88
89impl TypedHeader for Expires {
90    const NAME: HeaderName = HeaderName::Expires;
91
92    fn decode(value: &[u8]) -> Result<Self, HeaderError> {
93        let n = parse_u64(trim(value), "Expires")?;
94        u32::try_from(n)
95            .map(Self)
96            .map_err(|_| HeaderError::OutOfRange { header: "Expires" })
97    }
98}
99
100/// The `Content-Length` header (RFC 3261 §20.14).
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub struct ContentLength(pub u64);
103
104impl TypedHeader for ContentLength {
105    const NAME: HeaderName = HeaderName::ContentLength;
106
107    fn decode(value: &[u8]) -> Result<Self, HeaderError> {
108        parse_u64(trim(value), "Content-Length").map(Self)
109    }
110}
111
112/// The `Content-Type` header (RFC 3261 §20.15).
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct ContentType {
115    /// The type, lowercased — `application`.
116    pub media_type: Vec<u8>,
117    /// The subtype, lowercased — `sdp`.
118    pub subtype: Vec<u8>,
119    /// Any parameters, such as a multipart `boundary`.
120    pub params: Vec<HeaderParam>,
121}
122
123impl ContentType {
124    /// Whether this is the given type and subtype, compared case-insensitively.
125    #[must_use]
126    pub fn is(&self, media_type: &str, subtype: &str) -> bool {
127        self.media_type == media_type.as_bytes() && self.subtype == subtype.as_bytes()
128    }
129
130    /// A parameter value by name.
131    #[must_use]
132    pub fn param(&self, name: &str) -> Option<&[u8]> {
133        grammar::param(&self.params, name).and_then(|p| p.value.as_deref())
134    }
135}
136
137impl TypedHeader for ContentType {
138    const NAME: HeaderName = HeaderName::ContentType;
139
140    fn decode(value: &[u8]) -> Result<Self, HeaderError> {
141        const LABEL: &str = "Content-Type";
142        let value = trim(value);
143        let (before_params, tail) = match grammar::find_param_start(value) {
144            Some(semi) => (
145                value.get(..semi).unwrap_or(&[]),
146                value.get(semi..).unwrap_or(&[]),
147            ),
148            None => (value, &[][..]),
149        };
150        let slash = before_params
151            .iter()
152            .position(|&b| b == b'/')
153            .ok_or(HeaderError::Syntax { header: LABEL })?;
154        let media_type = trim(before_params.get(..slash).unwrap_or(&[]));
155        let subtype = trim(before_params.get(slash + 1..).unwrap_or(&[]));
156
157        if media_type.is_empty()
158            || subtype.is_empty()
159            || !media_type.iter().all(|&b| is_token_char(b))
160            || !subtype.iter().all(|&b| is_token_char(b))
161        {
162            return Err(HeaderError::Syntax { header: LABEL });
163        }
164
165        Ok(Self {
166            media_type: media_type.to_ascii_lowercase(),
167            subtype: subtype.to_ascii_lowercase(),
168            params: grammar::parse_params(trim(tail), LABEL)?,
169        })
170    }
171}
172
173/// The `Date` header (RFC 3261 §20.17).
174///
175/// SIP narrows HTTP's three date formats to one: RFC 1123 with the zone spelled `GMT` and
176/// nothing else. RFC 4475 §3.1.2.12 sends `EST` and expects it to be refused, so the zone is
177/// not cosmetic — accepting it would mean accepting a time that is wrong by hours.
178#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct Date(pub Vec<u8>);
180
181impl TypedHeader for Date {
182    const NAME: HeaderName = HeaderName::Date;
183
184    fn decode(value: &[u8]) -> Result<Self, HeaderError> {
185        const LABEL: &str = "Date";
186        // The alphabets are fixed by the grammar, and matched case-sensitively: rfc1123-date
187        // is case-sensitive (RFC 2616 §3.3.1), and its dates are what SIP-date narrows down
188        // to (RFC 3261 §20.17).
189        const WKDAYS: [&[u8]; 7] = [b"Mon", b"Tue", b"Wed", b"Thu", b"Fri", b"Sat", b"Sun"];
190        const MONTHS: [&[u8]; 12] = [
191            b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov",
192            b"Dec",
193        ];
194        let value = trim(value);
195
196        // SIP-date = wkday "," SP date1 SP time SP "GMT". Every field is of fixed width, so
197        // "Mon, 01 Jan 2010 16:00:00 GMT" is 29 octets and each field sits at a fixed offset.
198        if value.len() != 29 {
199            return Err(HeaderError::Syntax { header: LABEL });
200        }
201        let field = |from: usize, to: usize| value.get(from..to).unwrap_or(&[]);
202        if field(3, 5) != b", "
203            || field(7, 8) != b" "
204            || field(11, 12) != b" "
205            || field(16, 17) != b" "
206            || field(19, 20) != b":"
207            || field(22, 23) != b":"
208            || field(25, 26) != b" "
209            || field(26, 29) != b"GMT"
210        {
211            return Err(HeaderError::Syntax { header: LABEL });
212        }
213        if !WKDAYS.contains(&field(0, 3))
214            || !MONTHS.contains(&field(8, 11))
215            || !field(12, 16).iter().all(u8::is_ascii_digit)
216        {
217            return Err(HeaderError::Syntax { header: LABEL });
218        }
219
220        let day = two_digits(value, 5).ok_or(HeaderError::Syntax { header: LABEL })?;
221        let hour = two_digits(value, 17).ok_or(HeaderError::Syntax { header: LABEL })?;
222        let minute = two_digits(value, 20).ok_or(HeaderError::Syntax { header: LABEL })?;
223        let second = two_digits(value, 23).ok_or(HeaderError::Syntax { header: LABEL })?;
224        // The grammar's own bounds: date1 has at most 31 days, and the comment on `time`
225        // stops it at 23:59:59 (RFC 3261 §25.1) — no leap-second allowance.
226        if !(1..=31).contains(&day) || hour > 23 || minute > 59 || second > 59 {
227            return Err(HeaderError::OutOfRange { header: LABEL });
228        }
229
230        Ok(Self(value.to_vec()))
231    }
232}
233
234/// The `2DIGIT` field starting at `at`, or `None` if either octet is not a digit.
235#[must_use]
236fn two_digits(value: &[u8], at: usize) -> Option<u8> {
237    let hi = value.get(at)?.checked_sub(b'0')?;
238    let lo = value.get(at + 1)?.checked_sub(b'0')?;
239    (hi <= 9 && lo <= 9).then_some(hi * 10 + lo)
240}
241
242/// A header whose value is a comma-separated list of tokens: `Allow`, `Supported`,
243/// `Require`, `Proxy-Require`, `Unsupported`.
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct TokenList(pub Vec<Vec<u8>>);
246
247impl TokenList {
248    fn decode_named(
249        value: &[u8],
250        header: &'static str,
251        may_be_empty: bool,
252    ) -> Result<Self, HeaderError> {
253        let mut tokens = Vec::new();
254        for part in grammar::split_list(value, header)? {
255            let token = trim(part);
256            // An entirely empty value is a legitimate "none of them" for `Allow` and
257            // `Supported`, whose ABNF brackets the whole value — but `Require`,
258            // `Proxy-Require` and `Unsupported` are `option-tag *(COMMA option-tag)` and
259            // demand at least one tag (RFC 3261 §25.1).
260            if token.is_empty() {
261                if may_be_empty && grammar::split_list(value, header)?.len() == 1 {
262                    return Ok(Self(Vec::new()));
263                }
264                return Err(HeaderError::Syntax { header });
265            }
266            if !token.iter().all(|&b| is_token_char(b)) {
267                return Err(HeaderError::Syntax { header });
268            }
269            tokens.push(token.to_vec());
270        }
271        Ok(Self(tokens))
272    }
273
274    /// Whether the list contains this token, compared case-insensitively.
275    #[must_use]
276    pub fn contains(&self, token: &str) -> bool {
277        self.0
278            .iter()
279            .any(|t| t.eq_ignore_ascii_case(token.as_bytes()))
280    }
281}
282
283macro_rules! token_list_header {
284    ($(#[$meta:meta])* $type:ident => $variant:ident, $label:literal, $may_be_empty:literal) => {
285        $(#[$meta])*
286        #[derive(Debug, Clone, PartialEq, Eq)]
287        pub struct $type(pub TokenList);
288
289        impl std::ops::Deref for $type {
290            type Target = TokenList;
291            fn deref(&self) -> &TokenList {
292                &self.0
293            }
294        }
295
296        impl TypedHeader for $type {
297            const NAME: HeaderName = HeaderName::$variant;
298
299            fn decode(value: &[u8]) -> Result<Self, HeaderError> {
300                TokenList::decode_named(value, $label, $may_be_empty).map(Self)
301            }
302        }
303    };
304}
305
306token_list_header!(
307    /// The `Allow` header (RFC 3261 §20.5).
308    Allow => Allow, "Allow", true
309);
310token_list_header!(
311    /// The `Supported` header (RFC 3261 §20.37).
312    Supported => Supported, "Supported", true
313);
314token_list_header!(
315    /// The `Require` header (RFC 3261 §20.32).
316    Require => Require, "Require", false
317);
318token_list_header!(
319    /// The `Proxy-Require` header (RFC 3261 §20.29).
320    ProxyRequire => ProxyRequire, "Proxy-Require", false
321);
322token_list_header!(
323    /// The `Unsupported` header (RFC 3261 §20.40).
324    Unsupported => Unsupported, "Unsupported", false
325);
326
327#[cfg(test)]
328#[allow(
329    clippy::unwrap_used,
330    clippy::expect_used,
331    clippy::panic,
332    clippy::indexing_slicing
333)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn cseq_parses_number_and_method() {
339        let c = CSeq::decode(b"8 INVITE").unwrap();
340        assert_eq!(c.sequence, 8);
341        assert_eq!(c.method, Method::Invite);
342    }
343
344    /// RFC 4475 3.1.1.1 sends `CSeq: 0009` folded onto the next line; once unfolded the
345    /// leading zeros are still legal.
346    #[test]
347    fn cseq_accepts_leading_zeros_and_extra_whitespace() {
348        let c = CSeq::decode(b"0009    INVITE").unwrap();
349        assert_eq!(c.sequence, 9);
350    }
351
352    /// RFC 4475 3.1.2.4 and 3.1.2.5: above 2^31-1, which is where RFC 3261 8.1.1.5 stops.
353    #[test]
354    fn cseq_rejects_overlarge_sequence_numbers() {
355        assert!(matches!(
356            CSeq::decode(b"2147483648 INVITE"),
357            Err(HeaderError::OutOfRange { header: "CSeq" })
358        ));
359        assert!(matches!(
360            CSeq::decode(b"9292394834772304023312 OPTIONS"),
361            Err(HeaderError::OutOfRange { header: "CSeq" })
362        ));
363        // The largest legal value still works.
364        assert_eq!(
365            CSeq::decode(b"2147483647 INVITE").unwrap().sequence,
366            i32::MAX as u32
367        );
368    }
369
370    #[test]
371    fn cseq_rejects_a_missing_or_non_token_method() {
372        assert!(CSeq::decode(b"8").is_err());
373        assert!(CSeq::decode(b"8 IN VITE").is_err());
374        assert!(CSeq::decode(b"x INVITE").is_err());
375    }
376
377    #[test]
378    fn max_forwards_is_bounded_at_255() {
379        assert_eq!(MaxForwards::decode(b"0068").unwrap().0, 68);
380        assert_eq!(MaxForwards::decode(b"0").unwrap().0, 0);
381        assert!(matches!(
382            MaxForwards::decode(b"256"),
383            Err(HeaderError::OutOfRange { .. })
384        ));
385    }
386
387    #[test]
388    fn content_type_lowercases_and_keeps_parameters() {
389        let ct = ContentType::decode(b"Application/SDP").unwrap();
390        assert!(ct.is("application", "sdp"));
391
392        let ct = ContentType::decode(b"multipart/mixed;boundary=unique-boundary-1").unwrap();
393        assert_eq!(ct.param("boundary"), Some(&b"unique-boundary-1"[..]));
394    }
395
396    #[test]
397    fn content_type_rejects_a_missing_subtype() {
398        assert!(ContentType::decode(b"application").is_err());
399        assert!(ContentType::decode(b"application/").is_err());
400        assert!(ContentType::decode(b"/sdp").is_err());
401    }
402
403    /// RFC 4475 3.1.2.12: SIP allows exactly one date format and exactly one zone.
404    #[test]
405    fn date_requires_gmt() {
406        assert!(Date::decode(b"Fri, 01 Jan 2010 16:00:00 GMT").is_ok());
407        assert!(Date::decode(b"Fri, 01 Jan 2010 16:00:00 EST").is_err());
408        assert!(Date::decode(b"Fri, 01 Jan 2010 16:00:00").is_err());
409        assert!(Date::decode(b"nonsense GMT").is_err());
410    }
411
412    /// RFC 3261 §25.1: `wkday` and `month` are fixed alphabets, the numeric fields are
413    /// `2DIGIT`/`4DIGIT`, and the time comment bounds them at 23:59:59. The right shape with
414    /// the wrong contents is not a date.
415    #[test]
416    fn date_validates_every_field_not_just_the_shape() {
417        for bad in [
418            &b"aaa, aaaaaaaaaaaaaaaaaaaa GMT"[..],
419            b"Fri, 32 Jan 2010 25:99:99 GMT",
420            b"Xyz, 01 Jan 2010 16:00:00 GMT",
421            b"Fri, 00 Jan 2010 16:00:00 GMT",
422            b"Fri, 01 Foo 2010 16:00:00 GMT",
423            b"Fri, 01 Jan 2010 24:00:00 GMT",
424            b"Fri, 01 Jan 2010 16:60:00 GMT",
425            b"Fri, 01 Jan 2010 16:00:60 GMT",
426            b"Fri, 01 Jan x010 16:00:00 GMT",
427            b"Fri, 01 Jan 2010 16.00.00 GMT",
428        ] {
429            assert!(
430                Date::decode(bad).is_err(),
431                "{:?} should be rejected",
432                String::from_utf8_lossy(bad)
433            );
434        }
435        for good in [
436            &b"Mon, 01 Jan 2010 00:00:00 GMT"[..],
437            b"Sat, 13 Nov 2010 23:29:00 GMT",
438            b"Sun, 31 Dec 2699 23:59:59 GMT",
439        ] {
440            assert!(
441                Date::decode(good).is_ok(),
442                "{:?} should parse",
443                String::from_utf8_lossy(good)
444            );
445        }
446    }
447
448    #[test]
449    fn token_lists_split_and_compare_case_insensitively() {
450        let allow = Allow::decode(b"INVITE, ACK, OPTIONS, CANCEL, BYE").unwrap();
451        assert_eq!(allow.0.0.len(), 5);
452        assert!(allow.contains("invite"));
453        assert!(!allow.contains("REFER"));
454
455        // An empty value says "none", which is different from the header being absent.
456        assert_eq!(Supported::decode(b"").unwrap().0.0.len(), 0);
457        // But a stray comma is a malformed list, not an empty one.
458        assert!(Supported::decode(b"100rel,,timer").is_err());
459    }
460
461    /// RFC 3261 §25.1 brackets the value of `Allow` and `Supported`, so those may be empty;
462    /// `Require`, `Proxy-Require` and `Unsupported` are `option-tag *(COMMA option-tag)` and
463    /// demand at least one tag.
464    #[test]
465    fn only_allow_and_supported_may_be_empty() {
466        assert!(Allow::decode(b"").is_ok());
467        assert!(Supported::decode(b"").is_ok());
468
469        assert!(Require::decode(b"").is_err());
470        assert!(ProxyRequire::decode(b"").is_err());
471        assert!(Unsupported::decode(b"").is_err());
472
473        // With a tag present all five parse alike.
474        assert!(Require::decode(b"100rel").is_ok());
475        assert!(ProxyRequire::decode(b"sec-agree").is_ok());
476        assert!(Unsupported::decode(b"foo, bar").is_ok());
477    }
478
479    #[test]
480    fn call_id_is_opaque_but_not_empty() {
481        assert_eq!(
482            CallId::decode(b"wsinv.ndaksdj@192.0.2.1").unwrap().0,
483            b"wsinv.ndaksdj@192.0.2.1"
484        );
485        assert!(CallId::decode(b"   ").is_err());
486    }
487}