1use crate::headers::{CSeq, CallId, From, MaxForwards, To, Via};
13use crate::message::{Headers, Message, Request, Response, TypedHeader};
14use crate::name::HeaderName;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum Finding {
19 MissingRequiredHeader(&'static str),
21 MalformedRequiredHeader(&'static str),
23 RepeatedSingleValueHeader(&'static str),
25 CSeqMethodMismatch,
27 UnsupportedVersion,
29 RequestUriHasHeaders,
32}
33
34impl Finding {
35 #[must_use]
37 pub fn status(&self) -> u16 {
38 match self {
39 Self::UnsupportedVersion => 505,
40 _ => 400,
41 }
42 }
43
44 #[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#[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 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#[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#[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 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}