jokoway 0.1.0-rc.1

Jokoway is a high-performance API Gateway built on Pingora (Rust) with dead-simple YAML configs.
Documentation
use crate::config::models::JokowayConfig;
use crate::error::JokowayError;
use crate::prelude::{core::*, *};
use crate::server::context::Context;
use crate::server::proxy::JokowayProxy;
use crate::server::router::{HTTPS_PROTOCOLS, Router};
use crate::server::service::ServiceManager;
use crate::server::upstream::UpstreamManager;
use boring::pkey::PKey;
use boring::ssl::{AlpnError, SslAcceptor, SslMethod, SslVerifyMode};
use boring::x509::X509;
use jokoway_core::tls::{AlpnProtocol, contains_alpn_protocol};
use pingora::listeners::tls::TlsSettings;
use pingora::proxy::ProxyServiceBuilder;
use std::fs;
use std::path::Path;
use std::sync::Arc;

pub struct HttpsExtension;

impl HttpsExtension {
    fn load_pem_pair_from_files(
        cert_path: &str,
        key_path: &str,
    ) -> Result<(X509, PKey<boring::pkey::Private>), Box<dyn std::error::Error>> {
        let cert_pem = fs::read(cert_path)?;
        let key_pem = fs::read(key_path)?;
        let cert = X509::from_pem(&cert_pem)?;
        let key = PKey::private_key_from_pem(&key_pem)?;
        Ok((cert, key))
    }

    fn self_signed_pair(
        tls: Option<&crate::config::models::TlsSettings>,
    ) -> Option<(X509, PKey<boring::pkey::Private>)> {
        let subject_alt_names = tls
            .and_then(|t| t.sans.clone())
            .filter(|sans| !sans.is_empty())
            .unwrap_or_else(|| vec!["localhost".to_string(), "127.0.0.1".to_string()]);

        // Custom cert parameters for self-signed
        let mut params = rcgen::CertificateParams::new(subject_alt_names).ok()?;
        params.distinguished_name.remove(rcgen::DnType::CommonName);
        params
            .distinguished_name
            .push(rcgen::DnType::CommonName, "Jokoway Gateway");

        let key_pair = rcgen::KeyPair::generate().ok()?;
        let cert = params.self_signed(&key_pair).ok()?;

        let priv_key_pem = key_pair.serialize_pem();
        let cert_pem = cert.pem();
        let key = PKey::private_key_from_pem(priv_key_pem.as_bytes()).ok()?;
        let cert = X509::from_pem(cert_pem.as_bytes()).ok()?;
        Some((cert, key))
    }
}

impl JokowayExtension for HttpsExtension {
    fn order(&self) -> i16 {
        -100
    }

    fn init(
        &self,
        server: &mut pingora::server::Server,
        app_ctx: &mut AppContext,
        middlewares: &mut Vec<std::sync::Arc<dyn JokowayMiddlewareDyn>>,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let config = app_ctx.get::<JokowayConfig>().ok_or_else(|| {
            JokowayError::Config("JokowayConfig not found in Context".to_string())
        })?;

        // Check if HTTPS is configured to listen
        if config.https_listen.is_none() {
            return Ok(());
        }

        let upstream_manager = app_ctx.get::<UpstreamManager>().ok_or_else(|| {
            JokowayError::Config("UpstreamManager not found in Context".to_string())
        })?;
        let service_manager = app_ctx.get::<ServiceManager>().ok_or_else(|| {
            JokowayError::Config("ServiceManager not found in Context".to_string())
        })?;

        let router = Router::new(service_manager, upstream_manager.clone(), &HTTPS_PROTOCOLS);
        let proxy =
            JokowayProxy::new(router, Arc::new(app_ctx.clone()), middlewares.clone(), true)?;

        let mut ssl_acceptor = match SslAcceptor::mozilla_intermediate_v5(SslMethod::tls()) {
            Ok(acceptor) => acceptor,
            Err(e) => {
                log::error!("Failed to create SSL acceptor: {}", e);
                return Err(Box::new(JokowayError::Tls(format!(
                    "Failed to create SSL acceptor: {}",
                    e
                ))));
            }
        };
        let tls_cfg = config.tls.as_ref();

        let cert_paths = match tls_cfg {
            Some(tls) => match (&tls.server_cert, &tls.server_key) {
                (Some(cert), Some(key)) => {
                    if !Path::new(cert).exists() {
                        return Err(Box::new(JokowayError::Tls(format!(
                            "Certificate file not found: {}",
                            cert
                        ))));
                    }
                    if !Path::new(key).exists() {
                        return Err(Box::new(JokowayError::Tls(format!(
                            "Private key file not found: {}",
                            key
                        ))));
                    }
                    Some((cert, key))
                }
                (Some(_), None) | (None, Some(_)) => {
                    log::error!("Both server_cert and server_key must be specified together");
                    return Err(Box::new(JokowayError::Tls(
                        "Both server_cert and server_key must be specified together".to_string(),
                    )));
                }
                _ => None,
            },
            None => None,
        };

        let base_pair = if let Some((cert_path, key_path)) = cert_paths {
            match Self::load_pem_pair_from_files(cert_path, key_path) {
                Ok(pair) => Some(pair),
                Err(e) => {
                    log::error!("Failed to load TLS certs from config: {}", e);
                    return Err(Box::new(JokowayError::Tls(format!(
                        "Failed to load TLS certs from config: {}",
                        e
                    ))));
                }
            }
        } else {
            None
        };

        let fallback_pair = match base_pair.clone() {
            Some(pair) => Some(pair),
            None => {
                let generated = Self::self_signed_pair(tls_cfg);
                if generated.is_some() {
                    log::debug!("Using self-signed certificate fallback");
                }
                generated
            }
        }
        .ok_or_else(|| {
            Box::new(JokowayError::Tls(
                "Failed to generate self-signed certificate".to_string(),
            )) as Box<dyn std::error::Error + Send + Sync>
        })?;

        if let Err(e) = ssl_acceptor.set_private_key(&fallback_pair.1) {
            log::error!("Failed to set fallback private key: {}", e);
            return Err(Box::new(JokowayError::Tls(format!(
                "Failed to set fallback private key: {}",
                e
            ))));
        }
        if let Err(e) = ssl_acceptor.set_certificate(&fallback_pair.0) {
            log::error!("Failed to set fallback certificate: {}", e);
            return Err(Box::new(JokowayError::Tls(format!(
                "Failed to set fallback certificate: {}",
                e
            ))));
        }

        let tls_callback = app_ctx.get::<TlsCallback>();

        let fallback_pair_for_sni = Some(fallback_pair.clone());
        let tls_cb_sni = tls_callback.clone();
        ssl_acceptor.set_servername_callback(move |ssl_ref, alert| {
            if let Some(c) = tls_cb_sni.as_ref() {
                let h_arc = c.get_handler();
                if let Some(handler) = h_arc.as_ref() {
                    match handler.servername_callback(ssl_ref, alert) {
                        Ok(()) => return Ok(()),
                        Err(_e) => {}
                    }
                }
            }

            if let Some((cert, key)) = fallback_pair_for_sni.as_ref() {
                let _ = ssl_ref.set_certificate(cert);
                let _ = ssl_ref.set_private_key(key);
            }
            Ok(())
        });

        let tls_cb_alpn = tls_callback.clone();
        ssl_acceptor.set_alpn_select_callback(move |ssl_ref, client_protos| {
            if let Some(c) = tls_cb_alpn.as_ref() {
                let h_arc = c.get_handler();
                if let Some(handler) = h_arc.as_ref() {
                    match handler.alpn_select_callback(ssl_ref, client_protos) {
                        Ok(idx) => return Ok(idx),
                        Err(AlpnError::NOACK) => {}
                        Err(e) => return Err(e),
                    }
                }
            }

            if contains_alpn_protocol(client_protos, AlpnProtocol::H2) {
                Ok(AlpnProtocol::H2.as_bytes())
            } else {
                Ok(AlpnProtocol::H1.as_bytes())
            }
        });

        let tls_cb_cert = tls_callback.clone();
        ssl_acceptor.set_select_certificate_callback(move |client_hello| {
            if let Some(c) = tls_cb_cert.as_ref() {
                let h_arc = c.get_handler();
                if let Some(handler) = h_arc.as_ref() {
                    return handler.select_certificate_callback(client_hello);
                }
            }
            Ok(())
        });

        let tls_cb_verify = tls_callback.clone();
        let verify_mode = if let Some(ca_path) = tls_cfg.and_then(|t| t.cacert.as_ref()) {
            if !Path::new(ca_path).exists() {
                return Err(Box::new(JokowayError::Tls(format!(
                    "CA certificate file not found: {}",
                    ca_path
                ))));
            }

            if let Err(e) = ssl_acceptor.set_ca_file(ca_path) {
                return Err(Box::new(JokowayError::Tls(format!(
                    "Failed to set CA file for client auth: {}",
                    e
                ))));
            }

            SslVerifyMode::PEER | SslVerifyMode::FAIL_IF_NO_PEER_CERT
        } else {
            SslVerifyMode::NONE
        };

        ssl_acceptor.set_verify_callback(verify_mode, move |preverify, x509_ctx| {
            if let Some(c) = tls_cb_verify.as_ref() {
                let h_arc = c.get_handler();
                if let Some(handler) = h_arc.as_ref() {
                    return handler.verify_callback(preverify, x509_ctx);
                }
            }
            preverify
        });

        let tls_cb_sess_new = tls_callback.clone();
        ssl_acceptor.set_new_session_callback(move |ssl, session| {
            if let Some(c) = tls_cb_sess_new.as_ref() {
                let h_arc = c.get_handler();
                if let Some(handler) = h_arc.as_ref() {
                    handler.new_session_callback(ssl, session);
                }
            }
        });

        let tls_cb_sess_remove = tls_callback.clone();
        ssl_acceptor.set_remove_session_callback(move |ctx, session| {
            if let Some(c) = tls_cb_sess_remove.as_ref() {
                let h_arc = c.get_handler();
                if let Some(handler) = h_arc.as_ref() {
                    handler.remove_session_callback(ctx, session);
                }
            }
        });

        unsafe {
            let tls_cb_sess_get = tls_callback.clone();
            ssl_acceptor.set_get_session_callback(move |ssl, id| {
                if let Some(c) = tls_cb_sess_get.as_ref() {
                    let h_arc = c.get_handler();
                    if let Some(handler) = h_arc.as_ref() {
                        return handler.get_session_callback(ssl, id);
                    }
                }
                Ok(None)
            });
        }

        let tls_cb_psk = tls_callback.clone();
        ssl_acceptor.set_psk_server_callback(move |ssl, id, psk| {
            if let Some(c) = tls_cb_psk.as_ref() {
                let h_arc = c.get_handler();
                if let Some(handler) = h_arc.as_ref() {
                    return handler.psk_server_callback(ssl, id, psk);
                }
            }
            Ok(0)
        });

        let tls_cb_status = tls_callback.clone();
        ssl_acceptor
            .set_status_callback(move |ssl| {
                if let Some(c) = tls_cb_status.as_ref() {
                    let h_arc = c.get_handler();
                    if let Some(handler) = h_arc.as_ref() {
                        return handler.status_callback(ssl);
                    }
                }
                Ok(true)
            })
            .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;

        let tls_cb_keylog = tls_callback.clone();
        ssl_acceptor.set_keylog_callback(move |ssl, line| {
            if let Some(c) = tls_cb_keylog.as_ref() {
                let h_arc = c.get_handler();
                if let Some(handler) = h_arc.as_ref() {
                    handler.keylog_callback(ssl, line);
                }
            }
        });

        if let Some(ciphers) = tls_cfg.and_then(|t| t.cipher_suites.as_ref()) {
            let ciphers_str = ciphers.join(":");
            if let Err(e) = ssl_acceptor.set_cipher_list(&ciphers_str) {
                log::error!("Failed to set cipher suites: {}", e);
                return Err(Box::new(JokowayError::Tls(format!(
                    "Failed to set cipher suites: {}",
                    e
                ))));
            }
        }
        let tls_settings = TlsSettings::from(ssl_acceptor);
        let mut builder = ProxyServiceBuilder::new(&server.configuration, proxy.clone())
            .name("Jokoway HTTPS Proxy Service");

        if let Some(opts) = &config.http_server_options {
            let mut server_options = pingora::apps::HttpServerOptions::default();
            server_options.keepalive_request_limit = opts.keepalive_request_limit;
            server_options.h2c = opts.h2c;
            server_options.allow_connect_method_proxying = opts.allow_connect_method_proxying;
            builder = builder.server_options(server_options);
        }

        let mut https_service = builder.build();
        https_service.add_tls_with_settings(
            config.https_listen.as_ref().unwrap(),
            None,
            tls_settings,
        );
        server.add_service(https_service);
        log::info!(
            "HTTPS proxy listening on {}",
            config.https_listen.as_ref().unwrap()
        );
        Ok(())
    }
}