penumbra_sdk_auto_https/
lib.rs

1//! Automatic HTTPS certificate management facilities.
2//!
3//! See [`axum_acceptor`] for more information.
4
5use {
6    anyhow::Error,
7    futures::Future,
8    rustls::ServerConfig,
9    rustls_acme::{axum::AxumAcceptor, caches::DirCache, AcmeConfig, AcmeState},
10    std::{fmt::Debug, path::PathBuf, sync::Arc},
11};
12
13/// Protocols supported by this server, in order of preference.
14///
15/// See [rfc7301] for more info on ALPN.
16///
17/// [rfc7301]: https://datatracker.ietf.org/doc/html/rfc7301
18//
19//  We also permit HTTP1.1 for backwards-compatibility, specifically for grpc-web.
20const ALPN_PROTOCOLS: [&[u8]; 2] = [b"h2", b"http/1.1"];
21
22/// The location of the file-based certificate cache.
23//  NB: this must not be an absolute path see [Path::join].
24const CACHE_DIR: &str = "tokio_rustls_acme_cache";
25
26/// Use ACME to resolve certificates and handle new connections.
27///
28/// This returns a tuple containing an [`AxumAcceptor`] that may be used with [`axum_server`], and
29/// a [`Future`] that represents the background task to poll and log for changes in the
30/// certificate environment.
31pub fn axum_acceptor(
32    home: PathBuf,
33    domain: String,
34    production_api: bool,
35) -> (AxumAcceptor, impl Future<Output = Result<(), Error>>) {
36    // Use a file-based cache located within the home directory.
37    let cache = home.join(CACHE_DIR);
38    let cache = DirCache::new(cache);
39
40    // Create an ACME client, which we will use to resolve certificates.
41    let state = AcmeConfig::new(vec![domain])
42        .cache(cache)
43        .directory_lets_encrypt(production_api)
44        .state();
45
46    // Define our server configuration, using the ACME certificate resolver.
47    let mut rustls_config = ServerConfig::builder()
48        .with_no_client_auth()
49        .with_cert_resolver(state.resolver());
50    rustls_config.alpn_protocols = self::alpn_protocols();
51    let rustls_config = Arc::new(rustls_config);
52
53    // Return our connection acceptor and our background worker task.
54    let acceptor = state.axum_acceptor(rustls_config.clone());
55    let worker = self::acme_worker(state);
56    (acceptor, worker)
57}
58
59/// This function defines the task responsible for handling ACME events.
60///
61/// This function will never return, unless an error is encountered.
62#[tracing::instrument(level = "error", skip_all)]
63async fn acme_worker<EC, EA>(mut state: AcmeState<EC, EA>) -> Result<(), anyhow::Error>
64where
65    EC: Debug + 'static,
66    EA: Debug + 'static,
67{
68    use futures::StreamExt;
69    loop {
70        match state.next().await {
71            Some(Ok(ok)) => tracing::debug!("received acme event: {:?}", ok),
72            Some(Err(err)) => {
73                tracing::error!("acme error: {:?}", err);
74                anyhow::bail!("exiting due to acme error");
75            }
76            None => {
77                debug_assert!(false, "acme worker unexpectedly reached end-of-stream");
78                tracing::error!("acme worker unexpectedly reached end-of-stream");
79                anyhow::bail!("unexpected end-of-stream");
80            }
81        }
82    }
83}
84
85/// Returns a vector of the protocols supported by this server.
86///
87/// This is a convenience method to retrieve an owned copy of [`ALPN_PROTOCOLS`].
88fn alpn_protocols() -> Vec<Vec<u8>> {
89    ALPN_PROTOCOLS.into_iter().map(<[u8]>::to_vec).collect()
90}