pub mod parser;
pub mod health;
pub(crate) mod upstream;
pub use upstream::{BreakerConfig, RetryConfig, StickyConfig};
pub mod builder;
pub mod validate;
pub mod mcp_gateway;
pub mod llm;
#[cfg(test)]
mod tests;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock};
use crate::app::App;
use crate::application::Application;
use crate::core::New;
use crate::request::Request;
use crate::header::Header;
use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
use crate::server::ConnectionInfo;
use crate::server_config::ServerConfig;
#[derive(Debug, Clone)]
pub struct ProxyConfig {
pub upstreams: Vec<UpstreamConfig>,
pub routes: Vec<RouteConfig>,
pub tcp_proxies: Vec<TcpProxyConfig>,
pub udp_proxies: Vec<UdpProxyConfig>,
pub ws_proxies: Vec<WsProxyConfig>,
pub global_middleware: MiddlewareConfig,
}
#[derive(Debug, Clone)]
pub struct UpstreamConfig {
pub name: String,
pub backends: Vec<String>,
pub strategy: String, pub health_check: Option<HealthCheckConfig>,
pub tls: bool,
pub tls_ca_file: Option<String>,
pub tls_client_cert_file: Option<String>,
pub tls_client_key_file: Option<String>,
pub weights: Vec<u32>,
pub sticky: Option<StickyConfig>,
pub circuit_breaker: Option<BreakerConfig>,
pub retry: Option<RetryConfig>,
}
#[derive(Debug, Clone)]
pub struct HealthCheckConfig {
pub path: String,
pub interval_secs: u64,
pub timeout_ms: u64,
pub healthy_threshold: u32,
pub unhealthy_threshold: u32,
}
#[derive(Debug, Clone)]
pub struct RouteConfig {
pub name: String,
pub match_: MatchConfig,
pub action: ActionConfig,
pub middleware: MiddlewareConfig,
}
#[derive(Debug, Clone, Default)]
pub struct MatchConfig {
pub host: Option<String>,
pub path: Option<String>,
pub method: Option<String>,
pub content_type: Option<String>,
}
#[derive(Debug, Clone)]
pub enum ActionConfig {
Proxy {
upstream: String,
connect_timeout_ms: u64,
read_timeout_ms: u64,
strip_path_prefix: Option<String>,
add_path_prefix: Option<String>,
},
Grpc {
upstream: String,
connect_timeout_ms: u64,
read_timeout_ms: u64,
},
Static {
root: String,
index: Vec<String>,
},
Redirect {
location: String,
status: u16,
},
Respond {
status: u16,
body: String,
content_type: String,
},
Mcp(Box<mcp_gateway::McpGatewayConfig>),
Llm(Box<llm::LlmConfig>),
Unknown(String),
}
#[derive(Debug, Clone, Default)]
pub struct MiddlewareConfig {
pub rate_limit: Option<RateLimitConfig>,
pub cache: Option<CacheConfig>,
pub auth: Option<AuthConfig>,
pub rewrite_request: Vec<RewriteRuleConfig>,
pub rewrite_response: Vec<RewriteRuleConfig>,
pub ip_allow: Vec<String>,
pub ip_deny: Vec<String>,
pub timeout_ms: Option<u64>,
pub max_body_size: Option<u64>,
pub wasm: Option<WasmConfig>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct WasmConfig {
pub module: String,
pub fuel: u64,
pub memory_mb: u64,
pub config: String,
}
#[derive(Debug, Clone)]
pub struct RateLimitConfig {
pub max_requests: u32,
pub window_secs: u64,
}
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub ttl_secs: u64,
pub vary_by: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum AuthConfig {
Basic { htpasswd_file: String },
Jwt { secret_env: String },
Bearer { token_env: String },
}
#[derive(Debug, Clone, Default)]
pub struct RewriteRuleConfig {
pub type_: String,
pub name: Option<String>,
pub value: Option<String>,
pub prefix: Option<String>,
pub from: Option<String>,
pub to: Option<String>,
pub code: Option<u16>,
pub reason: Option<String>,
}
#[derive(Debug, Clone)]
pub struct TcpProxyConfig {
pub name: String,
pub listen: String,
pub backends: Vec<String>,
pub connect_timeout_ms: u64,
}
#[derive(Debug, Clone)]
pub struct UdpProxyConfig {
pub name: String,
pub listen: String,
pub backends: Vec<String>,
pub reply_timeout_ms: u64,
pub buffer_size: usize,
}
#[derive(Debug, Clone)]
pub struct WsProxyConfig {
pub name: String,
pub listen: String,
pub backends: Vec<String>,
pub connect_timeout_ms: u64,
pub read_timeout_ms: u64,
pub health_check: Option<HealthCheckConfig>,
}
fn merge_action_keys(map: &mut parser::SectionMap, action_section: &str, action_type: &str) {
if action_type.is_empty() {
return;
}
let short_form: Vec<(String, String)> = match map.get(action_section) {
Some(entries) => entries.iter().filter(|(k, _)| k != "type").cloned().collect(),
None => return,
};
let nested = map.entry(format!("{action_section}.{action_type}")).or_default();
for (key, value) in short_form {
if !nested.iter().any(|(k, _)| *k == key) {
nested.push((key, value));
}
}
}
impl ProxyConfig {
pub fn is_proxy_mode() -> bool {
let path = config_file_path();
match std::fs::read_to_string(&path) {
Ok(contents) => {
contents.contains("[[route]]") || contents.contains("[[upstream]]")
}
Err(_) => false,
}
}
pub fn load() -> Self {
let path = config_file_path();
let contents = std::fs::read_to_string(&path).unwrap_or_default();
Self::from_str(&contents)
}
pub fn from_str(toml: &str) -> Self {
use parser::{get_array, get_str, get_u32, get_u64, section_exists};
let mut map = parser::parse(toml);
let mut upstreams = Vec::new();
let mut i = 0;
loop {
let sec = format!("upstream[{}]", i);
if !section_exists(&map, &sec) {
break;
}
let name = get_str(&map, &sec, "name");
let backends = get_array(&map, &sec, "backends");
let strategy = {
let s = get_str(&map, &sec, "strategy");
if s.is_empty() { "round_robin".to_string() } else { s }
};
let hc_sec = format!("{}.health_check", sec);
let health_check = if section_exists(&map, &hc_sec) {
Some(HealthCheckConfig {
path: {
let p = get_str(&map, &hc_sec, "path");
if p.is_empty() { "/health".to_string() } else { p }
},
interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
})
} else {
None
};
let tls = backends.iter().any(|b| ["https://", "h2s://", "grpcs://"].iter().any(|s| b.starts_with(s)));
let tls_ca_file = Some(get_str(&map, &sec, "tls_ca_file")).filter(|v| !v.is_empty());
let tls_client_cert_file = Some(get_str(&map, &sec, "tls_client_cert_file")).filter(|v| !v.is_empty());
let tls_client_key_file = Some(get_str(&map, &sec, "tls_client_key_file")).filter(|v| !v.is_empty());
let weights: Vec<u32> = get_array(&map, &sec, "weights").iter().map(|w| w.trim().parse().unwrap_or(0)).collect();
let sticky_sec = format!("{}.sticky", sec);
let sticky = section_exists(&map, &sticky_sec).then(|| StickyConfig {
cookie: Some(get_str(&map, &sticky_sec, "cookie")).filter(|c| !c.is_empty()).unwrap_or_else(|| "rws_backend".to_string()),
max_age_secs: Some(get_u64(&map, &sticky_sec, "max_age_secs", 0)).filter(|s| *s > 0),
});
let cb_sec = format!("{}.circuit_breaker", sec);
let circuit_breaker = section_exists(&map, &cb_sec).then(|| BreakerConfig {
failure_threshold: get_u32(&map, &cb_sec, "failure_threshold", 5),
recovery_secs: get_u64(&map, &cb_sec, "recovery_secs", 30),
half_open_probes: get_u32(&map, &cb_sec, "half_open_probes", 1),
});
let retry_sec = format!("{}.retry", sec);
let retry = section_exists(&map, &retry_sec).then(|| {
let status: Vec<i16> = get_array(&map, &retry_sec, "status").iter().filter_map(|s| s.trim().parse().ok()).collect();
RetryConfig { attempts: get_u32(&map, &retry_sec, "attempts", 1), status: if status.is_empty() { vec![502, 503, 504] } else { status } }
});
upstreams.push(UpstreamConfig {
name, backends, strategy, health_check, tls, tls_ca_file, tls_client_cert_file, tls_client_key_file,
weights, sticky, circuit_breaker, retry,
});
i += 1;
}
let mut routes = Vec::new();
let mut i = 0;
loop {
let sec = format!("route[{}]", i);
if !section_exists(&map, &sec) {
break;
}
let name = get_str(&map, &sec, "name");
let m_sec = format!("{}.match", sec);
let match_ = MatchConfig {
host: {
let h = get_str(&map, &m_sec, "host");
if h.is_empty() { None } else { Some(h) }
},
path: {
let p = get_str(&map, &m_sec, "path");
if p.is_empty() { None } else { Some(p) }
},
method: {
let m = get_str(&map, &m_sec, "method");
if m.is_empty() { None } else { Some(m.to_uppercase()) }
},
content_type: {
let c = get_str(&map, &m_sec, "content_type");
if c.is_empty() { None } else { Some(c) }
},
};
let a_sec = format!("{}.action", sec);
let action_type = get_str(&map, &a_sec, "type");
merge_action_keys(&mut map, &a_sec, &action_type);
let action = match action_type.as_str() {
"proxy" => {
let p_sec = format!("{}.action.proxy", sec);
ActionConfig::Proxy {
upstream: get_str(&map, &p_sec, "upstream"),
connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
strip_path_prefix: {
let v = get_str(&map, &p_sec, "strip_path_prefix");
if v.is_empty() { None } else { Some(v) }
},
add_path_prefix: {
let v = get_str(&map, &p_sec, "add_path_prefix");
if v.is_empty() { None } else { Some(v) }
},
}
}
"grpc" => {
let p_sec = format!("{}.action.grpc", sec);
ActionConfig::Grpc {
upstream: get_str(&map, &p_sec, "upstream"),
connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
}
}
"static" => {
let s_sec = format!("{}.action.static", sec);
ActionConfig::Static {
root: get_str(&map, &s_sec, "root"),
index: get_array(&map, &s_sec, "index"),
}
}
"redirect" => {
let r_sec = format!("{}.action.redirect", sec);
ActionConfig::Redirect {
location: get_str(&map, &r_sec, "location"),
status: get_u64(&map, &r_sec, "status", 301) as u16,
}
}
"respond" => {
let r_sec = format!("{}.action.respond", sec);
ActionConfig::Respond {
status: get_u64(&map, &r_sec, "status", 200) as u16,
body: get_str(&map, &r_sec, "body"),
content_type: {
let ct = get_str(&map, &r_sec, "content_type");
if ct.is_empty() { "text/plain".to_string() } else { ct }
},
}
}
"mcp" => ActionConfig::Mcp(Box::new(mcp_gateway::parse(&map, &a_sec))),
"llm" => ActionConfig::Llm(Box::new(llm::parse(&map, &a_sec))),
other => ActionConfig::Unknown(other.to_string()),
};
let mw_sec = format!("{}.middleware", sec);
let middleware = parse_middleware_config(&map, &mw_sec, i);
routes.push(RouteConfig { name, match_, action, middleware });
i += 1;
}
let mut tcp_proxies = Vec::new();
let mut i = 0;
loop {
let sec = format!("tcp_proxy[{}]", i);
if !section_exists(&map, &sec) {
break;
}
tcp_proxies.push(TcpProxyConfig {
name: get_str(&map, &sec, "name"),
listen: get_str(&map, &sec, "listen"),
backends: get_array(&map, &sec, "backends"),
connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
});
i += 1;
}
let mut udp_proxies = Vec::new();
let mut i = 0;
loop {
let sec = format!("udp_proxy[{}]", i);
if !section_exists(&map, &sec) {
break;
}
udp_proxies.push(UdpProxyConfig {
name: get_str(&map, &sec, "name"),
listen: get_str(&map, &sec, "listen"),
backends: get_array(&map, &sec, "backends"),
reply_timeout_ms: get_u64(&map, &sec, "reply_timeout_ms", 5000),
buffer_size: get_u64(&map, &sec, "buffer_size", 65536) as usize,
});
i += 1;
}
let mut ws_proxies = Vec::new();
let mut i = 0;
loop {
let sec = format!("ws_proxy[{}]", i);
if !section_exists(&map, &sec) {
break;
}
let hc_sec = format!("{}.health_check", sec);
let health_check = if section_exists(&map, &hc_sec) {
Some(HealthCheckConfig {
path: {
let p = get_str(&map, &hc_sec, "path");
if p.is_empty() { "/health".to_string() } else { p }
},
interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
})
} else {
None
};
ws_proxies.push(WsProxyConfig {
name: get_str(&map, &sec, "name"),
listen: get_str(&map, &sec, "listen"),
backends: get_array(&map, &sec, "backends"),
connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
read_timeout_ms: get_u64(&map, &sec, "read_timeout_ms", 30000),
health_check,
});
i += 1;
}
let global_middleware = parse_middleware_config(&map, "middleware", usize::MAX);
ProxyConfig {
upstreams,
routes,
tcp_proxies,
udp_proxies,
ws_proxies,
global_middleware,
}
}
}
fn parse_middleware_config(
map: &parser::SectionMap,
mw_sec: &str,
route_idx: usize,
) -> MiddlewareConfig {
use parser::{get_array, get_str, get_u32, get_u64, section_exists};
let rl_sec = format!("{}.rate_limit", mw_sec);
let rate_limit = if section_exists(map, &rl_sec) {
Some(RateLimitConfig {
max_requests: get_u32(map, &rl_sec, "max_requests", 1000),
window_secs: get_u64(map, &rl_sec, "window_secs", 60),
})
} else {
None
};
let c_sec = format!("{}.cache", mw_sec);
let cache = if section_exists(map, &c_sec) {
Some(CacheConfig {
ttl_secs: get_u64(map, &c_sec, "ttl_secs", 60),
vary_by: get_array(map, &c_sec, "vary_by"),
})
} else {
None
};
let a_sec = format!("{}.auth", mw_sec);
let auth = if section_exists(map, &a_sec) {
let auth_type = get_str(map, &a_sec, "type");
match auth_type.as_str() {
"bearer" => Some(AuthConfig::Bearer {
token_env: get_str(map, &a_sec, "token_env"),
}),
"jwt" => Some(AuthConfig::Jwt {
secret_env: get_str(map, &a_sec, "secret_env"),
}),
"basic" => Some(AuthConfig::Basic {
htpasswd_file: get_str(map, &a_sec, "htpasswd_file"),
}),
_ => None,
}
} else {
None
};
let rewrite_request = collect_rewrite_rules(map, mw_sec, "request");
let rewrite_response = collect_rewrite_rules(map, mw_sec, "response");
let ip_sec = format!("{}.ip_filter", mw_sec);
let ip_allow = if section_exists(map, &ip_sec) {
get_array(map, &ip_sec, "allow")
} else {
vec![]
};
let ip_deny = if section_exists(map, &ip_sec) {
get_array(map, &ip_sec, "deny")
} else {
vec![]
};
let _ = route_idx;
let timeout_ms = match get_u64(map, mw_sec, "timeout_ms", 0) {
0 => None,
ms => Some(ms),
};
let max_body_size = match get_u64(map, mw_sec, "max_body_size", 0) {
0 => None,
n => Some(n),
};
let w_sec = format!("{}.wasm", mw_sec);
let wasm = section_exists(map, &w_sec).then(|| WasmConfig {
module: get_str(map, &w_sec, "module"),
fuel: get_u64(map, &w_sec, "fuel", 50_000_000),
memory_mb: get_u64(map, &w_sec, "memory_mb", 16),
config: get_str(map, &w_sec, "config"),
});
MiddlewareConfig {
rate_limit, cache, auth, rewrite_request, rewrite_response, ip_allow, ip_deny,
timeout_ms, max_body_size, wasm,
}
}
fn collect_rewrite_rules(
map: &parser::SectionMap,
mw_sec: &str,
direction: &str,
) -> Vec<RewriteRuleConfig> {
use parser::{get_str, get_u64};
let mut rules = Vec::new();
let mut j = 0;
loop {
let rsec = format!("{}.rewrite.{}[{}]", mw_sec, direction, j);
if !parser::section_exists(map, &rsec) {
break;
}
let code_val = get_u64(map, &rsec, "code", 0);
rules.push(RewriteRuleConfig {
type_: get_str(map, &rsec, "type"),
name: {
let v = get_str(map, &rsec, "name");
if v.is_empty() { None } else { Some(v) }
},
value: {
let v = get_str(map, &rsec, "value");
if v.is_empty() { None } else { Some(v) }
},
prefix: {
let v = get_str(map, &rsec, "prefix");
if v.is_empty() { None } else { Some(v) }
},
from: {
let v = get_str(map, &rsec, "from");
if v.is_empty() { None } else { Some(v) }
},
to: {
let v = get_str(map, &rsec, "to");
if v.is_empty() { None } else { Some(v) }
},
code: if code_val == 0 { None } else { Some(code_val as u16) },
reason: {
let v = get_str(map, &rsec, "reason");
if v.is_empty() { None } else { Some(v) }
},
});
j += 1;
}
rules
}
pub(crate) fn config_file_path() -> String {
std::env::var("RWS_CONFIG_FILE").unwrap_or_else(|_| "rws.config.toml".to_string())
}
pub(crate) struct CompiledRoute {
pub(crate) matcher: RouteMatcher,
pub(crate) handler: Arc<dyn Application + Send + Sync>,
#[cfg_attr(not(feature = "http2"), allow(dead_code))]
pub(crate) async_proxy: Option<Arc<DynamicProxy>>,
}
#[derive(Clone, Default)]
pub(crate) struct RouteMatcher {
pub(crate) host: Option<String>,
pub(crate) path_prefix: Option<String>,
pub(crate) path_exact: Option<String>,
pub(crate) method: Option<String>,
pub(crate) content_type_prefix: Option<String>,
}
impl RouteMatcher {
pub(crate) fn from_match_config(cfg: &MatchConfig) -> Self {
let (path_prefix, path_exact) = match &cfg.path {
Some(p) if p.ends_with('*') => {
let stripped = p.trim_end_matches('*').to_string();
(Some(stripped), None)
}
Some(p) => (None, Some(p.clone())),
None => (None, None),
};
let content_type_prefix = cfg.content_type.as_ref().map(|ct| {
if ct.ends_with('*') {
ct.trim_end_matches('*').to_string()
} else {
ct.clone()
}
});
RouteMatcher {
host: cfg.host.clone(),
path_prefix,
path_exact,
method: cfg.method.clone(),
content_type_prefix,
}
}
pub(crate) fn matches(&self, request: &Request, conn: &ConnectionInfo) -> bool {
if let Some(ref expected_host) = self.host {
let actual_host = conn
.sni_hostname
.as_deref()
.or_else(|| {
request
.headers
.iter()
.find(|h| h.name.eq_ignore_ascii_case("host"))
.map(|h| h.value.as_ref())
})
.unwrap_or("");
if !host_matches(expected_host, actual_host) {
return false;
}
}
if let Some(ref m) = self.method {
if request.method.to_uppercase() != m.as_str() {
return false;
}
}
let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
if let Some(ref prefix) = self.path_prefix {
if !path.starts_with(prefix.as_str()) {
return false;
}
} else if let Some(ref exact) = self.path_exact {
if path != exact.as_str() {
return false;
}
}
if let Some(ref ct_prefix) = self.content_type_prefix {
let actual_ct = request
.headers
.iter()
.find(|h| h.name.eq_ignore_ascii_case("content-type"))
.map(|h| h.value.as_ref())
.unwrap_or("");
if !actual_ct.starts_with(ct_prefix.as_str()) {
return false;
}
}
true
}
}
#[derive(Clone)]
pub struct ConfigDrivenApp {
current: Arc<RwLock<Arc<Generation>>>,
reload_lock: Arc<std::sync::Mutex<()>>,
}
struct Generation {
dispatch: RouteDispatch,
entry: Option<Arc<dyn Application + Send + Sync>>,
config: ProxyConfig,
source: Option<String>,
health_stop: Arc<AtomicBool>,
}
impl Generation {
fn new(routes: Arc<Vec<CompiledRoute>>, fallback: App, config: ProxyConfig, health_stop: Arc<AtomicBool>, source: Option<String>) -> Self {
let dispatch = RouteDispatch { routes, fallback };
let entry = if config.global_middleware.is_empty() {
None
} else {
Some(builder::apply_middleware(arc_app(dispatch.clone()), &config.global_middleware))
};
Generation { dispatch, entry, config, source, health_stop }
}
}
#[derive(Clone)]
struct RouteDispatch {
routes: Arc<Vec<CompiledRoute>>,
fallback: App,
}
impl Application for RouteDispatch {
fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
for route in self.routes.iter() {
if route.matcher.matches(request, conn) {
return route.handler.execute(request, conn);
}
}
self.fallback.execute(request, conn)
}
fn streams_body(&self, request: &Request, conn: &ConnectionInfo) -> bool {
match self.routes.iter().find(|r| r.matcher.matches(request, conn)) {
Some(route) => route.handler.streams_body(request, conn),
None => self.fallback.streams_body(request, conn),
}
}
}
impl MiddlewareConfig {
pub fn is_empty(&self) -> bool {
self.rate_limit.is_none() && self.cache.is_none() && self.auth.is_none()
&& self.rewrite_request.is_empty() && self.rewrite_response.is_empty()
&& self.ip_allow.is_empty() && self.ip_deny.is_empty()
&& self.timeout_ms.is_none() && self.max_body_size.is_none() && self.wasm.is_none()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ReloadOutcome {
Unchanged,
Reloaded { routes: usize, warnings: Vec<String> },
}
impl ConfigDrivenApp {
pub(crate) fn new(routes: Vec<CompiledRoute>, config: ProxyConfig, health_stop: Arc<AtomicBool>, source: Option<String>) -> Self {
use crate::core::New;
let generation = Generation::new(Arc::new(routes), App::new(), config, health_stop, source);
ConfigDrivenApp {
current: Arc::new(RwLock::new(Arc::new(generation))),
reload_lock: Arc::new(std::sync::Mutex::new(())),
}
}
fn current(&self) -> Arc<Generation> {
Arc::clone(&self.current.read().unwrap_or_else(|e| e.into_inner()))
}
pub fn with_config(self, config: ServerConfig) -> Self {
{
let mut slot = self.current.write().unwrap_or_else(|e| e.into_inner());
let old = Arc::clone(&slot);
*slot = Arc::new(Generation::new(
Arc::clone(&old.dispatch.routes),
App::with_config(config),
old.config.clone(),
Arc::clone(&old.health_stop),
old.source.clone(),
));
}
self
}
pub fn reload_from_str(&self, toml: &str) -> Result<ReloadOutcome, crate::config_check::ConfigReport> {
let _serialized = self.reload_lock.lock().unwrap_or_else(|e| e.into_inner());
let old = self.current();
if old.source.as_deref() == Some(toml) {
return Ok(ReloadOutcome::Unchanged);
}
let config = ProxyConfig::from_str(toml);
let mut report = crate::config_check::ConfigReport::default();
validate::check_unknown_keys(toml, &mut report);
config.validate(&mut report);
if !report.is_ok() {
return Err(report);
}
let mut warnings = report.warnings;
let listeners = |c: &ProxyConfig| format!("{:?}{:?}{:?}", c.tcp_proxies, c.udp_proxies, c.ws_proxies);
if listeners(&config) != listeners(&old.config) {
warnings.push("[[tcp_proxy]] / [[udp_proxy]] / [[ws_proxy]] changes need a restart; the old listeners keep running".to_string());
}
let health_stop = Arc::new(AtomicBool::new(false));
let routes = builder::compile_routes(&config, &health_stop);
let route_count = routes.len();
let generation = Generation::new(Arc::new(routes), old.dispatch.fallback.clone(), config, health_stop, Some(toml.to_string()));
let replaced = {
let mut slot = self.current.write().unwrap_or_else(|e| e.into_inner());
std::mem::replace(&mut *slot, Arc::new(generation))
};
replaced.health_stop.store(true, Ordering::Relaxed);
Ok(ReloadOutcome::Reloaded { routes: route_count, warnings })
}
pub fn reload_from_file(&self) {
let path = config_file_path();
let toml = match std::fs::read_to_string(&path) {
Ok(toml) => toml,
Err(e) => {
eprintln!("[proxy_config] reload: cannot read {path}: {e} — keeping the running routes");
crate::config_reload::report_error(format!("cannot read {path}: {e}"));
return;
}
};
match self.reload_from_str(&toml) {
Ok(ReloadOutcome::Unchanged) => {}
Ok(ReloadOutcome::Reloaded { routes, warnings }) => {
for w in &warnings {
eprintln!("[proxy_config] Warning: {w}");
}
println!("[proxy_config] reloaded {path}: {routes} route{}", if routes == 1 { "" } else { "s" });
}
Err(report) => {
for e in &report.errors {
eprintln!("[proxy_config] reload ERROR: {e}");
crate::config_reload::report_error(e.clone());
}
eprintln!("[proxy_config] {path} has errors — keeping the running routes");
}
}
}
}
fn normalized(request: &Request) -> std::borrow::Cow<'_, Request> {
match normalize_request_target(&request.request_uri) {
std::borrow::Cow::Borrowed(_) => std::borrow::Cow::Borrowed(request),
std::borrow::Cow::Owned(target) => {
let mut owned = request.clone();
owned.request_uri = target;
std::borrow::Cow::Owned(owned)
}
}
}
pub(crate) fn normalize_request_target(target: &str) -> std::borrow::Cow<'_, str> {
let (path, query) = match target.find('?') {
Some(i) => (&target[..i], &target[i..]),
None => (target, ""),
};
if !path.starts_with('/') || !(path.contains('%') || path.contains("//") || path.contains("/.")) {
return std::borrow::Cow::Borrowed(target);
}
let bytes = path.as_bytes();
let mut decoded = String::with_capacity(path.len());
let mut i = 0;
while i < bytes.len() {
let hex = |b: u8| (b as char).to_digit(16);
if bytes[i] == b'%' && i + 2 < bytes.len() {
if let (Some(h), Some(l)) = (hex(bytes[i + 1]), hex(bytes[i + 2])) {
let value = (h * 16 + l) as u8;
if value.is_ascii_alphanumeric() || matches!(value, b'-' | b'.' | b'_' | b'~') {
decoded.push(value as char);
} else {
decoded.push('%');
decoded.push(bytes[i + 1].to_ascii_uppercase() as char);
decoded.push(bytes[i + 2].to_ascii_uppercase() as char);
}
i += 3;
continue;
}
}
if bytes[i] == b'%' {
decoded.push_str("%25");
i += 1;
continue;
}
let ch = path[i..].chars().next().unwrap_or('\u{FFFD}');
decoded.push(ch);
i += ch.len_utf8().max(1);
}
let mut segments: Vec<&str> = Vec::new();
let parts: Vec<&str> = decoded.split('/').collect();
let last = parts.len().saturating_sub(1);
let mut trailing_slash = false;
for (n, segment) in parts.iter().enumerate() {
match *segment {
"" => trailing_slash = n == last,
"." => trailing_slash = n == last,
".." => {
segments.pop();
trailing_slash = n == last;
}
s => {
segments.push(s);
trailing_slash = false;
}
}
}
let mut out = String::with_capacity(decoded.len() + query.len());
out.push('/');
out.push_str(&segments.join("/"));
if trailing_slash && !segments.is_empty() {
out.push('/');
}
out.push_str(query);
if out == target {
std::borrow::Cow::Borrowed(target)
} else {
std::borrow::Cow::Owned(out)
}
}
fn host_matches(expected: &str, actual: &str) -> bool {
let actual = if expected.contains(':') {
actual
} else if let Some(rest) = actual.strip_prefix('[') {
rest.split(']').next().unwrap_or(rest)
} else {
actual.rsplit_once(':').map_or(actual, |(host, port)| if port.bytes().all(|b| b.is_ascii_digit()) { host } else { actual })
};
let expected = expected.trim_start_matches('[').trim_end_matches(']');
actual.eq_ignore_ascii_case(expected)
}
impl Application for ConfigDrivenApp {
fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
let request = normalized(request);
let generation = self.current();
match &generation.entry {
Some(entry) => entry.execute(&request, conn),
None => generation.dispatch.execute(&request, conn),
}
}
fn streams_body(&self, request: &Request, conn: &ConnectionInfo) -> bool {
let request = normalized(request);
let generation = self.current();
match &generation.entry {
Some(entry) => entry.streams_body(&request, conn),
None => generation.dispatch.streams_body(&request, conn),
}
}
fn runs_inline(&self, request: &Request, conn: &ConnectionInfo) -> bool {
let request = normalized(request);
let generation = self.current();
generation.entry.is_none()
&& !generation.dispatch.routes.iter().any(|r| r.matcher.matches(&request, conn))
&& App::is_inline_request(&request)
}
#[cfg(feature = "http2")]
fn execute_async<'a>(&'a self, request: &'a Request, conn: &'a ConnectionInfo) -> Option<crate::application::AsyncResponse<'a>> {
let generation = self.current();
if generation.entry.is_some() {
return None;
}
let request = normalized(request);
let route = generation.dispatch.routes.iter().find(|r| r.matcher.matches(&request, conn))?;
let proxy = Arc::clone(route.async_proxy.as_ref()?);
Some(Box::pin(async move { proxy.execute_async(&request, conn).await }))
}
}
#[cfg(feature = "http2")]
pub(crate) struct H2Route {
live: Arc<RwLock<Vec<String>>>,
tls: bool,
trust: Option<Arc<crate::proxy::UpstreamTls>>,
connect_timeout: std::time::Duration,
read_timeout: std::time::Duration,
counter: std::sync::atomic::AtomicUsize,
}
#[cfg(feature = "http2")]
impl H2Route {
pub(crate) fn new(
live: Arc<RwLock<Vec<String>>>,
tls: bool,
trust: Option<Arc<crate::proxy::UpstreamTls>>,
connect_timeout_ms: u64,
read_timeout_ms: u64,
) -> H2Route {
H2Route {
live,
tls,
trust,
connect_timeout: std::time::Duration::from_millis(connect_timeout_ms),
read_timeout: std::time::Duration::from_millis(read_timeout_ms),
counter: std::sync::atomic::AtomicUsize::new(0),
}
}
}
#[cfg(feature = "http2")]
impl Application for H2Route {
fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
use crate::middleware::Middleware;
crate::proxy::GrpcWebLayer::new().handle(request, conn, &H2Forward(self))
}
fn streams_body(&self, _request: &Request, _conn: &ConnectionInfo) -> bool {
true
}
}
#[cfg(feature = "http2")]
struct H2Forward<'a>(&'a H2Route);
#[cfg(feature = "http2")]
impl Application for H2Forward<'_> {
fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
let route = self.0;
let backends = route.live.read().unwrap_or_else(|e| e.into_inner()).clone();
if backends.is_empty() {
return Ok(crate::proxy::bad_gateway());
}
let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { backends.len() };
let start = route.counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
for attempt in 0..attempts {
let url = &backends[(start + attempt) % backends.len()];
let result = crate::proxy::forward_h2(
request, crate::proxy::Peer::of(conn), url, route.tls, route.trust.as_deref(), route.connect_timeout, route.read_timeout,
);
match result {
Ok(response) => return Ok(response),
Err(e) => eprintln!("grpc route: {e}"),
}
}
Ok(crate::proxy::bad_gateway())
}
}
#[derive(Clone, Copy)]
pub(crate) struct NullApp;
impl Application for NullApp {
fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
Ok(r)
}
}
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::atomic::AtomicUsize;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum LoadBalanceStrategy {
RoundRobin,
Random,
IpHash,
LeastConnections,
}
impl LoadBalanceStrategy {
fn parse(s: &str) -> Self {
match s {
"random" => LoadBalanceStrategy::Random,
"ip_hash" => LoadBalanceStrategy::IpHash,
"least_connections" => LoadBalanceStrategy::LeastConnections,
_ => LoadBalanceStrategy::RoundRobin,
}
}
}
#[derive(Clone)]
pub(crate) struct DynamicProxy {
live: Arc<RwLock<Vec<String>>>,
counter: Arc<AtomicUsize>,
connect_timeout: Duration,
read_timeout: Duration,
strip_prefix: Option<Arc<String>>,
add_prefix: Option<Arc<String>>,
tls: bool,
strategy: LoadBalanceStrategy,
connections: Arc<RwLock<HashMap<String, Arc<AtomicUsize>>>>,
trust: Option<Arc<crate::proxy::UpstreamTls>>,
upstream: Arc<String>,
policy: Option<Arc<upstream::UpstreamPolicy>>,
}
impl DynamicProxy {
pub(crate) fn with_trust(mut self, trust: Option<Arc<crate::proxy::UpstreamTls>>) -> Self {
self.trust = trust;
self
}
pub(crate) fn with_policy(mut self, upstream: &str, policy: Option<Arc<upstream::UpstreamPolicy>>) -> Self {
self.upstream = Arc::new(upstream.to_string());
self.policy = policy;
self
}
pub(crate) fn new(
live: Arc<RwLock<Vec<String>>>,
connect_timeout_ms: u64,
read_timeout_ms: u64,
strip_prefix: Option<String>,
add_prefix: Option<String>,
tls: bool,
strategy: String,
) -> Self {
DynamicProxy {
live,
counter: Arc::new(AtomicUsize::new(0)),
connect_timeout: Duration::from_millis(connect_timeout_ms),
read_timeout: Duration::from_millis(read_timeout_ms),
strip_prefix: strip_prefix.map(Arc::new),
add_prefix: add_prefix.map(Arc::new),
tls,
strategy: LoadBalanceStrategy::parse(&strategy),
connections: Arc::new(RwLock::new(HashMap::new())),
trust: None,
upstream: Arc::new(String::new()),
policy: None,
}
}
fn pick(&self, candidates: &[String], client_ip: &str) -> String {
let policy = self.policy.as_deref().filter(|p| p.weighted());
let idx = match self.strategy {
LoadBalanceStrategy::RoundRobin => match policy {
Some(policy) => return policy.weighted_round_robin(candidates).unwrap_or_else(|| candidates[0].clone()),
None => self.counter.fetch_add(1, Ordering::Relaxed) % candidates.len(),
},
LoadBalanceStrategy::Random => {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0) as usize;
let salt = self.counter.fetch_add(1, Ordering::Relaxed);
let roll = nanos.wrapping_add(salt.wrapping_mul(2_654_435_761));
match policy {
Some(policy) => policy.weighted_index(candidates, roll),
None => roll % candidates.len(),
}
}
LoadBalanceStrategy::IpHash => {
let mut hasher = DefaultHasher::new();
client_ip.hash(&mut hasher);
(hasher.finish() as usize) % candidates.len()
}
LoadBalanceStrategy::LeastConnections => {
let connections = self.connections.read().unwrap();
let load = |backend: &String| connections.get(backend).map(|c| c.load(Ordering::Relaxed)).unwrap_or(0) as u64;
let weight = |backend: &String| self.policy.as_deref().map_or(1, |p| p.weight(backend)) as u64;
candidates
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| (load(a) * weight(b)).cmp(&(load(b) * weight(a))))
.map(|(i, _)| i)
.unwrap_or(0)
}
};
candidates[idx].clone()
}
fn next_backend(&self, request: &Request, client_ip: &str, tried: &[String]) -> Result<String, bool> {
let mut candidates: Vec<String> = self.live.read().unwrap().iter().filter(|b| !tried.contains(b)).cloned().collect();
if !candidates.is_empty() {
candidates.retain(|b| !upstream::is_drained(&self.upstream, b));
if candidates.is_empty() {
return Err(true);
}
}
let Some(policy) = self.policy.as_deref() else {
return if candidates.is_empty() { Err(false) } else { Ok(self.pick(&candidates, client_ip)) };
};
if let Some(sticky) = &policy.sticky {
if let Some(id) = upstream::cookie_value(request, &sticky.cookie) {
if let Some(backend) = candidates.iter().find(|b| upstream::backend_id(b) == id).cloned() {
if policy.admits(&backend) {
return Ok(backend);
}
candidates.retain(|b| *b != backend);
}
}
}
let ejected = policy.any_open(&candidates);
while !candidates.is_empty() {
let backend = self.pick(&candidates, client_ip);
if policy.admits(&backend) {
return Ok(backend);
}
candidates.retain(|b| *b != backend);
}
Err(ejected)
}
fn connection_counter(&self, backend: &str) -> Arc<AtomicUsize> {
if let Some(counter) = self.connections.read().unwrap().get(backend) {
return Arc::clone(counter);
}
let mut connections = self.connections.write().unwrap();
Arc::clone(
connections
.entry(backend.to_string())
.or_insert_with(|| Arc::new(AtomicUsize::new(0))),
)
}
}
struct ConnectionGuard {
counter: Arc<AtomicUsize>,
}
impl Drop for ConnectionGuard {
fn drop(&mut self) {
self.counter.fetch_sub(1, Ordering::Relaxed);
}
}
struct Prepared<'a> {
backend: String,
host: String,
port: u16,
request: std::borrow::Cow<'a, Request>,
_guard: Option<ConnectionGuard>,
}
enum Attempt {
Done(Response),
Failed(Option<Response>),
}
impl DynamicProxy {
fn prepare<'a>(&self, request: &'a Request, conn: &ConnectionInfo, tried: &[String]) -> Result<Prepared<'a>, Response> {
let backend = self.next_backend(request, &conn.client.ip, tried).map_err(|ejected| if ejected { service_unavailable() } else { bad_gateway() })?;
let guard = if self.strategy == LoadBalanceStrategy::LeastConnections {
let counter = self.connection_counter(&backend);
counter.fetch_add(1, Ordering::Relaxed);
Some(ConnectionGuard { counter })
} else {
None
};
let (host, port, _) = crate::proxy_config::health::parse_backend_url(&backend).ok_or_else(bad_gateway)?;
let request = if self.strip_prefix.is_some() || self.add_prefix.is_some() {
let mut req_clone = request.clone();
if let Some(ref sp) = self.strip_prefix {
if let Some(stripped) = req_clone.request_uri.strip_prefix(sp.as_str()) {
req_clone.request_uri = if stripped.is_empty() || !stripped.starts_with('/') {
format!("/{}", stripped)
} else {
stripped.to_string()
};
}
}
if let Some(ref ap) = self.add_prefix {
req_clone.request_uri = format!("{}{}", ap, req_clone.request_uri);
}
std::borrow::Cow::Owned(req_clone)
} else {
std::borrow::Cow::Borrowed(request)
};
Ok(Prepared { backend, host, port, request, _guard: guard })
}
fn settle(&self, backend: &str, result: Result<Response, ()>, started: std::time::Instant) -> Attempt {
let status = result.as_ref().ok().map(|r| r.status_code);
upstream::observe(&self.upstream, backend, status, started);
let failed = status.is_none_or(|s| matches!(s, 502..=504));
if let Some(policy) = &self.policy {
policy.record(backend, failed);
}
match result {
Ok(response) if self.policy.as_deref().is_some_and(|p| p.retries_status(response.status_code)) => Attempt::Failed(Some(response)),
Ok(response) => Attempt::Done(response),
Err(()) => Attempt::Failed(None),
}
}
fn attempts(&self, request: &Request) -> u32 {
match self.policy.as_deref().and_then(|p| p.retry.as_ref()) {
Some(retry) if upstream::replayable(request) => 1 + retry.attempts,
_ => 1,
}
}
fn stick(&self, request: &Request, backend: &str, response: &mut Response) {
let Some(sticky) = self.policy.as_deref().and_then(|p| p.sticky.as_ref()) else { return };
let id = upstream::backend_id(backend);
if upstream::cookie_value(request, &sticky.cookie).as_deref() == Some(id.as_str()) {
return;
}
let max_age = sticky.max_age_secs.map(|s| format!("; Max-Age={s}")).unwrap_or_default();
response.headers.push(crate::header::Header {
name: "Set-Cookie".into(),
value: format!("{}={id}; Path=/; HttpOnly; SameSite=Lax{max_age}", sticky.cookie).into(),
});
}
#[cfg(feature = "http2")]
pub(crate) async fn execute_async(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
let mut tried: Vec<String> = Vec::new();
let mut last: Option<Response> = None;
for attempt in 0..self.attempts(request) {
let prepared = match self.prepare(request, conn, &tried) {
Ok(p) => p,
Err(response) => return Ok(last.unwrap_or(response)),
};
if attempt > 0 {
upstream::observe_retry(&self.upstream);
}
let tls = self.tls.then(|| match &self.trust {
Some(trust) => trust.async_tls(),
None => crate::proxy::async_upstream::AsyncTls {
config: crate::proxy::tls_client_config(),
pool: crate::proxy::async_upstream::default_tls_pool(),
},
});
let started = std::time::Instant::now();
let result = crate::proxy::async_upstream::forward(
&prepared.request,
crate::proxy::Peer::of(conn),
&prepared.host,
prepared.port,
tls,
self.connect_timeout,
self.read_timeout,
)
.await
.map_err(|_| ());
match self.settle(&prepared.backend, result, started) {
Attempt::Done(mut response) => {
self.stick(request, &prepared.backend, &mut response);
return Ok(response);
}
Attempt::Failed(response) => last = response,
}
tried.push(prepared.backend.clone());
}
Ok(last.unwrap_or_else(bad_gateway))
}
pub(crate) fn supports_async(&self) -> bool {
cfg!(feature = "http2")
}
}
impl Application for DynamicProxy {
fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
let mut tried: Vec<String> = Vec::new();
let mut last: Option<Response> = None;
for attempt in 0..self.attempts(request) {
let prepared = match self.prepare(request, conn, &tried) {
Ok(p) => p,
Err(response) => return Ok(last.unwrap_or(response)),
};
if attempt > 0 {
upstream::observe_retry(&self.upstream);
}
let started = std::time::Instant::now();
let result = crate::proxy::forward_upstream_trusting(
crate::proxy::shared_pool(),
&prepared.request,
crate::proxy::Peer::of(conn),
&prepared.host,
prepared.port,
self.tls,
self.trust.as_deref(),
self.connect_timeout,
self.read_timeout,
)
.map_err(|_| ());
match self.settle(&prepared.backend, result, started) {
Attempt::Done(mut response) => {
self.stick(request, &prepared.backend, &mut response);
return Ok(response);
}
Attempt::Failed(response) => last = response,
}
tried.push(prepared.backend.clone());
}
Ok(last.unwrap_or_else(bad_gateway))
}
}
fn service_unavailable() -> Response {
use crate::mime_type::MimeType;
use crate::range::Range;
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n503_service_unavailable.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n503_service_unavailable.reason_phrase.to_string();
r.headers.push(crate::header::Header { name: "Retry-After".into(), value: "5".into() });
r.content_range_list = vec![Range::get_content_range(b"503 Service Unavailable: every backend is out of rotation".to_vec(), MimeType::TEXT_PLAIN.to_string())];
r
}
fn bad_gateway() -> Response {
use crate::mime_type::MimeType;
use crate::range::Range;
let cr = Range::get_content_range(
b"502 Bad Gateway".to_vec(),
MimeType::TEXT_PLAIN.to_string(),
);
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n502_bad_gateway.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n502_bad_gateway.reason_phrase.to_string();
r.content_range_list = vec![cr];
r
}
#[derive(Clone)]
pub(crate) struct RedirectAdapter {
location_template: Arc<String>,
status: i16,
reason: Arc<String>,
}
impl RedirectAdapter {
pub(crate) fn new(location: String, status: u16) -> Self {
let (code, reason) = redirect_status(status);
RedirectAdapter {
location_template: Arc::new(location),
status: code,
reason: Arc::new(reason),
}
}
}
fn redirect_status(code: u16) -> (i16, String) {
let phrase = match code {
301 => STATUS_CODE_REASON_PHRASE.n301_moved_permanently.reason_phrase,
302 => STATUS_CODE_REASON_PHRASE.n302_found.reason_phrase,
307 => STATUS_CODE_REASON_PHRASE.n307_temporary_redirect.reason_phrase,
308 => STATUS_CODE_REASON_PHRASE.n308_permanent_redirect.reason_phrase,
_ => "Redirect",
};
(code as i16, phrase.to_string())
}
impl Application for RedirectAdapter {
fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
let location = self
.location_template
.replace("$path", &request.request_uri);
use crate::header::Header;
let mut r = Response::new();
r.status_code = self.status;
r.reason_phrase = self.reason.as_ref().clone();
r.headers.push(Header { name: "Location".into(), value: location.into() });
Ok(r)
}
}
#[derive(Clone)]
pub(crate) struct RespondAdapter {
status: i16,
reason: Arc<String>,
body: Arc<Vec<u8>>,
content_type: Arc<String>,
}
impl RespondAdapter {
pub(crate) fn new(status: u16, body: String, content_type: String) -> Self {
use crate::response::STATUS_CODE_REASON_PHRASE;
let reason = match status {
200 => STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string(),
201 => STATUS_CODE_REASON_PHRASE.n201_created.reason_phrase.to_string(),
204 => STATUS_CODE_REASON_PHRASE.n204_no_content.reason_phrase.to_string(),
400 => STATUS_CODE_REASON_PHRASE.n400_bad_request.reason_phrase.to_string(),
401 => STATUS_CODE_REASON_PHRASE.n401_unauthorized.reason_phrase.to_string(),
403 => STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string(),
404 => STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string(),
500 => STATUS_CODE_REASON_PHRASE.n500_internal_server_error.reason_phrase.to_string(),
_ => "OK".to_string(),
};
RespondAdapter {
status: status as i16,
reason: Arc::new(reason),
body: Arc::new(body.into_bytes()),
content_type: Arc::new(content_type),
}
}
}
impl Application for RespondAdapter {
fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
use crate::range::Range;
let mut r = Response::new();
r.status_code = self.status;
r.reason_phrase = self.reason.as_ref().clone();
if !self.body.is_empty() {
r.content_range_list = vec![Range::get_content_range(
self.body.as_ref().clone(),
self.content_type.as_ref().clone(),
)];
}
Ok(r)
}
}
#[derive(Clone)]
pub(crate) struct StaticAdapter {
root: Arc<std::path::PathBuf>,
index: Arc<Vec<String>>,
}
impl StaticAdapter {
pub(crate) fn new(root: String, index: Vec<String>) -> Self {
let index = if index.is_empty() { vec!["index.html".to_string()] } else { index };
StaticAdapter {
root: Arc::new(std::path::PathBuf::from(root)),
index: Arc::new(index),
}
}
fn resolve(&self, request_uri: &str) -> Option<std::path::PathBuf> {
let raw_path = request_uri.split('?').next().unwrap_or(request_uri);
let decoded = crate::url::URL::percent_decode(raw_path);
if decoded.split('/').any(|segment| segment == "..") {
return None;
}
let relative = decoded.trim_start_matches('/');
Some(self.root.join(relative))
}
}
impl Application for StaticAdapter {
fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
let mut response = Response::new();
let not_found = |mut response: Response| {
response.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
response.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
response
};
let candidate = match self.resolve(&request.request_uri) {
Some(p) => p,
None => {
response.status_code = *STATUS_CODE_REASON_PHRASE.n403_forbidden.status_code;
response.reason_phrase = STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string();
return Ok(response);
}
};
let mut file_path = candidate;
if file_path.is_dir() {
let indexed = self
.index
.iter()
.map(|name| file_path.join(name))
.find(|p| p.is_file());
file_path = match indexed {
Some(p) => p,
None => return Ok(not_found(response)),
};
}
if !file_path.is_file() {
return Ok(not_found(response));
}
if let (Ok(root_canon), Ok(file_canon)) =
(self.root.canonicalize(), file_path.canonicalize())
{
if !file_canon.starts_with(&root_canon) {
return Ok(not_found(response));
}
}
let path_str = match file_path.to_str() {
Some(s) => s,
None => return Ok(not_found(response)),
};
match crate::range::Range::get_content_range_of_a_file(path_str) {
Ok(content_range) => {
response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
response.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
response.content_range_list = vec![content_range];
Ok(response)
}
Err(_) => Ok(not_found(response)),
}
}
}
pub(crate) struct PerRouteRateLimit(pub(crate) Arc<crate::rate_limit::RateLimiter>);
impl crate::middleware::Middleware for PerRouteRateLimit {
fn handle(
&self,
request: &Request,
conn: &ConnectionInfo,
next: &dyn Application,
) -> Result<Response, String> {
use crate::error::{AppError, IntoResponse};
if self.0.check(&conn.client.ip) {
next.execute(request, conn)
} else {
let mut response = AppError::TooManyRequests.into_response();
let retry_after = self.0.retry_after(&conn.client.ip);
response.headers.push(Header {
name: Header::_RETRY_AFTER.into(),
value: retry_after.to_string().into(),
});
Ok(response)
}
}
}
pub(crate) struct PerRouteMaxBodySize(pub(crate) u64);
impl crate::middleware::Middleware for PerRouteMaxBodySize {
fn handle(
&self,
request: &Request,
conn: &ConnectionInfo,
next: &dyn Application,
) -> Result<Response, String> {
use crate::error::{AppError, IntoResponse};
if request.body.len() as u64 > self.0 {
Ok(AppError::PayloadTooLarge(format!(
"request body of {} bytes exceeds this route's {}-byte limit",
request.body.len(),
self.0,
))
.into_response())
} else {
next.execute(request, conn)
}
}
}
pub(crate) struct BearerAuthMiddleware {
pub(crate) token: Arc<String>,
}
impl crate::middleware::Middleware for BearerAuthMiddleware {
fn handle(
&self,
request: &Request,
conn: &ConnectionInfo,
next: &dyn Application,
) -> Result<Response, String> {
use crate::error::{AppError, IntoResponse};
let expected = format!("Bearer {}", self.token);
let authorized = request
.headers
.iter()
.any(|h| h.name.eq_ignore_ascii_case("authorization") && constant_time_eq(h.value.as_bytes(), expected.as_bytes()));
if authorized {
next.execute(request, conn)
} else {
Ok(AppError::Unauthorized.into_response())
}
}
}
pub(crate) fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
a.iter().zip(b).fold(0u8, |diff, (x, y)| diff | (x ^ y)) == 0
}
pub(crate) struct AuthMisconfigured;
pub(crate) struct PluginMisconfigured;
impl crate::middleware::Middleware for PluginMisconfigured {
fn handle(&self, _request: &Request, _conn: &ConnectionInfo, _next: &dyn Application) -> Result<Response, String> {
Ok(Response::text(
STATUS_CODE_REASON_PHRASE.n503_service_unavailable,
"503 Service Unavailable: this route's plugin is misconfigured",
))
}
}
impl crate::middleware::Middleware for AuthMisconfigured {
fn handle(
&self,
_request: &Request,
_conn: &ConnectionInfo,
_next: &dyn Application,
) -> Result<Response, String> {
Ok(Response::text(
STATUS_CODE_REASON_PHRASE.n503_service_unavailable,
"503 Service Unavailable: authentication for this route is misconfigured",
))
}
}
pub(crate) fn arc_app<A: Application + Send + Sync + 'static>(
a: A,
) -> Arc<dyn Application + Send + Sync> {
Arc::new(a)
}
pub fn build_from_file() -> (ConfigDrivenApp, Vec<std::thread::JoinHandle<()>>) {
builder::build_from_file()
}