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 export::AppState; const SKIP_PREFIXES: [&str; 5] = [ "/healthz", "/readyz", "/api/docs", "/api/v1/internal", "/pages", ]; pub async fn csrf( request: ServiceRequest, next: Next, ) -> Result, 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::>() .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": null, }))) .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()) }