//! 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(server: Server, config: &Config) -> Server 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) -> 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::( 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"), } }