use crate::error::{Error, Result};
use crate::middleware::{Middleware, Next};
use crate::request::Request;
use crate::response::Response;
use crate::security::constant_time_eq;
use async_trait::async_trait;
use std::sync::Arc;
type Exempt = dyn Fn(&Request) -> bool + Send + Sync;
pub struct Csrf {
header: String,
field: String,
allowed_origins: Vec<String>,
exempt: Option<Arc<Exempt>>,
}
impl Csrf {
pub fn new() -> Self {
Self {
header: "x-csrf-token".into(),
field: "csrf_token".into(),
allowed_origins: Vec::new(),
exempt: None,
}
}
pub fn header_name(mut self, name: impl Into<String>) -> Self {
self.header = name.into().to_ascii_lowercase();
self
}
pub fn field_name(mut self, name: impl Into<String>) -> Self {
self.field = name.into();
self
}
pub fn allow_origin(mut self, origin: impl Into<String>) -> Self {
self.allowed_origins.push(origin.into());
self
}
pub fn exempt<F>(mut self, f: F) -> Self
where
F: Fn(&Request) -> bool + Send + Sync + 'static,
{
self.exempt = Some(Arc::new(f));
self
}
fn origin_ok(&self, req: &Request) -> bool {
let header = |name: &str| req.headers.get(name).and_then(|v| v.to_str().ok());
if let Some(origin) = header("origin") {
if self.allowed_origins.iter().any(|o| o == origin) {
return true;
}
let authority = origin.split_once("://").map(|(_, rest)| rest);
return match (authority, header("host")) {
(Some(a), Some(h)) => a.eq_ignore_ascii_case(h),
_ => false,
};
}
match header("sec-fetch-site") {
Some("same-origin") | Some("none") | None => true,
Some(_) => false,
}
}
fn submitted_token(&self, req: &Request) -> Option<String> {
if let Some(t) = req
.headers
.get(self.header.as_str())
.and_then(|v| v.to_str().ok())
{
return Some(t.to_string());
}
let content_type = req
.headers
.get(http::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_ascii_lowercase();
if content_type.starts_with("application/x-www-form-urlencoded") {
req.form_data().ok()?.get(&self.field).map(str::to_owned)
} else if content_type.starts_with("multipart/form-data") {
req.multipart().ok()?.text(&self.field).map(str::to_owned)
} else {
None
}
}
}
impl Default for Csrf {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Middleware for Csrf {
async fn handle(&self, req: Request, next: Next) -> Result<Response> {
let safe = matches!(
req.method,
http::Method::GET | http::Method::HEAD | http::Method::OPTIONS | http::Method::TRACE
);
if safe || self.exempt.as_ref().is_some_and(|f| f(&req)) {
return next.run(req).await;
}
if !self.origin_ok(&req) {
return Err(Error::forbidden("Cross-site request blocked"));
}
let expected: Option<String> = req.session()?.get("_csrf");
let submitted = self.submitted_token(&req);
match (expected, submitted) {
(Some(expected), Some(given))
if constant_time_eq(expected.as_bytes(), given.as_bytes()) =>
{
next.run(req).await
}
_ => Err(Error::forbidden("CSRF token missing or invalid")),
}
}
}