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