prest 0.5.1

Progressive RESTful framework
Documentation
use crate::*;

use axum::handler::HandlerWithoutStateExt;
use axum_server::Handle;
use http::uri::Authority;
use rustls_acme::{caches::DirCache, AcmeConfig};
use std::net::{Ipv6Addr, SocketAddr};

pub async fn start(router: Router) -> Result<(), Error> {
    let name = APP_CONFIG.name;
    let domain = APP_CONFIG.domain;
    let data_dir = APP_CONFIG.data_dir.clone();

    let handle = RT.new_server_handle();

    if *IS_REMOTE && domain.is_some() {
        let domain = domain.expect("Already validated is_some");
        let mut certs_path = data_dir.clone();
        certs_path.push("certs");

        let mut state = AcmeConfig::new(vec![domain])
            .cache_option(Some(DirCache::new(certs_path)))
            .directory_lets_encrypt(true)
            .state();
        let acceptor = state.axum_acceptor(state.default_rustls_config());

        tokio::spawn(async move {
            loop {
                match state.next().await {
                    Some(Ok(ok)) => trace!(target: "server", "TLS acme event: {:?}", ok),
                    Some(Err(err)) => error!(target: "server", "TLS acme error: {:?}", err),
                    None => tokio::time::sleep(std::time::Duration::from_millis(100)).await,
                }
            }
        });

        let redirect_handle = RT.new_server_handle();
        tokio::spawn(redirect_http_to_https(redirect_handle));

        info!(target: "server", "Starting serving {name} at https://{domain}");
        axum_server::bind(SocketAddr::from((Ipv6Addr::UNSPECIFIED, 443)))
            .acceptor(acceptor)
            .handle(handle)
            .serve(router.into_make_service_with_connect_info::<SocketAddr>())
            .await?;
    } else {
        #[cfg(debug_assertions)]
        info!(target: "server", "Starting serving {name} at http://localhost");

        axum_server::bind(SocketAddr::from((Ipv6Addr::UNSPECIFIED, check_port())))
            .handle(handle)
            .serve(router.into_make_service_with_connect_info::<SocketAddr>())
            .await?;
    }
    OK
}

async fn redirect_http_to_https(handle: Handle) {
    fn make_https(host: &str, uri: Uri, https_port: u16) -> Result<Uri, tower::BoxError> {
        let mut parts = uri.into_parts();

        parts.scheme = Some(axum::http::uri::Scheme::HTTPS);

        if parts.path_and_query.is_none() {
            parts.path_and_query = Some("/".parse().expect("Valid root path"));
        }

        let authority: Authority = host.parse()?;
        let bare_host = match authority.port() {
            Some(port_struct) => authority
                .as_str()
                .strip_suffix(port_struct.as_str())
                .map(|a| a.strip_suffix(':'))
                .flatten()
                .expect("Authority.port() is Some(port) then we can be sure authority ends with :{port}"),
            None => authority.as_str(),
        };

        parts.authority = Some(format!("{bare_host}:{https_port}").parse()?);

        Ok(Uri::from_parts(parts)?)
    }

    let redirect = move |Host(host): Host, uri: Uri| async move {
        match make_https(&host, uri, 443) {
            Ok(uri) => Ok(Redirect::permanent(&uri.to_string())),
            Err(error) => {
                tracing::warn!(target: "https redirect", %error, "failed to convert URI to HTTPS");
                Err(StatusCode::BAD_REQUEST)
            }
        }
    };

    let addr = SocketAddr::from(([127, 0, 0, 1], 80));

    axum_server::bind(addr)
        .handle(handle)
        .serve(redirect.into_make_service_with_connect_info::<SocketAddr>())
        .await
        .expect("HTTP -> HTTPS redirection service should start and end gracefully");
}

fn check_port() -> u16 {
    if let Ok(v) = env_var("PORT") {
        v.parse::<u16>().unwrap_or(80)
    } else {
        80
    }
}