6db412f
use actix_web::body::{BoxBody, MessageBody};
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::http::Method;
use actix_web::middleware::Next;
use actix_web::web::Data;
use actix_web::{Error, HttpResponse};
use ::bootstrap::AppState;
const SKIP_PREFIXES: [&str; 5] = [
"/healthz",
"/readyz",
"/api/docs",
"/api/v1/internal",
"/pages",
];
pub async fn csrf<B>(
request: ServiceRequest,
next: Next<B>,
) -> Result<ServiceResponse<BoxBody>, Error>
where
B: MessageBody + 'static,
{
if matches!(
*request.method(),
Method::GET | Method::HEAD | Method::OPTIONS
) || SKIP_PREFIXES
.iter()
.any(|prefix| request.path().starts_with(prefix))
{
return Ok(next.call(request).await?.map_into_boxed_body());
}
let Some(state) = request
.app_data::<Data<AppState>>()
.map(|value| value.get_ref().clone())
else {
return Ok(next.call(request).await?.map_into_boxed_body());
};
let origin = request
.headers()
.get("origin")
.and_then(|value| value.to_str().ok())
.map(str::trim)
.filter(|value| !value.is_empty())
.or_else(|| {
request
.headers()
.get("referer")
.and_then(|value| value.to_str().ok())
.and_then(origin_of)
});
if let Some(origin) = origin {
if !allowed(&state, &request, origin) {
return Ok(request
.into_response(HttpResponse::Forbidden().json(serde_json::json!({
"code": 403,
"message": "cross-site request rejected",
"data": null,
"error": null,
"error_code": "csrf_rejected",
"request_id": api::middleware::request_id(),
})))
.map_into_boxed_body());
}
}
Ok(next.call(request).await?.map_into_boxed_body())
}
fn allowed(state: &AppState, request: &ServiceRequest, origin: &str) -> bool {
let origin = origin.trim_end_matches('/');
if state
.config
.web
.cors_allow_origins
.iter()
.any(|allowed| allowed.trim_end_matches('/') == origin)
{
return true;
}
let Some(host) = origin_host(origin) else {
return false;
};
host == request.connection_info().host()
}
fn origin_of(referer: &str) -> Option<&str> {
let scheme_end = referer.find("://")?;
let rest = &referer[scheme_end + 3..];
let end = rest.find('/').unwrap_or(rest.len());
Some(&referer[..scheme_end + 3 + end])
}
fn origin_host(origin: &str) -> Option<&str> {
let rest = origin.split_once("://")?.1;
rest.split('/')
.next()
.and_then(|authority| authority.rsplit('@').next())
}