use axum::extract::Request;
use axum::http::{Method, StatusCode, header};
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
pub fn allowed(
method: &Method,
origin: Option<&str>,
site: Option<&str>,
ours: &[String],
) -> Result<(), String> {
if !matches!(
*method,
Method::POST | Method::PUT | Method::PATCH | Method::DELETE
) {
return Ok(());
}
if let Some(origin) = origin
&& !ours.iter().any(|o| o == origin)
{
return Err(format!(
"a request from {origin} may not change this store. This server answers the page \
it serves and programs on this machine, not other websites."
));
}
if let Some(site) = site
&& site != "same-origin"
&& site != "none"
{
return Err(format!(
"a {site} request may not change this store. This server answers the page it \
serves and programs on this machine, not other websites."
));
}
Ok(())
}
pub async fn guard(ours: std::sync::Arc<Vec<String>>, req: Request, next: Next) -> Response {
let (origin, site, method) = {
let headers = req.headers();
let owned = |name: &str| {
headers
.get(name)
.and_then(|v| v.to_str().ok())
.map(str::to_string)
};
(
owned(header::ORIGIN.as_str()),
owned("sec-fetch-site"),
req.method().clone(),
)
};
match allowed(&method, origin.as_deref(), site.as_deref(), &ours) {
Ok(()) => next.run(req).await,
Err(why) => (
StatusCode::FORBIDDEN,
axum::Json(serde_json::json!({
"error": { "code": "policy-refusal", "message": why, "exit_code": 3 }
})),
)
.into_response(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ours() -> Vec<String> {
vec![
"http://127.0.0.1:7777".to_string(),
"http://localhost:7777".to_string(),
]
}
#[test]
fn reading_is_never_refused_whoever_asks() {
for site in [None, Some("cross-site"), Some("same-site")] {
assert!(
allowed(&Method::GET, Some("https://evil.example"), site, &ours()).is_ok(),
"a cross-site read cannot see its own answer; refusing it protects nothing"
);
}
}
#[test]
fn our_own_page_may_change_things() {
assert!(
allowed(
&Method::POST,
Some("http://127.0.0.1:7777"),
Some("same-origin"),
&ours()
)
.is_ok()
);
assert!(allowed(&Method::POST, Some("http://localhost:7777"), None, &ours()).is_ok());
}
#[test]
fn a_form_on_another_website_may_not() {
let why = allowed(
&Method::POST,
Some("https://evil.example"),
Some("cross-site"),
&ours(),
)
.unwrap_err();
assert!(why.contains("evil.example"), "{why}");
}
#[test]
fn a_cross_site_request_without_an_origin_is_still_refused() {
let why = allowed(&Method::POST, None, Some("cross-site"), &ours()).unwrap_err();
assert!(why.contains("cross-site"), "{why}");
assert!(allowed(&Method::POST, None, Some("same-site"), &ours()).is_err());
}
#[test]
fn a_program_on_this_machine_is_admitted() {
assert!(allowed(&Method::POST, None, None, &ours()).is_ok());
assert!(allowed(&Method::DELETE, None, Some("none"), &ours()).is_ok());
}
#[test]
fn every_method_that_changes_something_is_checked() {
for method in [Method::POST, Method::PUT, Method::PATCH, Method::DELETE] {
assert!(
allowed(&method, Some("https://evil.example"), None, &ours()).is_err(),
"{method} was not checked"
);
}
}
}