jokoway-core 0.1.0-rc.1

Core traits and types for Jokoway API Gateway
Documentation
pub use pingora::server::configuration::ServerConf;
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::HashMap;
use std::sync::Arc;

#[cfg(feature = "api")]
use utoipa::ToSchema;

#[derive(Debug, Serialize, Deserialize)]
pub struct RootConfig {
    pub jokoway: JokowayConfig,
    pub pingora: Option<ServerConf>,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct HttpServerOptionsConfig {
    pub keepalive_request_limit: Option<u32>,
    #[serde(default = "default_false")]
    pub h2c: bool,
    #[serde(default = "default_true")]
    pub allow_connect_method_proxying: bool,
}

fn default_false() -> bool {
    false
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct JokowayConfig {
    pub http_listen: String,
    pub https_listen: Option<String>,
    pub api: Option<ApiSettings>,
    pub tls: Option<TlsSettings>,
    pub http_server_options: Option<HttpServerOptionsConfig>,

    #[serde(default)]
    pub upstreams: Vec<Upstream>,
    #[serde(default)]
    pub services: Vec<Arc<Service>>,
    #[serde(default)]
    pub dns: Option<DnsSettings>,

    // Allow for extra configuration that might not be strictly defined
    #[serde(flatten)]
    pub extra: HashMap<String, serde_yaml::Value>,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct ApiSettings {
    pub listen: String,
    #[serde(default, deserialize_with = "deserialize_basic_auth_credentials")]
    pub basic_auth: Option<Vec<BasicAuth>>,
    #[serde(default, deserialize_with = "deserialize_api_keys")]
    pub api_keys: Option<Vec<String>>,
    pub rate_limit: Option<RateLimit>,
    pub openapi: Option<OpenApiSettings>,
}

#[derive(Deserialize)]
#[serde(untagged)]
enum BasicAuthCredentials {
    Single(BasicAuth),
    Multiple(Vec<BasicAuth>),
}

#[derive(Deserialize)]
#[serde(untagged)]
enum ApiKeyCredentials {
    Single(String),
    Multiple(Vec<String>),
}

fn deserialize_basic_auth_credentials<'de, D>(
    deserializer: D,
) -> Result<Option<Vec<BasicAuth>>, D::Error>
where
    D: Deserializer<'de>,
{
    let value = Option::<BasicAuthCredentials>::deserialize(deserializer)?;
    Ok(value.map(|v| match v {
        BasicAuthCredentials::Single(auth) => vec![auth],
        BasicAuthCredentials::Multiple(auths) => auths,
    }))
}

fn deserialize_api_keys<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
where
    D: Deserializer<'de>,
{
    let value = Option::<ApiKeyCredentials>::deserialize(deserializer)?;
    Ok(value.map(|v| match v {
        ApiKeyCredentials::Single(key) => vec![key],
        ApiKeyCredentials::Multiple(keys) => keys,
    }))
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct BasicAuth {
    pub username: String,
    pub password: String,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct RateLimit {
    pub requests_per_second: u32,
    pub burst: u32,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct DnsSettings {
    pub nameservers: Option<Vec<String>>,
    pub timeout: Option<u64>,
    pub attempts: Option<usize>,
    pub strategy: Option<String>,
    pub cache_size: Option<usize>,
    #[serde(default = "default_true")]
    pub use_hosts_file: bool,
    #[serde(default = "default_true")]
    pub system_conf: bool,
}

fn default_true() -> bool {
    true
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "api", derive(ToSchema))]
#[serde(deny_unknown_fields)]
pub struct TcpKeepaliveConfig {
    pub idle: Option<u64>,
    pub interval: Option<u64>,
    pub count: Option<u32>,
    #[cfg(target_os = "linux")]
    pub user_timeout: Option<u64>,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "api", derive(ToSchema))]
#[serde(deny_unknown_fields)]
pub struct PeerOptions {
    pub connection_timeout: Option<u64>,
    pub read_timeout: Option<u64>,
    pub idle_timeout: Option<u64>,
    pub write_timeout: Option<u64>,
    pub verify_cert: Option<bool>,
    pub verify_hostname: Option<bool>,
    pub alternative_cn: Option<String>,
    /// ALPN protocol negotiation: "h1", "h2", or "h1h2"
    pub alpn: Option<String>,
    pub tcp_keepalive: Option<TcpKeepaliveConfig>,
    pub tcp_recv_buf: Option<usize>,
    pub dscp: Option<u8>,
    pub h2_ping_interval: Option<u64>,
    pub max_h2_streams: Option<usize>,
    pub allow_h1_response_invalid_content_length: Option<bool>,
    pub extra_proxy_headers: Option<std::collections::HashMap<String, String>>,
    pub curves: Option<String>,
    pub second_keyshare: Option<bool>,
    pub tcp_fast_open: Option<bool>,
    pub cacert: Option<String>,
    pub client_cert: Option<String>,
    pub client_key: Option<String>,
    pub sni: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct TlsSettings {
    pub cacert: Option<String>,
    pub server_cert: Option<String>,
    pub server_key: Option<String>,
    pub sans: Option<Vec<String>>,
    pub cipher_suites: Option<Vec<String>>,
}

fn default_openapi_title() -> String {
    "Jokoway API".to_string()
}

fn default_openapi_description() -> String {
    "Jokoway Management API".to_string()
}

fn default_openapi_root_path() -> String {
    "/docs".to_string()
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct OpenApiSettings {
    #[serde(default = "default_openapi_title")]
    pub title: String,
    #[serde(default = "default_openapi_description")]
    pub description: String,
    #[serde(default = "default_openapi_root_path")]
    pub root_path: String,
}

impl Default for OpenApiSettings {
    fn default() -> Self {
        Self {
            title: default_openapi_title(),
            description: default_openapi_description(),
            root_path: default_openapi_root_path(),
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "api", derive(ToSchema))]
#[serde(deny_unknown_fields)]
pub struct Upstream {
    pub name: String,
    pub peer_options: Option<PeerOptions>,
    #[serde(default)]
    pub servers: Vec<UpstreamServer>,
    pub health_check: Option<HealthCheckConfig>,
    pub update_frequency: Option<u64>,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "api", derive(ToSchema))]
#[serde(deny_unknown_fields)]
pub struct UpstreamServer {
    pub host: String,
    pub weight: Option<u32>,
    pub tls: Option<bool>,
    pub peer_options: Option<PeerOptions>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "api", derive(ToSchema))]
pub enum ServiceProtocol {
    #[serde(rename = "http")]
    Http,
    #[serde(rename = "https")]
    Https,
    #[serde(rename = "ws")]
    Ws,
    #[serde(rename = "wss")]
    Wss,
    #[serde(rename = "grpc")]
    Grpc,
    #[serde(rename = "grpcs")]
    Grpcs,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "api", derive(ToSchema))]
#[serde(deny_unknown_fields)]
pub struct Service {
    pub name: String,
    pub host: String,
    pub protocols: Vec<ServiceProtocol>,
    pub max_retries: Option<u32>,
    #[serde(default)]
    pub routes: Vec<Route>,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "api", derive(ToSchema))]
#[serde(deny_unknown_fields)]
pub struct Route {
    pub name: String,
    pub rule: String,
    pub priority: Option<i32>,
    pub max_retries: Option<u32>,
    pub request_transformer: Option<String>,
    pub response_transformer: Option<String>,
}

// Health Check Configuration

fn default_health_check_interval() -> u64 {
    10
}

fn default_health_check_timeout() -> u64 {
    3
}

fn default_unhealthy_threshold() -> u32 {
    3
}

fn default_healthy_threshold() -> u32 {
    2
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[cfg_attr(feature = "api", derive(ToSchema))]
#[serde(rename_all = "lowercase")]
pub enum HealthCheckType {
    Http,
    Https,
    Tcp,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "api", derive(ToSchema))]
#[serde(deny_unknown_fields)]
pub struct HealthCheckConfig {
    #[serde(rename = "type")]
    pub check_type: HealthCheckType,

    #[serde(default = "default_health_check_interval")]
    pub interval: u64, // seconds

    #[serde(default = "default_health_check_timeout")]
    pub timeout: u64, // seconds

    #[serde(default = "default_unhealthy_threshold")]
    pub unhealthy_threshold: u32,

    #[serde(default = "default_healthy_threshold")]
    pub healthy_threshold: u32,

    // HTTP/HTTPS specific
    pub path: Option<String>,
    pub method: Option<String>, // GET, HEAD, POST
    pub expected_status: Option<Vec<u16>>,
    pub headers: Option<HashMap<String, String>>,
}