#![cfg(feature = "acme")]
use camber::acme::AcmeConfig;
use rustls::pki_types::pem::PemObject;
use rustls_acme::CertCache;
use rustls_acme::caches::DirCache;
fn run_async<F: std::future::Future>(f: F) -> F::Output {
tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(f))
}
#[test]
fn filesystem_cache_round_trip() {
let tmp = tempfile::tempdir().unwrap();
let cache = DirCache::new(tmp.path().to_path_buf());
let domains = vec!["example.com".to_string()];
let directory_url = "https://acme-staging-v02.api.letsencrypt.org/directory";
let fake_cert = b"-----BEGIN FAKE CERT-----\ntest data\n-----END FAKE CERT-----\n";
camber::runtime::test(|| {
run_async(async {
cache
.store_cert(&domains, directory_url, fake_cert)
.await
.unwrap();
let loaded = cache.load_cert(&domains, directory_url).await.unwrap();
assert_eq!(loaded.as_deref(), Some(&fake_cert[..]));
});
})
.unwrap();
}
#[test]
fn filesystem_cache_loads_on_startup() {
let tmp = tempfile::tempdir().unwrap();
let cache = DirCache::new(tmp.path().to_path_buf());
let domains = vec!["startup.example.com".to_string()];
let directory_url = "https://acme-staging-v02.api.letsencrypt.org/directory";
let fake_cert = b"pre-populated cert data";
camber::runtime::test(|| {
run_async(async {
cache
.store_cert(&domains, directory_url, fake_cert)
.await
.unwrap();
});
})
.unwrap();
let _config = AcmeConfig::new("camber", ["startup.example.com"])
.cache_dir(tmp.path())
.staging(true);
let cache2 = DirCache::new(tmp.path().to_path_buf());
camber::runtime::test(|| {
run_async(async {
let loaded = cache2.load_cert(&domains, directory_url).await.unwrap();
assert_eq!(
loaded.as_deref(),
Some(&fake_cert[..]),
"cert should be loadable from the pre-populated cache"
);
});
})
.unwrap();
}
#[test]
fn cert_persists_across_restarts() {
use std::sync::Arc;
let tmp = tempfile::tempdir().unwrap();
let cache = DirCache::new(tmp.path().to_path_buf());
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
let cert_pem = cert.cert.pem();
let key_pem = cert.signing_key.serialize_pem();
let cached_pem = format!("{key_pem}\n{cert_pem}");
let domains = vec!["localhost".to_string()];
let directory_url = "https://acme-staging-v02.api.letsencrypt.org/directory";
camber::runtime::test(|| {
run_async(async {
cache
.store_cert(&domains, directory_url, cached_pem.as_bytes())
.await
.unwrap();
});
})
.unwrap();
let acme_config = AcmeConfig::new("camber", ["localhost"])
.email("test@example.com")
.cache_dir(tmp.path())
.staging(true);
let cert_pem_bytes = cert_pem.into_bytes();
camber::runtime::builder()
.keepalive_timeout(std::time::Duration::from_millis(200))
.shutdown_timeout(std::time::Duration::from_secs(2))
.tls_auto(acme_config)
.run(|| {
let mut router = camber::http::Router::new();
router.get("/cached", |_req: &camber::http::Request| async {
camber::http::Response::text(200, "from cache")
});
let listener = camber::net::listen("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().tcp().unwrap();
camber::spawn(move || -> Result<(), camber::RuntimeError> {
camber::http::serve_listener(listener, router)
});
std::thread::sleep(std::time::Duration::from_millis(500));
let mut root_store = rustls::RootCertStore::empty();
let certs = rustls::pki_types::CertificateDer::pem_slice_iter(&cert_pem_bytes)
.collect::<Result<Vec<_>, _>>()
.unwrap();
for cert in certs {
root_store.add(cert).unwrap();
}
let client_config = rustls::ClientConfig::builder_with_provider(Arc::new(
rustls::crypto::aws_lc_rs::default_provider(),
))
.with_safe_default_protocol_versions()
.unwrap()
.with_root_certificates(root_store)
.with_no_client_auth();
let connector = tokio_rustls::TlsConnector::from(Arc::new(client_config));
let status = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
let tcp = tokio::net::TcpStream::connect(addr).await.unwrap();
let server_name = rustls::pki_types::ServerName::try_from("localhost").unwrap();
let tls_stream = connector.connect(server_name, tcp).await.unwrap();
let io = hyper_util::rt::TokioIo::new(tls_stream);
let (mut sender, conn) =
hyper::client::conn::http1::handshake(io).await.unwrap();
tokio::spawn(conn);
let req = hyper::Request::get("http://localhost/cached")
.body(http_body_util::Empty::<bytes::Bytes>::new())
.unwrap();
let resp = sender.send_request(req).await.unwrap();
resp.status().as_u16()
})
});
assert_eq!(status, 200);
camber::runtime::request_shutdown();
})
.unwrap();
}
#[test]
fn tls_auto_rejects_combined_with_manual_cert() {
let tmp = tempfile::tempdir().unwrap();
let cert_path = tmp.path().join("cert.pem");
let key_path = tmp.path().join("key.pem");
std::fs::write(&cert_path, b"not a real cert").unwrap();
std::fs::write(&key_path, b"not a real key").unwrap();
let acme_config = AcmeConfig::new("camber", ["example.com"])
.email("test@test.com")
.cache_dir(tmp.path())
.staging(true);
let result = camber::runtime::builder()
.tls_auto(acme_config)
.tls_cert(&cert_path)
.tls_key(&key_path)
.run(|| {});
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("mutually exclusive"),
"expected mutual exclusion error, got: {err}"
);
}
#[test]
fn tls_auto_builds_config() {
let tmp = tempfile::tempdir().unwrap();
let acme_config = AcmeConfig::new("camber", ["example.com"])
.email("admin@example.com")
.cache_dir(tmp.path())
.staging(true);
let result = camber::runtime::builder()
.shutdown_timeout(std::time::Duration::from_secs(1))
.tls_auto(acme_config)
.run(|| {
camber::runtime::request_shutdown();
});
assert!(result.is_ok(), "tls_auto build failed: {result:?}");
}