use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct ProxyConfig {
#[serde(default)]
pub server: ServerConfig,
#[serde(default)]
pub tls: TlsConfig,
#[serde(default)]
pub rate_limit: RateLimitDefaults,
#[serde(default)]
pub routes: Vec<Route>,
#[serde(default)]
pub upstreams: Vec<Upstream>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ServerConfig {
#[serde(default = "default_http_host")]
pub http_host: String,
#[serde(default = "default_http_port")]
pub http_port: u16,
#[serde(default)]
pub http2_port: Option<u16>,
#[serde(default = "default_https_host")]
pub https_host: String,
#[serde(default = "default_https_port")]
pub https_port: u16,
#[serde(default)]
pub https_enabled: bool,
#[serde(default = "default_true")]
pub database_config: bool,
pub config_file: Option<String>,
#[serde(default)]
pub watch_config_file: bool,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
http_host: default_http_host(),
http_port: default_http_port(),
http2_port: None,
https_host: default_https_host(),
https_port: default_https_port(),
https_enabled: false,
database_config: true,
config_file: None,
watch_config_file: false,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TlsConfig {
#[serde(default)]
pub acme_enabled: bool,
#[serde(default = "default_acme_directory")]
pub acme_directory: String,
pub acme_email: Option<String>,
#[serde(default = "default_cert_dir")]
pub cert_dir: String,
#[serde(default)]
pub acme_staging: bool,
#[serde(default)]
pub cert_file: Option<String>,
#[serde(default)]
pub key_file: Option<String>,
}
impl Default for TlsConfig {
fn default() -> Self {
Self {
acme_enabled: false,
acme_directory: default_acme_directory(),
acme_email: None,
cert_dir: default_cert_dir(),
acme_staging: false,
cert_file: None,
key_file: None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RateLimitDefaults {
#[serde(default = "default_rate_limit_requests")]
pub requests: u32,
#[serde(default = "default_rate_limit_window")]
pub window_secs: u32,
#[serde(default = "default_burst")]
pub burst: u32,
}
impl Default for RateLimitDefaults {
fn default() -> Self {
Self {
requests: default_rate_limit_requests(),
window_secs: default_rate_limit_window(),
burst: default_burst(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Route {
pub id: Option<Uuid>,
pub name: String,
pub description: Option<String>,
#[serde(default, rename = "match")]
pub match_: RouteMatch,
#[serde(default = "default_priority")]
pub priority: i32,
pub upstream: String,
#[serde(default)]
pub strip_path: bool,
#[serde(default)]
pub add_headers: HashMap<String, String>,
#[serde(default)]
pub remove_headers: Vec<String>,
pub rate_limit: Option<RouteRateLimit>,
#[serde(default = "default_timeout")]
pub timeout_secs: u32,
#[serde(default)]
pub retry_count: u32,
#[serde(default = "default_true")]
pub enabled: bool,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct RouteMatch {
pub host: Option<String>,
pub path: Option<String>,
#[serde(default)]
pub path_type: PathMatchType,
#[serde(default)]
pub headers: HashMap<String, String>,
pub methods: Option<Vec<String>>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum PathMatchType {
#[default]
Prefix,
Exact,
Regex,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RouteRateLimit {
pub requests: u32,
pub window_secs: u32,
#[serde(default)]
pub key: RateLimitKey,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RateLimitKey {
#[default]
ClientIp,
Header(String),
Route,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Upstream {
pub id: Option<Uuid>,
pub name: String,
pub description: Option<String>,
#[serde(default)]
pub lb_strategy: LoadBalanceStrategy,
#[serde(default)]
pub backends: Vec<Backend>,
#[serde(default)]
pub health_check: HealthCheckConfig,
#[serde(default = "default_true")]
pub enabled: bool,
}
impl Upstream {
pub fn resolved_id(&self) -> Uuid {
self.id
.unwrap_or_else(|| Uuid::new_v5(&Uuid::NAMESPACE_OID, self.name.as_bytes()))
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum LoadBalanceStrategy {
#[default]
RoundRobin,
LeastConnections,
Weighted,
Random,
Sticky,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Backend {
pub id: Option<Uuid>,
pub address: String,
#[serde(default = "default_scheme")]
pub scheme: String,
#[serde(default = "default_weight")]
pub weight: u32,
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub http_version: UpstreamHttpVersion,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum UpstreamHttpVersion {
#[default]
#[serde(alias = "http1", alias = "http/1.1")]
Http11,
#[serde(alias = "http2", alias = "h2")]
H2c,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HealthCheckConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_health_path")]
pub path: String,
#[serde(default = "default_health_interval")]
pub interval_secs: u32,
#[serde(default = "default_health_timeout")]
pub timeout_secs: u32,
#[serde(default = "default_healthy_threshold")]
pub healthy_threshold: u32,
#[serde(default = "default_unhealthy_threshold")]
pub unhealthy_threshold: u32,
}
impl Default for HealthCheckConfig {
fn default() -> Self {
Self {
enabled: true,
path: default_health_path(),
interval_secs: default_health_interval(),
timeout_secs: default_health_timeout(),
healthy_threshold: default_healthy_threshold(),
unhealthy_threshold: default_unhealthy_threshold(),
}
}
}
fn default_http_host() -> String {
"0.0.0.0".into()
}
fn default_http_port() -> u16 {
8080
}
fn default_https_host() -> String {
"0.0.0.0".into()
}
fn default_https_port() -> u16 {
8443
}
fn default_true() -> bool {
true
}
fn default_acme_directory() -> String {
"https://acme-v02.api.letsencrypt.org/directory".into()
}
fn default_cert_dir() -> String {
"./certs".into()
}
fn default_rate_limit_requests() -> u32 {
1000
}
fn default_rate_limit_window() -> u32 {
60
}
fn default_burst() -> u32 {
50
}
fn default_priority() -> i32 {
100
}
fn default_timeout() -> u32 {
30
}
fn default_scheme() -> String {
"http".into()
}
fn default_weight() -> u32 {
100
}
fn default_health_path() -> String {
"/health".into()
}
fn default_health_interval() -> u32 {
10
}
fn default_health_timeout() -> u32 {
5
}
fn default_healthy_threshold() -> u32 {
2
}
fn default_unhealthy_threshold() -> u32 {
3
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct AcmeConfig {
#[serde(default)]
pub enabled: bool,
pub email: Option<String>,
#[serde(default)]
pub staging: bool,
#[serde(default)]
pub domains: Vec<String>,
}
#[cfg(test)]
mod upstream_version_tests {
use super::*;
#[test]
fn test_backend_defaults_to_http11() {
let backend: Backend = toml::from_str(r#"address = "127.0.0.1:8080""#).unwrap();
assert_eq!(backend.http_version, UpstreamHttpVersion::Http11);
}
#[test]
fn test_backend_h2c_aliases() {
for spelling in ["h2c", "h2", "http2"] {
let toml_src = format!("address = \"127.0.0.1:8080\"\nhttp_version = \"{spelling}\"");
let backend: Backend = toml::from_str(&toml_src).unwrap();
assert_eq!(
backend.http_version,
UpstreamHttpVersion::H2c,
"spelling {spelling} should select h2c"
);
}
}
#[test]
fn test_backend_http11_aliases() {
for spelling in ["http11", "http1"] {
let toml_src = format!("address = \"127.0.0.1:8080\"\nhttp_version = \"{spelling}\"");
let backend: Backend = toml::from_str(&toml_src).unwrap();
assert_eq!(backend.http_version, UpstreamHttpVersion::Http11);
}
}
#[test]
fn test_http2_port_is_optional_and_off_by_default() {
let server: ServerConfig = toml::from_str("").unwrap();
assert_eq!(server.http2_port, None);
let server: ServerConfig = toml::from_str("http2_port = 19081").unwrap();
assert_eq!(server.http2_port, Some(19081));
}
}