trillium_acme/lib.rs
1//! trillium-acme helps you serve HTTPS with [Trillium](https://trillium.rs) using automatic
2//! certificates, via Let’s Encrypt and ACME tls-alpn-01 challenges.
3//!
4//! To use `trillium-acme`, create an [`AcmeConfig`] to configure the certificate you want, then
5//! call [`trillium_acme::new`] to create an [`Acceptor`] and a future. Spawn the future using the
6//! same stopper as the server, then pass the [`Acceptor`] to the server configuration:
7//!
8//! ```rust,no_run
9//! use trillium_acme::AcmeConfig;
10//! use trillium_acme::rustls_acme::caches::DirCache;
11//!
12//! let config = AcmeConfig::new(["domain.example"])
13//! .contact_push("mailto:admin@example.org")
14//! .cache(DirCache::new("/srv/example/acme-cache-dir"));
15//!
16//! let (acceptor, future) = trillium_acme::new(config);
17//! let stopper = trillium_smol::Stopper::new();
18//! let future = stopper.stop_future(future);
19//! trillium_smol::spawn(async {
20//! future.await;
21//! });
22//! trillium_smol::config()
23//! .with_port(443)
24//! .with_host("0.0.0.0")
25//! .with_nodelay()
26//! .with_acceptor(acceptor)
27//! .with_stopper(stopper)
28//! .run(|conn: trillium::Conn| async move {
29//! conn.ok("Hello TLS!")
30//! });
31//! ```
32//!
33//! This will configure the TLS stack to obtain a certificate for the domain `domain.example`,
34//! which must be a domain for which your Trillium server handles HTTPS traffic.
35//!
36//! On initial startup, your server will register a certificate via Let's Encrypt. Let's Encrypt
37//! will verify your server's control of the domain via an
38//! [ACME tls-alpn-01 challenge](https://tools.ietf.org/html/rfc8737), which the TLS listener
39//! configured by `trillium-acme` will respond to.
40//!
41//! You must supply a cache via [`AcmeConfig::cache`] or one of the other cache methods. This cache
42//! will keep the ACME account key and registered certificates between runs, needed to avoid
43//! hitting rate limits. You can use [`rustls_acme::caches::DirCache`] for a simple filesystem
44//! cache, or implement your own caching using the `rustls_acme` cache traits.
45//!
46//! By default, `trillium-acme` will use the Let's Encrypt staging environment, which is suitable
47//! for testing purposes; it produces certificates signed by a staging root so that you can verify
48//! your stack is working, but those certificates will not be trusted in browsers or other HTTPS
49//! clients. The staging environment has more generous rate limits for use while testing.
50//!
51//! When you're ready to deploy to production, you can call `.directory_lets_encrypt(true)` to
52//! switch to the production Let's Encrypt environment, which produces certificates trusted in
53//! browsers and other HTTPS clients. The production environment has
54//! [stricter rate limits](https://letsencrypt.org/docs/rate-limits/).
55//!
56//! `trillium-acme` builds upon the [`rustls-acme`](https://crates.io/crates/rustls-acme) crate.
57
58#![forbid(unsafe_code)]
59#![deny(
60 clippy::dbg_macro,
61 missing_copy_implementations,
62 rustdoc::missing_crate_level_docs,
63 missing_debug_implementations,
64 missing_docs,
65 nonstandard_style,
66 unused_qualifications
67)]
68
69use std::fmt::Debug;
70use std::future::Future;
71use std::sync::Arc;
72
73use futures_lite::{AsyncWriteExt, StreamExt};
74use rustls_acme::futures_rustls::{rustls::ServerConfig, LazyConfigAcceptor};
75use trillium::log::{error, info};
76use trillium_server_common::async_trait;
77
78pub use rustls_acme::{self, AcmeConfig};
79
80mod transport;
81pub use transport::Transport;
82
83/// An acceptor that handles ACME tls-alpn-01 challenges.
84///
85/// After processing a challenge, this acceptor will return a Transport representing a closed
86/// connection.
87#[derive(Clone, Debug)]
88pub struct Acceptor {
89 challenge_server_config: Arc<ServerConfig>,
90 default_server_config: Arc<ServerConfig>,
91}
92
93/// Create a new [`Acceptor`] to pass to [`trillium_server_common::Config::with_acceptor`], and a
94/// new future that must be spawned detached in the background.
95pub fn new<EC: 'static + Debug, EA: 'static + Debug>(
96 config: AcmeConfig<EC, EA>,
97) -> (Acceptor, impl Future) {
98 let mut state = config.state();
99 let challenge_server_config = state.challenge_rustls_config();
100 let default_server_config = state.default_rustls_config();
101
102 let future = async move {
103 loop {
104 match state.next().await.unwrap() {
105 Ok(ok) => info!("ACME event: {:?}", ok),
106 Err(err) => error!("ACME error: {:?}", err),
107 }
108 }
109 };
110
111 (
112 Acceptor {
113 challenge_server_config,
114 default_server_config,
115 },
116 future,
117 )
118}
119
120#[async_trait]
121impl<Input> trillium_server_common::Acceptor<Input> for Acceptor
122where
123 Input: trillium_server_common::Transport,
124{
125 type Output = Transport<Input>;
126 type Error = std::io::Error;
127 async fn accept(&self, input: Input) -> Result<Self::Output, Self::Error> {
128 let start_handshake = LazyConfigAcceptor::new(Default::default(), input).await?;
129 if rustls_acme::is_tls_alpn_challenge(&start_handshake.client_hello()) {
130 let mut tls = start_handshake
131 .into_stream(self.challenge_server_config.clone())
132 .await?;
133 tls.close().await?;
134 Ok(Transport(None))
135 } else {
136 Ok(Transport(Some(
137 start_handshake
138 .into_stream(self.default_server_config.clone())
139 .await?,
140 )))
141 }
142 }
143}