use std::future::Future;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use axum::Router;
use axum_server::Handle;
use axum_server::tls_rustls::RustlsConfig;
use futures::StreamExt;
use rustls::ServerConfig;
use rustls_acme::caches::DirCache;
use rustls_acme::{AcmeConfig, EventOk};
#[derive(Debug, Clone)]
pub enum Tls {
Files { cert: PathBuf, key: PathBuf },
Acme {
domain: String,
email: String,
cache: PathBuf,
staging: bool,
},
}
const DRAIN: Duration = Duration::from_secs(30);
const CERT_OVERDUE: Duration = Duration::from_secs(5 * 60);
const PROBE_TIMEOUT: Duration = Duration::from_secs(5);
fn with_http2(config: &ServerConfig) -> Arc<ServerConfig> {
let mut config = config.clone();
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
Arc::new(config)
}
async fn reachable(addr: SocketAddr, timeout: Duration) -> bool {
matches!(
tokio::time::timeout(timeout, tokio::net::TcpStream::connect(addr)).await,
Ok(Ok(_))
)
}
fn reachability_problem(addrs: &[(SocketAddr, bool)]) -> Option<String> {
let first = addrs.first()?;
if first.1 {
return None;
}
if addrs.iter().all(|(_, ok)| !ok) {
return Some(
"Let's Encrypt cannot be reached, so no certificate can be ordered; check this \
server's outgoing connections on port 443"
.to_string(),
);
}
let family = if first.0.is_ipv6() { "IPv6" } else { "IPv4" };
let remedy = if first.0.is_ipv6() {
"fix IPv6 on this server, or prefer IPv4 with `precedence ::ffff:0:0/96 100` in /etc/gai.conf"
} else {
"fix IPv4 on this server"
};
Some(format!(
"Let's Encrypt cannot be reached over {family}, which is tried first, so a certificate \
order will take very long or stall without an error; {remedy}"
))
}
async fn check_acme_reachable(staging: bool) {
let host = if staging {
"acme-staging-v02.api.letsencrypt.org"
} else {
"acme-v02.api.letsencrypt.org"
};
let addrs: Vec<SocketAddr> = match tokio::net::lookup_host((host, 443)).await {
Ok(addrs) => addrs.collect(),
Err(error) => {
tracing::error!(host, %error, "cannot resolve Let's Encrypt, so no certificate can be ordered");
return;
}
};
let probes = addrs.iter().map(|addr| reachable(*addr, PROBE_TIMEOUT));
let results = futures::future::join_all(probes).await;
let addrs: Vec<(SocketAddr, bool)> = addrs.into_iter().zip(results).collect();
if let Some(problem) = reachability_problem(&addrs) {
tracing::warn!(host, "{problem}");
}
}
pub async fn serve(
addr: SocketAddr,
app: Router,
tls: Option<Tls>,
shutdown: impl Future<Output = ()> + Send + 'static,
ready: impl FnOnce(SocketAddr) + Send + 'static,
) -> Result<(), String> {
let service = app.into_make_service();
let handle: Handle<SocketAddr> = Handle::new();
let on_shutdown = handle.clone();
tokio::spawn(async move {
shutdown.await;
on_shutdown.graceful_shutdown(Some(DRAIN));
});
let on_ready = handle.clone();
tokio::spawn(async move {
if let Some(addr) = on_ready.listening().await {
ready(addr);
}
});
match tls {
None => axum_server::bind(addr)
.handle(handle)
.serve(service)
.await
.map_err(|e| format!("serving on {addr}: {e}")),
Some(Tls::Acme {
domain,
email,
cache,
staging,
}) => {
std::fs::create_dir_all(&cache)
.map_err(|e| format!("creating the certificate cache {}: {e}", cache.display()))?;
let mut acme = AcmeConfig::new([domain.as_str()])
.contact([format!("mailto:{email}").as_str()])
.cache(DirCache::new(cache))
.directory_lets_encrypt(!staging)
.state();
let acceptor = acme.axum_acceptor(with_http2(&acme.default_rustls_config()));
tokio::spawn(check_acme_reachable(staging));
tokio::spawn(async move {
let overdue = tokio::time::sleep(CERT_OVERDUE);
tokio::pin!(overdue);
let mut waiting = true;
loop {
tokio::select! {
event = acme.next() => match event {
Some(Ok(ok)) => {
if matches!(ok, EventOk::DeployedNewCert | EventOk::DeployedCachedCert) {
waiting = false;
}
tracing::info!(event = ?ok, "ACME");
}
Some(Err(error)) => tracing::error!(%error, "ACME"),
None => break,
},
() = &mut overdue, if waiting => {
waiting = false;
tracing::warn!(
%domain,
minutes = CERT_OVERDUE.as_secs() / 60,
"no certificate from Let's Encrypt yet, and HTTPS fails until there is \
one; check that this hostname's DNS and port 443 reach this server and \
that this server can reach Let's Encrypt, IPv6 included"
);
}
}
}
});
axum_server::bind(addr)
.acceptor(acceptor)
.handle(handle)
.serve(service)
.await
.map_err(|e| format!("serving HTTPS on {addr}: {e}"))
}
Some(Tls::Files { cert, key }) => {
let config = RustlsConfig::from_pem_file(&cert, &key)
.await
.map_err(|e| {
format!(
"loading the certificate {} and key {}: {e}",
cert.display(),
key.display()
)
})?;
axum_server::bind_rustls(addr, config)
.handle(handle)
.serve(service)
.await
.map_err(|e| format!("serving HTTPS on {addr}: {e}"))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::routing::get;
#[test]
fn lets_encrypt_certificates_offer_http2() {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let dir = tempfile::tempdir().unwrap();
let acme = AcmeConfig::new(["defter.example.org"])
.contact(["mailto:me@example.org"])
.cache(DirCache::new(dir.path().to_path_buf()))
.state();
let config = with_http2(&acme.default_rustls_config());
assert_eq!(config.alpn_protocols[0], b"h2");
assert_eq!(config.alpn_protocols[1], b"http/1.1");
}
#[test]
fn says_what_is_wrong_with_reaching_lets_encrypt() {
let v4: SocketAddr = "192.0.2.1:443".parse().unwrap();
let v6: SocketAddr = "[2001:db8::1]:443".parse().unwrap();
assert_eq!(reachability_problem(&[(v4, true), (v6, false)]), None);
assert_eq!(reachability_problem(&[(v6, true), (v4, true)]), None);
assert_eq!(reachability_problem(&[]), None);
let problem = reachability_problem(&[(v6, false), (v4, true)]).unwrap();
assert!(problem.contains("over IPv6"), "{problem}");
assert!(problem.contains("/etc/gai.conf"), "{problem}");
let problem = reachability_problem(&[(v4, false), (v6, true)]).unwrap();
assert!(problem.contains("over IPv4"), "{problem}");
assert!(!problem.contains("gai.conf"), "{problem}");
let problem = reachability_problem(&[(v6, false), (v4, false)]).unwrap();
assert!(problem.contains("cannot be reached, so"), "{problem}");
}
#[tokio::test]
async fn tells_reachable_addresses_from_others() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let open = listener.local_addr().unwrap();
assert!(reachable(open, PROBE_TIMEOUT).await);
drop(listener);
assert!(!reachable(open, PROBE_TIMEOUT).await);
}
async fn start(
app: Router,
tls: Option<Tls>,
) -> (SocketAddr, tokio::sync::oneshot::Sender<()>) {
let (stop, stopped) = tokio::sync::oneshot::channel::<()>();
let (bound, address) = tokio::sync::oneshot::channel();
tokio::spawn(async move {
let addr = "127.0.0.1:0".parse().unwrap();
serve(
addr,
app,
tls,
async {
let _ = stopped.await;
},
move |addr| {
let _ = bound.send(addr);
},
)
.await
.unwrap();
});
(address.await.unwrap(), stop)
}
fn app() -> Router {
Router::new().route("/", get(|| async { "defter" }))
}
fn client() -> reqwest::ClientBuilder {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
reqwest::Client::builder().no_proxy()
}
#[tokio::test]
async fn serves_plain_http() {
let (addr, _stop) = start(app(), None).await;
let body = client()
.build()
.unwrap()
.get(format!("http://{addr}/"))
.send()
.await
.unwrap()
.text()
.await
.unwrap();
assert_eq!(body, "defter");
}
#[tokio::test]
async fn serves_https_with_the_given_certificate() {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let dir = tempfile::tempdir().unwrap();
let cert = rcgen::generate_simple_self_signed(vec!["defter.test".into()]).unwrap();
let (cert_path, key_path) = (dir.path().join("cert.pem"), dir.path().join("key.pem"));
let tls = Tls::Files {
cert: cert_path.clone(),
key: key_path.clone(),
};
std::fs::write(&cert_path, cert.cert.pem()).unwrap();
std::fs::write(&key_path, cert.signing_key.serialize_pem()).unwrap();
let (addr, _stop) = start(app(), Some(tls)).await;
let trusting = client()
.tls_certs_only([reqwest::Certificate::from_pem(cert.cert.pem().as_bytes()).unwrap()])
.resolve("defter.test", addr)
.build()
.unwrap();
let response = trusting
.get(format!("https://defter.test:{}/", addr.port()))
.send()
.await
.unwrap();
assert_eq!(response.version(), reqwest::Version::HTTP_2);
assert_eq!(response.text().await.unwrap(), "defter");
let untrusting = client().resolve("defter.test", addr).build().unwrap();
assert!(
untrusting
.get(format!("https://defter.test:{}/", addr.port()))
.send()
.await
.is_err()
);
}
#[tokio::test]
async fn a_missing_certificate_is_an_error() {
let tls = Tls::Files {
cert: "/nonexistent/cert.pem".into(),
key: "/nonexistent/key.pem".into(),
};
let addr = "127.0.0.1:0".parse().unwrap();
let error = serve(addr, app(), Some(tls), std::future::pending(), |_| {})
.await
.unwrap_err();
assert!(error.contains("/nonexistent/cert.pem"), "{error}");
}
}