Skip to main content

rws_proxy/proxy_config/
mod.rs

1//! Config-driven proxy application.
2//!
3//! When `rws.config.toml` contains `[[route]]` or `[[upstream]]` sections,
4//! `ConfigDrivenApp` is used as the top-level `Application` instead of the
5//! hardcoded `build_app()` in `main.rs`.
6//!
7//! # Quick start
8//!
9//! ```toml
10//! # rws.config.toml
11//! [[upstream]]
12//! name = "api"
13//! backends = ["localhost:3000"]
14//!
15//! [[route]]
16//! name = "api-proxy"
17//!
18//! [route.match]
19//! path = "/api/*"
20//!
21//! [route.action]
22//! type = "proxy"
23//!
24//! [route.action.proxy]
25//! upstream = "api"
26//! ```
27
28pub mod parser;
29pub mod health;
30pub(crate) mod upstream;
31
32pub use upstream::{BreakerConfig, RetryConfig, StickyConfig};
33pub mod builder;
34pub mod validate;
35pub mod mcp_gateway;
36pub mod a2a_gateway;
37pub mod llm;
38#[cfg(feature = "sso")]
39#[doc(hidden)]
40pub mod oidc_route;
41
42#[cfg(test)]
43mod tests;
44
45use std::sync::atomic::{AtomicBool, Ordering};
46use std::sync::{Arc, RwLock};
47
48use crate::app::App;
49use crate::application::Application;
50use crate::core::New;
51use crate::request::Request;
52use crate::header::Header;
53use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
54use crate::router::{host_matches, request_host};
55use crate::server::ConnectionInfo;
56use crate::server_config::ServerConfig;
57
58// ── Public config types ────────────────────────────────────────────────────────
59
60#[derive(Debug, Clone)]
61pub struct ProxyConfig {
62    pub upstreams: Vec<UpstreamConfig>,
63    pub routes: Vec<RouteConfig>,
64    pub tcp_proxies: Vec<TcpProxyConfig>,
65    pub udp_proxies: Vec<UdpProxyConfig>,
66    pub ws_proxies: Vec<WsProxyConfig>,
67    pub global_middleware: MiddlewareConfig,
68    /// `[authz]`: the relationship schema and seed tuples. Since 18.14.0.
69    pub authz: Option<AuthzConfig>,
70}
71
72/// `[authz]` and its `[authz.type.<name>]` tables.
73#[derive(Debug, Clone, Default)]
74pub struct AuthzConfig {
75    /// `store`: `"memory"` (default).
76    pub store: String,
77    /// `tuples`: `object#relation@subject` written at every (re)load.
78    pub tuples: Vec<String>,
79    /// `(type, [(relation, definition)])`, from `[authz.type.<type>]`.
80    pub types: Vec<(String, Vec<(String, String)>)>,
81}
82
83/// `[route.middleware.authz]`.
84#[derive(Debug, Clone)]
85pub struct RouteAuthzConfig {
86    pub relation: String,
87    /// An object template, e.g. `doc:{path:2}`.
88    pub object: String,
89}
90
91#[derive(Debug, Clone)]
92pub struct UpstreamConfig {
93    pub name: String,
94    pub backends: Vec<String>,
95    pub strategy: String, // "round_robin" | "random" | "ip_hash"
96    pub health_check: Option<HealthCheckConfig>,
97    /// `true` when all backends use `https://` scheme — connections to the
98    /// upstream are made over TLS. Requires the `http-client` or `http2`
99    /// feature (which bring in `rustls` + `webpki-roots`).
100    pub tls: bool,
101    /// `tls_ca_file` — PEM file of CA certificates to trust for this
102    /// upstream's `https://` backends **instead of** the public WebPKI roots
103    /// (a private CA or a self-signed certificate). `None` = WebPKI.
104    pub tls_ca_file: Option<String>,
105    /// `tls_client_cert_file` / `tls_client_key_file` — a client certificate
106    /// (PEM chain and key) presented to TLS backends that require one.
107    /// Since 18.3.0.
108    pub tls_client_cert_file: Option<String>,
109    pub tls_client_key_file: Option<String>,
110    /// `weights` — one per backend, in order (default 1 each). Since 18.6.0.
111    pub weights: Vec<u32>,
112    /// `[upstream.sticky]` — keep a client on one backend. Since 18.6.0.
113    pub sticky: Option<StickyConfig>,
114    /// `[upstream.circuit_breaker]` — take failing backends out of rotation
115    /// (passive health checks). Since 18.6.0.
116    pub circuit_breaker: Option<BreakerConfig>,
117    /// `[upstream.retry]` — try another backend. Since 18.6.0.
118    pub retry: Option<RetryConfig>,
119    /// `protocol` — `"h3"` sends requests over HTTP/3 (QUIC) to the
120    /// `https://` backends, falling back to TCP for a backend QUIC can't
121    /// reach. Empty (the default) or `"http1"`: HTTP/1.1 as before. Since
122    /// 18.13.0.
123    pub protocol: String,
124}
125
126#[derive(Debug, Clone)]
127pub struct HealthCheckConfig {
128    pub path: String,
129    pub interval_secs: u64,
130    pub timeout_ms: u64,
131    pub healthy_threshold: u32,
132    pub unhealthy_threshold: u32,
133}
134
135#[derive(Debug, Clone)]
136pub struct RouteConfig {
137    pub name: String,
138    pub match_: MatchConfig,
139    pub action: ActionConfig,
140    pub middleware: MiddlewareConfig,
141}
142
143#[derive(Debug, Clone, Default)]
144pub struct MatchConfig {
145    pub host: Option<String>,
146    pub path: Option<String>,
147    pub method: Option<String>,
148    pub content_type: Option<String>,
149}
150
151/// The `Host` a proxy route sends its upstream.
152#[derive(Debug, Clone, Default, PartialEq, Eq)]
153pub enum UpstreamHost {
154    /// The backend's address (the default).
155    #[default]
156    Backend,
157    /// The client's `Host` (`preserve_host = true`), like nginx's
158    /// `proxy_set_header Host $host`.
159    Client,
160    /// This name (`upstream_host = "…"`).
161    Fixed(String),
162}
163
164#[derive(Debug, Clone)]
165pub enum ActionConfig {
166    Proxy {
167        upstream: String,
168        connect_timeout_ms: u64,
169        read_timeout_ms: u64,
170        strip_path_prefix: Option<String>,
171        add_path_prefix: Option<String>,
172        /// The `Host` sent upstream (`preserve_host`, `upstream_host`).
173        upstream_host: UpstreamHost,
174    },
175    Grpc {
176        upstream: String,
177        connect_timeout_ms: u64,
178        read_timeout_ms: u64,
179    },
180    Static {
181        root: String,
182        index: Vec<String>,
183    },
184    Redirect {
185        location: String,
186        status: u16,
187    },
188    Respond {
189        status: u16,
190        body: String,
191        content_type: String,
192    },
193    /// FastCGI ([`crate::fastcgi`]): php-fpm and other FastCGI servers.
194    FastCgi(Box<FastCgiAction>),
195    /// The MCP gateway ([`mcp_gateway`]).
196    Mcp(Box<mcp_gateway::McpGatewayConfig>),
197    /// The A2A gateway ([`a2a_gateway`]).
198    A2a(Box<a2a_gateway::A2aGatewayConfig>),
199    /// The LLM proxy ([`llm`]).
200    Llm(Box<llm::LlmConfig>),
201    Unknown(String),
202}
203
204/// `type = "fastcgi"`: the [`crate::fastcgi::FastCgiConfig`] fields.
205#[derive(Debug, Clone, Default, PartialEq)]
206pub struct FastCgiAction {
207    pub address: String,
208    pub root: String,
209    pub index: Vec<String>,
210    pub split_path: String,
211    pub front_controller: String,
212    pub params: Vec<(String, String)>,
213    pub connect_timeout_ms: u64,
214    pub read_timeout_ms: u64,
215    pub max_idle: u64,
216}
217
218/// `[route.middleware.waf]` / `[middleware.waf]`, parsed; `errors` names
219/// values that didn't parse (`rws --check` reports them; the WAF then runs
220/// with the rest).
221#[derive(Debug, Clone, PartialEq)]
222pub struct WafSettings {
223    pub config: crate::waf::WafConfig,
224    pub errors: Vec<String>,
225}
226
227#[derive(Debug, Clone, Default)]
228pub struct MiddlewareConfig {
229    pub rate_limit: Option<RateLimitConfig>,
230    pub cache: Option<CacheConfig>,
231    pub auth: Option<AuthConfig>,
232    pub rewrite_request: Vec<RewriteRuleConfig>,
233    pub rewrite_response: Vec<RewriteRuleConfig>,
234    pub ip_allow: Vec<String>,
235    pub ip_deny: Vec<String>,
236    /// `timeout_ms` — if the route (including all its other middleware)
237    /// doesn't produce a response within this many milliseconds, the client
238    /// gets `504 Gateway Timeout` instead of waiting further. See
239    /// `crate::timeout` for the underlying mechanism and its limitations.
240    pub timeout_ms: Option<u64>,
241    /// `max_body_size` — reject this route's request with `413 Payload Too
242    /// Large` if its body exceeds this many bytes.
243    ///
244    /// This is a *route-specific* ceiling on top of the global, process-wide
245    /// `RWS_CONFIG_MAX_BODY_SIZE_IN_BYTES` (see `crate::entry_point::get_max_body_size`)
246    /// — e.g. a small JSON API route can be capped stricter than a
247    /// file-upload route without lowering the global limit for everyone.
248    /// It is **not** a substitute for the global limit: by the time any
249    /// route-level middleware runs, `Request` (including its body) has
250    /// already been fully read off the socket — route matching itself
251    /// requires the complete request, so a per-route check can only run
252    /// after that memory is already spent, not before. The global limit is
253    /// what protects total server memory from an oversized upload;
254    /// `max_body_size` protects only this route's own logic/upstream from
255    /// an oversized-but-still-within-the-global-limit body.
256    pub max_body_size: Option<u64>,
257    /// `[route.middleware.wasm]`: a WebAssembly plugin (feature
258    /// `wasm-plugins`, 18.10.0).
259    pub wasm: Option<WasmConfig>,
260    /// `early_hints = ["</app.css>; rel=preload; as=style", …]`: `Link`
261    /// values sent as a `103 Early Hints` before the route answers (18.12.0).
262    pub early_hints: Vec<String>,
263    /// `[route.middleware.authz]`: the caller needs `relation` on `object`.
264    /// Since 18.14.0.
265    pub authz: Option<RouteAuthzConfig>,
266    /// `[route.middleware.mirror]`: copy a share of requests to other
267    /// backends (18.11.0).
268    pub mirror: Option<MirrorConfig>,
269    /// `[route.middleware.waf]` (18.22.0): the web application firewall.
270    pub waf: Option<WafSettings>,
271}
272
273/// `[route.middleware.mirror]`.
274#[derive(Debug, Clone, Default, PartialEq)]
275pub struct MirrorConfig {
276    /// A declared `[[upstream]]` (its health-checked backends) ...
277    pub upstream: String,
278    /// ... or backend URLs.
279    pub backends: Vec<String>,
280    /// Share of requests copied, 0–100.
281    pub percent: f64,
282}
283
284/// `[route.middleware.wasm]`.
285#[derive(Debug, Clone, Default, PartialEq)]
286pub struct WasmConfig {
287    /// Path of the `.wasm` file.
288    pub module: String,
289    pub fuel: u64,
290    pub memory_mb: u64,
291    /// The string the plugin's `rws_init` gets.
292    pub config: String,
293}
294
295#[derive(Debug, Clone)]
296pub struct RateLimitConfig {
297    pub max_requests: u32,
298    pub window_secs: u64,
299}
300
301#[derive(Debug, Clone, Default)]
302pub struct CacheConfig {
303    pub ttl_secs: u64,
304    pub vary_by: Vec<String>,
305    /// Keep entries in files under this directory (18.11.0); empty = memory.
306    pub dir: String,
307    /// Bound on the disk store's total size, e.g. `"512M"` (default 256 MiB).
308    pub max_size: String,
309    /// Environment variable with the bearer token `PURGE` requests need;
310    /// empty = no `PURGE` (18.11.0).
311    pub purge_token_env: String,
312}
313
314#[derive(Debug, Clone, PartialEq)]
315pub enum AuthConfig {
316    /// `auth = { type = "basic", htpasswd_file = ".htpasswd" }`. See
317    /// `crate::auth::BasicAuthLayer::from_htpasswd_file` for the supported
318    /// file format (plain text and `{SHA256}` only — not Apache's `{SHA}`,
319    /// `$apr1$`, or bcrypt).
320    Basic { htpasswd_file: String },
321    /// `auth = { type = "jwt", secret_env = "JWT_SECRET" }`. Requires the
322    /// `auth` feature; verifies HS256 JWTs via `crate::auth::JwtLayer`.
323    /// `previous_secret_env` (18.20.0): a second accepted secret while
324    /// rotating; tokens signed with either verify.
325    Jwt { secret_env: String, previous_secret_env: String },
326    Bearer { token_env: String },
327    /// `type = "oidc"` (18.21.0): sign in with an OpenID Connect provider.
328    Oidc(Box<OidcRouteAuth>),
329    /// `type = "forward"` (18.21.0): ask an auth service (Authelia,
330    /// Authentik, oauth2-proxy) — `crate::auth::ForwardAuthLayer`.
331    Forward { url: String, copy_headers: Vec<String>, timeout_ms: u64 },
332    /// An auth table with a `type` rws doesn't know (a typo): the route
333    /// answers 503 rather than going unprotected (since 18.21.0).
334    Unknown(String),
335}
336
337/// `[route.middleware.auth] type = "oidc"`.
338#[derive(Debug, Clone, Default, PartialEq)]
339pub struct OidcRouteAuth {
340    /// Discovery URL base (`https://accounts.example.com/realms/main`).
341    pub issuer: String,
342    /// `google`, `microsoft` or `github` instead of an issuer.
343    pub provider: String,
344    /// Azure AD tenant for `provider = "microsoft"` (default `common`).
345    pub tenant: String,
346    pub client_id: String,
347    pub client_secret_env: String,
348    /// Empty: `<scheme>://<Host>/auth/callback` of the request.
349    pub redirect_uri: String,
350    pub scopes: Vec<String>,
351    pub allowed_emails: Vec<String>,
352    pub allowed_domains: Vec<String>,
353    pub allowed_groups: Vec<String>,
354    pub groups_claim: String,
355    /// Signs the session cookie; unset: a random key per process.
356    pub cookie_secret_env: String,
357    pub session_ttl_secs: u64,
358    pub exclude: Vec<String>,
359    pub forward_headers: bool,
360    /// Where `/auth/login`, `/auth/callback` and `/auth/logout` live (the
361    /// admin API uses `/admin`); empty for routes.
362    pub path_prefix: String,
363}
364
365#[derive(Debug, Clone, Default)]
366pub struct RewriteRuleConfig {
367    pub type_: String,
368    pub name: Option<String>,
369    pub value: Option<String>,
370    pub prefix: Option<String>,
371    pub from: Option<String>,
372    pub to: Option<String>,
373    pub code: Option<u16>,
374    pub reason: Option<String>,
375}
376
377#[derive(Debug, Clone)]
378pub struct TcpProxyConfig {
379    pub name: String,
380    pub listen: String,
381    pub backends: Vec<String>,
382    pub connect_timeout_ms: u64,
383}
384
385#[derive(Debug, Clone)]
386pub struct UdpProxyConfig {
387    pub name: String,
388    pub listen: String,
389    pub backends: Vec<String>,
390    pub reply_timeout_ms: u64,
391    pub buffer_size: usize,
392}
393
394#[derive(Debug, Clone)]
395pub struct WsProxyConfig {
396    pub name: String,
397    pub listen: String,
398    pub backends: Vec<String>,
399    pub connect_timeout_ms: u64,
400    pub read_timeout_ms: u64,
401    /// Optional `[ws_proxy.health_check]` — same shape and semantics as
402    /// `[upstream.health_check]`. The probe is a plain HTTP `GET {path}`
403    /// against the backend's `host:port` (the `ws://`/`wss://` scheme only
404    /// determines whether the probe connects over TLS); most WebSocket
405    /// backends serve a regular HTTP health endpoint alongside their upgrade
406    /// route, the same way nginx/Traefik health-check WS upstreams.
407    pub health_check: Option<HealthCheckConfig>,
408}
409
410// ── ProxyConfig loading ────────────────────────────────────────────────────────
411
412
413/// An action's settings may sit directly under `[route.action]` next to
414/// `type` (the short form README.md and the docs use), or in a nested
415/// `[route.action.<type>]` table (the long form). Copy short-form keys into
416/// the nested table so the parser reads one place; a key set in both keeps
417/// the nested value.
418fn merge_action_keys(map: &mut parser::SectionMap, action_section: &str, action_type: &str) {
419    if action_type.is_empty() {
420        return;
421    }
422    let short_form: Vec<(String, String)> = match map.get(action_section) {
423        Some(entries) => entries.iter().filter(|(k, _)| k != "type").cloned().collect(),
424        None => return,
425    };
426    let nested = map.entry(format!("{action_section}.{action_type}")).or_default();
427    for (key, value) in short_form {
428        if !nested.iter().any(|(k, _)| *k == key) {
429            nested.push((key, value));
430        }
431    }
432}
433
434impl ProxyConfig {
435    /// Returns `true` if `rws.config.toml` (or `RWS_CONFIG_FILE`) contains
436    /// `[[route]]` or `[[upstream]]` sections, meaning config-driven mode
437    /// should be used.
438    pub fn is_proxy_mode() -> bool {
439        let path = config_file_path();
440        match std::fs::read_to_string(&path) {
441            Ok(contents) => {
442                contents.contains("[[route]]") || contents.contains("[[upstream]]")
443            }
444            Err(_) => false,
445        }
446    }
447
448    /// Parse the config file and return a `ProxyConfig`.
449    pub fn load() -> Self {
450        let path = config_file_path();
451        let contents = std::fs::read_to_string(&path).unwrap_or_default();
452        Self::from_str(&contents)
453    }
454
455    /// Parse `toml` text directly into a `ProxyConfig`. Used in tests.
456    pub fn from_str(toml: &str) -> Self {
457        use parser::{get_array, get_str, get_u32, get_u64, section_exists};
458
459        let mut map = parser::parse(toml);
460
461        // ── upstreams ──────────────────────────────────────────────────────────
462        let mut upstreams = Vec::new();
463        let mut i = 0;
464        loop {
465            let sec = format!("upstream[{}]", i);
466            if !section_exists(&map, &sec) {
467                break;
468            }
469            let name = get_str(&map, &sec, "name");
470            let backends = get_array(&map, &sec, "backends");
471            let strategy = {
472                let s = get_str(&map, &sec, "strategy");
473                if s.is_empty() { "round_robin".to_string() } else { s }
474            };
475            let hc_sec = format!("{}.health_check", sec);
476            let health_check = if section_exists(&map, &hc_sec) {
477                Some(HealthCheckConfig {
478                    path: {
479                        let p = get_str(&map, &hc_sec, "path");
480                        if p.is_empty() { "/health".to_string() } else { p }
481                    },
482                    interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
483                    timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
484                    healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
485                    unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
486                })
487            } else {
488                None
489            };
490            let tls = backends.iter().any(|b| ["https://", "h2s://", "grpcs://"].iter().any(|s| b.starts_with(s)));
491            let tls_ca_file = Some(get_str(&map, &sec, "tls_ca_file")).filter(|v| !v.is_empty());
492            let tls_client_cert_file = Some(get_str(&map, &sec, "tls_client_cert_file")).filter(|v| !v.is_empty());
493            let tls_client_key_file = Some(get_str(&map, &sec, "tls_client_key_file")).filter(|v| !v.is_empty());
494            let weights: Vec<u32> = get_array(&map, &sec, "weights").iter().map(|w| w.trim().parse().unwrap_or(0)).collect();
495            let sticky_sec = format!("{}.sticky", sec);
496            let sticky = section_exists(&map, &sticky_sec).then(|| StickyConfig {
497                cookie: Some(get_str(&map, &sticky_sec, "cookie")).filter(|c| !c.is_empty()).unwrap_or_else(|| "rws_backend".to_string()),
498                max_age_secs: Some(get_u64(&map, &sticky_sec, "max_age_secs", 0)).filter(|s| *s > 0),
499            });
500            let cb_sec = format!("{}.circuit_breaker", sec);
501            let circuit_breaker = section_exists(&map, &cb_sec).then(|| BreakerConfig {
502                failure_threshold: get_u32(&map, &cb_sec, "failure_threshold", 5),
503                recovery_secs: get_u64(&map, &cb_sec, "recovery_secs", 30),
504                half_open_probes: get_u32(&map, &cb_sec, "half_open_probes", 1),
505            });
506            let retry_sec = format!("{}.retry", sec);
507            let retry = section_exists(&map, &retry_sec).then(|| {
508                let status: Vec<i16> = get_array(&map, &retry_sec, "status").iter().filter_map(|s| s.trim().parse().ok()).collect();
509                RetryConfig { attempts: get_u32(&map, &retry_sec, "attempts", 1), status: if status.is_empty() { vec![502, 503, 504] } else { status } }
510            });
511            upstreams.push(UpstreamConfig {
512                name, backends, strategy, health_check, tls, tls_ca_file, tls_client_cert_file, tls_client_key_file,
513                weights, sticky, circuit_breaker, retry,
514                protocol: get_str(&map, &sec, "protocol"),
515            });
516            i += 1;
517        }
518
519        // ── routes ─────────────────────────────────────────────────────────────
520        let mut routes = Vec::new();
521        let mut i = 0;
522        loop {
523            let sec = format!("route[{}]", i);
524            if !section_exists(&map, &sec) {
525                break;
526            }
527            let name = get_str(&map, &sec, "name");
528
529            // match
530            let m_sec = format!("{}.match", sec);
531            let match_ = MatchConfig {
532                host: {
533                    let h = get_str(&map, &m_sec, "host");
534                    if h.is_empty() { None } else { Some(h) }
535                },
536                path: {
537                    let p = get_str(&map, &m_sec, "path");
538                    if p.is_empty() { None } else { Some(p) }
539                },
540                method: {
541                    let m = get_str(&map, &m_sec, "method");
542                    if m.is_empty() { None } else { Some(m.to_uppercase()) }
543                },
544                content_type: {
545                    let c = get_str(&map, &m_sec, "content_type");
546                    if c.is_empty() { None } else { Some(c) }
547                },
548            };
549
550            // action
551            let a_sec = format!("{}.action", sec);
552            let action_type = get_str(&map, &a_sec, "type");
553            merge_action_keys(&mut map, &a_sec, &action_type);
554            let action = match action_type.as_str() {
555                "proxy" => {
556                    let p_sec = format!("{}.action.proxy", sec);
557                    ActionConfig::Proxy {
558                        upstream: get_str(&map, &p_sec, "upstream"),
559                        connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
560                        read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
561                        strip_path_prefix: {
562                            let v = get_str(&map, &p_sec, "strip_path_prefix");
563                            if v.is_empty() { None } else { Some(v) }
564                        },
565                        add_path_prefix: {
566                            let v = get_str(&map, &p_sec, "add_path_prefix");
567                            if v.is_empty() { None } else { Some(v) }
568                        },
569                        upstream_host: {
570                            let fixed = get_str(&map, &p_sec, "upstream_host");
571                            if !fixed.is_empty() {
572                                UpstreamHost::Fixed(fixed)
573                            } else if get_str(&map, &p_sec, "preserve_host") == "true" {
574                                UpstreamHost::Client
575                            } else {
576                                UpstreamHost::Backend
577                            }
578                        },
579                    }
580                }
581                "grpc" => {
582                    let p_sec = format!("{}.action.grpc", sec);
583                    ActionConfig::Grpc {
584                        upstream: get_str(&map, &p_sec, "upstream"),
585                        connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
586                        read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
587                    }
588                }
589                "static" => {
590                    let s_sec = format!("{}.action.static", sec);
591                    ActionConfig::Static {
592                        root: get_str(&map, &s_sec, "root"),
593                        index: get_array(&map, &s_sec, "index"),
594                    }
595                }
596                "redirect" => {
597                    let r_sec = format!("{}.action.redirect", sec);
598                    ActionConfig::Redirect {
599                        location: get_str(&map, &r_sec, "location"),
600                        status: get_u64(&map, &r_sec, "status", 301) as u16,
601                    }
602                }
603                "respond" => {
604                    let r_sec = format!("{}.action.respond", sec);
605                    ActionConfig::Respond {
606                        status: get_u64(&map, &r_sec, "status", 200) as u16,
607                        body: get_str(&map, &r_sec, "body"),
608                        content_type: {
609                            let ct = get_str(&map, &r_sec, "content_type");
610                            if ct.is_empty() { "text/plain".to_string() } else { ct }
611                        },
612                    }
613                }
614                "fastcgi" => {
615                    let f_sec = format!("{}.action.fastcgi", sec);
616                    let or = |key: &str, default: &str| parser::get(&map, &f_sec, key).unwrap_or_else(|| default.to_string());
617                    let mut params: Vec<(String, String)> = Vec::new();
618                    for table in [format!("{a_sec}.params"), format!("{f_sec}.params")] {
619                        for (k, v) in map.get(&table).cloned().unwrap_or_default() {
620                            params.retain(|(name, _)| *name != k);
621                            params.push((k, v));
622                        }
623                    }
624                    let index = get_array(&map, &f_sec, "index");
625                    ActionConfig::FastCgi(Box::new(FastCgiAction {
626                        address: get_str(&map, &f_sec, "address"),
627                        root: get_str(&map, &f_sec, "root"),
628                        index: if index.is_empty() { vec!["index.php".to_string()] } else { index },
629                        split_path: or("split_path", ".php"),
630                        front_controller: or("front_controller", "index.php"),
631                        params,
632                        connect_timeout_ms: get_u64(&map, &f_sec, "connect_timeout_ms", 5000),
633                        read_timeout_ms: get_u64(&map, &f_sec, "read_timeout_ms", 60000),
634                        max_idle: get_u64(&map, &f_sec, "max_idle", 16),
635                    }))
636                }
637                "mcp" => ActionConfig::Mcp(Box::new(mcp_gateway::parse(&map, &a_sec))),
638                "a2a" => ActionConfig::A2a(Box::new(a2a_gateway::parse(&map, &a_sec))),
639                "llm" => ActionConfig::Llm(Box::new(llm::parse(&map, &a_sec))),
640                other => ActionConfig::Unknown(other.to_string()),
641            };
642
643            // middleware
644            let mw_sec = format!("{}.middleware", sec);
645            let middleware = parse_middleware_config(&map, &mw_sec, i);
646
647            routes.push(RouteConfig { name, match_, action, middleware });
648            i += 1;
649        }
650
651        // ── tcp_proxy ──────────────────────────────────────────────────────────
652        let mut tcp_proxies = Vec::new();
653        let mut i = 0;
654        loop {
655            let sec = format!("tcp_proxy[{}]", i);
656            if !section_exists(&map, &sec) {
657                break;
658            }
659            tcp_proxies.push(TcpProxyConfig {
660                name: get_str(&map, &sec, "name"),
661                listen: get_str(&map, &sec, "listen"),
662                backends: get_array(&map, &sec, "backends"),
663                connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
664            });
665            i += 1;
666        }
667
668        // ── udp_proxy ──────────────────────────────────────────────────────────
669        let mut udp_proxies = Vec::new();
670        let mut i = 0;
671        loop {
672            let sec = format!("udp_proxy[{}]", i);
673            if !section_exists(&map, &sec) {
674                break;
675            }
676            udp_proxies.push(UdpProxyConfig {
677                name: get_str(&map, &sec, "name"),
678                listen: get_str(&map, &sec, "listen"),
679                backends: get_array(&map, &sec, "backends"),
680                reply_timeout_ms: get_u64(&map, &sec, "reply_timeout_ms", 5000),
681                buffer_size: get_u64(&map, &sec, "buffer_size", 65536) as usize,
682            });
683            i += 1;
684        }
685
686        // ── ws_proxy ───────────────────────────────────────────────────────────
687        let mut ws_proxies = Vec::new();
688        let mut i = 0;
689        loop {
690            let sec = format!("ws_proxy[{}]", i);
691            if !section_exists(&map, &sec) {
692                break;
693            }
694            let hc_sec = format!("{}.health_check", sec);
695            let health_check = if section_exists(&map, &hc_sec) {
696                Some(HealthCheckConfig {
697                    path: {
698                        let p = get_str(&map, &hc_sec, "path");
699                        if p.is_empty() { "/health".to_string() } else { p }
700                    },
701                    interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
702                    timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
703                    healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
704                    unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
705                })
706            } else {
707                None
708            };
709            ws_proxies.push(WsProxyConfig {
710                name: get_str(&map, &sec, "name"),
711                listen: get_str(&map, &sec, "listen"),
712                backends: get_array(&map, &sec, "backends"),
713                connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
714                read_timeout_ms: get_u64(&map, &sec, "read_timeout_ms", 30000),
715                health_check,
716            });
717            i += 1;
718        }
719
720        // ── global middleware ──────────────────────────────────────────────────
721        let global_middleware = parse_middleware_config(&map, "middleware", usize::MAX);
722
723        // ── [authz] ───────────────────────────────────────────────────────────
724        let authz = map.keys().any(|k| k == "authz" || k.starts_with("authz.")).then(|| {
725            let mut type_sections: Vec<&String> = map.keys().filter(|k| k.starts_with("authz.type.")).collect();
726            type_sections.sort();
727            AuthzConfig {
728                store: get_str(&map, "authz", "store"),
729                tuples: get_array(&map, "authz", "tuples"),
730                types: type_sections
731                    .into_iter()
732                    .map(|sec| {
733                        let relations = map[sec].iter().map(|(k, _)| (k.clone(), get_str(&map, sec, k))).collect();
734                        (sec["authz.type.".len()..].to_string(), relations)
735                    })
736                    .collect(),
737            }
738        });
739
740        ProxyConfig {
741            upstreams,
742            routes,
743            tcp_proxies,
744            udp_proxies,
745            ws_proxies,
746            global_middleware,
747            authz,
748        }
749    }
750}
751
752/// Parse a `MiddlewareConfig` from the section map at a given base path.
753/// `route_idx` is used only to build inner-array section paths for rewrite rules.
754fn parse_middleware_config(
755    map: &parser::SectionMap,
756    mw_sec: &str,
757    route_idx: usize,
758) -> MiddlewareConfig {
759    use parser::{get_array, get_str, get_u32, get_u64, section_exists};
760
761    let rl_sec = format!("{}.rate_limit", mw_sec);
762    let rate_limit = if section_exists(map, &rl_sec) {
763        Some(RateLimitConfig {
764            max_requests: get_u32(map, &rl_sec, "max_requests", 1000),
765            window_secs: get_u64(map, &rl_sec, "window_secs", 60),
766        })
767    } else {
768        None
769    };
770
771    let c_sec = format!("{}.cache", mw_sec);
772    let cache = if section_exists(map, &c_sec) {
773        Some(CacheConfig {
774            ttl_secs: get_u64(map, &c_sec, "ttl_secs", 60),
775            vary_by: get_array(map, &c_sec, "vary_by"),
776            dir: get_str(map, &c_sec, "dir"),
777            max_size: get_str(map, &c_sec, "max_size"),
778            purge_token_env: get_str(map, &c_sec, "purge_token_env"),
779        })
780    } else {
781        None
782    };
783
784    let a_sec = format!("{}.auth", mw_sec);
785    let auth = if section_exists(map, &a_sec) {
786        let auth_type = get_str(map, &a_sec, "type");
787        match auth_type.as_str() {
788            "bearer" => Some(AuthConfig::Bearer {
789                token_env: get_str(map, &a_sec, "token_env"),
790            }),
791            "jwt" => Some(AuthConfig::Jwt {
792                secret_env: get_str(map, &a_sec, "secret_env"),
793                previous_secret_env: get_str(map, &a_sec, "previous_secret_env"),
794            }),
795            "basic" => Some(AuthConfig::Basic {
796                htpasswd_file: get_str(map, &a_sec, "htpasswd_file"),
797            }),
798            "oidc" => Some(AuthConfig::Oidc(Box::new(OidcRouteAuth {
799                issuer: get_str(map, &a_sec, "issuer").trim_end_matches('/').to_string(),
800                provider: get_str(map, &a_sec, "provider").to_ascii_lowercase(),
801                tenant: get_str(map, &a_sec, "tenant"),
802                client_id: get_str(map, &a_sec, "client_id"),
803                client_secret_env: get_str(map, &a_sec, "client_secret_env"),
804                redirect_uri: get_str(map, &a_sec, "redirect_uri"),
805                scopes: get_array(map, &a_sec, "scopes"),
806                allowed_emails: get_array(map, &a_sec, "allowed_emails"),
807                allowed_domains: get_array(map, &a_sec, "allowed_domains"),
808                allowed_groups: get_array(map, &a_sec, "allowed_groups"),
809                groups_claim: get_str(map, &a_sec, "groups_claim"),
810                cookie_secret_env: get_str(map, &a_sec, "cookie_secret_env"),
811                session_ttl_secs: get_u64(map, &a_sec, "session_ttl_secs", 86_400),
812                exclude: get_array(map, &a_sec, "exclude"),
813                forward_headers: get_str(map, &a_sec, "forward_headers") != "false",
814                path_prefix: String::new(),
815            }))),
816            "forward" => Some(AuthConfig::Forward {
817                url: get_str(map, &a_sec, "url"),
818                copy_headers: get_array(map, &a_sec, "copy_headers"),
819                timeout_ms: get_u64(map, &a_sec, "timeout_ms", 5000),
820            }),
821            other => Some(AuthConfig::Unknown(other.to_string())),
822        }
823    } else {
824        None
825    };
826
827    // Rewrite rules — the section paths use route_idx for route-scoped rules
828    // or a flat path for global middleware. We look for:
829    //   route[N].middleware.rewrite.request[0], [1], …
830    //   route[N].middleware.rewrite.response[0], [1], …
831    // For global: middleware.rewrite.request[0], etc.
832    let rewrite_request = collect_rewrite_rules(map, mw_sec, "request");
833    let rewrite_response = collect_rewrite_rules(map, mw_sec, "response");
834
835    let ip_sec = format!("{}.ip_filter", mw_sec);
836    let ip_allow = if section_exists(map, &ip_sec) {
837        get_array(map, &ip_sec, "allow")
838    } else {
839        vec![]
840    };
841    let ip_deny = if section_exists(map, &ip_sec) {
842        get_array(map, &ip_sec, "deny")
843    } else {
844        vec![]
845    };
846
847    let _ = route_idx; // used implicitly via mw_sec paths
848
849    // Flat scalar directly under [route.middleware] (or the global
850    // [middleware] table), not a nested sub-table like rate_limit/cache —
851    // 0/absent both mean "no timeout configured".
852    let timeout_ms = match get_u64(map, mw_sec, "timeout_ms", 0) {
853        0 => None,
854        ms => Some(ms),
855    };
856
857    // Same flat-scalar convention as timeout_ms — 0/absent means "no
858    // route-specific limit" (the global RWS_CONFIG_MAX_BODY_SIZE_IN_BYTES
859    // still applies regardless).
860    let max_body_size = match get_u64(map, mw_sec, "max_body_size", 0) {
861        0 => None,
862        n => Some(n),
863    };
864
865    let w_sec = format!("{}.wasm", mw_sec);
866    let wasm = section_exists(map, &w_sec).then(|| WasmConfig {
867        module: get_str(map, &w_sec, "module"),
868        fuel: get_u64(map, &w_sec, "fuel", 50_000_000),
869        memory_mb: get_u64(map, &w_sec, "memory_mb", 16),
870        config: get_str(map, &w_sec, "config"),
871    });
872
873    let m_sec = format!("{}.mirror", mw_sec);
874    let mirror = section_exists(map, &m_sec).then(|| MirrorConfig {
875        upstream: get_str(map, &m_sec, "upstream"),
876        backends: get_array(map, &m_sec, "backends"),
877        percent: {
878            let raw = get_str(map, &m_sec, "percent");
879            if raw.is_empty() { 100.0 } else { raw.parse::<f64>().unwrap_or(f64::NAN) }
880        },
881    });
882
883    let waf = parse_waf(map, &format!("{mw_sec}.waf"));
884
885    MiddlewareConfig {
886        rate_limit, cache, auth, rewrite_request, rewrite_response, ip_allow, ip_deny,
887        timeout_ms, max_body_size, wasm, mirror, waf,
888        early_hints: get_array(map, mw_sec, "early_hints"),
889        authz: section_exists(map, &format!("{mw_sec}.authz")).then(|| RouteAuthzConfig {
890            relation: get_str(map, &format!("{mw_sec}.authz"), "relation"),
891            object: get_str(map, &format!("{mw_sec}.authz"), "object"),
892        }),
893    }
894}
895
896/// Collect `[[{mw_sec}.rewrite.{direction}]]` entries.
897/// `[…waf]` and its `[[…waf.rule]]` tables.
898fn parse_waf(map: &parser::SectionMap, sec: &str) -> Option<WafSettings> {
899    use crate::waf::{Action, CustomRule, Exclusion, Field, Mode, WafConfig};
900    use parser::{get_array, get_str, get_u64, section_exists};
901    if !section_exists(map, sec) {
902        return None;
903    }
904    let mut errors = Vec::new();
905    let defaults = WafConfig::default();
906    let mode_text = get_str(map, sec, "mode");
907    let mode = Mode::parse(&mode_text).unwrap_or_else(|| {
908        errors.push(format!("[waf] mode = {mode_text:?}: expected block, detect or off"));
909        Mode::Block
910    });
911    let paranoia = get_u64(map, sec, "paranoia", 1);
912    if !(1..=3).contains(&paranoia) {
913        errors.push(format!("[waf] paranoia = {paranoia}: expected 1, 2 or 3"));
914    }
915    let mut rules = Vec::new();
916    let mut j = 0;
917    loop {
918        let rsec = format!("{sec}.rule[{j}]");
919        if !section_exists(map, &rsec) {
920            break;
921        }
922        j += 1;
923        let id = get_u64(map, &rsec, "id", 0) as u32;
924        let field_text = get_str(map, &rsec, "field");
925        let Some(field) = Field::parse(&field_text) else {
926            errors.push(format!("[waf] rule {id}: field = {field_text:?}: expected path, query, args, arg:<name>, header:<name>, method, ip or body"));
927            continue;
928        };
929        let action_text = get_str(map, &rsec, "action");
930        let action = match action_text.to_ascii_lowercase().as_str() {
931            "deny" | "" => Action::Deny,
932            "allow" => Action::Allow,
933            "log" => Action::Log,
934            "score" => Action::Score(get_u64(map, &rsec, "score", 5) as u32),
935            other => {
936                errors.push(format!("[waf] rule {id}: action = {other:?}: expected deny, allow, log or score"));
937                continue;
938            }
939        };
940        let rule = CustomRule {
941            id,
942            field,
943            contains: get_array(map, &rsec, "contains"),
944            prefix: get_array(map, &rsec, "prefix"),
945            equals: get_array(map, &rsec, "equals"),
946            cidr: get_array(map, &rsec, "cidr"),
947            action,
948        };
949        if rule.contains.is_empty() && rule.prefix.is_empty() && rule.equals.is_empty() && rule.cidr.is_empty() {
950            errors.push(format!("[waf] rule {id}: needs contains, prefix, equals or cidr"));
951            continue;
952        }
953        if let Some(bad) = rule.cidr.iter().find(|c| !crate::ip_filter::is_valid_entry(c)) {
954            errors.push(format!("[waf] rule {id}: cidr {bad:?} is not an address or range"));
955        }
956        rules.push(rule);
957    }
958    Some(WafSettings {
959        config: WafConfig {
960            mode,
961            paranoia: paranoia.clamp(1, 3) as u8,
962            threshold: get_u64(map, sec, "threshold", defaults.threshold as u64).max(1) as u32,
963            inspect_body_bytes: get_u64(map, sec, "inspect_body_bytes", defaults.inspect_body_bytes as u64) as usize,
964            max_args: get_u64(map, sec, "max_args", defaults.max_args as u64) as usize,
965            exclude: get_array(map, sec, "exclude").iter().map(|e| Exclusion::parse(e)).collect(),
966            rules,
967        },
968        errors,
969    })
970}
971
972fn collect_rewrite_rules(
973    map: &parser::SectionMap,
974    mw_sec: &str,
975    direction: &str,
976) -> Vec<RewriteRuleConfig> {
977    use parser::{get_str, get_u64};
978
979    let mut rules = Vec::new();
980    let mut j = 0;
981    loop {
982        let rsec = format!("{}.rewrite.{}[{}]", mw_sec, direction, j);
983        if !parser::section_exists(map, &rsec) {
984            break;
985        }
986        let code_val = get_u64(map, &rsec, "code", 0);
987        rules.push(RewriteRuleConfig {
988            type_: get_str(map, &rsec, "type"),
989            name: {
990                let v = get_str(map, &rsec, "name");
991                if v.is_empty() { None } else { Some(v) }
992            },
993            value: {
994                let v = get_str(map, &rsec, "value");
995                if v.is_empty() { None } else { Some(v) }
996            },
997            prefix: {
998                let v = get_str(map, &rsec, "prefix");
999                if v.is_empty() { None } else { Some(v) }
1000            },
1001            from: {
1002                let v = get_str(map, &rsec, "from");
1003                if v.is_empty() { None } else { Some(v) }
1004            },
1005            to: {
1006                let v = get_str(map, &rsec, "to");
1007                if v.is_empty() { None } else { Some(v) }
1008            },
1009            code: if code_val == 0 { None } else { Some(code_val as u16) },
1010            reason: {
1011                let v = get_str(map, &rsec, "reason");
1012                if v.is_empty() { None } else { Some(v) }
1013            },
1014        });
1015        j += 1;
1016    }
1017    rules
1018}
1019
1020#[doc(hidden)]
1021pub fn config_file_path() -> String {
1022    std::env::var("RWS_CONFIG_FILE").unwrap_or_else(|_| "rws.config.toml".to_string())
1023}
1024
1025// ── ConfigDrivenApp ────────────────────────────────────────────────────────────
1026
1027/// A compiled route: a matcher paired with a handler application.
1028#[doc(hidden)]
1029pub struct CompiledRoute {
1030    #[doc(hidden)]
1031    pub matcher: RouteMatcher,
1032    /// Shared, type-erased handler. `Arc` makes `Clone` cheap (pointer copy).
1033    #[doc(hidden)]
1034    pub handler: Arc<dyn Application + Send + Sync>,
1035    /// The route's proxy when it can be served without blocking a thread
1036    /// (plain-HTTP upstream, no route middleware) — see
1037    /// [`DynamicProxy::execute_async`].
1038    #[cfg_attr(not(feature = "http2"), allow(dead_code))]
1039    #[doc(hidden)]
1040    pub async_proxy: Option<Arc<DynamicProxy>>,
1041}
1042
1043/// Matching criteria for a single route.
1044#[derive(Clone, Default)]
1045#[doc(hidden)]
1046pub struct RouteMatcher {
1047    /// Optional SNI hostname / `Host` header match.
1048    #[doc(hidden)]
1049    pub host: Option<String>,
1050    /// Path prefix to match (derived from `path = "/v1/*"`).
1051    #[doc(hidden)]
1052    pub path_prefix: Option<String>,
1053    /// Exact path to match (derived from `path = "/v1/ping"`).
1054    #[doc(hidden)]
1055    pub path_exact: Option<String>,
1056    /// Uppercase HTTP method, or `None` for any.
1057    #[doc(hidden)]
1058    pub method: Option<String>,
1059    /// `Content-Type` prefix (e.g. `"application/grpc"`).
1060    #[doc(hidden)]
1061    pub content_type_prefix: Option<String>,
1062}
1063
1064impl RouteMatcher {
1065    #[doc(hidden)]
1066    pub fn from_match_config(cfg: &MatchConfig) -> Self {
1067        let (path_prefix, path_exact) = match &cfg.path {
1068            Some(p) if p.ends_with('*') => {
1069                // "/v1/*" → prefix "/v1/"
1070                let stripped = p.trim_end_matches('*').to_string();
1071                (Some(stripped), None)
1072            }
1073            Some(p) => (None, Some(p.clone())),
1074            None => (None, None),
1075        };
1076        let content_type_prefix = cfg.content_type.as_ref().map(|ct| {
1077            if ct.ends_with('*') {
1078                ct.trim_end_matches('*').to_string()
1079            } else {
1080                ct.clone()
1081            }
1082        });
1083        RouteMatcher {
1084            host: cfg.host.clone(),
1085            path_prefix,
1086            path_exact,
1087            method: cfg.method.clone(),
1088            content_type_prefix,
1089        }
1090    }
1091
1092    /// Returns `true` if `request` and `conn` match all configured criteria.
1093    #[doc(hidden)]
1094    pub fn matches(&self, request: &Request, conn: &ConnectionInfo) -> bool {
1095        // Host matching: the Host header (HTTP/2 and HTTP/3 :authority), then SNI
1096        if let Some(ref expected_host) = self.host {
1097            if !host_matches(expected_host, request_host(request, conn)) {
1098                return false;
1099            }
1100        }
1101
1102        // Method matching
1103        if let Some(ref m) = self.method {
1104            if request.method.to_uppercase() != m.as_str() {
1105                return false;
1106            }
1107        }
1108
1109        // Path matching: strip query string for comparison
1110        let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
1111        if let Some(ref prefix) = self.path_prefix {
1112            if !path.starts_with(prefix.as_str()) {
1113                return false;
1114            }
1115        } else if let Some(ref exact) = self.path_exact {
1116            if path != exact.as_str() {
1117                return false;
1118            }
1119        }
1120
1121        // Content-Type prefix matching
1122        if let Some(ref ct_prefix) = self.content_type_prefix {
1123            let actual_ct = request
1124                .headers
1125                .iter()
1126                .find(|h| h.name.eq_ignore_ascii_case("content-type"))
1127                .map(|h| h.value.as_ref())
1128                .unwrap_or("");
1129            if !actual_ct.starts_with(ct_prefix.as_str()) {
1130                return false;
1131            }
1132        }
1133
1134        true
1135    }
1136}
1137
1138/// An `Application` that routes requests based on a parsed `ProxyConfig`.
1139///
1140/// Clones share one live configuration: a hot reload
1141/// ([`ConfigDrivenApp::reload_from_str`], run on `SIGHUP` for an app built by
1142/// [`build_from_file`]) swaps it for every clone at once. In-flight requests
1143/// finish on the configuration they started with.
1144#[derive(Clone)]
1145pub struct ConfigDrivenApp {
1146    current: Arc<RwLock<Arc<Generation>>>,
1147    /// Serializes reloads: one SIGHUP reaches every accept loop.
1148    reload_lock: Arc<std::sync::Mutex<()>>,
1149}
1150
1151/// One loaded configuration: its compiled routes plus what a reload needs.
1152struct Generation {
1153    dispatch: RouteDispatch,
1154    /// `dispatch` wrapped in the top-level `[middleware]` table; `None` when
1155    /// that table is empty.
1156    entry: Option<Arc<dyn Application + Send + Sync>>,
1157    config: ProxyConfig,
1158    /// The file text this generation was built from, to skip no-op reloads.
1159    source: Option<String>,
1160    /// Set when this generation is replaced: stops its health checkers.
1161    health_stop: Arc<AtomicBool>,
1162}
1163
1164impl Generation {
1165    fn new(routes: Arc<Vec<CompiledRoute>>, fallback: App, config: ProxyConfig, health_stop: Arc<AtomicBool>, source: Option<String>) -> Self {
1166        let dispatch = RouteDispatch { routes, fallback };
1167        let global = &config.global_middleware;
1168        let entry = if global.is_empty() && global.early_hints.is_empty() {
1169            None
1170        } else {
1171            let mut entry = builder::apply_middleware(arc_app(dispatch.clone()), global);
1172            // Global early hints go out first, for every request (routes,
1173            // static files, health checks alike).
1174            if !global.early_hints.is_empty() {
1175                let layer = crate::early_hints::EarlyHintsLayer::new(global.early_hints.iter().cloned());
1176                entry = arc_app(crate::middleware::WithMiddleware::new(builder::ArcApp(entry)).wrap(layer));
1177            }
1178            Some(entry)
1179        };
1180        Generation { dispatch, entry, config, source, health_stop }
1181    }
1182}
1183
1184/// First-match routing over the compiled routes, then the fallback.
1185#[derive(Clone)]
1186struct RouteDispatch {
1187    routes: Arc<Vec<CompiledRoute>>,
1188    /// Fallback for unmatched requests — handles /healthz, /readyz, /metrics,
1189    /// static files, and the 404 controller. Reads `RWS_CONFIG_*` env vars
1190    /// per request (`App::new()`'s default) unless pinned via
1191    /// [`ConfigDrivenApp::with_config`].
1192    fallback: App,
1193}
1194
1195impl Application for RouteDispatch {
1196    fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1197        for route in self.routes.iter() {
1198            if route.matcher.matches(request, conn) {
1199                if let Some(preflight) = route_cors::preflight(request, &crate::server_config::ServerConfig::current()) {
1200                    return Ok(preflight);
1201                }
1202                return route.handler.execute(request, conn).map(|r| route_cors::add(request, r, &crate::server_config::ServerConfig::current()));
1203            }
1204        }
1205        self.fallback.execute(request, conn)
1206    }
1207
1208    fn streams_body(&self, request: &Request, conn: &ConnectionInfo) -> bool {
1209        match self.routes.iter().find(|r| r.matcher.matches(request, conn)) {
1210            Some(route) => route.handler.streams_body(request, conn),
1211            None => self.fallback.streams_body(request, conn),
1212        }
1213    }
1214}
1215
1216impl MiddlewareConfig {
1217    /// `true` when no layer is configured. A mirror and early hints don't
1218    /// count: proxy routes send both from `DynamicProxy` itself, so such a
1219    /// route keeps the non-blocking forwarder.
1220    pub fn is_empty(&self) -> bool {
1221        self.rate_limit.is_none() && self.cache.is_none() && self.auth.is_none()
1222            && self.rewrite_request.is_empty() && self.rewrite_response.is_empty()
1223            && self.ip_allow.is_empty() && self.ip_deny.is_empty()
1224            && self.timeout_ms.is_none() && self.max_body_size.is_none() && self.wasm.is_none()
1225            && self.authz.is_none() && self.waf.is_none()
1226    }
1227}
1228
1229/// What a successful [`ConfigDrivenApp::reload_from_str`] changed.
1230#[derive(Debug, Clone, PartialEq)]
1231pub enum ReloadOutcome {
1232    /// The configuration text is unchanged; nothing was rebuilt.
1233    Unchanged,
1234    /// New routes are live. `warnings` includes `[[tcp_proxy]]` /
1235    /// `[[udp_proxy]]` / `[[ws_proxy]]` changes, which need a restart.
1236    Reloaded { routes: usize, warnings: Vec<String> },
1237}
1238
1239impl ConfigDrivenApp {
1240    #[doc(hidden)]
1241    pub fn new(routes: Vec<CompiledRoute>, config: ProxyConfig, health_stop: Arc<AtomicBool>, source: Option<String>) -> Self {
1242        use crate::core::New;
1243        let generation = Generation::new(Arc::new(routes), App::new(), config, health_stop, source);
1244        ConfigDrivenApp {
1245            current: Arc::new(RwLock::new(Arc::new(generation))),
1246            reload_lock: Arc::new(std::sync::Mutex::new(())),
1247        }
1248    }
1249
1250    fn current(&self) -> Arc<Generation> {
1251        Arc::clone(&self.current.read().unwrap_or_else(|e| e.into_inner()))
1252    }
1253
1254    /// Pin the fallback [`App`] (used for any request none of the
1255    /// config-driven routes match) to an explicit [`ServerConfig`], instead
1256    /// of reading `RWS_CONFIG_*` environment variables per request.
1257    ///
1258    /// Mirrors [`App::with_config`] / [`crate::state::AppWithState::with_config`]
1259    /// — same rationale: safe for parallel tests, and lets multiple
1260    /// differently-configured proxy instances coexist in one process. The
1261    /// pinned fallback is kept across hot reloads.
1262    ///
1263    /// ```rust,no_run
1264    /// # extern crate rws_proxy as rust_web_server;
1265    /// use rust_web_server::proxy_config::build_from_file;
1266    /// use rust_web_server::server_config::ServerConfig;
1267    ///
1268    /// let (app, _handles) = build_from_file();
1269    /// let app = app.with_config(ServerConfig::default());
1270    /// ```
1271    pub fn with_config(self, config: ServerConfig) -> Self {
1272        {
1273            let mut slot = self.current.write().unwrap_or_else(|e| e.into_inner());
1274            let old = Arc::clone(&slot);
1275            *slot = Arc::new(Generation::new(
1276                Arc::clone(&old.dispatch.routes),
1277                App::with_config(config),
1278                old.config.clone(),
1279                Arc::clone(&old.health_stop),
1280                old.source.clone(),
1281            ));
1282        }
1283        self
1284    }
1285
1286    /// Replace the routes, upstreams and `[middleware]` with those in `toml`.
1287    ///
1288    /// The new configuration is validated first ([`ProxyConfig::validate`]
1289    /// plus unknown keys); if it has errors, the running configuration stays
1290    /// in place and the errors are returned — a typo never takes the proxy
1291    /// down. Otherwise the new routes go live for every clone of this app,
1292    /// the previous configuration's health checkers stop, and new ones start.
1293    /// `[[tcp_proxy]]` / `[[udp_proxy]]` / `[[ws_proxy]]` listeners are not
1294    /// rebound; a change to them is reported as a warning.
1295    pub fn reload_from_str(&self, toml: &str) -> Result<ReloadOutcome, crate::config_check::ConfigReport> {
1296        let _serialized = self.reload_lock.lock().unwrap_or_else(|e| e.into_inner());
1297        let old = self.current();
1298        if old.source.as_deref() == Some(toml) {
1299            return Ok(ReloadOutcome::Unchanged);
1300        }
1301        let config = ProxyConfig::from_str(toml);
1302        let mut report = crate::config_check::ConfigReport::default();
1303        validate::check_unknown_keys(toml, &mut report);
1304        config.validate(&mut report);
1305        if !report.is_ok() {
1306            return Err(report);
1307        }
1308
1309        let mut warnings = report.warnings;
1310        let listeners = |c: &ProxyConfig| format!("{:?}{:?}{:?}", c.tcp_proxies, c.udp_proxies, c.ws_proxies);
1311        if listeners(&config) != listeners(&old.config) {
1312            warnings.push("[[tcp_proxy]] / [[udp_proxy]] / [[ws_proxy]] changes need a restart; the old listeners keep running".to_string());
1313        }
1314
1315        let health_stop = Arc::new(AtomicBool::new(false));
1316        let routes = builder::compile_routes(&config, &health_stop);
1317        let route_count = routes.len();
1318        let generation = Generation::new(Arc::new(routes), old.dispatch.fallback.clone(), config, health_stop, Some(toml.to_string()));
1319        let replaced = {
1320            let mut slot = self.current.write().unwrap_or_else(|e| e.into_inner());
1321            std::mem::replace(&mut *slot, Arc::new(generation))
1322        };
1323        replaced.health_stop.store(true, Ordering::Relaxed);
1324        Ok(ReloadOutcome::Reloaded { routes: route_count, warnings })
1325    }
1326
1327    /// Re-read the configuration file (`RWS_CONFIG_FILE` or
1328    /// `rws.config.toml`) and [`reload_from_str`](Self::reload_from_str) it,
1329    /// logging the outcome. Registered as a [`crate::config_reload`] hook by
1330    /// [`build_from_file`].
1331    pub fn reload_from_file(&self) {
1332        let path = config_file_path();
1333        let toml = match std::fs::read_to_string(&path) {
1334            Ok(toml) => toml,
1335            Err(e) => {
1336                eprintln!("[proxy_config] reload: cannot read {path}: {e} — keeping the running routes");
1337                crate::config_reload::report_error(format!("cannot read {path}: {e}"));
1338                return;
1339            }
1340        };
1341        match self.reload_from_str(&toml) {
1342            Ok(ReloadOutcome::Unchanged) => {}
1343            Ok(ReloadOutcome::Reloaded { routes, warnings }) => {
1344                if crate::traffic::enabled() {
1345                    crate::traffic::configure_sites(&toml);
1346                }
1347                for w in &warnings {
1348                    eprintln!("[proxy_config] Warning: {w}");
1349                }
1350                println!("[proxy_config] reloaded {path}: {routes} route{}", if routes == 1 { "" } else { "s" });
1351            }
1352            Err(report) => {
1353                for e in &report.errors {
1354                    eprintln!("[proxy_config] reload ERROR: {e}");
1355                    crate::config_reload::report_error(e.clone());
1356                }
1357                eprintln!("[proxy_config] {path} has errors — keeping the running routes");
1358            }
1359        }
1360    }
1361}
1362
1363/// The request with its target normalized ([`normalize_request_target`]),
1364/// borrowed when it already is.
1365fn normalized(request: &Request) -> std::borrow::Cow<'_, Request> {
1366    match normalize_request_target(&request.request_uri) {
1367        std::borrow::Cow::Borrowed(_) => std::borrow::Cow::Borrowed(request),
1368        std::borrow::Cow::Owned(target) => {
1369            let mut owned = request.clone();
1370            owned.request_uri = target;
1371            std::borrow::Cow::Owned(owned)
1372        }
1373    }
1374}
1375
1376/// A request target's path normalized before routes match it, and sent
1377/// upstream that way (#50): unreserved characters percent-decoded (`%61` →
1378/// `a`; other escapes kept, hex upper-cased), repeated slashes collapsed,
1379/// `.` and `..` segments resolved (never above `/`). Otherwise `/%61dmin`,
1380/// `//admin` or `/x/../admin` missed a protected `/admin` route and reached
1381/// the backend through a catch-all route. The query is kept as is; a
1382/// target that isn't a path (`*`) is returned unchanged.
1383#[doc(hidden)]
1384pub fn normalize_request_target(target: &str) -> std::borrow::Cow<'_, str> {
1385    let (path, query) = match target.find('?') {
1386        Some(i) => (&target[..i], &target[i..]),
1387        None => (target, ""),
1388    };
1389    if !path.starts_with('/') || !(path.contains('%') || path.contains("//") || path.contains("/.")) {
1390        return std::borrow::Cow::Borrowed(target);
1391    }
1392    let bytes = path.as_bytes();
1393    let mut decoded = String::with_capacity(path.len());
1394    let mut i = 0;
1395    while i < bytes.len() {
1396        let hex = |b: u8| (b as char).to_digit(16);
1397        if bytes[i] == b'%' && i + 2 < bytes.len() {
1398            if let (Some(h), Some(l)) = (hex(bytes[i + 1]), hex(bytes[i + 2])) {
1399                let value = (h * 16 + l) as u8;
1400                if value.is_ascii_alphanumeric() || matches!(value, b'-' | b'.' | b'_' | b'~') {
1401                    decoded.push(value as char);
1402                } else {
1403                    decoded.push('%');
1404                    decoded.push(bytes[i + 1].to_ascii_uppercase() as char);
1405                    decoded.push(bytes[i + 2].to_ascii_uppercase() as char);
1406                }
1407                i += 3;
1408                continue;
1409            }
1410        }
1411        // A `%` that starts no escape is a literal percent sign: encoded, so
1412        // what follows can't form a new escape with it (normalizing twice
1413        // must change nothing — the fuzzer found `%%611` → `%a1`).
1414        if bytes[i] == b'%' {
1415            decoded.push_str("%25");
1416            i += 1;
1417            continue;
1418        }
1419        // `path` is a &str: copy whole UTF-8 sequences.
1420        let ch = path[i..].chars().next().unwrap_or('\u{FFFD}');
1421        decoded.push(ch);
1422        i += ch.len_utf8().max(1);
1423    }
1424    let mut segments: Vec<&str> = Vec::new();
1425    let parts: Vec<&str> = decoded.split('/').collect();
1426    let last = parts.len().saturating_sub(1);
1427    let mut trailing_slash = false;
1428    for (n, segment) in parts.iter().enumerate() {
1429        match *segment {
1430            "" => trailing_slash = n == last,
1431            "." => trailing_slash = n == last,
1432            ".." => {
1433                segments.pop();
1434                trailing_slash = n == last;
1435            }
1436            s => {
1437                segments.push(s);
1438                trailing_slash = false;
1439            }
1440        }
1441    }
1442    let mut out = String::with_capacity(decoded.len() + query.len());
1443    out.push('/');
1444    out.push_str(&segments.join("/"));
1445    if trailing_slash && !segments.is_empty() {
1446        out.push('/');
1447    }
1448    out.push_str(query);
1449    if out == target {
1450        std::borrow::Cow::Borrowed(target)
1451    } else {
1452        std::borrow::Cow::Owned(out)
1453    }
1454}
1455
1456impl Application for ConfigDrivenApp {
1457    fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1458        let request = normalized(request);
1459        let generation = self.current();
1460        match &generation.entry {
1461            Some(entry) => entry.execute(&request, conn),
1462            None => generation.dispatch.execute(&request, conn),
1463        }
1464    }
1465
1466    /// The matched route decides: a `grpc` route streams request bodies
1467    /// (client-streaming and bidirectional calls), the others don't yet.
1468    fn streams_body(&self, request: &Request, conn: &ConnectionInfo) -> bool {
1469        let request = normalized(request);
1470        let generation = self.current();
1471        match &generation.entry {
1472            Some(entry) => entry.streams_body(&request, conn),
1473            None => generation.dispatch.streams_body(&request, conn),
1474        }
1475    }
1476
1477    /// Routes proxy upstream or run configured middleware: only requests
1478    /// that match no route and fall through to the static files run inline,
1479    /// and only without a global `[middleware]` table.
1480    fn runs_inline(&self, request: &Request, conn: &ConnectionInfo) -> bool {
1481        let request = normalized(request);
1482        let generation = self.current();
1483        generation.entry.is_none()
1484            && !generation.dispatch.routes.iter().any(|r| r.matcher.matches(&request, conn))
1485            && App::is_inline_request(&request)
1486    }
1487
1488    /// A request for a proxy route with a plain-HTTP upstream and no
1489    /// middleware (route or global) waits for the backend without holding
1490    /// a thread.
1491    #[cfg(feature = "http2")]
1492    fn execute_async<'a>(&'a self, request: &'a Request, conn: &'a ConnectionInfo) -> Option<crate::application::AsyncResponse<'a>> {
1493        let generation = self.current();
1494        // An upgrade hands the connection over: the blocking path does that.
1495        if generation.entry.is_some() || crate::proxy::upgrade::is_upgrade(request) {
1496            return None;
1497        }
1498        let request = normalized(request);
1499        let route = generation.dispatch.routes.iter().find(|r| r.matcher.matches(&request, conn))?;
1500        let proxy = Arc::clone(route.async_proxy.as_ref()?);
1501        if let Some(preflight) = route_cors::preflight(&request, &crate::server_config::ServerConfig::current()) {
1502            return Some(Box::pin(std::future::ready(Ok(preflight))));
1503        }
1504        Some(Box::pin(async move {
1505            let response = proxy.execute_async(&request, conn).await;
1506            response.map(|r| route_cors::add(&request, r, &crate::server_config::ServerConfig::current()))
1507        }))
1508    }
1509}
1510
1511/// `[cors]` on `[[route]]` responses (18.27.0; before, only the static
1512/// files had it): a preflight from an allowed origin is answered here, and
1513/// an actual response gets the headers unless the route (its backend) set
1514/// its own `Access-Control-Allow-Origin`.
1515mod route_cors {
1516    use crate::header::Header;
1517    use crate::request::{Request, METHOD};
1518    use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
1519    use crate::server_config::ServerConfig;
1520
1521    fn headers(request: &Request, config: &ServerConfig) -> Vec<Header> {
1522        if request.header(Header::_ORIGIN).is_none() {
1523            return Vec::new();
1524        }
1525        rws_server::cors::Cors::get_headers_from_config(request, config)
1526    }
1527
1528    /// `204` with the CORS headers for an allowed origin's preflight; `None`
1529    /// sends the request to the route (no `[cors]`, another origin, or not a
1530    /// preflight).
1531    pub(super) fn preflight(request: &Request, config: &ServerConfig) -> Option<Response> {
1532        if request.method != METHOD.options || request.header(Header::_ACCESS_CONTROL_REQUEST_METHOD).is_none() {
1533            return None;
1534        }
1535        let headers = headers(request, config);
1536        if headers.is_empty() {
1537            return None;
1538        }
1539        let mut response = Response::default();
1540        response.status_code = *STATUS_CODE_REASON_PHRASE.n204_no_content.status_code;
1541        response.reason_phrase = STATUS_CODE_REASON_PHRASE.n204_no_content.reason_phrase.to_string();
1542        response.headers = headers;
1543        response.headers.push(Header { name: Header::_VARY.into(), value: "Origin".into() });
1544        Some(response)
1545    }
1546
1547    pub(super) fn add(request: &Request, mut response: Response, config: &ServerConfig) -> Response {
1548        if response.headers.iter().any(|h| h.name.eq_ignore_ascii_case(Header::_ACCESS_CONTROL_ALLOW_ORIGIN)) {
1549            return response;
1550        }
1551        let headers = headers(request, config);
1552        if headers.is_empty() {
1553            return response;
1554        }
1555        response.headers.extend(headers);
1556        let varies = response.headers.iter().any(|h| h.name.eq_ignore_ascii_case(Header::_VARY) && h.value.split(',').any(|v| v.trim().eq_ignore_ascii_case("origin") || v.trim() == "*"));
1557        if !varies {
1558            response.headers.push(Header { name: Header::_VARY.into(), value: "Origin".into() });
1559        }
1560        response
1561    }
1562}
1563
1564// ── H2Route ───────────────────────────────────────────────────────────────────
1565
1566/// The `grpc` action: requests forwarded over HTTP/2 to the upstream's live
1567/// backends in turn, bodies streamed both ways, trailers kept; gRPC-web
1568/// requests translated to gRPC on the way ([`GrpcWebLayer`](crate::proxy::GrpcWebLayer)).
1569#[cfg(feature = "http2")]
1570#[doc(hidden)]
1571pub struct H2Route {
1572    live: Arc<RwLock<Vec<String>>>,
1573    tls: bool,
1574    trust: Option<Arc<crate::proxy::UpstreamTls>>,
1575    connect_timeout: std::time::Duration,
1576    read_timeout: std::time::Duration,
1577    counter: std::sync::atomic::AtomicUsize,
1578}
1579
1580#[cfg(feature = "http2")]
1581impl H2Route {
1582    #[doc(hidden)]
1583    pub fn new(
1584        live: Arc<RwLock<Vec<String>>>,
1585        tls: bool,
1586        trust: Option<Arc<crate::proxy::UpstreamTls>>,
1587        connect_timeout_ms: u64,
1588        read_timeout_ms: u64,
1589    ) -> H2Route {
1590        H2Route {
1591            live,
1592            tls,
1593            trust,
1594            connect_timeout: std::time::Duration::from_millis(connect_timeout_ms),
1595            read_timeout: std::time::Duration::from_millis(read_timeout_ms),
1596            counter: std::sync::atomic::AtomicUsize::new(0),
1597        }
1598    }
1599}
1600
1601#[cfg(feature = "http2")]
1602impl Application for H2Route {
1603    fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1604        use crate::middleware::Middleware;
1605        crate::proxy::GrpcWebLayer::new().handle(request, conn, &H2Forward(self))
1606    }
1607
1608    fn streams_body(&self, _request: &Request, _conn: &ConnectionInfo) -> bool {
1609        true
1610    }
1611}
1612
1613/// [`H2Route`] after the gRPC-web translation.
1614#[cfg(feature = "http2")]
1615struct H2Forward<'a>(&'a H2Route);
1616
1617#[cfg(feature = "http2")]
1618impl Application for H2Forward<'_> {
1619    fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1620        let route = self.0;
1621        let backends = route.live.read().unwrap_or_else(|e| e.into_inner()).clone();
1622        if backends.is_empty() {
1623            let mut response = crate::proxy::bad_gateway();
1624            crate::proxy::status::failed(&mut response, crate::proxy::status::ProxyError::DestinationUnavailable, None, None);
1625            return Ok(response);
1626        }
1627        // A streamed request body can be sent once: no second backend then.
1628        let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { backends.len() };
1629        let start = route.counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1630        let mut failure: Option<(String, String)> = None;
1631        for attempt in 0..attempts {
1632            let url = &backends[(start + attempt) % backends.len()];
1633            let result = crate::proxy::forward_h2(
1634                request, crate::proxy::Peer::of(conn), url, route.tls, route.trust.as_deref(), route.connect_timeout, route.read_timeout,
1635            );
1636            match result {
1637                Ok(mut response) => {
1638                    crate::proxy::status::received(&mut response, Some(url));
1639                    return Ok(response);
1640                }
1641                Err(e) => {
1642                    eprintln!("grpc route: {e}");
1643                    failure = Some((e, url.clone()));
1644                }
1645            }
1646        }
1647        let mut response = crate::proxy::bad_gateway();
1648        if let Some((e, url)) = &failure {
1649            crate::proxy::status::failed_with(&mut response, e, Some(url));
1650        }
1651        Ok(response)
1652    }
1653}
1654
1655// ── NullApp ────────────────────────────────────────────────────────────────────
1656
1657/// A dead-end `Application` that always returns 404.
1658/// Used as the `next` parameter when calling `Middleware::handle` directly.
1659#[derive(Clone, Copy)]
1660#[doc(hidden)]
1661pub struct NullApp;
1662
1663impl Application for NullApp {
1664    fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1665        let mut r = Response::new();
1666        r.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
1667        r.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
1668        Ok(r)
1669    }
1670}
1671
1672// ── DynamicProxy ──────────────────────────────────────────────────────────────
1673
1674use std::collections::HashMap;
1675use std::collections::hash_map::DefaultHasher;
1676use std::hash::{Hash, Hasher};
1677use std::sync::atomic::AtomicUsize;
1678use std::time::{Duration, SystemTime, UNIX_EPOCH};
1679
1680/// Backend-selection strategy for `DynamicProxy`, configured via the
1681/// `strategy` field on `[[upstream]]` in `rws.config.toml`. Unknown or empty
1682/// values fall back to `RoundRobin`, matching the parser's own default.
1683#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1684#[doc(hidden)]
1685pub enum LoadBalanceStrategy {
1686    RoundRobin,
1687    Random,
1688    IpHash,
1689    LeastConnections,
1690}
1691
1692impl LoadBalanceStrategy {
1693    fn parse(s: &str) -> Self {
1694        match s {
1695            "random" => LoadBalanceStrategy::Random,
1696            "ip_hash" => LoadBalanceStrategy::IpHash,
1697            "least_connections" => LoadBalanceStrategy::LeastConnections,
1698            _ => LoadBalanceStrategy::RoundRobin,
1699        }
1700    }
1701}
1702
1703/// A proxy adapter that reads its backend list from a shared, health-checker-
1704/// maintained live list at request time. Supports dynamic removal/restoration
1705/// of backends without restarting, and — for a declared `[[upstream]]` — its
1706/// [`upstream::UpstreamPolicy`]: weights, stickiness, the circuit breaker,
1707/// retries and metrics.
1708///
1709/// This type is `Clone + Send + Sync` and implements `Application`.
1710#[derive(Clone)]
1711#[doc(hidden)]
1712pub struct DynamicProxy {
1713    live: Arc<RwLock<Vec<String>>>,
1714    counter: Arc<AtomicUsize>,
1715    connect_timeout: Duration,
1716    read_timeout: Duration,
1717    strip_prefix: Option<Arc<String>>,
1718    add_prefix: Option<Arc<String>>,
1719    tls: bool,
1720    strategy: LoadBalanceStrategy,
1721    connections: Arc<RwLock<HashMap<String, Arc<AtomicUsize>>>>,
1722    /// The upstream's `tls_ca_file` trust settings; `None` = WebPKI roots.
1723    trust: Option<Arc<crate::proxy::UpstreamTls>>,
1724    /// The upstream's name, for metrics.
1725    upstream: Arc<String>,
1726    /// The declared upstream's policy; `None` for an inline backend.
1727    policy: Option<Arc<upstream::UpstreamPolicy>>,
1728    /// `[route.middleware.mirror]`: copies of requests, sent once per
1729    /// request (not per retry).
1730    mirror: Option<crate::proxy::mirror::Mirror>,
1731    /// `early_hints`: sent once per request, before the first attempt.
1732    early_hints: Option<Arc<Vec<String>>>,
1733    /// `protocol = "h3"`: requests go over HTTP/3; backends QUIC couldn't
1734    /// reach are on TCP until the time kept here.
1735    #[cfg(feature = "http3")]
1736    h3: Option<Arc<std::sync::Mutex<HashMap<String, std::time::Instant>>>>,
1737    /// The `Host` sent upstream.
1738    upstream_host: UpstreamHost,
1739}
1740
1741impl DynamicProxy {
1742    /// Send this `Host` upstream instead of the backend's address.
1743    #[doc(hidden)]
1744    pub fn with_upstream_host(mut self, host: UpstreamHost) -> Self {
1745        self.upstream_host = host;
1746        self
1747    }
1748
1749    /// The `Host` to send instead of the backend's address, if any.
1750    fn host_override<'a>(&'a self, request: &'a Request) -> Option<&'a str> {
1751        match &self.upstream_host {
1752            UpstreamHost::Backend => None,
1753            UpstreamHost::Client => request.header(Header::_HOST).map(|h| h.value.as_ref()).filter(|v| !v.is_empty()),
1754            UpstreamHost::Fixed(host) => Some(host),
1755        }
1756    }
1757
1758    /// Trust the given CA for TLS backends instead of the WebPKI roots.
1759    #[doc(hidden)]
1760    pub fn with_trust(mut self, trust: Option<Arc<crate::proxy::UpstreamTls>>) -> Self {
1761        self.trust = trust;
1762        self
1763    }
1764
1765    /// Apply the upstream's weights, stickiness, circuit breaker, retries
1766    /// and metrics.
1767    /// Send these `Link` values as a `103` for each request (once, not per
1768    /// retry).
1769    #[doc(hidden)]
1770    pub fn with_early_hints(mut self, links: &[String]) -> Self {
1771        self.early_hints = (!links.is_empty()).then(|| Arc::new(links.to_vec()));
1772        self
1773    }
1774
1775    /// Send requests over HTTP/3 (`protocol = "h3"`); needs TLS backends
1776    /// and the `http3` feature (a no-op without it — `rws --check` reports
1777    /// that).
1778    #[cfg_attr(not(feature = "http3"), allow(unused_mut))]
1779    #[doc(hidden)]
1780    pub fn with_h3(mut self, enabled: bool) -> Self {
1781        #[cfg(feature = "http3")]
1782        {
1783            self.h3 = (enabled && self.tls).then(Default::default);
1784        }
1785        #[cfg(not(feature = "http3"))]
1786        let _ = enabled;
1787        self
1788    }
1789
1790    /// The HTTP/3 target for `backend`, unless this proxy doesn't use
1791    /// HTTP/3 or the backend is on TCP for now.
1792    #[cfg(feature = "http3")]
1793    fn h3_target(&self, backend: &str) -> Option<Arc<rustls::ClientConfig>> {
1794        let tcp_until = self.h3.as_ref()?;
1795        let mut tcp_until = tcp_until.lock().unwrap_or_else(|e| e.into_inner());
1796        if let Some(until) = tcp_until.get(backend) {
1797            if std::time::Instant::now() < *until {
1798                return None;
1799            }
1800            tcp_until.remove(backend);
1801        }
1802        Some(match &self.trust {
1803            Some(trust) => trust.client_config(),
1804            None => crate::proxy::tls_client_config(),
1805        })
1806    }
1807
1808    /// QUIC couldn't reach `backend`: TCP for [`H3_FALLBACK`], counted.
1809    #[cfg(feature = "http3")]
1810    fn fall_back_to_tcp(&self, backend: &str, error: &str) {
1811        if let Some(tcp_until) = &self.h3 {
1812            tcp_until.lock().unwrap_or_else(|e| e.into_inner()).insert(backend.to_string(), std::time::Instant::now() + H3_FALLBACK);
1813        }
1814        upstream::observe_h3_fallback(&self.upstream);
1815        eprintln!("upstream {}: HTTP/3 to {backend} failed ({error}); using TCP for {} s", self.upstream, H3_FALLBACK.as_secs());
1816    }
1817
1818    /// Send a copy of each request to `mirror` (once, not per retry).
1819    #[doc(hidden)]
1820    pub fn with_mirror(mut self, mirror: Option<crate::proxy::mirror::Mirror>) -> Self {
1821        self.mirror = mirror;
1822        self
1823    }
1824
1825    #[doc(hidden)]
1826    pub fn with_policy(mut self, upstream: &str, policy: Option<Arc<upstream::UpstreamPolicy>>) -> Self {
1827        self.upstream = Arc::new(upstream.to_string());
1828        self.policy = policy;
1829        self
1830    }
1831
1832    #[doc(hidden)]
1833    pub fn new(
1834        live: Arc<RwLock<Vec<String>>>,
1835        connect_timeout_ms: u64,
1836        read_timeout_ms: u64,
1837        strip_prefix: Option<String>,
1838        add_prefix: Option<String>,
1839        tls: bool,
1840        strategy: String,
1841    ) -> Self {
1842        DynamicProxy {
1843            live,
1844            counter: Arc::new(AtomicUsize::new(0)),
1845            connect_timeout: Duration::from_millis(connect_timeout_ms),
1846            read_timeout: Duration::from_millis(read_timeout_ms),
1847            strip_prefix: strip_prefix.map(Arc::new),
1848            add_prefix: add_prefix.map(Arc::new),
1849            tls,
1850            strategy: LoadBalanceStrategy::parse(&strategy),
1851            connections: Arc::new(RwLock::new(HashMap::new())),
1852            trust: None,
1853            upstream: Arc::new(String::new()),
1854            policy: None,
1855            mirror: None,
1856            early_hints: None,
1857            #[cfg(feature = "http3")]
1858            h3: None,
1859            upstream_host: UpstreamHost::Backend,
1860        }
1861    }
1862
1863    /// Pick by the strategy among `candidates` (not empty).
1864    fn pick(&self, candidates: &[String], client_ip: &str) -> String {
1865        let policy = self.policy.as_deref().filter(|p| p.weighted());
1866        let idx = match self.strategy {
1867            LoadBalanceStrategy::RoundRobin => match policy {
1868                Some(policy) => return policy.weighted_round_robin(candidates).unwrap_or_else(|| candidates[0].clone()),
1869                None => self.counter.fetch_add(1, Ordering::Relaxed) % candidates.len(),
1870            },
1871            LoadBalanceStrategy::Random => {
1872                let nanos = SystemTime::now()
1873                    .duration_since(UNIX_EPOCH)
1874                    .map(|d| d.subsec_nanos())
1875                    .unwrap_or(0) as usize;
1876                let salt = self.counter.fetch_add(1, Ordering::Relaxed);
1877                let roll = nanos.wrapping_add(salt.wrapping_mul(2_654_435_761));
1878                match policy {
1879                    Some(policy) => policy.weighted_index(candidates, roll),
1880                    None => roll % candidates.len(),
1881                }
1882            }
1883            LoadBalanceStrategy::IpHash => {
1884                let mut hasher = DefaultHasher::new();
1885                client_ip.hash(&mut hasher);
1886                (hasher.finish() as usize) % candidates.len()
1887            }
1888            LoadBalanceStrategy::LeastConnections => {
1889                let connections = self.connections.read().unwrap();
1890                let load = |backend: &String| connections.get(backend).map(|c| c.load(Ordering::Relaxed)).unwrap_or(0) as u64;
1891                let weight = |backend: &String| self.policy.as_deref().map_or(1, |p| p.weight(backend)) as u64;
1892                // Fewest in flight per unit of weight (compared crosswise).
1893                candidates
1894                    .iter()
1895                    .enumerate()
1896                    .min_by(|(_, a), (_, b)| (load(a) * weight(b)).cmp(&(load(b) * weight(a))))
1897                    .map(|(i, _)| i)
1898                    .unwrap_or(0)
1899            }
1900        };
1901        candidates[idx].clone()
1902    }
1903
1904    /// The backend for this attempt: the sticky one if it's still in
1905    /// rotation, else one by the strategy, skipping `tried` and backends
1906    /// whose circuit breaker is open. `Err(true)`: none, because the breaker
1907    /// holds the rest open; `Err(false)`: none at all.
1908    fn next_backend(&self, request: &Request, client_ip: &str, tried: &[String]) -> Result<String, bool> {
1909        let mut candidates: Vec<String> = self.live.read().unwrap().iter().filter(|b| !tried.contains(b)).cloned().collect();
1910        if !candidates.is_empty() {
1911            candidates.retain(|b| !upstream::is_drained(&self.upstream, b));
1912            if candidates.is_empty() {
1913                // Every live backend drained: 503, like an open breaker.
1914                return Err(true);
1915            }
1916        }
1917        let Some(policy) = self.policy.as_deref() else {
1918            return if candidates.is_empty() { Err(false) } else { Ok(self.pick(&candidates, client_ip)) };
1919        };
1920        if let Some(sticky) = &policy.sticky {
1921            if let Some(id) = upstream::cookie_value(request, &sticky.cookie) {
1922                if let Some(backend) = candidates.iter().find(|b| upstream::backend_id(b) == id).cloned() {
1923                    if policy.admits(&backend) {
1924                        return Ok(backend);
1925                    }
1926                    candidates.retain(|b| *b != backend);
1927                }
1928            }
1929        }
1930        let ejected = policy.any_open(&candidates);
1931        while !candidates.is_empty() {
1932            let backend = self.pick(&candidates, client_ip);
1933            if policy.admits(&backend) {
1934                return Ok(backend);
1935            }
1936            candidates.retain(|b| *b != backend);
1937        }
1938        Err(ejected)
1939    }
1940
1941    /// Returns the shared in-flight connection counter for `backend`,
1942    /// creating it on first use. Only consulted under `LeastConnections`.
1943    fn connection_counter(&self, backend: &str) -> Arc<AtomicUsize> {
1944        if let Some(counter) = self.connections.read().unwrap().get(backend) {
1945            return Arc::clone(counter);
1946        }
1947        let mut connections = self.connections.write().unwrap();
1948        Arc::clone(
1949            connections
1950                .entry(backend.to_string())
1951                .or_insert_with(|| Arc::new(AtomicUsize::new(0))),
1952        )
1953    }
1954}
1955
1956/// Decrements a backend's in-flight connection count when the request
1957/// finishes (including early returns), keeping `least_connections` accurate.
1958struct ConnectionGuard {
1959    counter: Arc<AtomicUsize>,
1960}
1961
1962impl Drop for ConnectionGuard {
1963    fn drop(&mut self) {
1964        self.counter.fetch_sub(1, Ordering::Relaxed);
1965    }
1966}
1967
1968/// A request ready to send: the chosen backend and the (possibly rewritten)
1969/// request. Holding it keeps the backend's least-connections count raised.
1970struct Prepared<'a> {
1971    backend: String,
1972    host: String,
1973    port: u16,
1974    request: std::borrow::Cow<'a, Request>,
1975    _guard: Option<ConnectionGuard>,
1976}
1977
1978/// How one attempt went.
1979enum Attempt {
1980    /// A response to pass on.
1981    Done(Response),
1982    /// A failure, retried when the policy allows: the backend's response (a
1983    /// retried status) or none (no response).
1984    Failed(Option<Response>),
1985}
1986
1987impl DynamicProxy {
1988    /// An upgrade request (`Connection: upgrade`) to a live backend over its
1989    /// own connection. Plain-HTTP backends only: a TLS backend gets `502`.
1990    fn forward_upgrade(&self, request: &Request, conn: &ConnectionInfo) -> Response {
1991        let prepared = match self.prepare(request, conn, &[]) {
1992            Ok(p) => p,
1993            Err(response) => return response,
1994        };
1995        if self.tls || prepared.backend.starts_with("https://") || prepared.backend.starts_with("wss://") {
1996            eprintln!("[proxy] upgrade to {}: TLS backends can't take upgraded connections yet", prepared.backend);
1997            return bad_gateway();
1998        }
1999        let default_host = if prepared.port == 80 { prepared.host.clone() } else { format!("{}:{}", prepared.host, prepared.port) };
2000        let host_header = self.host_override(request).map(str::to_string).unwrap_or(default_host);
2001        match crate::proxy::upgrade::forward(&prepared.request, crate::proxy::Peer::of(conn), &prepared.host, prepared.port, &host_header, self.read_timeout) {
2002            Ok(response) => response,
2003            Err(e) => {
2004                eprintln!("[proxy] upgrade to {}: {e}", prepared.backend);
2005                let mut response = bad_gateway();
2006                crate::proxy::status::failed(&mut response, crate::proxy::status::ProxyError::ConnectionRefused, Some(&prepared.backend), None);
2007                response
2008            }
2009        }
2010    }
2011
2012    /// Pick a backend (not one of `tried`) and apply the path rewriting;
2013    /// `Err` is the response to send instead (no backend in rotation — `503`
2014    /// when the circuit breaker took them out, `502` otherwise — or an
2015    /// unparsable backend address).
2016    fn prepare<'a>(&self, request: &'a Request, conn: &ConnectionInfo, tried: &[String]) -> Result<Prepared<'a>, Response> {
2017        let backend = self.next_backend(request, &crate::ip_filter::client_ip(request, &conn.client.ip), tried).map_err(|ejected| {
2018            let mut response = if ejected { service_unavailable() } else { bad_gateway() };
2019            crate::proxy::status::failed(&mut response, crate::proxy::status::ProxyError::DestinationUnavailable, None, None);
2020            response
2021        })?;
2022
2023        let guard = if self.strategy == LoadBalanceStrategy::LeastConnections {
2024            let counter = self.connection_counter(&backend);
2025            counter.fetch_add(1, Ordering::Relaxed);
2026            Some(ConnectionGuard { counter })
2027        } else {
2028            None
2029        };
2030
2031        let (host, port, _) = crate::proxy_config::health::parse_backend_url(&backend).ok_or_else(|| {
2032            let mut response = bad_gateway();
2033            crate::proxy::status::failed(&mut response, crate::proxy::status::ProxyError::ProxyConfigurationError, Some(&backend), None);
2034            response
2035        })?;
2036
2037        // Apply path rewriting if configured
2038        let request = if self.strip_prefix.is_some() || self.add_prefix.is_some() {
2039            let mut req_clone = request.clone();
2040            if let Some(ref sp) = self.strip_prefix {
2041                if let Some(stripped) = req_clone.request_uri.strip_prefix(sp.as_str()) {
2042                    req_clone.request_uri = if stripped.is_empty() || !stripped.starts_with('/') {
2043                        format!("/{}", stripped)
2044                    } else {
2045                        stripped.to_string()
2046                    };
2047                }
2048            }
2049            if let Some(ref ap) = self.add_prefix {
2050                req_clone.request_uri = format!("{}{}", ap, req_clone.request_uri);
2051            }
2052            std::borrow::Cow::Owned(req_clone)
2053        } else {
2054            std::borrow::Cow::Borrowed(request)
2055        };
2056
2057        Ok(Prepared { backend, host, port, request, _guard: guard })
2058    }
2059
2060    /// Record an attempt's outcome (breaker, metrics) and classify it.
2061    fn settle(&self, backend: &str, result: Result<Response, String>, started: std::time::Instant, failure: &mut Option<(String, String)>) -> Attempt {
2062        let result = match result {
2063            Ok(mut response) => {
2064                crate::proxy::status::received(&mut response, Some(backend));
2065                Ok(response)
2066            }
2067            Err(e) => {
2068                *failure = Some((e, backend.to_string()));
2069                Err(())
2070            }
2071        };
2072        let status = result.as_ref().ok().map(|r| r.status_code);
2073        upstream::observe(&self.upstream, backend, status, started);
2074        let failed = status.is_none_or(|s| matches!(s, 502..=504));
2075        if let Some(policy) = &self.policy {
2076            policy.record(backend, failed);
2077        }
2078        match result {
2079            Ok(response) if self.policy.as_deref().is_some_and(|p| p.retries_status(response.status_code)) => Attempt::Failed(Some(response)),
2080            Ok(response) => Attempt::Done(response),
2081            Err(()) => Attempt::Failed(None),
2082        }
2083    }
2084
2085    /// How many attempts `request` may get.
2086    fn attempts(&self, request: &Request) -> u32 {
2087        match self.policy.as_deref().and_then(|p| p.retry.as_ref()) {
2088            Some(retry) if upstream::replayable(request) => 1 + retry.attempts,
2089            _ => 1,
2090        }
2091    }
2092
2093    /// Set the sticky cookie when the client doesn't hold this backend's.
2094    fn stick(&self, request: &Request, backend: &str, response: &mut Response) {
2095        let Some(sticky) = self.policy.as_deref().and_then(|p| p.sticky.as_ref()) else { return };
2096        let id = upstream::backend_id(backend);
2097        if upstream::cookie_value(request, &sticky.cookie).as_deref() == Some(id.as_str()) {
2098            return;
2099        }
2100        let max_age = sticky.max_age_secs.map(|s| format!("; Max-Age={s}")).unwrap_or_default();
2101        response.headers.push(crate::header::Header {
2102            name: "Set-Cookie".into(),
2103            value: format!("{}={id}; Path=/; HttpOnly; SameSite=Lax{max_age}", sticky.cookie).into(),
2104        });
2105    }
2106
2107    /// The same as [`Application::execute`], without blocking a thread while
2108    /// the backend answers, for HTTP and HTTPS backends alike.
2109    #[cfg(feature = "http2")]
2110    #[doc(hidden)]
2111    pub async fn execute_async(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
2112        if let Some(mirror) = &self.mirror {
2113            mirror.send(request, conn);
2114        }
2115        if let Some(links) = &self.early_hints {
2116            crate::early_hints::send_from(crate::early_hints::Source::Configured, links);
2117        }
2118        let mut tried: Vec<String> = Vec::new();
2119        let mut last: Option<Response> = None;
2120        let mut failure: Option<(String, String)> = None;
2121        for attempt in 0..self.attempts(request) {
2122            let prepared = match self.prepare(request, conn, &tried) {
2123                Ok(p) => p,
2124                Err(response) => return Ok(last.unwrap_or(response)),
2125            };
2126            if attempt > 0 {
2127                upstream::observe_retry(&self.upstream);
2128            }
2129            let tls = self.tls.then(|| match &self.trust {
2130                Some(trust) => trust.async_tls(),
2131                None => crate::proxy::async_upstream::AsyncTls {
2132                    config: crate::proxy::tls_client_config(),
2133                    pool: crate::proxy::async_upstream::default_tls_pool(),
2134                },
2135            });
2136            let started = std::time::Instant::now();
2137            #[cfg(feature = "http3")]
2138            if let Some(config) = self.h3_target(&prepared.backend) {
2139                let target = crate::proxy::h3_upstream::Target {
2140                    host: &prepared.host,
2141                    port: prepared.port,
2142                    authority: self.host_override(request),
2143                    tls: &config,
2144                    connect_timeout: self.connect_timeout,
2145                    read_timeout: self.read_timeout,
2146                };
2147                match crate::proxy::h3_upstream::forward_async(&prepared.request, crate::proxy::Peer::of(conn), &target).await {
2148                    Err(crate::proxy::h3_upstream::H3Error::Connect(e)) => self.fall_back_to_tcp(&prepared.backend, &e),
2149                    result => {
2150                        match self.settle(&prepared.backend, result.map_err(|e| e.to_string()), started, &mut failure) {
2151                            Attempt::Done(mut response) => {
2152                                self.stick(request, &prepared.backend, &mut response);
2153                                return Ok(response);
2154                            }
2155                            Attempt::Failed(response) => last = response,
2156                        }
2157                        tried.push(prepared.backend.clone());
2158                        continue;
2159                    }
2160                }
2161            }
2162            let result = crate::proxy::async_upstream::forward_as(
2163                &prepared.request,
2164                crate::proxy::Peer::of(conn),
2165                &prepared.host,
2166                prepared.port,
2167                self.host_override(request).unwrap_or(&prepared.host),
2168                tls,
2169                self.connect_timeout,
2170                self.read_timeout,
2171            )
2172            .await;
2173            match self.settle(&prepared.backend, result, started, &mut failure) {
2174                Attempt::Done(mut response) => {
2175                    self.stick(request, &prepared.backend, &mut response);
2176                    return Ok(response);
2177                }
2178                Attempt::Failed(response) => last = response,
2179            }
2180            tried.push(prepared.backend.clone());
2181        }
2182        Ok(last.unwrap_or_else(|| {
2183            let mut response = bad_gateway();
2184            match &failure {
2185                Some((e, backend)) => crate::proxy::status::failed_with(&mut response, e, Some(backend)),
2186                None => crate::proxy::status::failed(&mut response, crate::proxy::status::ProxyError::ProxyInternalError, None, None),
2187            }
2188            response
2189        }))
2190    }
2191
2192    /// Whether [`execute_async`](Self::execute_async) can serve this proxy.
2193    #[doc(hidden)]
2194    pub fn supports_async(&self) -> bool {
2195        cfg!(feature = "http2")
2196    }
2197}
2198
2199impl Application for DynamicProxy {
2200    fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
2201        // WebSocket and other upgrades (18.21.0): the backend's `101` hands
2202        // the client connection to a byte relay with it.
2203        if crate::proxy::upgrade::is_upgrade(request) {
2204            return Ok(self.forward_upgrade(request, conn));
2205        }
2206        if let Some(mirror) = &self.mirror {
2207            mirror.send(request, conn);
2208        }
2209        if let Some(links) = &self.early_hints {
2210            crate::early_hints::send_from(crate::early_hints::Source::Configured, links);
2211        }
2212        let mut tried: Vec<String> = Vec::new();
2213        let mut last: Option<Response> = None;
2214        let mut failure: Option<(String, String)> = None;
2215        for attempt in 0..self.attempts(request) {
2216            let prepared = match self.prepare(request, conn, &tried) {
2217                Ok(p) => p,
2218                Err(response) => return Ok(last.unwrap_or(response)),
2219            };
2220            if attempt > 0 {
2221                upstream::observe_retry(&self.upstream);
2222            }
2223            // Pooled keep-alive connections to the backend, TLS or not (a
2224            // fresh connection per request halved throughput vs. Caddy/nginx).
2225            let started = std::time::Instant::now();
2226            #[cfg(feature = "http3")]
2227            if let Some(config) = self.h3_target(&prepared.backend) {
2228                let target = crate::proxy::h3_upstream::Target {
2229                    host: &prepared.host,
2230                    port: prepared.port,
2231                    authority: self.host_override(request),
2232                    tls: &config,
2233                    connect_timeout: self.connect_timeout,
2234                    read_timeout: self.read_timeout,
2235                };
2236                match crate::proxy::h3_upstream::forward(&prepared.request, crate::proxy::Peer::of(conn), &target) {
2237                    Err(crate::proxy::h3_upstream::H3Error::Connect(e)) => self.fall_back_to_tcp(&prepared.backend, &e),
2238                    result => {
2239                        match self.settle(&prepared.backend, result.map_err(|e| e.to_string()), started, &mut failure) {
2240                            Attempt::Done(mut response) => {
2241                                self.stick(request, &prepared.backend, &mut response);
2242                                return Ok(response);
2243                            }
2244                            Attempt::Failed(response) => last = response,
2245                        }
2246                        tried.push(prepared.backend.clone());
2247                        continue;
2248                    }
2249                }
2250            }
2251            let result = crate::proxy::forward_upstream_as(
2252                crate::proxy::shared_pool(),
2253                &prepared.request,
2254                crate::proxy::Peer::of(conn),
2255                &prepared.host,
2256                prepared.port,
2257                self.host_override(request).unwrap_or(&prepared.host),
2258                self.tls,
2259                self.trust.as_deref(),
2260                self.connect_timeout,
2261                self.read_timeout,
2262            );
2263            match self.settle(&prepared.backend, result, started, &mut failure) {
2264                Attempt::Done(mut response) => {
2265                    self.stick(request, &prepared.backend, &mut response);
2266                    return Ok(response);
2267                }
2268                Attempt::Failed(response) => last = response,
2269            }
2270            tried.push(prepared.backend.clone());
2271        }
2272        Ok(last.unwrap_or_else(|| {
2273            let mut response = bad_gateway();
2274            match &failure {
2275                Some((e, backend)) => crate::proxy::status::failed_with(&mut response, e, Some(backend)),
2276                None => crate::proxy::status::failed(&mut response, crate::proxy::status::ProxyError::ProxyInternalError, None, None),
2277            }
2278            response
2279        }))
2280    }
2281}
2282
2283/// How long a backend QUIC couldn't reach is sent requests over TCP.
2284#[cfg(feature = "http3")]
2285#[doc(hidden)]
2286pub const H3_FALLBACK: Duration = Duration::from_secs(300);
2287
2288fn service_unavailable() -> Response {
2289    let mut r = crate::error::problem(STATUS_CODE_REASON_PHRASE.n503_service_unavailable, "every backend is out of rotation");
2290    r.headers.push(crate::header::Header { name: "Retry-After".into(), value: "5".into() });
2291    r
2292}
2293
2294fn bad_gateway() -> Response {
2295    crate::error::problem(STATUS_CODE_REASON_PHRASE.n502_bad_gateway, "")
2296}
2297
2298// ── RedirectAdapter ────────────────────────────────────────────────────────────
2299
2300/// Action adapter that issues HTTP redirects.
2301///
2302/// `$path` in `location_template` is replaced with the request URI at runtime.
2303#[derive(Clone)]
2304#[doc(hidden)]
2305pub struct RedirectAdapter {
2306    location_template: Arc<String>,
2307    status: i16,
2308    reason: Arc<String>,
2309}
2310
2311impl RedirectAdapter {
2312    #[doc(hidden)]
2313    pub fn new(location: String, status: u16) -> Self {
2314        let (code, reason) = redirect_status(status);
2315        RedirectAdapter {
2316            location_template: Arc::new(location),
2317            status: code,
2318            reason: Arc::new(reason),
2319        }
2320    }
2321}
2322
2323fn redirect_status(code: u16) -> (i16, String) {
2324    let phrase = match code {
2325        301 => STATUS_CODE_REASON_PHRASE.n301_moved_permanently.reason_phrase,
2326        302 => STATUS_CODE_REASON_PHRASE.n302_found.reason_phrase,
2327        307 => STATUS_CODE_REASON_PHRASE.n307_temporary_redirect.reason_phrase,
2328        308 => STATUS_CODE_REASON_PHRASE.n308_permanent_redirect.reason_phrase,
2329        _ => "Redirect",
2330    };
2331    (code as i16, phrase.to_string())
2332}
2333
2334impl Application for RedirectAdapter {
2335    fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
2336        let location = self
2337            .location_template
2338            .replace("$path", &request.request_uri);
2339        use crate::header::Header;
2340        let mut r = Response::new();
2341        r.status_code = self.status;
2342        r.reason_phrase = self.reason.as_ref().clone();
2343        r.headers.push(Header { name: "Location".into(), value: location.into() });
2344        Ok(r)
2345    }
2346}
2347
2348// ── RespondAdapter ─────────────────────────────────────────────────────────────
2349
2350/// Action adapter that returns a fixed response body.
2351#[derive(Clone)]
2352#[doc(hidden)]
2353pub struct RespondAdapter {
2354    status: i16,
2355    reason: Arc<String>,
2356    body: Arc<Vec<u8>>,
2357    content_type: Arc<String>,
2358}
2359
2360impl RespondAdapter {
2361    #[doc(hidden)]
2362    pub fn new(status: u16, body: String, content_type: String) -> Self {
2363        use crate::response::STATUS_CODE_REASON_PHRASE;
2364        let reason = match status {
2365            200 => STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string(),
2366            201 => STATUS_CODE_REASON_PHRASE.n201_created.reason_phrase.to_string(),
2367            204 => STATUS_CODE_REASON_PHRASE.n204_no_content.reason_phrase.to_string(),
2368            400 => STATUS_CODE_REASON_PHRASE.n400_bad_request.reason_phrase.to_string(),
2369            401 => STATUS_CODE_REASON_PHRASE.n401_unauthorized.reason_phrase.to_string(),
2370            403 => STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string(),
2371            404 => STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string(),
2372            500 => STATUS_CODE_REASON_PHRASE.n500_internal_server_error.reason_phrase.to_string(),
2373            _ => "OK".to_string(),
2374        };
2375        RespondAdapter {
2376            status: status as i16,
2377            reason: Arc::new(reason),
2378            body: Arc::new(body.into_bytes()),
2379            content_type: Arc::new(content_type),
2380        }
2381    }
2382}
2383
2384impl Application for RespondAdapter {
2385    fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
2386        use crate::range::Range;
2387        let mut r = Response::new();
2388        r.status_code = self.status;
2389        r.reason_phrase = self.reason.as_ref().clone();
2390        if !self.body.is_empty() {
2391            r.content_range_list = vec![Range::get_content_range(
2392                self.body.as_ref().clone(),
2393                self.content_type.as_ref().clone(),
2394            )];
2395        }
2396        Ok(r)
2397    }
2398}
2399
2400// ── StaticAdapter ──────────────────────────────────────────────────────────────
2401
2402/// Action adapter that serves static files from a configured `root` directory.
2403///
2404/// Unlike `StaticResourceController` (which always resolves paths relative to
2405/// the process's current working directory), this adapter is parameterized
2406/// per-route by `ActionConfig::Static { root, index }` from `rws.config.toml`,
2407/// so a config-driven proxy can serve an arbitrary directory without Rust code.
2408/// A path with no file answers `404` with `<root>/404.html` when it exists.
2409#[derive(Clone)]
2410#[doc(hidden)]
2411pub struct StaticAdapter {
2412    root: Arc<std::path::PathBuf>,
2413    index: Arc<Vec<String>>,
2414}
2415
2416impl StaticAdapter {
2417    #[doc(hidden)]
2418    pub fn new(root: String, index: Vec<String>) -> Self {
2419        let index = if index.is_empty() { vec!["index.html".to_string()] } else { index };
2420        StaticAdapter {
2421            root: Arc::new(std::path::PathBuf::from(root)),
2422            index: Arc::new(index),
2423        }
2424    }
2425
2426    /// Resolves `request_uri` against `root`. Returns `None` if the decoded
2427    /// path contains a `..` segment, which would otherwise let a request
2428    /// escape the configured root directory.
2429    fn resolve(&self, request_uri: &str) -> Option<std::path::PathBuf> {
2430        let raw_path = request_uri.split('?').next().unwrap_or(request_uri);
2431        let decoded = crate::url::URL::percent_decode(raw_path);
2432
2433        if decoded.split('/').any(|segment| segment == "..") {
2434            return None;
2435        }
2436
2437        let relative = decoded.trim_start_matches('/');
2438        Some(self.root.join(relative))
2439    }
2440}
2441
2442impl Application for StaticAdapter {
2443    fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
2444        let mut response = Response::new();
2445
2446        // A miss answers 404 with the root's own 404.html when it has one (as
2447        // the default static server does with ./404.html), else an empty body.
2448        let not_found = |mut response: Response| {
2449            response.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
2450            response.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
2451            let page = self.root.join("404.html");
2452            if let (true, Some(page)) = (page.is_file(), page.to_str()) {
2453                if let Ok(content_range) = crate::range::Range::get_content_range_of_a_file(page) {
2454                    response.content_range_list = vec![content_range];
2455                }
2456            }
2457            response
2458        };
2459
2460        let candidate = match self.resolve(&request.request_uri) {
2461            Some(p) => p,
2462            None => {
2463                response.status_code = *STATUS_CODE_REASON_PHRASE.n403_forbidden.status_code;
2464                response.reason_phrase = STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string();
2465                return Ok(response);
2466            }
2467        };
2468
2469        let mut file_path = candidate;
2470        if file_path.is_dir() {
2471            let indexed = self
2472                .index
2473                .iter()
2474                .map(|name| file_path.join(name))
2475                .find(|p| p.is_file());
2476
2477            file_path = match indexed {
2478                Some(p) => p,
2479                None => return Ok(not_found(response)),
2480            };
2481        }
2482
2483        if !file_path.is_file() {
2484            return Ok(not_found(response));
2485        }
2486
2487        // Defense-in-depth against symlinks inside `root` that point outside it —
2488        // the `..`-segment check above only catches traversal in the request URI.
2489        if let (Ok(root_canon), Ok(file_canon)) =
2490            (self.root.canonicalize(), file_path.canonicalize())
2491        {
2492            if !file_canon.starts_with(&root_canon) {
2493                return Ok(not_found(response));
2494            }
2495        }
2496
2497        let path_str = match file_path.to_str() {
2498            Some(s) => s,
2499            None => return Ok(not_found(response)),
2500        };
2501
2502        match crate::range::Range::get_content_range_of_a_file(path_str) {
2503            Ok(content_range) => {
2504                response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
2505                response.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
2506                response.content_range_list = vec![content_range];
2507                Ok(response)
2508            }
2509            Err(_) => Ok(not_found(response)),
2510        }
2511    }
2512}
2513
2514// ── PerRouteRateLimit middleware ───────────────────────────────────────────────
2515
2516/// A per-route rate limiter middleware backed by a shared `RateLimiter`.
2517#[doc(hidden)]
2518pub struct PerRouteRateLimit(pub(crate) Arc<crate::rate_limit::RateLimiter>);
2519
2520impl crate::middleware::Middleware for PerRouteRateLimit {
2521    fn handle(
2522        &self,
2523        request: &Request,
2524        conn: &ConnectionInfo,
2525        next: &dyn Application,
2526    ) -> Result<Response, String> {
2527        use crate::error::{AppError, IntoResponse};
2528        let ip = crate::ip_filter::client_ip(request, &conn.client.ip);
2529        if self.0.check(&ip) {
2530            next.execute(request, conn)
2531        } else {
2532            let mut response = AppError::TooManyRequests.into_response();
2533            let retry_after = self.0.retry_after(&ip);
2534            response.headers.push(Header {
2535                name: Header::_RETRY_AFTER.into(),
2536                value: retry_after.to_string().into(),
2537            });
2538            Ok(response)
2539        }
2540    }
2541}
2542
2543// ── PerRouteMaxBodySize ───────────────────────────────────────────────────────
2544
2545/// A route-specific request body size limit, from `[route.middleware]
2546/// max_body_size`.
2547///
2548/// Rejects with `413 Payload Too Large` if `request.body` is bigger than the
2549/// configured ceiling. This runs *after* the request has already been fully
2550/// read off the socket — route middleware only ever sees an already-built
2551/// `Request` — so, unlike the global `RWS_CONFIG_MAX_BODY_SIZE_IN_BYTES`
2552/// (checked against the declared `Content-Length` before any of an
2553/// oversized body is buffered), this can't prevent the memory from being
2554/// spent for a single oversized request. What it protects is this route's
2555/// own downstream logic/upstream backend from a body that's within the
2556/// global limit but still too big for this specific route.
2557#[doc(hidden)]
2558pub struct PerRouteMaxBodySize(pub(crate) u64);
2559
2560impl crate::middleware::Middleware for PerRouteMaxBodySize {
2561    fn handle(
2562        &self,
2563        request: &Request,
2564        conn: &ConnectionInfo,
2565        next: &dyn Application,
2566    ) -> Result<Response, String> {
2567        use crate::error::{AppError, IntoResponse};
2568        if request.body.len() as u64 > self.0 {
2569            Ok(AppError::PayloadTooLarge(format!(
2570                "request body of {} bytes exceeds this route's {}-byte limit",
2571                request.body.len(),
2572                self.0,
2573            ))
2574            .into_response())
2575        } else {
2576            next.execute(request, conn)
2577        }
2578    }
2579}
2580
2581// ── BearerAuthMiddleware ───────────────────────────────────────────────────────
2582
2583/// Bearer token authentication middleware.
2584#[doc(hidden)]
2585pub struct BearerAuthMiddleware {
2586    #[doc(hidden)]
2587    pub token: Arc<String>,
2588}
2589
2590impl crate::middleware::Middleware for BearerAuthMiddleware {
2591    fn handle(
2592        &self,
2593        request: &Request,
2594        conn: &ConnectionInfo,
2595        next: &dyn Application,
2596    ) -> Result<Response, String> {
2597        use crate::error::{AppError, IntoResponse};
2598        let expected = format!("Bearer {}", self.token);
2599        let authorized = request
2600            .headers
2601            .iter()
2602            .any(|h| h.name.eq_ignore_ascii_case("authorization") && constant_time_eq(h.value.as_bytes(), expected.as_bytes()));
2603        if authorized {
2604            next.execute(request, conn)
2605        } else {
2606            Ok(AppError::Unauthorized.into_response())
2607        }
2608    }
2609}
2610
2611pub(crate) use crate::core::constant_time_eq;
2612
2613/// Stands in for an auth layer whose configuration can't be honoured (see
2614/// `builder::apply_middleware`): every request gets `503`, never the route.
2615#[doc(hidden)]
2616pub struct AuthMisconfigured;
2617
2618/// A disk cache's `max_size`, or the default.
2619#[doc(hidden)]
2620pub fn cache_max_bytes(max_size: &str) -> Option<u64> {
2621    if max_size.trim().is_empty() {
2622        Some(256 * 1024 * 1024)
2623    } else {
2624        crate::log::access_sink::parse_size(max_size.trim()).filter(|n| *n > 0)
2625    }
2626}
2627
2628/// A `[route.middleware.wasm]` plugin that couldn't be loaded: the route
2629/// fails closed.
2630#[doc(hidden)]
2631pub struct PluginMisconfigured;
2632
2633impl crate::middleware::Middleware for PluginMisconfigured {
2634    fn handle(&self, _request: &Request, _conn: &ConnectionInfo, _next: &dyn Application) -> Result<Response, String> {
2635        Ok(crate::error::problem(STATUS_CODE_REASON_PHRASE.n503_service_unavailable, "this route's plugin is misconfigured"))
2636    }
2637}
2638
2639impl crate::middleware::Middleware for AuthMisconfigured {
2640    fn handle(
2641        &self,
2642        _request: &Request,
2643        _conn: &ConnectionInfo,
2644        _next: &dyn Application,
2645    ) -> Result<Response, String> {
2646        Ok(crate::error::problem(STATUS_CODE_REASON_PHRASE.n503_service_unavailable, "authentication for this route is misconfigured"))
2647    }
2648}
2649
2650// ── arc_app helper ─────────────────────────────────────────────────────────────
2651
2652/// Box any `Application + Send + Sync + 'static` into an `Arc<dyn …>`.
2653#[doc(hidden)]
2654pub fn arc_app<A: Application + Send + Sync + 'static>(
2655    a: A,
2656) -> Arc<dyn Application + Send + Sync> {
2657    Arc::new(a)
2658}
2659
2660// ── Public entry points ────────────────────────────────────────────────────────
2661
2662/// Build a `ConfigDrivenApp` from `rws.config.toml` and spawn L4/WS proxy
2663/// threads. Returns the app and a list of thread handles.
2664pub fn build_from_file() -> (ConfigDrivenApp, Vec<std::thread::JoinHandle<()>>) {
2665    builder::build_from_file()
2666}