use csrf_sec::{Csrf, Reason, RequestFacts};
use form_net::{FormKind, Inspector, Limits, PartHead, Policy, Refusal, TokenCheck, Verdict};
use serve_net::HttpRequest;
pub(crate) const fn code(r: Reason) -> &'static str {
match r {
Reason::Missing => "csrf.missing",
Reason::Malformed => "csrf.malformed",
Reason::UnknownKey => "csrf.unknown_key",
Reason::BadMac => "csrf.bad_mac",
Reason::StaleSession => "csrf.stale_session",
Reason::Expired => "csrf.expired",
Reason::NotYetValid => "csrf.not_yet_valid",
Reason::WrongScope => "csrf.wrong_scope",
Reason::CrossSite => "csrf.cross_site",
Reason::OriginMismatch => "csrf.origin_mismatch",
Reason::Unverifiable => "csrf.unverifiable",
_ => "csrf.rejected",
}
}
pub(crate) fn token_check<'a>(
csrf: &'a Csrf,
binding: &'a [u8],
) -> TokenCheck<impl FnMut(&str) -> Result<(), Refusal> + 'a> {
TokenCheck::new(csrf_sec::FIELD_NAME, move |token| {
csrf.verify(binding, token)
.map(|_| ())
.map_err(|r| Refusal::new(403, code(r.reason())))
})
}
const TOKEN_POLICY: Policy =
Policy::DEFAULT.with_token_field(form_net::TokenField::new(csrf_sec::FIELD_NAME));
const SCAN_DONE: &str = "web.token_scan_done";
struct StopAtFile;
impl Inspector for StopAtFile {
fn part_start(&mut self, head: &PartHead<'_>) -> Verdict {
if head.is_file() {
Verdict::Refuse(Refusal::new(200, SCAN_DONE))
} else {
Verdict::Continue
}
}
}
pub(crate) fn check(
csrf: &Csrf,
req: &HttpRequest<'_>,
binding: Option<&[u8]>,
own_origin: &str,
limits: &Limits,
) -> Result<(), &'static str> {
let Some(binding) = binding.filter(|b| !b.is_empty()) else {
return Err("csrf.no_binding");
};
let method = req.head().method_str();
let mut facts = RequestFacts::new(method).own_origin(own_origin);
if let Some(v) = req.header("sec-fetch-site") {
facts = facts.fetch_site(v);
}
if let Some(v) = req.header("origin") {
facts = facts.origin(v);
}
if let Some(v) = req.header("referer") {
facts = facts.referer(v);
}
csrf.policy(&facts).map_err(|r| code(r.reason()))?;
let mut headers = req.head().header_all(csrf_sec::HEADER_NAME);
if let Some(token) = headers.next() {
if headers.next().is_some() {
return Err("csrf.duplicate");
}
return csrf
.verify(binding, token)
.map(|_| ())
.map_err(|r| code(r.reason()));
}
let content_type = req.header("content-type").unwrap_or(b"");
match FormKind::detect(content_type) {
Ok(FormKind::Urlencoded) => {
let mut check = token_check(csrf, binding);
match form_net::parse_urlencoded_inspected(
req.body_bytes(),
limits,
&Policy::DEFAULT,
&mut check,
) {
Ok(_) if check.verified() => Ok(()),
Ok(_) => Err("csrf.missing"),
Err(e) if e.code().starts_with("csrf.") => Err(e.code()),
Err(_) => Err("csrf.unreadable_form"),
}
}
Ok(FormKind::Multipart(_)) => {
let mut check = token_check(csrf, binding);
let scanned = form_net::parse_multipart_inspected(
content_type,
req.body_bytes(),
limits,
&TOKEN_POLICY,
&mut (&mut check, StopAtFile),
);
match scanned {
Ok(_) if check.verified() => Ok(()),
Err(e) if e.code() == SCAN_DONE && check.verified() => Ok(()),
Ok(_) => Err("csrf.missing"),
Err(e) if e.code() == SCAN_DONE => Err("csrf.missing"),
Err(e) if e.code().starts_with("csrf.") => Err(e.code()),
Err(e) if e.code() == "policy.duplicate_token" => Err("csrf.duplicate"),
Err(e) if e.code() == "policy.token_field_late" => Err("csrf.missing"),
Err(_) => Err("csrf.unreadable_form"),
}
}
_ => Err("csrf.missing"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_codes_are_namespaced() {
for r in [
Reason::Missing,
Reason::BadMac,
Reason::CrossSite,
Reason::Expired,
] {
assert!(code(r).starts_with("csrf."));
}
}
}