1use crate::error::{Error, Result};
40use crate::middleware::{Middleware, Next};
41use crate::request::Request;
42use crate::response::Response;
43use crate::security::constant_time_eq;
44use async_trait::async_trait;
45use std::sync::Arc;
46
47type Exempt = dyn Fn(&Request) -> bool + Send + Sync;
48
49pub struct Csrf {
51 header: String,
52 field: String,
53 allowed_origins: Vec<String>,
54 exempt: Option<Arc<Exempt>>,
55}
56
57impl Csrf {
58 pub fn new() -> Self {
59 Self {
60 header: "x-csrf-token".into(),
61 field: "csrf_token".into(),
62 allowed_origins: Vec::new(),
63 exempt: None,
64 }
65 }
66
67 pub fn header_name(mut self, name: impl Into<String>) -> Self {
69 self.header = name.into().to_ascii_lowercase();
70 self
71 }
72
73 pub fn field_name(mut self, name: impl Into<String>) -> Self {
75 self.field = name.into();
76 self
77 }
78
79 pub fn allow_origin(mut self, origin: impl Into<String>) -> Self {
81 self.allowed_origins.push(origin.into());
82 self
83 }
84
85 pub fn exempt<F>(mut self, f: F) -> Self
88 where
89 F: Fn(&Request) -> bool + Send + Sync + 'static,
90 {
91 self.exempt = Some(Arc::new(f));
92 self
93 }
94
95 fn origin_ok(&self, req: &Request) -> bool {
96 let header = |name: &str| req.headers.get(name).and_then(|v| v.to_str().ok());
97 if let Some(origin) = header("origin") {
98 if self.allowed_origins.iter().any(|o| o == origin) {
99 return true;
100 }
101 let authority = origin.split_once("://").map(|(_, rest)| rest);
103 return match (authority, header("host")) {
104 (Some(a), Some(h)) => a.eq_ignore_ascii_case(h),
105 _ => false,
106 };
107 }
108 match header("sec-fetch-site") {
109 Some("same-origin") | Some("none") | None => true,
110 Some(_) => false,
111 }
112 }
113
114 fn submitted_token(&self, req: &Request) -> Option<String> {
115 if let Some(t) = req
116 .headers
117 .get(self.header.as_str())
118 .and_then(|v| v.to_str().ok())
119 {
120 return Some(t.to_string());
121 }
122 let content_type = req
123 .headers
124 .get(http::header::CONTENT_TYPE)
125 .and_then(|v| v.to_str().ok())
126 .unwrap_or("")
127 .to_ascii_lowercase();
128 if content_type.starts_with("application/x-www-form-urlencoded") {
129 req.form_data().ok()?.get(&self.field).map(str::to_owned)
130 } else if content_type.starts_with("multipart/form-data") {
131 req.multipart().ok()?.text(&self.field).map(str::to_owned)
132 } else {
133 None
134 }
135 }
136}
137
138impl Default for Csrf {
139 fn default() -> Self {
140 Self::new()
141 }
142}
143
144#[async_trait]
145impl Middleware for Csrf {
146 async fn handle(&self, req: Request, next: Next) -> Result<Response> {
147 let safe = matches!(
148 req.method,
149 http::Method::GET | http::Method::HEAD | http::Method::OPTIONS | http::Method::TRACE
150 );
151 if safe || self.exempt.as_ref().is_some_and(|f| f(&req)) {
152 return next.run(req).await;
153 }
154
155 if !self.origin_ok(&req) {
156 return Err(Error::forbidden("Cross-site request blocked"));
157 }
158 let expected: Option<String> = req.session()?.get("_csrf");
160 let submitted = self.submitted_token(&req);
161 match (expected, submitted) {
162 (Some(expected), Some(given))
163 if constant_time_eq(expected.as_bytes(), given.as_bytes()) =>
164 {
165 next.run(req).await
166 }
167 _ => Err(Error::forbidden("CSRF token missing or invalid")),
168 }
169 }
170}