use crate::error::AppError;
use axum::{
extract::Request,
http::{header, Method},
middleware::Next,
response::{IntoResponse, Response},
};
use std::future::Future;
use std::pin::Pin;
const ALLOWED_HOSTS_ENV: &str = "MOADIM_ALLOWED_HOSTS";
pub(crate) fn allowed_hosts() -> Vec<String> {
let bind = crate::cli::bind_addr();
let port = bind.rsplit_once(':').map(|(_, port)| port);
let mut hosts = vec![
"localhost".to_string(),
"127.0.0.1".to_string(),
"[::1]".to_string(),
bind.clone(),
];
if let Some(port) = port {
hosts.push(format!("localhost:{port}"));
hosts.push(format!("127.0.0.1:{port}"));
hosts.push(format!("[::1]:{port}"));
}
if let Ok(extra) = std::env::var(ALLOWED_HOSTS_ENV) {
hosts.extend(
extra
.split(',')
.map(str::trim)
.filter(|host| !host.is_empty())
.map(str::to_string),
);
}
hosts
}
fn origin_host(origin: &str) -> &str {
origin.split_once("://").map_or(origin, |(_, rest)| rest)
}
type ValidationFuture = Pin<Box<dyn Future<Output = Response> + Send>>;
pub(crate) fn host_validation(
allowed: Vec<String>,
) -> impl Fn(Request, Next) -> ValidationFuture + Clone + Send + Sync + 'static {
move |req: Request, next: Next| {
let allowed = allowed.clone();
Box::pin(async move {
match req.headers().get(header::HOST) {
None => {}
Some(value) => match value.to_str() {
Err(_) => {
return AppError::Forbidden("host header is not valid UTF-8".to_string())
.into_response();
}
Ok(host) => {
if !allowed.iter().any(|entry| entry.eq_ignore_ascii_case(host)) {
return AppError::Forbidden(format!("host '{host}' is not allowed"))
.into_response();
}
}
},
}
let is_state_changing = matches!(
*req.method(),
Method::POST | Method::PUT | Method::PATCH | Method::DELETE
);
if is_state_changing {
match req.headers().get(header::ORIGIN) {
None => {}
Some(value) => match value.to_str() {
Err(_) => {
return AppError::Forbidden(
"origin header is not valid UTF-8".to_string(),
)
.into_response();
}
Ok(origin) => {
let host = origin_host(origin);
if !allowed.iter().any(|entry| entry.eq_ignore_ascii_case(host)) {
return AppError::Forbidden(format!(
"origin '{origin}' is not allowed"
))
.into_response();
}
}
},
}
}
next.run(req).await
})
}
}
#[cfg(test)]
#[path = "host_validation_tests.rs"]
mod host_validation_tests;