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