Skip to main content

rust_web_server/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 mod builder;
31
32#[cfg(test)]
33mod tests;
34
35use std::sync::Arc;
36
37use crate::app::App;
38use crate::application::Application;
39use crate::core::New;
40use crate::request::Request;
41use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
42use crate::server::ConnectionInfo;
43
44// ── Public config types ────────────────────────────────────────────────────────
45
46#[derive(Debug, Clone)]
47pub struct ProxyConfig {
48    pub upstreams: Vec<UpstreamConfig>,
49    pub routes: Vec<RouteConfig>,
50    pub tcp_proxies: Vec<TcpProxyConfig>,
51    pub udp_proxies: Vec<UdpProxyConfig>,
52    pub ws_proxies: Vec<WsProxyConfig>,
53    pub global_middleware: MiddlewareConfig,
54}
55
56#[derive(Debug, Clone)]
57pub struct UpstreamConfig {
58    pub name: String,
59    pub backends: Vec<String>,
60    pub strategy: String, // "round_robin" | "random" | "ip_hash"
61    pub health_check: Option<HealthCheckConfig>,
62}
63
64#[derive(Debug, Clone)]
65pub struct HealthCheckConfig {
66    pub path: String,
67    pub interval_secs: u64,
68    pub timeout_ms: u64,
69    pub healthy_threshold: u32,
70    pub unhealthy_threshold: u32,
71}
72
73#[derive(Debug, Clone)]
74pub struct RouteConfig {
75    pub name: String,
76    pub match_: MatchConfig,
77    pub action: ActionConfig,
78    pub middleware: MiddlewareConfig,
79}
80
81#[derive(Debug, Clone, Default)]
82pub struct MatchConfig {
83    pub host: Option<String>,
84    pub path: Option<String>,
85    pub method: Option<String>,
86    pub content_type: Option<String>,
87}
88
89#[derive(Debug, Clone)]
90pub enum ActionConfig {
91    Proxy {
92        upstream: String,
93        connect_timeout_ms: u64,
94        read_timeout_ms: u64,
95        strip_path_prefix: Option<String>,
96        add_path_prefix: Option<String>,
97    },
98    Grpc {
99        upstream: String,
100        connect_timeout_ms: u64,
101        read_timeout_ms: u64,
102    },
103    Static {
104        root: String,
105        index: Vec<String>,
106    },
107    Redirect {
108        location: String,
109        status: u16,
110    },
111    Respond {
112        status: u16,
113        body: String,
114        content_type: String,
115    },
116    Mcp,
117    Unknown(String),
118}
119
120#[derive(Debug, Clone, Default)]
121pub struct MiddlewareConfig {
122    pub rate_limit: Option<RateLimitConfig>,
123    pub cache: Option<CacheConfig>,
124    pub auth: Option<AuthConfig>,
125    pub rewrite_request: Vec<RewriteRuleConfig>,
126    pub rewrite_response: Vec<RewriteRuleConfig>,
127    pub ip_allow: Vec<String>,
128    pub ip_deny: Vec<String>,
129}
130
131#[derive(Debug, Clone)]
132pub struct RateLimitConfig {
133    pub max_requests: u32,
134    pub window_secs: u64,
135}
136
137#[derive(Debug, Clone)]
138pub struct CacheConfig {
139    pub ttl_secs: u64,
140    pub vary_by: Vec<String>,
141}
142
143#[derive(Debug, Clone)]
144pub enum AuthConfig {
145    Basic { users_file: String },
146    Jwt { secret_env: String },
147    Bearer { token_env: String },
148}
149
150#[derive(Debug, Clone, Default)]
151pub struct RewriteRuleConfig {
152    pub type_: String,
153    pub name: Option<String>,
154    pub value: Option<String>,
155    pub prefix: Option<String>,
156    pub from: Option<String>,
157    pub to: Option<String>,
158    pub code: Option<u16>,
159    pub reason: Option<String>,
160}
161
162#[derive(Debug, Clone)]
163pub struct TcpProxyConfig {
164    pub name: String,
165    pub listen: String,
166    pub backends: Vec<String>,
167    pub connect_timeout_ms: u64,
168}
169
170#[derive(Debug, Clone)]
171pub struct UdpProxyConfig {
172    pub name: String,
173    pub listen: String,
174    pub backends: Vec<String>,
175    pub reply_timeout_ms: u64,
176    pub buffer_size: usize,
177}
178
179#[derive(Debug, Clone)]
180pub struct WsProxyConfig {
181    pub name: String,
182    pub listen: String,
183    pub backends: Vec<String>,
184    pub connect_timeout_ms: u64,
185    pub read_timeout_ms: u64,
186}
187
188// ── ProxyConfig loading ────────────────────────────────────────────────────────
189
190impl ProxyConfig {
191    /// Returns `true` if `rws.config.toml` (or `RWS_CONFIG_FILE`) contains
192    /// `[[route]]` or `[[upstream]]` sections, meaning config-driven mode
193    /// should be used.
194    pub fn is_proxy_mode() -> bool {
195        let path = config_file_path();
196        match std::fs::read_to_string(&path) {
197            Ok(contents) => {
198                contents.contains("[[route]]") || contents.contains("[[upstream]]")
199            }
200            Err(_) => false,
201        }
202    }
203
204    /// Parse the config file and return a `ProxyConfig`.
205    pub fn load() -> Self {
206        let path = config_file_path();
207        let contents = std::fs::read_to_string(&path).unwrap_or_default();
208        Self::from_str(&contents)
209    }
210
211    /// Parse `toml` text directly into a `ProxyConfig`. Used in tests.
212    pub fn from_str(toml: &str) -> Self {
213        use parser::{get_array, get_str, get_u32, get_u64, section_exists};
214
215        let map = parser::parse(toml);
216
217        // ── upstreams ──────────────────────────────────────────────────────────
218        let mut upstreams = Vec::new();
219        let mut i = 0;
220        loop {
221            let sec = format!("upstream[{}]", i);
222            if !section_exists(&map, &sec) {
223                break;
224            }
225            let name = get_str(&map, &sec, "name");
226            let backends = get_array(&map, &sec, "backends");
227            let strategy = {
228                let s = get_str(&map, &sec, "strategy");
229                if s.is_empty() { "round_robin".to_string() } else { s }
230            };
231            let hc_sec = format!("{}.health_check", sec);
232            let health_check = if section_exists(&map, &hc_sec) {
233                Some(HealthCheckConfig {
234                    path: {
235                        let p = get_str(&map, &hc_sec, "path");
236                        if p.is_empty() { "/health".to_string() } else { p }
237                    },
238                    interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
239                    timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
240                    healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
241                    unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
242                })
243            } else {
244                None
245            };
246            upstreams.push(UpstreamConfig { name, backends, strategy, health_check });
247            i += 1;
248        }
249
250        // ── routes ─────────────────────────────────────────────────────────────
251        let mut routes = Vec::new();
252        let mut i = 0;
253        loop {
254            let sec = format!("route[{}]", i);
255            if !section_exists(&map, &sec) {
256                break;
257            }
258            let name = get_str(&map, &sec, "name");
259
260            // match
261            let m_sec = format!("{}.match", sec);
262            let match_ = MatchConfig {
263                host: {
264                    let h = get_str(&map, &m_sec, "host");
265                    if h.is_empty() { None } else { Some(h) }
266                },
267                path: {
268                    let p = get_str(&map, &m_sec, "path");
269                    if p.is_empty() { None } else { Some(p) }
270                },
271                method: {
272                    let m = get_str(&map, &m_sec, "method");
273                    if m.is_empty() { None } else { Some(m.to_uppercase()) }
274                },
275                content_type: {
276                    let c = get_str(&map, &m_sec, "content_type");
277                    if c.is_empty() { None } else { Some(c) }
278                },
279            };
280
281            // action
282            let a_sec = format!("{}.action", sec);
283            let action_type = get_str(&map, &a_sec, "type");
284            let action = match action_type.as_str() {
285                "proxy" => {
286                    let p_sec = format!("{}.action.proxy", sec);
287                    ActionConfig::Proxy {
288                        upstream: get_str(&map, &p_sec, "upstream"),
289                        connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
290                        read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
291                        strip_path_prefix: {
292                            let v = get_str(&map, &p_sec, "strip_path_prefix");
293                            if v.is_empty() { None } else { Some(v) }
294                        },
295                        add_path_prefix: {
296                            let v = get_str(&map, &p_sec, "add_path_prefix");
297                            if v.is_empty() { None } else { Some(v) }
298                        },
299                    }
300                }
301                "grpc" => {
302                    let p_sec = format!("{}.action.grpc", sec);
303                    ActionConfig::Grpc {
304                        upstream: get_str(&map, &p_sec, "upstream"),
305                        connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
306                        read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
307                    }
308                }
309                "static" => {
310                    let s_sec = format!("{}.action.static", sec);
311                    ActionConfig::Static {
312                        root: get_str(&map, &s_sec, "root"),
313                        index: get_array(&map, &s_sec, "index"),
314                    }
315                }
316                "redirect" => {
317                    let r_sec = format!("{}.action.redirect", sec);
318                    ActionConfig::Redirect {
319                        location: get_str(&map, &r_sec, "location"),
320                        status: get_u64(&map, &r_sec, "status", 301) as u16,
321                    }
322                }
323                "respond" => {
324                    let r_sec = format!("{}.action.respond", sec);
325                    ActionConfig::Respond {
326                        status: get_u64(&map, &r_sec, "status", 200) as u16,
327                        body: get_str(&map, &r_sec, "body"),
328                        content_type: {
329                            let ct = get_str(&map, &r_sec, "content_type");
330                            if ct.is_empty() { "text/plain".to_string() } else { ct }
331                        },
332                    }
333                }
334                "mcp" => ActionConfig::Mcp,
335                other => ActionConfig::Unknown(other.to_string()),
336            };
337
338            // middleware
339            let mw_sec = format!("{}.middleware", sec);
340            let middleware = parse_middleware_config(&map, &mw_sec, i);
341
342            routes.push(RouteConfig { name, match_, action, middleware });
343            i += 1;
344        }
345
346        // ── tcp_proxy ──────────────────────────────────────────────────────────
347        let mut tcp_proxies = Vec::new();
348        let mut i = 0;
349        loop {
350            let sec = format!("tcp_proxy[{}]", i);
351            if !section_exists(&map, &sec) {
352                break;
353            }
354            tcp_proxies.push(TcpProxyConfig {
355                name: get_str(&map, &sec, "name"),
356                listen: get_str(&map, &sec, "listen"),
357                backends: get_array(&map, &sec, "backends"),
358                connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
359            });
360            i += 1;
361        }
362
363        // ── udp_proxy ──────────────────────────────────────────────────────────
364        let mut udp_proxies = Vec::new();
365        let mut i = 0;
366        loop {
367            let sec = format!("udp_proxy[{}]", i);
368            if !section_exists(&map, &sec) {
369                break;
370            }
371            udp_proxies.push(UdpProxyConfig {
372                name: get_str(&map, &sec, "name"),
373                listen: get_str(&map, &sec, "listen"),
374                backends: get_array(&map, &sec, "backends"),
375                reply_timeout_ms: get_u64(&map, &sec, "reply_timeout_ms", 5000),
376                buffer_size: get_u64(&map, &sec, "buffer_size", 65536) as usize,
377            });
378            i += 1;
379        }
380
381        // ── ws_proxy ───────────────────────────────────────────────────────────
382        let mut ws_proxies = Vec::new();
383        let mut i = 0;
384        loop {
385            let sec = format!("ws_proxy[{}]", i);
386            if !section_exists(&map, &sec) {
387                break;
388            }
389            ws_proxies.push(WsProxyConfig {
390                name: get_str(&map, &sec, "name"),
391                listen: get_str(&map, &sec, "listen"),
392                backends: get_array(&map, &sec, "backends"),
393                connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
394                read_timeout_ms: get_u64(&map, &sec, "read_timeout_ms", 30000),
395            });
396            i += 1;
397        }
398
399        // ── global middleware ──────────────────────────────────────────────────
400        let global_middleware = parse_middleware_config(&map, "middleware", usize::MAX);
401
402        ProxyConfig {
403            upstreams,
404            routes,
405            tcp_proxies,
406            udp_proxies,
407            ws_proxies,
408            global_middleware,
409        }
410    }
411}
412
413/// Parse a `MiddlewareConfig` from the section map at a given base path.
414/// `route_idx` is used only to build inner-array section paths for rewrite rules.
415fn parse_middleware_config(
416    map: &parser::SectionMap,
417    mw_sec: &str,
418    route_idx: usize,
419) -> MiddlewareConfig {
420    use parser::{get_array, get_str, get_u32, get_u64, section_exists};
421
422    let rl_sec = format!("{}.rate_limit", mw_sec);
423    let rate_limit = if section_exists(map, &rl_sec) {
424        Some(RateLimitConfig {
425            max_requests: get_u32(map, &rl_sec, "max_requests", 1000),
426            window_secs: get_u64(map, &rl_sec, "window_secs", 60),
427        })
428    } else {
429        None
430    };
431
432    let c_sec = format!("{}.cache", mw_sec);
433    let cache = if section_exists(map, &c_sec) {
434        Some(CacheConfig {
435            ttl_secs: get_u64(map, &c_sec, "ttl_secs", 60),
436            vary_by: get_array(map, &c_sec, "vary_by"),
437        })
438    } else {
439        None
440    };
441
442    let a_sec = format!("{}.auth", mw_sec);
443    let auth = if section_exists(map, &a_sec) {
444        let auth_type = get_str(map, &a_sec, "type");
445        match auth_type.as_str() {
446            "bearer" => Some(AuthConfig::Bearer {
447                token_env: get_str(map, &a_sec, "token_env"),
448            }),
449            "jwt" => Some(AuthConfig::Jwt {
450                secret_env: get_str(map, &a_sec, "secret_env"),
451            }),
452            "basic" => Some(AuthConfig::Basic {
453                users_file: get_str(map, &a_sec, "users_file"),
454            }),
455            _ => None,
456        }
457    } else {
458        None
459    };
460
461    // Rewrite rules — the section paths use route_idx for route-scoped rules
462    // or a flat path for global middleware. We look for:
463    //   route[N].middleware.rewrite.request[0], [1], …
464    //   route[N].middleware.rewrite.response[0], [1], …
465    // For global: middleware.rewrite.request[0], etc.
466    let rewrite_request = collect_rewrite_rules(map, mw_sec, "request");
467    let rewrite_response = collect_rewrite_rules(map, mw_sec, "response");
468
469    let ip_sec = format!("{}.ip_filter", mw_sec);
470    let ip_allow = if section_exists(map, &ip_sec) {
471        get_array(map, &ip_sec, "allow")
472    } else {
473        vec![]
474    };
475    let ip_deny = if section_exists(map, &ip_sec) {
476        get_array(map, &ip_sec, "deny")
477    } else {
478        vec![]
479    };
480
481    let _ = route_idx; // used implicitly via mw_sec paths
482
483    MiddlewareConfig { rate_limit, cache, auth, rewrite_request, rewrite_response, ip_allow, ip_deny }
484}
485
486/// Collect `[[{mw_sec}.rewrite.{direction}]]` entries.
487fn collect_rewrite_rules(
488    map: &parser::SectionMap,
489    mw_sec: &str,
490    direction: &str,
491) -> Vec<RewriteRuleConfig> {
492    use parser::{get_str, get_u64};
493
494    let mut rules = Vec::new();
495    let mut j = 0;
496    loop {
497        let rsec = format!("{}.rewrite.{}[{}]", mw_sec, direction, j);
498        if !parser::section_exists(map, &rsec) {
499            break;
500        }
501        let code_val = get_u64(map, &rsec, "code", 0);
502        rules.push(RewriteRuleConfig {
503            type_: get_str(map, &rsec, "type"),
504            name: {
505                let v = get_str(map, &rsec, "name");
506                if v.is_empty() { None } else { Some(v) }
507            },
508            value: {
509                let v = get_str(map, &rsec, "value");
510                if v.is_empty() { None } else { Some(v) }
511            },
512            prefix: {
513                let v = get_str(map, &rsec, "prefix");
514                if v.is_empty() { None } else { Some(v) }
515            },
516            from: {
517                let v = get_str(map, &rsec, "from");
518                if v.is_empty() { None } else { Some(v) }
519            },
520            to: {
521                let v = get_str(map, &rsec, "to");
522                if v.is_empty() { None } else { Some(v) }
523            },
524            code: if code_val == 0 { None } else { Some(code_val as u16) },
525            reason: {
526                let v = get_str(map, &rsec, "reason");
527                if v.is_empty() { None } else { Some(v) }
528            },
529        });
530        j += 1;
531    }
532    rules
533}
534
535fn config_file_path() -> String {
536    std::env::var("RWS_CONFIG_FILE").unwrap_or_else(|_| "rws.config.toml".to_string())
537}
538
539// ── ConfigDrivenApp ────────────────────────────────────────────────────────────
540
541/// A compiled route: a matcher paired with a handler application.
542pub(crate) struct CompiledRoute {
543    pub(crate) matcher: RouteMatcher,
544    /// Shared, type-erased handler. `Arc` makes `Clone` cheap (pointer copy).
545    pub(crate) handler: Arc<dyn Application + Send + Sync>,
546}
547
548/// Matching criteria for a single route.
549#[derive(Clone, Default)]
550pub(crate) struct RouteMatcher {
551    /// Optional SNI hostname / `Host` header match.
552    pub(crate) host: Option<String>,
553    /// Path prefix to match (derived from `path = "/v1/*"`).
554    pub(crate) path_prefix: Option<String>,
555    /// Exact path to match (derived from `path = "/v1/ping"`).
556    pub(crate) path_exact: Option<String>,
557    /// Uppercase HTTP method, or `None` for any.
558    pub(crate) method: Option<String>,
559    /// `Content-Type` prefix (e.g. `"application/grpc"`).
560    pub(crate) content_type_prefix: Option<String>,
561}
562
563impl RouteMatcher {
564    pub(crate) fn from_match_config(cfg: &MatchConfig) -> Self {
565        let (path_prefix, path_exact) = match &cfg.path {
566            Some(p) if p.ends_with('*') => {
567                // "/v1/*" → prefix "/v1/"
568                let stripped = p.trim_end_matches('*').to_string();
569                (Some(stripped), None)
570            }
571            Some(p) => (None, Some(p.clone())),
572            None => (None, None),
573        };
574        let content_type_prefix = cfg.content_type.as_ref().map(|ct| {
575            if ct.ends_with('*') {
576                ct.trim_end_matches('*').to_string()
577            } else {
578                ct.clone()
579            }
580        });
581        RouteMatcher {
582            host: cfg.host.clone(),
583            path_prefix,
584            path_exact,
585            method: cfg.method.clone(),
586            content_type_prefix,
587        }
588    }
589
590    /// Returns `true` if `request` and `conn` match all configured criteria.
591    pub(crate) fn matches(&self, request: &Request, conn: &ConnectionInfo) -> bool {
592        // Host matching: SNI first, then Host header
593        if let Some(ref expected_host) = self.host {
594            let actual_host = conn
595                .sni_hostname
596                .as_deref()
597                .or_else(|| {
598                    request
599                        .headers
600                        .iter()
601                        .find(|h| h.name.eq_ignore_ascii_case("host"))
602                        .map(|h| h.value.as_str())
603                })
604                .unwrap_or("");
605            if actual_host != expected_host.as_str() {
606                return false;
607            }
608        }
609
610        // Method matching
611        if let Some(ref m) = self.method {
612            if request.method.to_uppercase() != m.as_str() {
613                return false;
614            }
615        }
616
617        // Path matching: strip query string for comparison
618        let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
619        if let Some(ref prefix) = self.path_prefix {
620            if !path.starts_with(prefix.as_str()) {
621                return false;
622            }
623        } else if let Some(ref exact) = self.path_exact {
624            if path != exact.as_str() {
625                return false;
626            }
627        }
628
629        // Content-Type prefix matching
630        if let Some(ref ct_prefix) = self.content_type_prefix {
631            let actual_ct = request
632                .headers
633                .iter()
634                .find(|h| h.name.eq_ignore_ascii_case("content-type"))
635                .map(|h| h.value.as_str())
636                .unwrap_or("");
637            if !actual_ct.starts_with(ct_prefix.as_str()) {
638                return false;
639            }
640        }
641
642        true
643    }
644}
645
646/// An `Application` that routes requests based on a parsed `ProxyConfig`.
647///
648/// `Clone` is cheap: `routes` is an `Arc<Vec<...>>` (pointer copy), and
649/// `fallback` is `App` which is `Copy`.
650#[derive(Clone)]
651pub struct ConfigDrivenApp {
652    routes: Arc<Vec<CompiledRoute>>,
653    /// Fallback for unmatched requests — handles /healthz, /readyz, /metrics,
654    /// static files, and the 404 controller.
655    fallback: App,
656}
657
658impl ConfigDrivenApp {
659    pub(crate) fn new(routes: Vec<CompiledRoute>) -> Self {
660        use crate::core::New;
661        ConfigDrivenApp {
662            routes: Arc::new(routes),
663            fallback: App::new(),
664        }
665    }
666}
667
668impl Application for ConfigDrivenApp {
669    fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
670        for route in self.routes.iter() {
671            if route.matcher.matches(request, conn) {
672                return route.handler.execute(request, conn);
673            }
674        }
675        self.fallback.execute(request, conn)
676    }
677}
678
679// ── NullApp ────────────────────────────────────────────────────────────────────
680
681/// A dead-end `Application` that always returns 404.
682/// Used as the `next` parameter when calling `Middleware::handle` directly.
683#[derive(Clone, Copy)]
684pub(crate) struct NullApp;
685
686impl Application for NullApp {
687    fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
688        let mut r = Response::new();
689        r.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
690        r.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
691        Ok(r)
692    }
693}
694
695// ── DynamicProxy ──────────────────────────────────────────────────────────────
696
697use std::sync::atomic::{AtomicUsize, Ordering};
698use std::sync::RwLock;
699use std::time::Duration;
700
701/// A proxy adapter that reads its backend list from a shared, health-checker-
702/// maintained live list at request time. Supports dynamic removal/restoration
703/// of backends without restarting.
704///
705/// This type is `Clone + Send + Sync` and implements `Application`.
706#[derive(Clone)]
707pub(crate) struct DynamicProxy {
708    live: Arc<RwLock<Vec<String>>>,
709    counter: Arc<AtomicUsize>,
710    connect_timeout: Duration,
711    read_timeout: Duration,
712    strip_prefix: Option<Arc<String>>,
713    add_prefix: Option<Arc<String>>,
714}
715
716impl DynamicProxy {
717    pub(crate) fn new(
718        live: Arc<RwLock<Vec<String>>>,
719        connect_timeout_ms: u64,
720        read_timeout_ms: u64,
721        strip_prefix: Option<String>,
722        add_prefix: Option<String>,
723    ) -> Self {
724        DynamicProxy {
725            live,
726            counter: Arc::new(AtomicUsize::new(0)),
727            connect_timeout: Duration::from_millis(connect_timeout_ms),
728            read_timeout: Duration::from_millis(read_timeout_ms),
729            strip_prefix: strip_prefix.map(Arc::new),
730            add_prefix: add_prefix.map(Arc::new),
731        }
732    }
733
734    fn next_backend(&self) -> Option<String> {
735        let live = self.live.read().unwrap();
736        if live.is_empty() {
737            return None;
738        }
739        let idx = self.counter.fetch_add(1, Ordering::Relaxed) % live.len();
740        Some(live[idx].clone())
741    }
742}
743
744impl Application for DynamicProxy {
745    fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
746        let backend = match self.next_backend() {
747            Some(b) => b,
748            None => {
749                return Ok(bad_gateway());
750            }
751        };
752
753        let (host, port) = match crate::proxy_config::health::parse_host_port(&backend) {
754            Some(hp) => hp,
755            None => return Ok(bad_gateway()),
756        };
757
758        // Apply path rewriting if configured
759        let mut req_clone;
760        let effective_request = if self.strip_prefix.is_some() || self.add_prefix.is_some() {
761            req_clone = request.clone();
762            if let Some(ref sp) = self.strip_prefix {
763                if let Some(stripped) = req_clone.request_uri.strip_prefix(sp.as_str()) {
764                    req_clone.request_uri = if stripped.is_empty() || !stripped.starts_with('/') {
765                        format!("/{}", stripped)
766                    } else {
767                        stripped.to_string()
768                    };
769                }
770            }
771            if let Some(ref ap) = self.add_prefix {
772                req_clone.request_uri = format!("{}{}", ap, req_clone.request_uri);
773            }
774            &req_clone
775        } else {
776            request
777        };
778
779        match crate::proxy::proxy_http1(
780            effective_request,
781            &conn.client.ip,
782            &host,
783            port,
784            self.connect_timeout,
785            self.read_timeout,
786        ) {
787            Ok(r) => Ok(r),
788            Err(_) => Ok(bad_gateway()),
789        }
790    }
791}
792
793fn bad_gateway() -> Response {
794    use crate::mime_type::MimeType;
795    use crate::range::Range;
796    let cr = Range::get_content_range(
797        b"502 Bad Gateway".to_vec(),
798        MimeType::TEXT_PLAIN.to_string(),
799    );
800    let mut r = Response::new();
801    r.status_code = *STATUS_CODE_REASON_PHRASE.n502_bad_gateway.status_code;
802    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n502_bad_gateway.reason_phrase.to_string();
803    r.content_range_list = vec![cr];
804    r
805}
806
807// ── RedirectAdapter ────────────────────────────────────────────────────────────
808
809/// Action adapter that issues HTTP redirects.
810///
811/// `$path` in `location_template` is replaced with the request URI at runtime.
812#[derive(Clone)]
813pub(crate) struct RedirectAdapter {
814    location_template: Arc<String>,
815    status: i16,
816    reason: Arc<String>,
817}
818
819impl RedirectAdapter {
820    pub(crate) fn new(location: String, status: u16) -> Self {
821        let (code, reason) = redirect_status(status);
822        RedirectAdapter {
823            location_template: Arc::new(location),
824            status: code,
825            reason: Arc::new(reason),
826        }
827    }
828}
829
830fn redirect_status(code: u16) -> (i16, String) {
831    let phrase = match code {
832        301 => STATUS_CODE_REASON_PHRASE.n301_moved_permanently.reason_phrase,
833        302 => STATUS_CODE_REASON_PHRASE.n302_found.reason_phrase,
834        307 => STATUS_CODE_REASON_PHRASE.n307_temporary_redirect.reason_phrase,
835        308 => STATUS_CODE_REASON_PHRASE.n308_permanent_redirect.reason_phrase,
836        _ => "Redirect",
837    };
838    (code as i16, phrase.to_string())
839}
840
841impl Application for RedirectAdapter {
842    fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
843        let location = self
844            .location_template
845            .replace("$path", &request.request_uri);
846        use crate::header::Header;
847        let mut r = Response::new();
848        r.status_code = self.status;
849        r.reason_phrase = self.reason.as_ref().clone();
850        r.headers.push(Header { name: "Location".to_string(), value: location });
851        Ok(r)
852    }
853}
854
855// ── RespondAdapter ─────────────────────────────────────────────────────────────
856
857/// Action adapter that returns a fixed response body.
858#[derive(Clone)]
859pub(crate) struct RespondAdapter {
860    status: i16,
861    reason: Arc<String>,
862    body: Arc<Vec<u8>>,
863    content_type: Arc<String>,
864}
865
866impl RespondAdapter {
867    pub(crate) fn new(status: u16, body: String, content_type: String) -> Self {
868        use crate::response::STATUS_CODE_REASON_PHRASE;
869        let reason = match status {
870            200 => STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string(),
871            201 => STATUS_CODE_REASON_PHRASE.n201_created.reason_phrase.to_string(),
872            204 => STATUS_CODE_REASON_PHRASE.n204_no_content.reason_phrase.to_string(),
873            400 => STATUS_CODE_REASON_PHRASE.n400_bad_request.reason_phrase.to_string(),
874            401 => STATUS_CODE_REASON_PHRASE.n401_unauthorized.reason_phrase.to_string(),
875            403 => STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string(),
876            404 => STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string(),
877            500 => STATUS_CODE_REASON_PHRASE.n500_internal_server_error.reason_phrase.to_string(),
878            _ => "OK".to_string(),
879        };
880        RespondAdapter {
881            status: status as i16,
882            reason: Arc::new(reason),
883            body: Arc::new(body.into_bytes()),
884            content_type: Arc::new(content_type),
885        }
886    }
887}
888
889impl Application for RespondAdapter {
890    fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
891        use crate::range::Range;
892        let mut r = Response::new();
893        r.status_code = self.status;
894        r.reason_phrase = self.reason.as_ref().clone();
895        if !self.body.is_empty() {
896            r.content_range_list = vec![Range::get_content_range(
897                self.body.as_ref().clone(),
898                self.content_type.as_ref().clone(),
899            )];
900        }
901        Ok(r)
902    }
903}
904
905// ── PerRouteRateLimit middleware ───────────────────────────────────────────────
906
907/// A per-route rate limiter middleware backed by a shared `RateLimiter`.
908pub(crate) struct PerRouteRateLimit(pub(crate) Arc<crate::rate_limit::RateLimiter>);
909
910impl crate::middleware::Middleware for PerRouteRateLimit {
911    fn handle(
912        &self,
913        request: &Request,
914        conn: &ConnectionInfo,
915        next: &dyn Application,
916    ) -> Result<Response, String> {
917        use crate::error::{AppError, IntoResponse};
918        if self.0.check(&conn.client.ip) {
919            next.execute(request, conn)
920        } else {
921            Ok(AppError::TooManyRequests.into_response())
922        }
923    }
924}
925
926// ── BearerAuthMiddleware ───────────────────────────────────────────────────────
927
928/// Bearer token authentication middleware.
929pub(crate) struct BearerAuthMiddleware {
930    pub(crate) token: Arc<String>,
931}
932
933impl crate::middleware::Middleware for BearerAuthMiddleware {
934    fn handle(
935        &self,
936        request: &Request,
937        conn: &ConnectionInfo,
938        next: &dyn Application,
939    ) -> Result<Response, String> {
940        use crate::error::{AppError, IntoResponse};
941        let expected = format!("Bearer {}", self.token);
942        let authorized = request
943            .headers
944            .iter()
945            .any(|h| h.name.eq_ignore_ascii_case("authorization") && h.value == expected);
946        if authorized {
947            next.execute(request, conn)
948        } else {
949            Ok(AppError::Unauthorized.into_response())
950        }
951    }
952}
953
954// ── arc_app helper ─────────────────────────────────────────────────────────────
955
956/// Box any `Application + Send + Sync + 'static` into an `Arc<dyn …>`.
957pub(crate) fn arc_app<A: Application + Send + Sync + 'static>(
958    a: A,
959) -> Arc<dyn Application + Send + Sync> {
960    Arc::new(a)
961}
962
963// ── Public entry points ────────────────────────────────────────────────────────
964
965/// Build a `ConfigDrivenApp` from `rws.config.toml` and spawn L4/WS proxy
966/// threads. Returns the app and a list of thread handles.
967pub fn build_from_file() -> (ConfigDrivenApp, Vec<std::thread::JoinHandle<()>>) {
968    builder::build_from_file()
969}