Skip to content

Commit

Permalink
pd: 🔐 extract auto-https into a standalone crate
Browse files Browse the repository at this point in the history
fixes #3119. see also, #1886.

this pulls the auto-https code (see #3627, #3652) into a standalone
library crate.
  • Loading branch information
cratelyn committed Jan 31, 2024
1 parent d991530 commit f06d892
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 94 deletions.
15 changes: 13 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ members = [
"crates/custody",
"crates/wallet",
"crates/view",
"crates/util/auto-https",
"crates/util/tendermint-proxy",
"crates/util/tower-trace",
"crates/bin/pd",
Expand Down
3 changes: 1 addition & 2 deletions crates/bin/pd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ penumbra-app = { path = "../../core/app" }
penumbra-custody = { path = "../../custody" }
penumbra-tower-trace = { path = "../../util/tower-trace" }
penumbra-tendermint-proxy = { path = "../../util/tendermint-proxy" }
penumbra-auto-https = { path = "../../util/auto-https" }

# Penumbra dependencies
decaf377 = { version = "0.5", features = ["parallel"] }
Expand Down Expand Up @@ -126,8 +127,6 @@ atty = "0.2"
fs_extra = "1.3.0"

axum-server = { version = "0.4.7", features = ["tls-rustls"] }
rustls = "0.20.9"
rustls-acme = { version = "0.6.0", features = ["axum"] }

[dev-dependencies]
penumbra-proof-params = { path = "../../crypto/proof-params", features = [
Expand Down
88 changes: 0 additions & 88 deletions crates/bin/pd/src/auto_https.rs
Original file line number Diff line number Diff line change
@@ -1,88 +0,0 @@
//! Automatic HTTPS certificate management facilities.
//!
//! See [`axum_acceptor`] for more information.
use {
anyhow::Error,
futures::Future,
rustls::ServerConfig,
rustls_acme::{axum::AxumAcceptor, caches::DirCache, AcmeConfig, AcmeState},
std::{fmt::Debug, path::PathBuf, sync::Arc},
};

/// Protocols supported by this server, in order of preference.
///
/// See [rfc7301] for more info on ALPN.
///
/// [rfc7301]: https://datatracker.ietf.org/doc/html/rfc7301
//
// We also permit HTTP1.1 for backwards-compatibility, specifically for grpc-web.
const ALPN_PROTOCOLS: [&[u8]; 2] = [b"h2", b"http/1.1"];

/// The location of the file-based certificate cache.
// NB: this must not be an absolute path see [Path::join].
const CACHE_DIR: &str = "tokio_rustls_acme_cache";

/// Use ACME to resolve certificates and handle new connections.
///
/// This returns a tuple containing an [`AxumAcceptor`] that may be used with [`axum_server`], and
/// a [`Future`] that represents the background task to poll and log for changes in the
/// certificate environment.
pub fn axum_acceptor(
home: PathBuf,
domain: String,
production_api: bool,
) -> (AxumAcceptor, impl Future<Output = Result<(), Error>>) {
// Use a file-based cache located within the home directory.
let cache = home.join(CACHE_DIR);
let cache = DirCache::new(cache);

// Create an ACME client, which we will use to resolve certificates.
let state = AcmeConfig::new(vec![domain])
.cache(cache)
.directory_lets_encrypt(production_api)
.state();

// Define our server configuration, using the ACME certificate resolver.
let mut rustls_config = ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_cert_resolver(state.resolver());
rustls_config.alpn_protocols = self::alpn_protocols();
let rustls_config = Arc::new(rustls_config);

// Return our connection acceptor and our background worker task.
let acceptor = state.axum_acceptor(rustls_config.clone());
let worker = self::acme_worker(state);
(acceptor, worker)
}

/// This function defines the task responsible for handling ACME events.
///
/// This function will never return, unless an error is encountered.
#[tracing::instrument(level = "error", skip_all)]
async fn acme_worker<EC, EA>(mut state: AcmeState<EC, EA>) -> Result<(), anyhow::Error>
where
EC: Debug + 'static,
EA: Debug + 'static,
{
use futures::StreamExt;
loop {
match state.next().await {
Some(Ok(ok)) => tracing::debug!("received acme event: {:?}", ok),
Some(Err(err)) => tracing::error!("acme error: {:?}", err),
None => {
debug_assert!(false, "acme worker unexpectedly reached end-of-stream");
tracing::error!("acme worker unexpectedly reached end-of-stream");
anyhow::bail!("unexpected end-of-stream");
}
}
}
}

/// Returns a vector of the protocols supported by this server.
///
/// This is a convenience method to retrieve an owned copy of [`ALPN_PROTOCOLS`].
fn alpn_protocols() -> Vec<Vec<u8>> {
ALPN_PROTOCOLS.into_iter().map(<[u8]>::to_vec).collect()
}
1 change: 0 additions & 1 deletion crates/bin/pd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ mod mempool;
mod metrics;
mod snapshot;

pub mod auto_https;
pub mod cli;
pub mod events;
pub mod migrate;
Expand Down
2 changes: 1 addition & 1 deletion crates/bin/pd/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ async fn main() -> anyhow::Result<()> {
let grpc_server = match grpc_auto_https {
Some(domain) => {
let (acceptor, acme_worker) =
pd::auto_https::axum_acceptor(pd_home, domain, !acme_staging);
penumbra_auto_https::axum_acceptor(pd_home, domain, !acme_staging);
// TODO(kate): we should eventually propagate errors from the ACME worker task.
tokio::spawn(acme_worker);
spawn_grpc_server!(grpc_server.acceptor(acceptor))
Expand Down
20 changes: 20 additions & 0 deletions crates/util/auto-https/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "penumbra-auto-https"
version = "0.65.0-alpha.1"
authors = ["Penumbra Labs <team@penumbra.zone>"]
edition = "2021"
description = "Automatic HTTPS management for Penumbra"
repository = "https://github.com/penumbra-zone/penumbra/"
homepage = "https://penumbra.zone"
license = "MIT OR Apache-2.0"
publish = false

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1"
futures = "0.3"
rustls = "0.20.9"
axum-server = { version = "0.4.7", features = [] }
rustls-acme = { version = "0.6.0", features = ["axum"] }
tracing = "0.1"
88 changes: 88 additions & 0 deletions crates/util/auto-https/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//! Automatic HTTPS certificate management facilities.
//!
//! See [`axum_acceptor`] for more information.
use {
anyhow::Error,
futures::Future,
rustls::ServerConfig,
rustls_acme::{axum::AxumAcceptor, caches::DirCache, AcmeConfig, AcmeState},
std::{fmt::Debug, path::PathBuf, sync::Arc},
};

/// Protocols supported by this server, in order of preference.
///
/// See [rfc7301] for more info on ALPN.
///
/// [rfc7301]: https://datatracker.ietf.org/doc/html/rfc7301
//
// We also permit HTTP1.1 for backwards-compatibility, specifically for grpc-web.
const ALPN_PROTOCOLS: [&[u8]; 2] = [b"h2", b"http/1.1"];

/// The location of the file-based certificate cache.
// NB: this must not be an absolute path see [Path::join].
const CACHE_DIR: &str = "tokio_rustls_acme_cache";

/// Use ACME to resolve certificates and handle new connections.
///
/// This returns a tuple containing an [`AxumAcceptor`] that may be used with [`axum_server`], and
/// a [`Future`] that represents the background task to poll and log for changes in the
/// certificate environment.
pub fn axum_acceptor(
home: PathBuf,
domain: String,
production_api: bool,
) -> (AxumAcceptor, impl Future<Output = Result<(), Error>>) {
// Use a file-based cache located within the home directory.
let cache = home.join(CACHE_DIR);
let cache = DirCache::new(cache);

// Create an ACME client, which we will use to resolve certificates.
let state = AcmeConfig::new(vec![domain])
.cache(cache)
.directory_lets_encrypt(production_api)
.state();

// Define our server configuration, using the ACME certificate resolver.
let mut rustls_config = ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_cert_resolver(state.resolver());
rustls_config.alpn_protocols = self::alpn_protocols();
let rustls_config = Arc::new(rustls_config);

// Return our connection acceptor and our background worker task.
let acceptor = state.axum_acceptor(rustls_config.clone());
let worker = self::acme_worker(state);
(acceptor, worker)
}

/// This function defines the task responsible for handling ACME events.
///
/// This function will never return, unless an error is encountered.
#[tracing::instrument(level = "error", skip_all)]
async fn acme_worker<EC, EA>(mut state: AcmeState<EC, EA>) -> Result<(), anyhow::Error>
where
EC: Debug + 'static,
EA: Debug + 'static,
{
use futures::StreamExt;
loop {
match state.next().await {
Some(Ok(ok)) => tracing::debug!("received acme event: {:?}", ok),
Some(Err(err)) => tracing::error!("acme error: {:?}", err),
None => {
debug_assert!(false, "acme worker unexpectedly reached end-of-stream");
tracing::error!("acme worker unexpectedly reached end-of-stream");
anyhow::bail!("unexpected end-of-stream");
}
}
}
}

/// Returns a vector of the protocols supported by this server.
///
/// This is a convenience method to retrieve an owned copy of [`ALPN_PROTOCOLS`].
fn alpn_protocols() -> Vec<Vec<u8>> {
ALPN_PROTOCOLS.into_iter().map(<[u8]>::to_vec).collect()
}

0 comments on commit f06d892

Please sign in to comment.