use derive_more::Display;
use foundation_iogate::ServerIo;
use serde::{Deserialize, Deserializer, Serialize};
use std::path::PathBuf;
use std::time::Duration;
#[derive(Clone)]
pub struct AcmeRuntime {
pub directory_url: Option<String>,
pub dns01_setter: crate::acme::Dns01Setter,
}
impl std::fmt::Debug for AcmeRuntime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AcmeRuntime")
.field("directory_url", &self.directory_url)
.field("dns01_setter", &"<callback>")
.finish()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProxyConfig {
pub domain: String,
pub public_ip: String,
pub ssl: SslConfig,
#[serde(default)]
pub bind_addr: Option<String>,
#[serde(default)]
pub services: Vec<ServiceConfig>,
#[serde(default)]
pub control_socket: Option<String>,
#[serde(default)]
pub state_dir: Option<String>,
#[serde(skip)]
pub acme: Option<AcmeRuntime>,
#[serde(default)]
pub h3_bind: Option<String>,
#[serde(default)]
pub redirect_bind: Option<String>,
#[serde(skip)]
pub io_mode: ServerIo,
}
impl ProxyConfig {
#[must_use]
pub fn new(domain: &str, public_ip: &str) -> Self {
Self {
domain: domain.to_string(),
public_ip: public_ip.to_string(),
ssl: SslConfig::default(),
bind_addr: None,
services: Vec::new(),
control_socket: None,
state_dir: None,
acme: None,
h3_bind: None,
redirect_bind: None,
io_mode: ServerIo::default(),
}
}
#[must_use]
pub fn io_mode(mut self, mode: ServerIo) -> Self {
self.io_mode = mode;
self
}
#[must_use]
pub fn bind(mut self, addr: &str) -> Self {
self.bind_addr = Some(addr.to_string());
self
}
#[must_use]
pub fn control_socket(mut self, path: &str) -> Self {
self.control_socket = Some(path.to_string());
self
}
#[must_use]
pub fn persist_to(mut self, dir: &str) -> Self {
self.state_dir = Some(dir.to_string());
self
}
#[must_use]
pub fn h3_bind(mut self, addr: &str) -> Self {
self.h3_bind = Some(addr.to_string());
self
}
#[must_use]
pub fn redirect_bind(mut self, addr: &str) -> Self {
self.redirect_bind = Some(addr.to_string());
self
}
#[must_use]
pub fn acme(
mut self,
dns01_setter: crate::acme::Dns01Setter,
directory_url: Option<String>,
) -> Self {
self.acme = Some(AcmeRuntime { directory_url, dns01_setter });
self
}
#[must_use]
pub fn ssl(mut self, ssl: SslConfig) -> Self {
self.ssl = ssl;
self
}
#[must_use]
pub fn service(mut self, svc: ServiceConfig) -> Self {
self.services.push(svc);
self
}
#[must_use]
pub fn build(self) -> Self {
self
}
pub fn load_file(path: &str) -> Result<Self, ProxyError> {
let contents = std::fs::read_to_string(path)
.map_err(|e| ProxyError::Config(format!("read {path}: {e}")))?;
toml::from_str(&contents).map_err(|e| ProxyError::Config(format!("parse {path}: {e}")))
}
pub fn start(self) -> Result<crate::server::ProxyServer, ProxyError> {
crate::server::ProxyServer::start(self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SslConfig {
pub provider: SslProvider,
}
impl SslConfig {
#[must_use]
pub fn lets_encrypt(email: &str) -> Self {
Self {
provider: SslProvider::LetsEncrypt {
email: email.to_string(),
},
}
}
#[must_use]
pub fn cloudflare(zone_id: &str) -> Self {
Self {
provider: SslProvider::Cloudflare {
zone_id: zone_id.to_string(),
},
}
}
#[must_use]
pub fn static_cert(cert: &str, key: &str) -> Self {
Self {
provider: SslProvider::Static {
cert: PathBuf::from(cert),
key: PathBuf::from(key),
},
}
}
}
impl Default for SslConfig {
fn default() -> Self {
Self {
provider: SslProvider::None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SslProvider {
LetsEncrypt { email: String },
Cloudflare { zone_id: String },
Static { cert: PathBuf, key: PathBuf },
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceConfig {
pub name: String,
pub host: String,
#[serde(default)]
pub path_prefix: Option<String>,
pub backends: Vec<BackendTarget>,
#[serde(default)]
pub health_check: Option<HealthCheckConfig>,
}
impl ServiceConfig {
#[must_use]
pub fn new(name: &str, host: &str) -> Self {
Self {
name: name.to_string(),
host: host.to_string(),
path_prefix: None,
backends: Vec::new(),
health_check: None,
}
}
#[must_use]
pub fn path_prefix(mut self, prefix: &str) -> Self {
self.path_prefix = Some(prefix.to_string());
self
}
#[must_use]
pub fn backend(mut self, url: &str) -> Self {
self.backends.push(BackendTarget::new(url));
self
}
#[must_use]
pub fn backend_target(mut self, target: BackendTarget) -> Self {
self.backends.push(target);
self
}
#[must_use]
pub fn health_check(mut self, path: &str, interval: Duration) -> Self {
self.health_check = Some(HealthCheckConfig {
path: path.to_string(),
interval,
..HealthCheckConfig::default()
});
self
}
#[must_use]
pub fn health_check_config(mut self, config: HealthCheckConfig) -> Self {
self.health_check = Some(config);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display)]
pub enum BackendProtocol {
#[display("http")]
Http,
#[display("https")]
Https,
#[display("tcp")]
Tcp,
#[display("udp")]
Udp,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct BackendTarget {
pub url: String,
pub weight: u32,
pub max_connections: u32,
}
impl BackendTarget {
#[must_use]
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
weight: default_weight(),
max_connections: default_max_conns(),
}
}
#[must_use]
pub fn with_weight(mut self, weight: u32) -> Self {
self.weight = weight;
self
}
#[must_use]
pub fn with_max_connections(mut self, max_connections: u32) -> Self {
self.max_connections = max_connections;
self
}
#[must_use]
pub fn protocol(&self) -> BackendProtocol {
let lower = self.url.to_ascii_lowercase();
if lower.starts_with("udp://") {
BackendProtocol::Udp
} else if lower.starts_with("tcp://") {
BackendProtocol::Tcp
} else if lower.starts_with("https://") {
BackendProtocol::Https
} else {
BackendProtocol::Http
}
}
#[must_use]
pub fn authority(&self) -> String {
let without_scheme = self
.url
.split_once("://")
.map_or(self.url.as_str(), |(_, rest)| rest);
without_scheme
.split(['/', '?'])
.next()
.unwrap_or(without_scheme)
.to_string()
}
}
fn default_weight() -> u32 {
1
}
fn default_max_conns() -> u32 {
1024
}
impl<'de> Deserialize<'de> for BackendTarget {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Repr {
Bare(String),
Full {
url: String,
#[serde(default = "default_weight")]
weight: u32,
#[serde(default = "default_max_conns")]
max_connections: u32,
},
}
Ok(match Repr::deserialize(deserializer)? {
Repr::Bare(url) => BackendTarget::new(url),
Repr::Full {
url,
weight,
max_connections,
} => BackendTarget {
url,
weight,
max_connections,
},
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display)]
#[serde(rename_all = "lowercase")]
pub enum BackendState {
#[display("active")]
Active,
#[display("draining")]
Draining,
#[display("paused")]
Paused,
}
impl Default for BackendState {
fn default() -> Self {
Self::Active
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthCheckConfig {
pub path: String,
#[serde(with = "duration_secs")]
pub interval: Duration,
#[serde(with = "duration_secs")]
pub timeout: Duration,
pub healthy_threshold: u32,
pub unhealthy_threshold: u32,
}
impl Default for HealthCheckConfig {
fn default() -> Self {
Self {
path: "/".to_string(),
interval: Duration::from_secs(5),
timeout: Duration::from_secs(2),
healthy_threshold: 2,
unhealthy_threshold: 3,
}
}
}
mod duration_secs {
use serde::{Deserialize, Deserializer, Serializer};
use std::time::Duration;
pub fn serialize<S>(d: &Duration, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
s.serialize_u64(d.as_secs())
}
pub fn deserialize<'de, D>(d: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
let secs: u64 = Deserialize::deserialize(d)?;
Ok(Duration::from_secs(secs))
}
}
#[derive(Debug, Display)]
pub enum ProxyError {
#[display("Configuration error: {_0}")]
Config(String),
#[display("DNS error: {_0}")]
Dns(String),
#[display("SSL error: {_0}")]
Ssl(String),
#[display("Startup error: {_0}")]
Startup(String),
}
impl std::error::Error for ProxyError {}