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}