Skip to main content

rust_web_server/proxy_config/
builder.rs

1//! Builder: converts a `ProxyConfig` into a live `ConfigDrivenApp` with
2//! health checkers and L4/WS proxy threads.
3
4use std::collections::HashMap;
5use std::sync::{Arc, RwLock};
6
7use crate::middleware::WithMiddleware;
8use crate::proxy_config::{
9    ActionConfig, AuthConfig, CompiledRoute, ConfigDrivenApp, DynamicProxy, MiddlewareConfig,
10    ProxyConfig, RedirectAdapter, RespondAdapter, RouteMatcher, StaticAdapter, arc_app,
11    BearerAuthMiddleware, PerRouteMaxBodySize, PerRouteRateLimit,
12};
13
14/// Build a `ConfigDrivenApp` from `rws.config.toml` and spawn L4/WS proxy
15/// threads. Returns the app and a list of background thread handles.
16pub fn build_from_file() -> (ConfigDrivenApp, Vec<std::thread::JoinHandle<()>>) {
17    let config = ProxyConfig::load();
18    build(config)
19}
20
21/// Build a `ConfigDrivenApp` from a `ProxyConfig` struct.
22pub fn build(config: ProxyConfig) -> (ConfigDrivenApp, Vec<std::thread::JoinHandle<()>>) {
23    // ── 1. Build upstream name → live backends map ─────────────────────────
24    let mut upstream_lives: HashMap<String, Arc<RwLock<Vec<String>>>> = HashMap::new();
25
26    for upstream in &config.upstreams {
27        let live = Arc::new(RwLock::new(upstream.backends.clone()));
28        upstream_lives.insert(upstream.name.clone(), Arc::clone(&live));
29
30        if let Some(ref hc) = upstream.health_check {
31            crate::proxy_config::health::start_health_checker(
32                upstream.name.clone(),
33                upstream.backends.clone(),
34                Arc::clone(&live),
35                hc.clone(),
36            );
37        }
38    }
39
40    // ── 2. Build compiled routes ───────────────────────────────────────────
41    let mut compiled: Vec<CompiledRoute> = Vec::new();
42
43    for route in &config.routes {
44        let matcher = RouteMatcher::from_match_config(&route.match_);
45
46        // Build the action handler (base application)
47        let base_handler: Arc<dyn crate::application::Application + Send + Sync> =
48            match &route.action {
49                ActionConfig::Proxy {
50                    upstream,
51                    connect_timeout_ms,
52                    read_timeout_ms,
53                    strip_path_prefix,
54                    add_path_prefix,
55                } => {
56                    let live = upstream_lives
57                        .get(upstream.as_str())
58                        .cloned()
59                        .unwrap_or_else(|| {
60                            // Fallback: upstream not declared, use name as single backend
61                            Arc::new(RwLock::new(vec![upstream.clone()]))
62                        });
63                    let upstream_cfg = config.upstreams.iter().find(|u| u.name == *upstream);
64                    let upstream_tls = upstream_cfg.map(|u| u.tls).unwrap_or(false);
65                    let upstream_strategy = upstream_cfg
66                        .map(|u| u.strategy.clone())
67                        .unwrap_or_else(|| "round_robin".to_string());
68                    arc_app(DynamicProxy::new(
69                        live,
70                        *connect_timeout_ms,
71                        *read_timeout_ms,
72                        strip_path_prefix.clone(),
73                        add_path_prefix.clone(),
74                        upstream_tls,
75                        upstream_strategy,
76                    ))
77                }
78
79                ActionConfig::Redirect { location, status } => {
80                    arc_app(RedirectAdapter::new(location.clone(), *status))
81                }
82
83                ActionConfig::Respond { status, body, content_type } => {
84                    arc_app(RespondAdapter::new(*status, body.clone(), content_type.clone()))
85                }
86
87                ActionConfig::Static { root, index } => {
88                    arc_app(StaticAdapter::new(root.clone(), index.clone()))
89                }
90
91                // Grpc action — use DynamicProxy (it forwards over HTTP/1.1;
92                // a future version could plug in H2ReverseProxy here)
93                ActionConfig::Grpc { upstream, connect_timeout_ms, read_timeout_ms } => {
94                    let live = upstream_lives
95                        .get(upstream.as_str())
96                        .cloned()
97                        .unwrap_or_else(|| Arc::new(RwLock::new(vec![upstream.clone()])));
98                    let upstream_cfg = config.upstreams.iter().find(|u| u.name == *upstream);
99                    let upstream_tls = upstream_cfg.map(|u| u.tls).unwrap_or(false);
100                    let upstream_strategy = upstream_cfg
101                        .map(|u| u.strategy.clone())
102                        .unwrap_or_else(|| "round_robin".to_string());
103                    arc_app(DynamicProxy::new(live, *connect_timeout_ms, *read_timeout_ms, None, None, upstream_tls, upstream_strategy))
104                }
105
106                ActionConfig::Mcp | ActionConfig::Unknown(_) => {
107                    // No-op: these fall through to fallback
108                    arc_app(crate::proxy_config::NullApp)
109                }
110            };
111
112        // Wrap the base handler with middleware layers
113        let handler = apply_middleware(base_handler, &route.middleware);
114
115        compiled.push(CompiledRoute { matcher, handler });
116    }
117
118    // ── 3. Spawn L4/WS proxy threads ──────────────────────────────────────
119    let mut handles: Vec<std::thread::JoinHandle<()>> = Vec::new();
120
121    for tcp_cfg in &config.tcp_proxies {
122        let listen = tcp_cfg.listen.clone();
123        let backends = tcp_cfg.backends.clone();
124        let timeout_ms = tcp_cfg.connect_timeout_ms;
125        let name = tcp_cfg.name.clone();
126        let h = std::thread::Builder::new()
127            .name(format!("tcp-proxy-{}", name))
128            .spawn(move || {
129                let proxy = crate::tcp_proxy::TcpProxy::new(backends)
130                    .connect_timeout_ms(timeout_ms);
131                if let Err(e) = proxy.bind(&listen) {
132                    eprintln!("[tcp_proxy:{}] {}", name, e);
133                }
134            })
135            .expect("failed to spawn tcp proxy thread");
136        handles.push(h);
137    }
138
139    for udp_cfg in &config.udp_proxies {
140        let listen = udp_cfg.listen.clone();
141        let backends = udp_cfg.backends.clone();
142        let reply_timeout_ms = udp_cfg.reply_timeout_ms;
143        let buffer_size = udp_cfg.buffer_size;
144        let name = udp_cfg.name.clone();
145        let h = std::thread::Builder::new()
146            .name(format!("udp-proxy-{}", name))
147            .spawn(move || {
148                let proxy = crate::udp_proxy::UdpProxy::new(backends)
149                    .reply_timeout_ms(reply_timeout_ms)
150                    .buffer_size(buffer_size);
151                if let Err(e) = proxy.bind(&listen) {
152                    eprintln!("[udp_proxy:{}] {}", name, e);
153                }
154            })
155            .expect("failed to spawn udp proxy thread");
156        handles.push(h);
157    }
158
159    for ws_cfg in &config.ws_proxies {
160        let listen = ws_cfg.listen.clone();
161        let backends = ws_cfg.backends.clone();
162        let connect_timeout_ms = ws_cfg.connect_timeout_ms;
163        let read_timeout_ms = ws_cfg.read_timeout_ms;
164        let name = ws_cfg.name.clone();
165
166        // With a health check configured, share a live-backend list with a
167        // background checker thread (same mechanism `[[upstream]]` pools
168        // use) instead of treating every configured backend as always live.
169        let live = Arc::new(RwLock::new(backends.clone()));
170        if let Some(ref hc) = ws_cfg.health_check {
171            crate::proxy_config::health::start_health_checker(
172                name.clone(),
173                backends.clone(),
174                Arc::clone(&live),
175                hc.clone(),
176            );
177        }
178
179        let h = std::thread::Builder::new()
180            .name(format!("ws-proxy-{}", name))
181            .spawn(move || {
182                let proxy = crate::ws_proxy::WsProxy::with_live_backends(backends, live)
183                    .connect_timeout_ms(connect_timeout_ms)
184                    .read_timeout_ms(read_timeout_ms);
185                if let Err(e) = proxy.bind(&listen) {
186                    eprintln!("[ws_proxy:{}] {}", name, e);
187                }
188            })
189            .expect("failed to spawn ws proxy thread");
190        handles.push(h);
191    }
192
193    (ConfigDrivenApp::new(compiled), handles)
194}
195
196/// Wrap `handler` with the middleware layers described by `mw`, innermost
197/// (closest to the handler) to outermost (runs first, on every request):
198///
199///   1. Cache (innermost)
200///   2. Rewrite (request + response rules combined)
201///   3. Auth
202///   4. Rate limit
203///   5. IP filter
204///   6. Timeout — bounds this route's total time, including every layer above
205///   7. Max body size (outermost) — a fast, non-blocking length check, so a
206///      request this route will reject outright doesn't pay for a timeout
207///      wrapper (which runs its target on a background thread) or engage
208///      any other layer at all
209fn apply_middleware(
210    handler: Arc<dyn crate::application::Application + Send + Sync>,
211    mw: &MiddlewareConfig,
212) -> Arc<dyn crate::application::Application + Send + Sync> {
213    // Wrap the Arc<dyn Application> in an ArcApp adapter so it implements
214    // Application itself (needed for WithMiddleware::new).
215    let mut app: Arc<dyn crate::application::Application + Send + Sync> = handler;
216
217    // ── Cache (applied first so it's the innermost middleware) ─────────────
218    if let Some(ref cache_cfg) = mw.cache {
219        let mut layer = crate::cache::CacheLayer::memory(1000).ttl(cache_cfg.ttl_secs);
220        for vh in &cache_cfg.vary_by {
221            layer = layer.vary_by_header(vh.as_str());
222        }
223        app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(layer));
224    }
225
226    // ── Rewrite ────────────────────────────────────────────────────────────
227    if !mw.rewrite_request.is_empty() || !mw.rewrite_response.is_empty() {
228        let mut layer = crate::rewrite::RewriteLayer::new();
229        for rule in &mw.rewrite_request {
230            layer = apply_request_rewrite_rule(layer, rule);
231        }
232        for rule in &mw.rewrite_response {
233            layer = apply_response_rewrite_rule(layer, rule);
234        }
235        app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(layer));
236    }
237
238    // ── Auth ───────────────────────────────────────────────────────────────
239    if let Some(ref auth_cfg) = mw.auth {
240        match auth_cfg {
241            AuthConfig::Bearer { token_env } => {
242                let token = std::env::var(token_env).unwrap_or_default();
243                if !token.is_empty() {
244                    app = arc_app(
245                        WithMiddleware::new(ArcApp(Arc::clone(&app)))
246                            .wrap(BearerAuthMiddleware { token: Arc::new(token) }),
247                    );
248                }
249            }
250            #[cfg(feature = "auth")]
251            AuthConfig::Jwt { secret_env } => {
252                let secret = std::env::var(secret_env).unwrap_or_default();
253                if !secret.is_empty() {
254                    app = arc_app(
255                        WithMiddleware::new(ArcApp(Arc::clone(&app)))
256                            .wrap(crate::auth::JwtLayer::new(secret)),
257                    );
258                } else {
259                    eprintln!(
260                        "[proxy_config] JWT auth: env var '{}' is unset or empty; skipping JwtLayer for this route.",
261                        secret_env
262                    );
263                }
264            }
265            #[cfg(not(feature = "auth"))]
266            AuthConfig::Jwt { .. } => {
267                eprintln!("[proxy_config] JWT auth requires the 'auth' feature; skipping.");
268            }
269            #[cfg(feature = "auth")]
270            AuthConfig::Basic { htpasswd_file } => {
271                match crate::auth::BasicAuthLayer::from_htpasswd_file(htpasswd_file) {
272                    Ok(layer) => {
273                        app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(layer));
274                    }
275                    Err(e) => {
276                        eprintln!("[proxy_config] Basic auth: {e}; skipping Basic auth for this route.");
277                    }
278                }
279            }
280            #[cfg(not(feature = "auth"))]
281            AuthConfig::Basic { .. } => {
282                eprintln!("[proxy_config] Basic auth requires the 'auth' feature; skipping.");
283            }
284        }
285    }
286
287    // ── Rate limit ─────────────────────────────────────────────────────────
288    if let Some(ref rl_cfg) = mw.rate_limit {
289        let limiter = Arc::new(crate::rate_limit::RateLimiter::new(
290            rl_cfg.max_requests,
291            rl_cfg.window_secs,
292        ));
293        app = arc_app(
294            WithMiddleware::new(ArcApp(Arc::clone(&app)))
295                .wrap(PerRouteRateLimit(limiter)),
296        );
297    }
298
299    // ── IP filter ────────────────────────────────────────────────────────────
300    if !mw.ip_allow.is_empty() {
301        let filter = crate::ip_filter::IpFilter::allow(mw.ip_allow.iter().map(|s| s.as_str()));
302        app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(filter));
303    } else if !mw.ip_deny.is_empty() {
304        let filter = crate::ip_filter::IpFilter::deny(mw.ip_deny.iter().map(|s| s.as_str()));
305        app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(filter));
306    }
307
308    // ── Timeout (bounds this route's total time, including every other
309    // middleware above) ─────────────────────────────────────────────────────
310    if let Some(timeout_ms) = mw.timeout_ms {
311        app = arc_app(crate::timeout::TimeoutLayer::from_arc(
312            app,
313            std::time::Duration::from_millis(timeout_ms),
314        ));
315    }
316
317    // ── Max body size (outermost — see PerRouteMaxBodySize's docs for why
318    // this is not a substitute for RWS_CONFIG_MAX_BODY_SIZE_IN_BYTES) ──────
319    if let Some(max_body_size) = mw.max_body_size {
320        app = arc_app(
321            WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(PerRouteMaxBodySize(max_body_size)),
322        );
323    }
324
325    app
326}
327
328fn apply_request_rewrite_rule(
329    layer: crate::rewrite::RewriteLayer,
330    rule: &crate::proxy_config::RewriteRuleConfig,
331) -> crate::rewrite::RewriteLayer {
332    match rule.type_.as_str() {
333        "header_set" => {
334            if let (Some(name), Some(value)) = (&rule.name, &rule.value) {
335                return layer.request_header_set(name, value);
336            }
337        }
338        "header_remove" => {
339            if let Some(name) = &rule.name {
340                return layer.request_header_remove(name);
341            }
342        }
343        "uri_set" => {
344            if let Some(value) = &rule.value {
345                return layer.request_uri_set(value);
346            }
347        }
348        "uri_strip_prefix" | "strip_prefix" => {
349            if let Some(prefix) = rule.prefix.as_ref().or(rule.value.as_ref()) {
350                return layer.request_uri_strip_prefix(prefix);
351            }
352        }
353        "uri_add_prefix" | "add_prefix" => {
354            if let Some(prefix) = rule.prefix.as_ref().or(rule.value.as_ref()) {
355                return layer.request_uri_add_prefix(prefix);
356            }
357        }
358        _ => {}
359    }
360    layer
361}
362
363fn apply_response_rewrite_rule(
364    layer: crate::rewrite::RewriteLayer,
365    rule: &crate::proxy_config::RewriteRuleConfig,
366) -> crate::rewrite::RewriteLayer {
367    match rule.type_.as_str() {
368        "header_set" => {
369            if let (Some(name), Some(value)) = (&rule.name, &rule.value) {
370                return layer.response_header_set(name, value);
371            }
372        }
373        "header_remove" => {
374            if let Some(name) = &rule.name {
375                return layer.response_header_remove(name);
376            }
377        }
378        "status" => {
379            if let (Some(code), Some(reason)) = (&rule.code, &rule.reason) {
380                return layer.response_status(*code as i16, reason);
381            }
382        }
383        "body_replace" => {
384            if let (Some(from), Some(to)) = (&rule.from, &rule.to) {
385                return layer.response_body_replace(from, to);
386            }
387        }
388        _ => {}
389    }
390    layer
391}
392
393// ── ArcApp adapter ─────────────────────────────────────────────────────────────
394
395/// Wraps `Arc<dyn Application + Send + Sync>` to implement `Application`
396/// (needed because you can't implement a foreign trait on a foreign type).
397struct ArcApp(Arc<dyn crate::application::Application + Send + Sync>);
398
399impl crate::application::Application for ArcApp {
400    fn execute(
401        &self,
402        request: &crate::request::Request,
403        conn: &crate::server::ConnectionInfo,
404    ) -> Result<crate::response::Response, String> {
405        self.0.execute(request, conn)
406    }
407}
408
409impl Clone for ArcApp {
410    fn clone(&self) -> Self {
411        ArcApp(Arc::clone(&self.0))
412    }
413}