tide_acme/
lib.rs

1//! `tide-acme` helps you serve HTTPS with Tide using automatic certificates, via Let's Encrypt and
2//! ACME tls-alpn-01 challenges.
3//!
4//! To use `tide-acme`, set up HTTPS with Tide normally using `tide_rustls`, but instead of
5//! specifying a certificate and key, call the `acme` method to configure automatic certificates in
6//! the TLS listener:
7//!
8//! ```no_run
9//! use tide_acme::{AcmeConfig, TideRustlsExt};
10//! use tide_acme::rustls_acme::caches::DirCache;
11//!
12//! # async_std::task::block_on(async {
13//! let mut app = tide::new();
14//! app.at("/").get(|_| async { Ok("Hello TLS") });
15//! app.listen(
16//!     tide_rustls::TlsListener::build().addrs("0.0.0.0:443").acme(
17//!         AcmeConfig::new(vec!["domain.example"])
18//!             .contact_push("mailto:admin@example.org")
19//!             .cache(DirCache::new("/srv/example/tide-acme-cache-dir")),
20//!     ),
21//! )
22//! .await?;
23//! # tide::Result::Ok(())
24//! # });
25//! ```
26//!
27//! This will configure the TLS stack to obtain a certificate for the domain `domain.example`,
28//! which must be a domain for which your Tide server handles HTTPS traffic.
29//!
30//! On initial startup, your server will register a certificate via Let's Encrypt. Let's Encrypt
31//! will verify your server's control of the domain via an [ACME tls-alpn-01
32//! challenge](https://tools.ietf.org/html/rfc8737), which the TLS listener configured by
33//! `tide-acme` will respond to.
34//!
35//! You must supply a cache via [`AcmeConfig::cache`] or one of the other cache methods. This cache
36//! will keep the ACME account key and registered certificates between runs, needed to avoid
37//! hitting rate limits. You can use [`rustls_acme::caches::DirCache`] for a simple filesystem
38//! cache, or implement your own caching using the `rustls_acme` cache traits.
39//!
40//! By default, `tide-acme` will use the Let's Encrypt staging environment, which is suitable for
41//! testing purposes; it produces certificates signed by a staging root so that you can verify your
42//! stack is working, but those certificates will not be trusted in browsers or other HTTPS
43//! clients. The staging environment has more generous rate limits for use while testing.
44//!
45//! When you're ready to deploy to production, you can call `.directory_lets_encrypt(true)` to
46//! switch to the production Let's Encrypt environment, which produces certificates trusted in
47//! browsers and other HTTPS clients. The production environment has [stricter rate
48//! limits](https://letsencrypt.org/docs/rate-limits/).
49//!
50//! `tide-acme` builds upon [`tide-rustls`](https://crates.io/crates/tide-rustls) and
51//! [`rustls-acme`](https://crates.io/crates/rustls-acme).
52
53#![forbid(unsafe_code)]
54#![deny(missing_docs)]
55
56use std::fmt::Debug;
57
58use async_std::{net::TcpStream, stream::StreamExt};
59use futures_lite::io::AsyncWriteExt;
60pub use rustls_acme::{self, AcmeConfig};
61use tide_rustls::async_rustls::{server::TlsStream, TlsAcceptor};
62use tide_rustls::rustls::Session;
63use tracing::{error, info, info_span, Instrument};
64
65/// Custom TLS acceptor that answers ACME tls-alpn-01 challenges.
66pub struct AcmeTlsAcceptor(TlsAcceptor);
67
68impl AcmeTlsAcceptor {
69    /// Create a new TLS acceptor that answers ACME tls-alpn-01 challenges, based on the specified
70    /// configuration.
71    ///
72    /// This will start a background task to manage certificates via ACME.
73    pub fn new<EC: 'static + Debug, EA: 'static + Debug>(config: AcmeConfig<EC, EA>) -> Self {
74        let mut state = config.state();
75        let acceptor = state.acceptor();
76        async_std::task::spawn(async move {
77            loop {
78                async {
79                    match state
80                        .next()
81                        .await
82                        .expect("AcmeState::next() always returns Some")
83                    {
84                        Ok(event) => info!(?event, "AcmeState::next() processed an event"),
85                        Err(event) => error!(?event, "AcmeState::next() returned an error"),
86                    }
87                }
88                .instrument(info_span!("AcmeState::next()"))
89                .await
90            }
91        });
92        Self(acceptor)
93    }
94}
95
96#[async_trait::async_trait]
97impl tide_rustls::CustomTlsAcceptor for AcmeTlsAcceptor {
98    async fn accept(&self, stream: TcpStream) -> std::io::Result<Option<TlsStream<TcpStream>>> {
99        let mut tls = self.0.accept(stream).await?;
100        match tls.get_ref().1.get_alpn_protocol() {
101            Some(rustls_acme::acme::ACME_TLS_ALPN_NAME) => {
102                info_span!("AcmeTlsAcceptor::accept()")
103                    .in_scope(|| info!("received acme-tls/1 validation request"));
104                tls.close().await?;
105                Ok(None)
106            }
107            _ => Ok(Some(tls)),
108        }
109    }
110}
111
112/// Extension trait for [`tide_rustls::TlsListenerBuilder`]
113///
114/// With this trait imported, `TlsListenerBuilder` will have an `acme` method to set up a custom
115/// TLS acceptor that answers ACME tls-alpn-01 challenges.
116pub trait TideRustlsExt {
117    /// Set up a custom TLS acceptor that answers ACME tls-alpn-01 challenges, using the specified
118    /// configuration.
119    ///
120    /// This creates an [`AcmeTlsAcceptor`], which will start a background task to manage
121    /// certificates via ACME.
122    fn acme<EC: 'static + Debug, EA: 'static + Debug>(self, config: AcmeConfig<EC, EA>) -> Self;
123}
124
125impl<State> TideRustlsExt for tide_rustls::TlsListenerBuilder<State> {
126    fn acme<EC: 'static + Debug, EA: 'static + Debug>(self, config: AcmeConfig<EC, EA>) -> Self {
127        self.tls_acceptor(std::sync::Arc::new(AcmeTlsAcceptor::new(config)))
128    }
129}