use crate::headers::{CSeq, CallId, From, MaxForwards, To, Via};
use crate::message::{Headers, Message, Request, Response, TypedHeader};
use crate::name::HeaderName;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Finding {
MissingRequiredHeader(&'static str),
MalformedRequiredHeader(&'static str),
RepeatedSingleValueHeader(&'static str),
CSeqMethodMismatch,
UnsupportedVersion,
RequestUriHasHeaders,
}
impl Finding {
#[must_use]
pub fn status(&self) -> u16 {
match self {
Self::UnsupportedVersion => 505,
_ => 400,
}
}
#[must_use]
pub fn is_repairable(&self) -> bool {
matches!(self, Self::MissingRequiredHeader("Max-Forwards"))
}
}
fn check_required<H: TypedHeader>(headers: &Headers, label: &'static str, out: &mut Vec<Finding>) {
match headers.typed::<H>() {
None => out.push(Finding::MissingRequiredHeader(label)),
Some(Err(_)) => out.push(Finding::MalformedRequiredHeader(label)),
Some(Ok(_)) => {}
}
}
fn check_single_value(
headers: &Headers,
name: &HeaderName,
label: &'static str,
out: &mut Vec<Finding>,
) {
if headers.count(name) > 1 {
out.push(Finding::RepeatedSingleValueHeader(label));
}
}
#[must_use]
pub fn validate_request(request: &Request) -> Vec<Finding> {
let mut out = Vec::new();
let headers = &request.headers;
if !request.version.is_supported() {
out.push(Finding::UnsupportedVersion);
}
if request.uri.has_headers() {
out.push(Finding::RequestUriHasHeaders);
}
check_required::<To>(headers, "To", &mut out);
check_required::<From>(headers, "From", &mut out);
check_required::<CallId>(headers, "Call-ID", &mut out);
check_required::<CSeq>(headers, "CSeq", &mut out);
check_required::<MaxForwards>(headers, "Max-Forwards", &mut out);
check_required::<Via>(headers, "Via", &mut out);
check_single_value(headers, &HeaderName::To, "To", &mut out);
check_single_value(headers, &HeaderName::From, "From", &mut out);
check_single_value(headers, &HeaderName::CallId, "Call-ID", &mut out);
check_single_value(headers, &HeaderName::CSeq, "CSeq", &mut out);
check_single_value(headers, &HeaderName::MaxForwards, "Max-Forwards", &mut out);
if let Some(Ok(cseq)) = headers.typed::<CSeq>()
&& cseq.method != request.method
{
out.push(Finding::CSeqMethodMismatch);
}
out
}
#[must_use]
pub fn validate_response(response: &Response) -> Vec<Finding> {
let mut out = Vec::new();
let headers = &response.headers;
if !response.version.is_supported() {
out.push(Finding::UnsupportedVersion);
}
check_required::<To>(headers, "To", &mut out);
check_required::<From>(headers, "From", &mut out);
check_required::<CallId>(headers, "Call-ID", &mut out);
check_required::<CSeq>(headers, "CSeq", &mut out);
check_required::<Via>(headers, "Via", &mut out);
check_single_value(headers, &HeaderName::To, "To", &mut out);
check_single_value(headers, &HeaderName::From, "From", &mut out);
check_single_value(headers, &HeaderName::CallId, "Call-ID", &mut out);
check_single_value(headers, &HeaderName::CSeq, "CSeq", &mut out);
out
}
#[must_use]
pub fn validate(message: &Message) -> Vec<Finding> {
match message {
Message::Request(r) => validate_request(r),
Message::Response(r) => validate_response(r),
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
mod tests {
use super::*;
use crate::{Limits, parse_datagram};
use bytes::Bytes;
fn findings(text: &str) -> Vec<Finding> {
let msg = parse_datagram(Bytes::from(text.to_owned()), &Limits::datagram())
.expect("should parse");
validate(&msg)
}
const GOOD: &str = "OPTIONS sip:a@b.com SIP/2.0\r\n\
Via: SIP/2.0/UDP h.example.com;branch=z9hG4bKx\r\n\
To: <sip:a@b.com>\r\n\
From: <sip:c@d.net>;tag=1\r\n\
Call-ID: x@y\r\n\
CSeq: 1 OPTIONS\r\n\
Max-Forwards: 70\r\n\
Content-Length: 0\r\n\r\n";
#[test]
fn a_well_formed_request_has_no_findings() {
assert_eq!(findings(GOOD), Vec::new());
}
#[test]
fn missing_required_headers_are_each_reported() {
let text = "OPTIONS sip:a@b.com SIP/2.0\r\nContent-Length: 0\r\n\r\n";
let found = findings(text);
for header in ["To", "From", "Call-ID", "CSeq", "Max-Forwards", "Via"] {
assert!(
found.contains(&Finding::MissingRequiredHeader(header)),
"{header} should be reported missing"
);
}
}
#[test]
fn a_cseq_method_mismatch_is_reported() {
let text = GOOD.replace("CSeq: 1 OPTIONS", "CSeq: 1 INVITE");
assert!(findings(&text).contains(&Finding::CSeqMethodMismatch));
}
#[test]
fn an_unsupported_version_asks_for_505_not_400() {
let text = GOOD.replace("SIP/2.0\r\nVia", "SIP/7.0\r\nVia");
let found = findings(&text);
assert!(found.contains(&Finding::UnsupportedVersion));
assert_eq!(Finding::UnsupportedVersion.status(), 505);
}
#[test]
fn a_repeated_single_value_header_is_reported() {
let text = GOOD.replace(
"To: <sip:a@b.com>",
"To: <sip:a@b.com>\r\nTo: <sip:e@f.org>",
);
assert!(findings(&text).contains(&Finding::RepeatedSingleValueHeader("To")));
}
#[test]
fn a_malformed_required_header_is_not_reported_as_missing() {
let text = GOOD.replace("CSeq: 1 OPTIONS", "CSeq: 99999999999 OPTIONS");
let found = findings(&text);
assert!(found.contains(&Finding::MalformedRequiredHeader("CSeq")));
assert!(!found.contains(&Finding::MissingRequiredHeader("CSeq")));
}
#[test]
fn a_request_uri_with_headers_is_reported() {
let text = GOOD.replace(
"OPTIONS sip:a@b.com SIP/2.0",
"OPTIONS sip:a@b.com?Route=%3Csip:x%3E SIP/2.0",
);
assert!(findings(&text).contains(&Finding::RequestUriHasHeaders));
}
#[test]
fn a_missing_max_forwards_is_the_one_repairable_finding() {
assert!(Finding::MissingRequiredHeader("Max-Forwards").is_repairable());
assert!(!Finding::MissingRequiredHeader("Via").is_repairable());
assert!(!Finding::CSeqMethodMismatch.is_repairable());
}
}