use crate::axum::{
body::Body,
extract::{Request, State},
middleware::{from_fn, from_fn_with_state, Next},
response::Response,
Router,
};
use crate::config::CorsConfig;
use http::{header, HeaderValue, Method, StatusCode};
use std::sync::Arc;
use tower_http::{
catch_panic::CatchPanicLayer,
cors::{Any, CorsLayer},
};
type RouterTransform = Box<dyn FnOnce(Router) -> Router + Send>;
pub struct MiddlewareStack {
cors: bool,
cors_config: Option<CorsConfig>,
allowed_hosts: Vec<String>,
csrf: bool,
force_ssl: bool,
before: Vec<RouterTransform>,
after: Vec<RouterTransform>,
}
impl MiddlewareStack {
pub fn new() -> Self {
Self {
cors: false,
cors_config: None,
allowed_hosts: Vec::new(),
csrf: false,
force_ssl: false,
before: Vec::new(),
after: Vec::new(),
}
}
pub fn with_csrf(mut self) -> Self {
self.csrf = true;
self
}
pub fn with_force_ssl(mut self) -> Self {
self.force_ssl = true;
self
}
pub fn insert_before(
mut self,
transform: impl FnOnce(Router) -> Router + Send + 'static,
) -> Self {
self.before.push(Box::new(transform));
self
}
pub fn insert_after(
mut self,
transform: impl FnOnce(Router) -> Router + Send + 'static,
) -> Self {
self.after.push(Box::new(transform));
self
}
pub fn with_cors(mut self) -> Self {
self.cors = true;
self
}
pub fn with_cors_config(mut self, config: CorsConfig) -> Self {
self.cors_config = Some(config);
self
}
pub fn with_allowed_hosts(mut self, hosts: Vec<String>) -> Self {
self.allowed_hosts = hosts;
self
}
pub fn apply(self, router: Router) -> Router {
let mut r = router;
for transform in self.before {
r = transform(r);
}
r = r
.layer(CatchPanicLayer::new())
.layer(from_fn(crate::logging::log_requests));
match &self.cors_config {
Some(config) if config.enabled => r = r.layer(build_cors(config)),
_ if self.cors => r = r.layer(CorsLayer::permissive()),
_ => {}
}
if !self.allowed_hosts.is_empty() {
r = r.layer(from_fn_with_state(Arc::new(self.allowed_hosts), host_guard));
}
if self.csrf {
r = r.layer(from_fn(csrf_guard));
}
if self.force_ssl {
r = r.layer(from_fn(force_ssl_guard));
}
for transform in self.after {
r = transform(r);
}
r
}
}
async fn force_ssl_guard(request: Request, next: Next) -> Response {
let forwarded_https = request
.headers()
.get("x-forwarded-proto")
.and_then(|v| v.to_str().ok())
.map(|p| p.eq_ignore_ascii_case("https"))
.unwrap_or(false);
let scheme_https = request.uri().scheme_str() == Some("https");
if forwarded_https || scheme_https {
return next.run(request).await;
}
let host = request
.headers()
.get(header::HOST)
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let path = request
.uri()
.path_and_query()
.map(|pq| pq.as_str())
.unwrap_or("/");
let location = format!("https://{host}{path}");
match HeaderValue::from_str(&location) {
Ok(value) => {
let mut response = Response::builder()
.status(StatusCode::MOVED_PERMANENTLY)
.body(Body::empty())
.expect("valid 301 response");
response.headers_mut().insert(header::LOCATION, value);
response
}
Err(_) => Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Body::empty())
.expect("valid 400 response"),
}
}
async fn csrf_guard(request: Request, next: Next) -> Response {
let method = request.method();
let is_safe = matches!(
*method,
Method::GET | Method::HEAD | Method::OPTIONS | Method::TRACE
);
if is_safe {
return next.run(request).await;
}
let cookie_token = request
.headers()
.get(header::COOKIE)
.and_then(|c| c.to_str().ok())
.and_then(crate::csrf::token_from_cookie_header);
let header_token = request
.headers()
.get("x-csrf-token")
.and_then(|h| h.to_str().ok())
.map(str::to_string);
match (cookie_token, header_token) {
(Some(cookie), Some(header)) if crate::csrf::tokens_match(&cookie, &header) => {
next.run(request).await
}
_ => Response::builder()
.status(StatusCode::FORBIDDEN)
.body(Body::from("CSRF token mismatch"))
.expect("valid 403 response"),
}
}
async fn host_guard(
State(allowed): State<Arc<Vec<String>>>,
request: Request,
next: Next,
) -> Response {
let host = request
.headers()
.get(header::HOST)
.and_then(|h| h.to_str().ok())
.map(|h| h.split(':').next().unwrap_or(h).to_string());
match host {
Some(h) if allowed.iter().any(|a| a == &h) => next.run(request).await,
_ => Response::builder()
.status(StatusCode::FORBIDDEN)
.body(Body::from("Forbidden host"))
.expect("valid 403 response"),
}
}
fn build_cors(config: &CorsConfig) -> CorsLayer {
let mut layer = CorsLayer::new();
if config.allowed_origins.iter().any(|o| o == "*") {
layer = layer.allow_origin(Any);
} else {
let origins: Vec<HeaderValue> = config
.allowed_origins
.iter()
.filter_map(|o| o.parse().ok())
.collect();
if !origins.is_empty() {
layer = layer.allow_origin(origins);
}
}
let methods: Vec<Method> = config
.allowed_methods
.iter()
.filter_map(|m| Method::from_bytes(m.as_bytes()).ok())
.collect();
if !methods.is_empty() {
layer = layer.allow_methods(methods);
}
layer
}
impl Default for MiddlewareStack {
fn default() -> Self {
Self::new()
}
}