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