Skip to main content

sipx_sip/
validate.rs

1//! Message validation — the checks that come *after* parsing.
2//!
3//! A message can frame perfectly, have every header parse, and still be one no element may
4//! act on: required headers missing, a `CSeq` naming a different method than the request
5//! line, a version nobody speaks. RFC 4475 files these under the application layer, and so do
6//! we, for a practical reason: answering `400` requires having parsed the message that is
7//! wrong, and forwarding requires not caring.
8//!
9//! Validation therefore returns a *list* of findings rather than failing at the first. An
10//! element picks a response from them; a proxy may ignore several of them entirely.
11
12use crate::headers::{CSeq, CallId, From, MaxForwards, To, Via};
13use crate::message::{Headers, Message, Request, Response, TypedHeader};
14use crate::name::HeaderName;
15
16/// Something wrong with a message that parsed.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum Finding {
19    /// A header RFC 3261 §8.1.1 requires is absent.
20    MissingRequiredHeader(&'static str),
21    /// A required header is present but its value does not parse.
22    MalformedRequiredHeader(&'static str),
23    /// A header the RFC permits at most once appears more than once (RFC 4475 §3.3.8).
24    RepeatedSingleValueHeader(&'static str),
25    /// The `CSeq` method does not match the request line (RFC 4475 §3.1.2.17, §3.1.2.18).
26    CSeqMethodMismatch,
27    /// The protocol version is one sipx does not speak; answer 505 (RFC 4475 §3.1.2.16).
28    UnsupportedVersion,
29    /// The Request-URI carries header components, which RFC 3261 §19.1.1 forbids
30    /// (RFC 4475 §3.1.2.11).
31    RequestUriHasHeaders,
32}
33
34impl Finding {
35    /// The response status an element should send for this finding.
36    #[must_use]
37    pub fn status(&self) -> u16 {
38        match self {
39            Self::UnsupportedVersion => 505,
40            _ => 400,
41        }
42    }
43
44    /// Whether a proxy may reasonably forward the message anyway.
45    ///
46    /// A missing `Max-Forwards` is the one finding a proxy is explicitly allowed to repair
47    /// rather than reject: RFC 3261 §16.6 step 3 says it MAY add the header itself. Every
48    /// other finding means the message cannot be safely acted on.
49    #[must_use]
50    pub fn is_repairable(&self) -> bool {
51        matches!(self, Self::MissingRequiredHeader("Max-Forwards"))
52    }
53}
54
55fn check_required<H: TypedHeader>(headers: &Headers, label: &'static str, out: &mut Vec<Finding>) {
56    match headers.typed::<H>() {
57        None => out.push(Finding::MissingRequiredHeader(label)),
58        Some(Err(_)) => out.push(Finding::MalformedRequiredHeader(label)),
59        Some(Ok(_)) => {}
60    }
61}
62
63fn check_single_value(
64    headers: &Headers,
65    name: &HeaderName,
66    label: &'static str,
67    out: &mut Vec<Finding>,
68) {
69    if headers.count(name) > 1 {
70        out.push(Finding::RepeatedSingleValueHeader(label));
71    }
72}
73
74/// Validate a request against RFC 3261 §8.1.1.
75#[must_use]
76pub fn validate_request(request: &Request) -> Vec<Finding> {
77    let mut out = Vec::new();
78    let headers = &request.headers;
79
80    if !request.version.is_supported() {
81        out.push(Finding::UnsupportedVersion);
82    }
83    if request.uri.has_headers() {
84        out.push(Finding::RequestUriHasHeaders);
85    }
86
87    check_required::<To>(headers, "To", &mut out);
88    check_required::<From>(headers, "From", &mut out);
89    check_required::<CallId>(headers, "Call-ID", &mut out);
90    check_required::<CSeq>(headers, "CSeq", &mut out);
91    check_required::<MaxForwards>(headers, "Max-Forwards", &mut out);
92    check_required::<Via>(headers, "Via", &mut out);
93
94    check_single_value(headers, &HeaderName::To, "To", &mut out);
95    check_single_value(headers, &HeaderName::From, "From", &mut out);
96    check_single_value(headers, &HeaderName::CallId, "Call-ID", &mut out);
97    check_single_value(headers, &HeaderName::CSeq, "CSeq", &mut out);
98    check_single_value(headers, &HeaderName::MaxForwards, "Max-Forwards", &mut out);
99
100    // The CSeq method must name the same method as the request line. A mismatch means one of
101    // the two is a forgery or a bug, and either way the transaction it would create is not
102    // the one the sender thinks.
103    if let Some(Ok(cseq)) = headers.typed::<CSeq>()
104        && cseq.method != request.method
105    {
106        out.push(Finding::CSeqMethodMismatch);
107    }
108
109    out
110}
111
112/// Validate a response.
113///
114/// A response has no Request-URI and no `Max-Forwards`, and its `CSeq` method names the
115/// request it answers rather than anything on its own start line, so there is nothing to
116/// cross-check there.
117#[must_use]
118pub fn validate_response(response: &Response) -> Vec<Finding> {
119    let mut out = Vec::new();
120    let headers = &response.headers;
121
122    if !response.version.is_supported() {
123        out.push(Finding::UnsupportedVersion);
124    }
125
126    check_required::<To>(headers, "To", &mut out);
127    check_required::<From>(headers, "From", &mut out);
128    check_required::<CallId>(headers, "Call-ID", &mut out);
129    check_required::<CSeq>(headers, "CSeq", &mut out);
130    check_required::<Via>(headers, "Via", &mut out);
131
132    check_single_value(headers, &HeaderName::To, "To", &mut out);
133    check_single_value(headers, &HeaderName::From, "From", &mut out);
134    check_single_value(headers, &HeaderName::CallId, "Call-ID", &mut out);
135    check_single_value(headers, &HeaderName::CSeq, "CSeq", &mut out);
136
137    out
138}
139
140/// Validate whichever kind of message this is.
141#[must_use]
142pub fn validate(message: &Message) -> Vec<Finding> {
143    match message {
144        Message::Request(r) => validate_request(r),
145        Message::Response(r) => validate_response(r),
146    }
147}
148
149#[cfg(test)]
150#[allow(
151    clippy::unwrap_used,
152    clippy::expect_used,
153    clippy::panic,
154    clippy::indexing_slicing
155)]
156mod tests {
157    use super::*;
158    use crate::{Limits, parse_datagram};
159    use bytes::Bytes;
160
161    fn findings(text: &str) -> Vec<Finding> {
162        let msg = parse_datagram(Bytes::from(text.to_owned()), &Limits::datagram())
163            .expect("should parse");
164        validate(&msg)
165    }
166
167    const GOOD: &str = "OPTIONS sip:a@b.com SIP/2.0\r\n\
168         Via: SIP/2.0/UDP h.example.com;branch=z9hG4bKx\r\n\
169         To: <sip:a@b.com>\r\n\
170         From: <sip:c@d.net>;tag=1\r\n\
171         Call-ID: x@y\r\n\
172         CSeq: 1 OPTIONS\r\n\
173         Max-Forwards: 70\r\n\
174         Content-Length: 0\r\n\r\n";
175
176    #[test]
177    fn a_well_formed_request_has_no_findings() {
178        assert_eq!(findings(GOOD), Vec::new());
179    }
180
181    #[test]
182    fn missing_required_headers_are_each_reported() {
183        let text = "OPTIONS sip:a@b.com SIP/2.0\r\nContent-Length: 0\r\n\r\n";
184        let found = findings(text);
185        for header in ["To", "From", "Call-ID", "CSeq", "Max-Forwards", "Via"] {
186            assert!(
187                found.contains(&Finding::MissingRequiredHeader(header)),
188                "{header} should be reported missing"
189            );
190        }
191    }
192
193    #[test]
194    fn a_cseq_method_mismatch_is_reported() {
195        let text = GOOD.replace("CSeq: 1 OPTIONS", "CSeq: 1 INVITE");
196        assert!(findings(&text).contains(&Finding::CSeqMethodMismatch));
197    }
198
199    #[test]
200    fn an_unsupported_version_asks_for_505_not_400() {
201        let text = GOOD.replace("SIP/2.0\r\nVia", "SIP/7.0\r\nVia");
202        let found = findings(&text);
203        assert!(found.contains(&Finding::UnsupportedVersion));
204        assert_eq!(Finding::UnsupportedVersion.status(), 505);
205    }
206
207    #[test]
208    fn a_repeated_single_value_header_is_reported() {
209        let text = GOOD.replace(
210            "To: <sip:a@b.com>",
211            "To: <sip:a@b.com>\r\nTo: <sip:e@f.org>",
212        );
213        assert!(findings(&text).contains(&Finding::RepeatedSingleValueHeader("To")));
214    }
215
216    #[test]
217    fn a_malformed_required_header_is_not_reported_as_missing() {
218        // The distinction the whole layering exists to preserve: present-and-broken is not
219        // the same as absent, and an element that conflates them answers the wrong thing.
220        let text = GOOD.replace("CSeq: 1 OPTIONS", "CSeq: 99999999999 OPTIONS");
221        let found = findings(&text);
222        assert!(found.contains(&Finding::MalformedRequiredHeader("CSeq")));
223        assert!(!found.contains(&Finding::MissingRequiredHeader("CSeq")));
224    }
225
226    #[test]
227    fn a_request_uri_with_headers_is_reported() {
228        let text = GOOD.replace(
229            "OPTIONS sip:a@b.com SIP/2.0",
230            "OPTIONS sip:a@b.com?Route=%3Csip:x%3E SIP/2.0",
231        );
232        assert!(findings(&text).contains(&Finding::RequestUriHasHeaders));
233    }
234
235    #[test]
236    fn a_missing_max_forwards_is_the_one_repairable_finding() {
237        assert!(Finding::MissingRequiredHeader("Max-Forwards").is_repairable());
238        assert!(!Finding::MissingRequiredHeader("Via").is_repairable());
239        assert!(!Finding::CSeqMethodMismatch.is_repairable());
240    }
241}