use std::sync::Arc;
use acme_proxy_signer::relay::http01::TokenStore;
use axum::body::Body;
use axum::http::{Request, StatusCode, header};
use tower::ServiceExt;
mod common;
use common::{
RejectingCheck, TokenStoreSigner, default_challenges, no_notifications, test_app,
test_app_full, test_app_with_signer,
};
use acme_proxy_core::config::Config;
fn well_known(token: &str) -> String {
format!("/.well-known/acme-challenge/{token}")
}
#[tokio::test]
async fn a_published_token_is_served() {
let signer = Arc::new(TokenStoreSigner::new().await);
signer.0.publish("tok", "tok.thumbprint").await.unwrap();
let (app, _db) = test_app_with_signer(signer).await;
let res = app
.oneshot(Request::get(well_known("tok")).body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers()
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok()),
Some("application/octet-stream"),
);
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(&body[..], b"tok.thumbprint");
}
#[tokio::test]
async fn an_unknown_token_is_a_plain_not_found() {
let (app, _db) = test_app_with_signer(Arc::new(TokenStoreSigner::new().await)).await;
let res = app
.oneshot(
Request::get(well_known("absent"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let content_type = res
.headers()
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or_default()
.to_string();
assert!(
!content_type.contains("problem+json"),
"a public file's 404 must not masquerade as an ACME problem: {content_type}"
);
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
.await
.unwrap();
let body = String::from_utf8_lossy(&body);
assert!(
!body.contains("urn:ietf:params:acme:error:"),
"the body must not carry an ACME error type: {body}"
);
}
#[tokio::test]
async fn a_retracted_token_is_no_longer_served() {
let signer = Arc::new(TokenStoreSigner::new().await);
signer.0.publish("tok", "tok.thumbprint").await.unwrap();
signer.0.retract("tok").await;
let (app, _db) = test_app_with_signer(signer).await;
let res = app
.oneshot(Request::get(well_known("tok")).body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn the_route_is_absent_without_a_token_store() {
let res = test_app()
.await
.oneshot(Request::get(well_known("tok")).body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
.await
.unwrap();
assert!(body.is_empty(), "no handler should have run");
}
#[tokio::test]
async fn the_route_carries_no_acme_layers() {
let signer = Arc::new(TokenStoreSigner::new().await);
signer.0.publish("tok", "tok.thumbprint").await.unwrap();
let (app, _db) = test_app_with_signer(signer).await;
let res = app
.oneshot(Request::get(well_known("tok")).body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert!(
res.headers().get("replay-nonce").is_none(),
"a CA fetching a file must not be minted an ACME nonce"
);
assert!(
res.headers().get(header::LINK).is_none(),
"nor pointed at the ACME directory"
);
for hardened in [
header::STRICT_TRANSPORT_SECURITY,
header::X_CONTENT_TYPE_OPTIONS,
header::X_FRAME_OPTIONS,
] {
assert!(
res.headers().contains_key(&hardened),
"missing {hardened}, so the responder was merged outside the hardening layers"
);
}
}
#[tokio::test]
async fn the_route_is_not_filtered() {
let signer = Arc::new(TokenStoreSigner::new().await);
signer.0.publish("tok", "tok.thumbprint").await.unwrap();
let filter = common::policy_with(Arc::new(RejectingCheck::connections()));
let (app, _db) = test_app_full(
Config::default(),
signer,
filter,
default_challenges(),
no_notifications().await,
)
.await;
let res = app
.oneshot(Request::get(well_known("tok")).body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(
res.status(),
StatusCode::OK,
"a filter refusing every connection must not reach the responder"
);
}
#[tokio::test]
async fn a_token_published_elsewhere_is_served() {
let database = Arc::new(
acme_proxy_store::db::Database::connect_in_memory()
.await
.unwrap(),
);
let app = common::test_app_over(
database.clone(),
Arc::new(TokenStoreSigner::over(database.clone())),
)
.await;
TokenStoreSigner::over(database)
.0
.publish("tok", "tok.thumbprint")
.await
.unwrap();
let res = app
.oneshot(Request::get(well_known("tok")).body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK);
}
#[tokio::test]
async fn an_unreadable_store_is_a_plain_server_error() {
let database = Arc::new(
acme_proxy_store::db::Database::connect_in_memory()
.await
.unwrap(),
);
let app = common::test_app_over(
database.clone(),
Arc::new(TokenStoreSigner::over(database.clone())),
)
.await;
database.close().await;
let res = app
.oneshot(Request::get(well_known("tok")).body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
let content_type = res
.headers()
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or_default()
.to_string();
assert!(!content_type.contains("problem+json"), "{content_type}");
}