use std::sync::Arc;
use axum::{
body::Body,
extract::State,
http::{HeaderValue, Method, Request, header::HeaderName},
middleware::Next,
response::IntoResponse,
};
use acme_proxy_store as sqlite;
use acme_proxy_store::db::Database;
const REPLAY_NONCE: HeaderName = HeaderName::from_static("replay-nonce");
fn mints_nonce(method: &Method, path: &str) -> bool {
method == Method::POST || path == acme_proxy_core::routes::NEW_NONCE
}
pub async fn add_nonce_middleware(
State(database): State<Arc<Database>>,
request: Request<Body>,
next: Next,
) -> impl IntoResponse {
let wanted = mints_nonce(request.method(), request.uri().path());
let mut response = next.run(request).await;
if !wanted {
return response;
}
let nonce = sqlite::nonce::Nonce::new();
match nonce.save(&database).await {
Ok(()) => {
if let Ok(header_value) = HeaderValue::from_str(&nonce.value) {
response.headers_mut().insert(REPLAY_NONCE, header_value);
} else {
tracing::error!(event = "nonce_header_invalid", outcome = "failure");
}
}
Err(error) => {
tracing::error!(event = "nonce_persist_failed", outcome = "failure", error = %error);
}
}
response
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_the_exchanges_rfc8555_asks_for_mint_a_nonce() {
for path in [
"/newAccount",
"/newOrder",
"/chall/x",
"/directory",
"/nope",
] {
assert!(mints_nonce(&Method::POST, path), "POST {path}");
}
for method in [Method::GET, Method::HEAD, Method::POST] {
assert!(mints_nonce(&method, "/newNonce"), "{method} /newNonce");
}
for path in ["/directory", "/crl", "/renewalInfo/abc.def", "/nope"] {
assert!(!mints_nonce(&Method::GET, path), "GET {path}");
assert!(!mints_nonce(&Method::HEAD, path), "HEAD {path}");
}
}
}