serve.rs
raw
//! Listening: plain HTTP for development or behind a proxy, or HTTPS with
//! a Let's Encrypt certificate that the server obtains and renews itself,
//! with the plain listener reduced to redirecting.
use std::{sync::Arc, time::Duration};
use anyhow::{Context, Error};
use poem::{
Endpoint, EndpointExt, IntoResponse, Request, Response, Server,
listener::{
Acceptor, Listener, TcpListener,
acme::{AutoCert, ChallengeType, LETS_ENCRYPT_PRODUCTION, LETS_ENCRYPT_STAGING},
},
web::Redirect,
};
use crate::{App, Config};
const SHUTDOWN_GRACE: Duration = Duration::from_secs(10);
/// Applies the connection settings every mugit listener shares. Chiefly
/// it drops connections that fall silent, so clients which open one and
/// then abandon it cannot accumulate until the process runs out of file
/// descriptors; poem imposes no such limit of its own. Both the listeners
/// below and the tests build their servers through here, so that what the
/// tests exercise is configured the way the deployed server is.
pub fn configure<L, A>(server: Server<L, A>, config: &Config) -> Server<L, A>
where
L: Listener,
L::Acceptor: 'static,
A: Acceptor + 'static,
{
server.idle_timeout(Duration::from_secs(config.http_idle_timeout))
}
/// Serves the application until SIGINT or SIGTERM.
pub async fn run(app: Arc<App>) -> Result<(), Error> {
let routes = app.routes();
let config = &app.config;
let Some(domain) = config.tls_domain.clone() else {
tracing::info!(listen = %config.listen, "serving HTTP");
return configure(Server::new(TcpListener::bind(&config.listen)), config)
.run_with_graceful_shutdown(routes, shutdown_signal(), Some(SHUTDOWN_GRACE))
.await
.context("HTTP server");
};
// The dependency graph enables more than one rustls crypto backend, so
// rustls cannot pick one on its own; a second install attempt (in
// tests, say) is harmless and reports the provider already present.
let _ = rustls::crypto::ring::default_provider().install_default();
let mut cert = AutoCert::builder()
.directory_url(if config.acme_staging {
LETS_ENCRYPT_STAGING
} else {
LETS_ENCRYPT_PRODUCTION
})
.domain(&domain)
.challenge_type(ChallengeType::TlsAlpn01)
.cache_path(&config.acme_cache);
if let Some(contact) = &config.acme_contact {
cert = cert.contact(contact);
}
let cert = cert.build().context("configuring ACME")?;
tracing::info!(listen = %config.tls_listen, %domain, "serving HTTPS");
tracing::info!(listen = %config.listen, "redirecting HTTP to HTTPS");
let https = configure(
Server::new(TcpListener::bind(&config.tls_listen).acme(cert)),
config,
)
.run_with_graceful_shutdown(routes, shutdown_signal(), Some(SHUTDOWN_GRACE));
let http = configure(Server::new(TcpListener::bind(&config.listen)), config)
.run_with_graceful_shutdown(redirect_to(domain), shutdown_signal(), Some(SHUTDOWN_GRACE));
tokio::try_join!(async { https.await.context("HTTPS server") }, async {
http.await.context("HTTP redirect server")
},)?;
Ok(())
}
// An endpoint that sends every request to the same path on the HTTPS site.
fn redirect_to(domain: String) -> impl Endpoint {
poem::endpoint::make(move |req: Request| {
let domain = domain.clone();
async move {
let path = req
.uri()
.path_and_query()
.map(|pq| pq.as_str())
.unwrap_or("/");
Ok::<Response, poem::Error>(
Redirect::permanent(format!("https://{domain}{path}")).into_response(),
)
}
})
.boxed()
}
async fn shutdown_signal() {
use tokio::signal::unix::{SignalKind, signal};
let mut sigterm = match signal(SignalKind::terminate()) {
Ok(sigterm) => sigterm,
Err(err) => {
tracing::warn!(error = %err, "cannot listen for SIGTERM; only SIGINT will stop the server");
let _ = tokio::signal::ctrl_c().await;
return;
}
};
tokio::select! {
_ = tokio::signal::ctrl_c() => tracing::info!("SIGINT received, shutting down"),
_ = sigterm.recv() => tracing::info!("SIGTERM received, shutting down"),
}
}