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