praxis-proxy-protocol 0.5.6

HTTP, TCP, and protocol adapters for Praxis
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024 Praxis Contributors

//! Shared TLS settings builder for HTTP and TCP listeners.

use pingora_core::listeners::tls::TlsSettings;
use praxis_core::ProxyError;
use praxis_tls::ListenerTls;
use tokio::sync::watch;

// -----------------------------------------------------------------------------
// TLS Settings Builder
// -----------------------------------------------------------------------------

/// Build [`TlsSettings`] for a listener.
///
/// When `hot_reload` is enabled (and the `config-reload` feature is
/// compiled in), uses a [`ReloadableCertResolver`] and spawns a
/// [`CertWatcher`] background task. Otherwise, or in a build without
/// `config-reload`, builds a static `ServerConfig` via
/// [`build_server_config`].
///
/// `context_label` appears in debug tracing to distinguish HTTP
/// from TCP callers (e.g. `"HTTP"`, `"TCP"`).
///
/// Returns the settings and an optional shutdown sender for the
/// cert watcher. The watcher runs for the process lifetime; the caller
/// keeps the sender to stop it early via `send(true)` (dropping it does
/// not stop the watcher).
///
/// [`TlsSettings`]: pingora_core::listeners::tls::TlsSettings
/// [`build_server_config`]: praxis_tls::setup::build_server_config
/// [`ReloadableCertResolver`]: praxis_tls::reload::ReloadableCertResolver
/// [`CertWatcher`]: praxis_tls::watcher::CertWatcher
#[expect(clippy::too_many_lines, reason = "hot-reload vs static TLS branching")]
pub(crate) fn build_tls_settings(
    tls: &ListenerTls,
    address: &str,
    context_label: &str,
    advertise_http_alpn: bool,
) -> Result<(TlsSettings, Option<watch::Sender<bool>>), ProxyError> {
    macro_rules! tls_err {
        ($e:expr) => {{
            let err = $e;
            ProxyError::Config(format!("TLS for {address}: {err}"))
        }};
    }

    #[cfg(feature = "config-reload")]
    if tls.is_hot_reload() {
        tracing::debug!(address, context_label, "building TLS ServerConfig with hot-reload");
        let result = praxis_tls::setup::build_reloadable_server_config(tls, advertise_http_alpn)
            .map_err(|e| ProxyError::Config(format!("TLS hot-reload for {address}: {e}")))?;

        let pair =
            tls.certificates.first().cloned().ok_or_else(|| {
                ProxyError::Config(format!("TLS hot-reload for {address}: no certificate configured"))
            })?;

        let verifier_reload = result.verifier_handle.and_then(|handle| {
            tls.client_ca
                .as_ref()
                .map(|ca_cfg| praxis_tls::watcher::ClientVerifierReload {
                    ca_path: ca_cfg.ca_path.clone(),
                    crl_paths: ca_cfg.crl_paths.clone(),
                    mode: tls.client_cert_mode,
                    swap_handle: handle,
                })
        });

        let (shutdown_tx, shutdown_rx) = watch::channel(false);
        praxis_tls::watcher::CertWatcher::spawn(result.cert_handle, pair, verifier_reload, shutdown_rx);

        let settings = TlsSettings::with_server_config(result.config).map_err(|e| tls_err!(e))?;
        return Ok((settings, Some(shutdown_tx)));
    }

    // Built without the `config-reload` feature: honor the static cert but warn
    // that a `hot_reload: true` listener will not actually watch for changes.
    #[cfg(not(feature = "config-reload"))]
    if tls.is_hot_reload() {
        tracing::warn!(
            address,
            context_label,
            "listener requests TLS hot_reload but this build lacks the `config-reload` feature; \
             serving a static certificate"
        );
    }

    tracing::debug!(address, context_label, "building TLS ServerConfig");
    let server_config = praxis_tls::setup::build_server_config(tls, advertise_http_alpn).map_err(|e| tls_err!(e))?;
    let settings = TlsSettings::with_server_config(server_config).map_err(|e| tls_err!(e))?;
    Ok((settings, None))
}