use crate::forms::prisme::{aegis, sentinel};
use crate::utils::aliases::{ARuniqueConfig, StrMap, StrVecMap};
use crate::utils::trad::t;
use crate::utils::{
constante::session_key::session::CSRF_TOKEN_KEY, middleware::csrf::unmask_csrf_token,
};
use axum::{
body::Body,
http::{Method, Request, StatusCode},
response::{IntoResponse, Response},
};
use subtle::ConstantTimeEq;
#[derive(Clone)]
pub struct Prisme {
pub(crate) data: StrMap,
pub csrf_valid: bool,
}
impl Prisme {
pub fn checked_data(&self) -> Option<&StrMap> {
if self.csrf_valid {
Some(&self.data)
} else {
None
}
}
#[doc(hidden)]
pub fn for_test(data: StrMap, csrf_valid: bool) -> Self {
Self { data, csrf_valid }
}
}
pub async fn prisme_pipeline<S>(req: Request<Body>, state: &S) -> Result<Prisme, Response>
where
S: Send + Sync,
{
let config = req
.extensions()
.get::<ARuniqueConfig>()
.cloned()
.ok_or_else(|| {
(
StatusCode::INTERNAL_SERVER_ERROR,
t("forms.config_not_found").to_string(),
)
.into_response()
})?;
sentinel(&req, &config).map_err(|boxed| *boxed)?;
let csrf_session = req
.extensions()
.get::<crate::utils::csrf::CsrfToken>()
.cloned()
.ok_or_else(|| {
(
StatusCode::INTERNAL_SERVER_ERROR,
t("csrf.missing").to_string(),
)
.into_response()
})?;
let method = req.method().clone();
let content_type = req
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_string();
let header_token = req
.headers()
.get("x-csrf-token")
.and_then(|v| v.to_str().ok())
.map(str::to_string);
let parsed = aegis(req, state, config, &content_type).await?;
let csrf_valid = check_csrf(
&parsed,
csrf_session.as_str(),
&method,
header_token.as_deref(),
);
let data = convert_for_form(parsed);
Ok(Prisme { data, csrf_valid })
}
pub(crate) fn csrf_required(method: &Method) -> bool {
!matches!(*method, Method::GET | Method::HEAD)
}
fn check_csrf(
parsed: &StrVecMap,
csrf_session: &str,
method: &Method,
header_token: Option<&str>,
) -> bool {
if !csrf_required(method) {
return true;
}
let body_token = parsed
.get(CSRF_TOKEN_KEY)
.and_then(|v| v.last())
.map(String::as_str);
body_token
.or(header_token)
.map(|s| match unmask_csrf_token(s) {
Ok(unmasked) => bool::from(unmasked.as_bytes().ct_eq(csrf_session.as_bytes())),
Err(_) => false,
})
.unwrap_or(false)
}
fn convert_for_form(parsed: StrVecMap) -> StrMap {
parsed
.into_iter()
.map(|(k, v)| {
if k == CSRF_TOKEN_KEY {
(k, v.into_iter().next().unwrap_or_default())
} else {
(k, v.join(","))
}
})
.collect()
}
#[cfg(test)]
mod checked_data_tests {
use super::*;
#[test]
fn checked_data_gates_on_csrf_valid() {
let mut data = StrMap::new();
data.insert("field".to_string(), "value".to_string());
let invalid = Prisme {
data: data.clone(),
csrf_valid: false,
};
assert!(invalid.checked_data().is_none(), "CSRF KO → aucune donnée");
assert!(
!invalid.data.is_empty(),
".data reste peuplé en interne (pub(crate)) — seul l'accès externe est fermé"
);
let valid = Prisme {
data,
csrf_valid: true,
};
assert!(valid.checked_data().is_some(), "CSRF OK → données dispo");
}
#[test]
fn csrf_required_only_exempts_safe_methods() {
assert!(!csrf_required(&Method::GET), "GET exempté");
assert!(!csrf_required(&Method::HEAD), "HEAD exempté");
for m in [
Method::POST,
Method::PUT,
Method::PATCH,
Method::DELETE,
Method::OPTIONS,
Method::TRACE,
] {
assert!(csrf_required(&m), "{m} doit exiger un token CSRF");
}
}
}