#![expect(
clippy::expect_used,
reason = "example/test/bench: panic-on-error and print-for-output are the standard patterns for demos and harnesses"
)]
use rama::{
Layer, Service,
crypto::{
dep::{
aws_lc_rs::rand::SystemRandom,
rcgen::{
self, CertificateParams, CertificateSigningRequest, DistinguishedName, DnType,
},
},
jose::EcdsaKey,
},
graceful,
http::{
client::EasyHttpWebClient,
headers::ContentType,
layer::{compression::CompressionLayer, trace::TraceLayer},
server::HttpServer,
service::web::WebService,
service::web::response::{Headers, IntoResponse},
},
layer::ConsumeErrLayer,
rt::Executor,
service::service_fn,
tcp::server::TcpListener,
telemetry::tracing::{
self,
level_filters::LevelFilter,
subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt},
},
tls::{
acme::{
AcmeClient,
proto::{
client::{CreateAccountOptions, KeyAuthorization, NewOrderPayload},
common::Identifier,
server::{ChallengeType, OrderStatus},
},
},
boring::server::TlsAcceptorLayer,
},
tls::{
client::{ServerVerifyMode, TlsClientConfig},
server::{ServerAuthData, TlsServerConfig},
},
};
use std::{convert::Infallible, sync::Arc, time::Duration};
use tokio::time::sleep;
const TEST_DIRECTORY_URL: &str = "https://localhost:14000/dir";
const ADDR: &str = "0.0.0.0:5002";
#[tokio::main]
async fn main() {
tracing::subscriber::registry()
.with(fmt::layer())
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::DEBUG.into())
.from_env_lossy(),
)
.init();
let tls_config = TlsClientConfig::default_http().with_server_verify(ServerVerifyMode::Disable);
let graceful = graceful::Shutdown::default();
let client = EasyHttpWebClient::connector_builder()
.with_default_transport_connector()
.with_default_dns_connector()
.without_tls_proxy_support()
.without_proxy_support()
.with_tls_support_using_boringssl(tls_config)
.with_default_http_connector(Executor::graceful(graceful.guard()))
.build_client()
.boxed();
let client = AcmeClient::try_new(TEST_DIRECTORY_URL, client)
.await
.expect("create acme client");
let account = client
.create_account(CreateAccountOptions {
terms_of_service_agreed: Some(true),
..Default::default()
})
.await
.expect("create account");
let alg = account.key().alg();
let pkcs8 = account.key().pkcs8_der().expect("create der");
let account_key =
EcdsaKey::from_pkcs8_der(alg, pkcs8.as_ref(), SystemRandom::new()).expect("load from der");
let account = client
.load_account(account_key)
.await
.expect("create account");
let mut order = account
.try_new_order(NewOrderPayload {
identifiers: vec![Identifier::dns("example.com")],
..Default::default()
})
.await
.expect("create order");
let authz = order
.get_authorizations()
.await
.expect("get order authorizations");
let auth = &authz[0];
let mut challenge = auth
.challenges
.iter()
.find(|challenge| challenge.r#type == ChallengeType::Http01)
.expect("find http challenge")
.to_owned();
let key_authorization = order
.create_key_authorization(&challenge)
.expect("create key authorization");
let path = format!(".well-known/acme-challenge/{}", challenge.token);
tracing::info!("running service at: {ADDR}");
let state = Arc::new(ChallengeState {
key_authorization: key_authorization.clone(),
});
let challenge_server_handle = graceful.spawn_task_fn(async move |guard| {
let exec = Executor::graceful(guard.clone());
HttpServer::auto(exec)
.listen(
ADDR,
(TraceLayer::new_for_http(), CompressionLayer::new()).into_layer(
WebService::default().with_get(&path, move || {
let state = state.clone();
std::future::ready((
Headers::single(ContentType::octet_stream()),
state.key_authorization.as_str().to_owned(),
))
}),
),
)
.await
.expect("http server");
});
sleep(Duration::from_millis(1000)).await;
order
.finish_challenge(&mut challenge)
.await
.expect("finish challenge");
let state = order
.wait_until_all_authorizations_finished()
.await
.expect("wait until authorizations are finished");
assert_eq!(state.status, OrderStatus::Ready);
let (key_pair, csr) = create_csr();
order.finalize(csr.der()).await.expect("finalize order");
let cert_chain = order
.download_certificate_chain()
.await
.expect("download certificate");
tracing::info!(?cert_chain, "received certificiate");
challenge_server_handle.abort();
let server_auth = ServerAuthData {
cert_chain,
private_key: key_pair.into(),
ocsp: None,
};
graceful.spawn_task_fn(async |guard| {
let exec = Executor::graceful(guard.clone());
let http_service = HttpServer::auto(exec.clone()).service(service_fn(async || {
Ok::<_, Infallible>("hello".into_response())
}));
let tls_server_config = TlsServerConfig::new().with_single_cert(server_auth);
let tcp_service = (
ConsumeErrLayer::default(),
TlsAcceptorLayer::new(tls_server_config),
)
.into_layer(http_service);
TcpListener::bind_address(ADDR, exec)
.await
.expect("bind TCP Listener: http")
.serve(tcp_service)
.await;
});
graceful
.shutdown_with_limit(Duration::from_secs(30))
.await
.expect("graceful shutdown");
}
#[derive(Debug)]
struct ChallengeState {
key_authorization: KeyAuthorization,
}
fn create_csr() -> (rcgen::KeyPair, CertificateSigningRequest) {
let key_pair = rcgen::KeyPair::generate().expect("create keypair");
let mut params =
CertificateParams::new(vec!["example.com".to_owned()]).expect("create certificate params");
let mut distinguished_name = DistinguishedName::new();
distinguished_name.push(DnType::CountryName, "BE");
distinguished_name.push(DnType::LocalityName, "Ghent");
distinguished_name.push(DnType::OrganizationName, "Plabayo");
distinguished_name.push(DnType::CommonName, "example.com");
params.distinguished_name = distinguished_name;
let csr = params
.serialize_request(&key_pair)
.expect("create certificate signing request");
(key_pair, csr)
}