|
1 |
| -use std::{ |
2 |
| - pin::Pin, |
3 |
| - task::{Context, Poll}, |
4 |
| -}; |
| 1 | +//! Automatic HTTPS certificate management facilities. |
| 2 | +//! |
| 3 | +//! See [`axum_acceptor`] for more information. |
5 | 4 |
|
6 |
| -use pin_project::pin_project; |
7 |
| -use rustls_acme::futures_rustls::server::TlsStream; |
8 |
| -use tokio::{ |
9 |
| - io::{AsyncRead, AsyncWrite, ReadBuf}, |
10 |
| - net::TcpStream, |
| 5 | +use { |
| 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 | 11 | };
|
12 |
| -use tokio_util::compat::Compat; |
13 |
| -use tonic::transport::server::Connected; |
14 |
| - |
15 |
| -/// Wrapper type needed to convert between futures_io and tokio traits |
16 |
| -#[pin_project] |
17 |
| -pub struct Wrapper { |
18 |
| - #[pin] |
19 |
| - pub inner: Compat<TlsStream<Compat<TcpStream>>>, |
20 |
| -} |
21 | 12 |
|
22 |
| -impl Connected for Wrapper { |
23 |
| - type ConnectInfo = <TcpStream as Connected>::ConnectInfo; |
| 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. |
| 20 | +const ALPN_PROTOCOLS: [&'static [u8]; 2] = [b"h2", b"http/1.1"]; |
24 | 21 |
|
25 |
| - fn connect_info(&self) -> Self::ConnectInfo { |
26 |
| - self.inner.get_ref().get_ref().0.get_ref().connect_info() |
27 |
| - } |
| 22 | +/// The location of the file-based certificate cache. |
| 23 | +// NB: this must not be an absolute path see [Path::join]. |
| 24 | +const CACHE_DIR: &'static str = "tokio_rustls_acme_cache"; |
| 25 | + |
| 26 | +/// If true, use the production Let's Encrypt environment. |
| 27 | +/// |
| 28 | +/// If false, the ACME resolver will use the [staging environment]. |
| 29 | +/// |
| 30 | +/// [staging environment]: https://letsencrypt.org/docs/staging-environment/ |
| 31 | +const PRODUCTION_LETS_ENCRYPT: bool = true; |
| 32 | + |
| 33 | +/// Use ACME to resolve certificates and handle new connections. |
| 34 | +/// |
| 35 | +/// This returns a tuple containing an [`AxumAcceptor`] that may be used with [`axum_server`], and |
| 36 | +/// a [`Future`] that represents the background task to poll and log for changes in the |
| 37 | +/// certificate environment. |
| 38 | +pub fn axum_acceptor( |
| 39 | + home: PathBuf, |
| 40 | + domain: String, |
| 41 | +) -> (AxumAcceptor, impl Future<Output = Result<(), Error>>) { |
| 42 | + // Use a file-based cache located within the home directory. |
| 43 | + let cache = home.join(CACHE_DIR); |
| 44 | + let cache = DirCache::new(cache); |
| 45 | + |
| 46 | + // Create an ACME client, which we will use to resolve certificates. |
| 47 | + let state = AcmeConfig::new(vec![domain]) |
| 48 | + .cache(cache) |
| 49 | + .directory_lets_encrypt(PRODUCTION_LETS_ENCRYPT) |
| 50 | + .state(); |
| 51 | + |
| 52 | + // Define our server configuration, using the ACME certificate resolver. |
| 53 | + let mut rustls_config = ServerConfig::builder() |
| 54 | + .with_safe_defaults() |
| 55 | + .with_no_client_auth() |
| 56 | + .with_cert_resolver(state.resolver()); |
| 57 | + rustls_config.alpn_protocols = self::alpn_protocols(); |
| 58 | + let rustls_config = Arc::new(rustls_config); |
| 59 | + |
| 60 | + // Return our connection acceptor and our background worker task. |
| 61 | + let acceptor = state.axum_acceptor(rustls_config.clone()); |
| 62 | + let worker = self::acme_worker(state); |
| 63 | + (acceptor, worker) |
28 | 64 | }
|
29 | 65 |
|
30 |
| -impl AsyncRead for Wrapper { |
31 |
| - fn poll_read( |
32 |
| - self: Pin<&mut Self>, |
33 |
| - cx: &mut Context<'_>, |
34 |
| - buf: &mut ReadBuf<'_>, |
35 |
| - ) -> Poll<std::io::Result<()>> { |
36 |
| - self.project().inner.poll_read(cx, buf) |
| 66 | +/// This function defines the task responsible for handling ACME events. |
| 67 | +/// |
| 68 | +/// This function will never return, unless an error is encountered. |
| 69 | +#[tracing::instrument(level = "error", skip_all)] |
| 70 | +async fn acme_worker<EC, EA>(mut state: AcmeState<EC, EA>) -> Result<(), anyhow::Error> |
| 71 | +where |
| 72 | + EC: Debug + 'static, |
| 73 | + EA: Debug + 'static, |
| 74 | +{ |
| 75 | + use futures::StreamExt; |
| 76 | + loop { |
| 77 | + match state.next().await { |
| 78 | + Some(Ok(ok)) => tracing::debug!("received acme event: {:?}", ok), |
| 79 | + Some(Err(err)) => tracing::error!("acme error: {:?}", err), |
| 80 | + None => { |
| 81 | + debug_assert!(false, "acme worker unexpectedly reached end-of-stream"); |
| 82 | + tracing::error!("acme worker unexpectedly reached end-of-stream"); |
| 83 | + anyhow::bail!("unexpected end-of-stream"); |
| 84 | + } |
| 85 | + } |
37 | 86 | }
|
38 | 87 | }
|
39 | 88 |
|
40 |
| -impl AsyncWrite for Wrapper { |
41 |
| - fn poll_write( |
42 |
| - self: Pin<&mut Self>, |
43 |
| - cx: &mut Context<'_>, |
44 |
| - buf: &[u8], |
45 |
| - ) -> Poll<std::io::Result<usize>> { |
46 |
| - self.project().inner.poll_write(cx, buf) |
47 |
| - } |
48 |
| - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> { |
49 |
| - self.project().inner.poll_flush(cx) |
50 |
| - } |
51 |
| - fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> { |
52 |
| - self.project().inner.poll_shutdown(cx) |
53 |
| - } |
| 89 | +/// Returns a vector of the protocols supported by this server. |
| 90 | +/// |
| 91 | +/// This is a convenience method to retrieve an owned copy of [`ALPN_PROTOCOLS`]. |
| 92 | +fn alpn_protocols() -> Vec<Vec<u8>> { |
| 93 | + ALPN_PROTOCOLS.into_iter().map(<[u8]>::to_vec).collect() |
54 | 94 | }
|
0 commit comments