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    // ── 0. The [authz] authorizer, for routes' authz middleware ─────────────
55    let authorizer = config_authorizer(config);
56    if let Some(Err(e)) = &authorizer {
57        eprintln!("[proxy_config] ERROR: [authz]: {e} — routes with authz answer 503 until it's fixed.");
58    }
59    crate::authz::config::set_current(authorizer);
60    // ── 1. Build upstream name → live backends map ─────────────────────────
61    let mut upstream_lives: HashMap<String, Arc<RwLock<Vec<String>>>> = HashMap::new();
62    // tls_ca_file trust settings per upstream, loaded once per compile (so a
63    // reload picks up a rotated CA). A CA file that can't be loaded fails
64    // closed: every handshake fails (502) instead of trusting public CAs.
65    let mut upstream_trust: HashMap<String, Arc<crate::proxy::UpstreamTls>> = HashMap::new();
66    // Weights, stickiness, circuit breaker, retries: one state per
67    // upstream, shared by its routes.
68    let mut upstream_policies: HashMap<String, Arc<crate::proxy_config::upstream::UpstreamPolicy>> = HashMap::new();
69
70    for upstream in &config.upstreams {
71        let live = Arc::new(RwLock::new(upstream.backends.clone()));
72        upstream_lives.insert(upstream.name.clone(), Arc::clone(&live));
73        upstream_policies.insert(
74            upstream.name.clone(),
75            Arc::new(crate::proxy_config::upstream::UpstreamPolicy::new(
76                &upstream.name,
77                Arc::clone(&live),
78                &upstream.backends,
79                &upstream.weights,
80                upstream.sticky.clone(),
81                upstream.circuit_breaker.as_ref(),
82                upstream.retry.clone(),
83            )),
84        );
85
86        let client = upstream.tls_client_cert_file.as_deref().zip(upstream.tls_client_key_file.as_deref());
87        let configured = upstream.tls_ca_file.is_some() || upstream.tls_client_cert_file.is_some() || upstream.tls_client_key_file.is_some();
88        let trust = configured.then(|| {
89            Arc::new(crate::proxy::UpstreamTls::new(upstream.tls_ca_file.as_deref(), client).unwrap_or_else(|e| {
90                eprintln!("[proxy_config] ERROR: upstream '{}': {e} — its TLS backends answer 502 until this is fixed", upstream.name);
91                crate::proxy::UpstreamTls::reject_all(&upstream.name)
92            }))
93        });
94        if let Some(trust) = &trust {
95            upstream_trust.insert(upstream.name.clone(), Arc::clone(trust));
96        }
97
98        if let Some(ref hc) = upstream.health_check {
99            crate::proxy_config::health::start_health_checker(
100                upstream.name.clone(),
101                upstream.backends.clone(),
102                Arc::clone(&live),
103                hc.clone(),
104                Arc::clone(health_stop),
105                trust,
106            );
107        }
108    }
109
110    crate::proxy_config::upstream::register(upstream_policies.values().cloned().collect());
111
112    // ── 2. Build compiled routes ───────────────────────────────────────────
113    let mut caches: Vec<(String, crate::cache::CacheLayer)> = Vec::new();
114    let mut compiled: Vec<CompiledRoute> = Vec::new();
115
116    for route in &config.routes {
117        let matcher = RouteMatcher::from_match_config(&route.match_);
118
119        let mut async_proxy: Option<Arc<DynamicProxy>> = None;
120        let cache_name = format!("route:{}", if route.name.is_empty() { format!("#{}", compiled.len() + 1) } else { route.name.clone() });
121        let mirror = route.middleware.mirror.as_ref().map(|m| build_mirror(&route.name, m, &upstream_lives));
122
123        // Build the action handler (base application)
124        let base_handler: Arc<dyn crate::application::Application + Send + Sync> =
125            match &route.action {
126                ActionConfig::Proxy {
127                    upstream,
128                    connect_timeout_ms,
129                    read_timeout_ms,
130                    strip_path_prefix,
131                    add_path_prefix,
132                    upstream_host,
133                } => {
134                    let live = upstream_lives
135                        .get(upstream.as_str())
136                        .cloned()
137                        .unwrap_or_else(|| {
138                            // Fallback: upstream not declared, use name as single backend
139                            Arc::new(RwLock::new(vec![upstream.clone()]))
140                        });
141                    let upstream_cfg = config.upstreams.iter().find(|u| u.name == *upstream);
142                    let upstream_tls = upstream_cfg.map_or_else(|| upstream.starts_with("https://"), |u| u.tls);
143                    let upstream_strategy = upstream_cfg
144                        .map(|u| u.strategy.clone())
145                        .unwrap_or_else(|| "round_robin".to_string());
146                    let proxy = Arc::new(DynamicProxy::new(
147                        live,
148                        *connect_timeout_ms,
149                        *read_timeout_ms,
150                        strip_path_prefix.clone(),
151                        add_path_prefix.clone(),
152                        upstream_tls,
153                        upstream_strategy,
154                    )
155                    .with_trust(upstream_trust.get(upstream.as_str()).cloned())
156                    .with_policy(upstream, upstream_policies.get(upstream.as_str()).cloned())
157                    .with_mirror(mirror.clone())
158                    .with_early_hints(&route.middleware.early_hints)
159                    .with_h3(upstream_cfg.is_some_and(|u| u.protocol.eq_ignore_ascii_case("h3")))
160                    .with_upstream_host(upstream_host.clone()));
161                    if proxy.supports_async() && route.middleware.is_empty() {
162                        async_proxy = Some(Arc::clone(&proxy));
163                    }
164                    proxy
165                }
166
167                ActionConfig::Redirect { location, status } => {
168                    arc_app(RedirectAdapter::new(location.clone(), *status))
169                }
170
171                ActionConfig::Respond { status, body, content_type } => {
172                    arc_app(RespondAdapter::new(*status, body.clone(), content_type.clone()))
173                }
174
175                ActionConfig::Static { root, index } => {
176                    arc_app(StaticAdapter::new(root.clone(), index.clone()))
177                }
178
179                // Grpc action: HTTP/2 to the upstream (HTTP/1.1 only in builds
180                // without the http2 feature, where gRPC can't work anyway).
181                ActionConfig::Grpc { upstream, connect_timeout_ms, read_timeout_ms } => {
182                    let live = upstream_lives
183                        .get(upstream.as_str())
184                        .cloned()
185                        .unwrap_or_else(|| Arc::new(RwLock::new(vec![upstream.clone()])));
186                    let upstream_cfg = config.upstreams.iter().find(|u| u.name == *upstream);
187                    let upstream_tls = upstream_cfg.map_or_else(|| upstream.starts_with("https://"), |u| u.tls);
188                    #[cfg(feature = "http2")]
189                    {
190                        let _ = upstream_cfg;
191                        arc_app(crate::proxy_config::H2Route::new(
192                            live,
193                            upstream_tls,
194                            upstream_trust.get(upstream.as_str()).cloned(),
195                            *connect_timeout_ms,
196                            *read_timeout_ms,
197                        ))
198                    }
199                    #[cfg(not(feature = "http2"))]
200                    {
201                        let upstream_strategy = upstream_cfg
202                            .map(|u| u.strategy.clone())
203                            .unwrap_or_else(|| "round_robin".to_string());
204                        arc_app(
205                            DynamicProxy::new(live, *connect_timeout_ms, *read_timeout_ms, None, None, upstream_tls, upstream_strategy)
206                                .with_trust(upstream_trust.get(upstream.as_str()).cloned())
207                                .with_policy(upstream, upstream_policies.get(upstream.as_str()).cloned()),
208                        )
209                    }
210                }
211
212                ActionConfig::Mcp(gateway) => arc_app(crate::proxy_config::mcp_gateway::GatewayAdapter::start(
213                    (**gateway).clone(),
214                    route.match_.path.clone().unwrap_or_else(|| "/mcp".to_string()),
215                )),
216                ActionConfig::Llm(llm) => arc_app(crate::proxy_config::llm::build(llm)),
217                ActionConfig::A2a(gateway) => arc_app(crate::proxy_config::a2a_gateway::A2aGateway::new(
218                    (**gateway).clone(),
219                    route.match_.path.as_deref().unwrap_or("/"),
220                )),
221                ActionConfig::Unknown(_) => {
222                    // No-op: these fall through to fallback
223                    arc_app(crate::proxy_config::NullApp)
224                }
225            };
226
227        // Wrap the base handler with middleware layers. A proxy route sends
228        // its mirror copies and early hints itself (so it keeps the async
229        // forwarder); any other action gets a MirrorLayer / EarlyHintsLayer.
230        let mut handler = apply_middleware_named(base_handler, &route.middleware, &cache_name, &mut caches);
231        if !matches!(route.action, ActionConfig::Proxy { .. }) {
232            if let Some(mirror) = &mirror {
233                handler = arc_app(WithMiddleware::new(ArcApp(handler)).wrap(crate::proxy::mirror::MirrorLayer::new(mirror.clone())));
234            }
235            if !route.middleware.early_hints.is_empty() {
236                let layer = crate::early_hints::EarlyHintsLayer::new(route.middleware.early_hints.iter().cloned());
237                handler = arc_app(WithMiddleware::new(ArcApp(handler)).wrap(layer));
238            }
239        }
240
241        compiled.push(CompiledRoute { matcher, handler, async_proxy });
242    }
243    crate::cache::registry::replace_group("route:", caches);
244    compiled
245}
246
247/// Start the `[[tcp_proxy]]` / `[[udp_proxy]]` / `[[ws_proxy]]` listeners.
248/// Startup only: a hot reload can't move bound sockets, so changes to these
249/// sections need a restart.
250fn spawn_listeners(config: &ProxyConfig) -> Vec<std::thread::JoinHandle<()>> {
251    let mut handles: Vec<std::thread::JoinHandle<()>> = Vec::new();
252
253    for tcp_cfg in &config.tcp_proxies {
254        let listen = tcp_cfg.listen.clone();
255        let backends = tcp_cfg.backends.clone();
256        let timeout_ms = tcp_cfg.connect_timeout_ms;
257        let name = tcp_cfg.name.clone();
258        let h = std::thread::Builder::new()
259            .name(format!("tcp-proxy-{}", name))
260            .spawn(move || {
261                let proxy = crate::tcp_proxy::TcpProxy::new(backends)
262                    .connect_timeout_ms(timeout_ms);
263                if let Err(e) = proxy.bind(&listen) {
264                    eprintln!("[tcp_proxy:{}] {}", name, e);
265                }
266            })
267            .expect("failed to spawn tcp proxy thread");
268        handles.push(h);
269    }
270
271    for udp_cfg in &config.udp_proxies {
272        let listen = udp_cfg.listen.clone();
273        let backends = udp_cfg.backends.clone();
274        let reply_timeout_ms = udp_cfg.reply_timeout_ms;
275        let buffer_size = udp_cfg.buffer_size;
276        let name = udp_cfg.name.clone();
277        let h = std::thread::Builder::new()
278            .name(format!("udp-proxy-{}", name))
279            .spawn(move || {
280                let proxy = crate::udp_proxy::UdpProxy::new(backends)
281                    .reply_timeout_ms(reply_timeout_ms)
282                    .buffer_size(buffer_size);
283                if let Err(e) = proxy.bind(&listen) {
284                    eprintln!("[udp_proxy:{}] {}", name, e);
285                }
286            })
287            .expect("failed to spawn udp proxy thread");
288        handles.push(h);
289    }
290
291    for ws_cfg in &config.ws_proxies {
292        let listen = ws_cfg.listen.clone();
293        let backends = ws_cfg.backends.clone();
294        let connect_timeout_ms = ws_cfg.connect_timeout_ms;
295        let read_timeout_ms = ws_cfg.read_timeout_ms;
296        let name = ws_cfg.name.clone();
297
298        // With a health check configured, share a live-backend list with a
299        // background checker thread (same mechanism `[[upstream]]` pools
300        // use) instead of treating every configured backend as always live.
301        let live = Arc::new(RwLock::new(backends.clone()));
302        if let Some(ref hc) = ws_cfg.health_check {
303            crate::proxy_config::health::start_health_checker(
304                name.clone(),
305                backends.clone(),
306                Arc::clone(&live),
307                hc.clone(),
308                Arc::new(AtomicBool::new(false)),
309                None,
310            );
311        }
312
313        let h = std::thread::Builder::new()
314            .name(format!("ws-proxy-{}", name))
315            .spawn(move || {
316                let proxy = crate::ws_proxy::WsProxy::with_live_backends(backends, live)
317                    .connect_timeout_ms(connect_timeout_ms)
318                    .read_timeout_ms(read_timeout_ms);
319                if let Err(e) = proxy.bind(&listen) {
320                    eprintln!("[ws_proxy:{}] {}", name, e);
321                }
322            })
323            .expect("failed to spawn ws proxy thread");
324        handles.push(h);
325    }
326
327    handles
328}
329
330/// Wrap `handler` with the middleware layers described by `mw`, innermost
331/// (closest to the handler) to outermost (runs first, on every request):
332///
333///   1. Cache (innermost)
334///   2. Rewrite (request + response rules combined)
335///   3. Auth
336///   4. Rate limit
337///   5. IP filter
338///   6. Timeout — bounds this route's total time, including every layer above
339///   7. Max body size (outermost) — a fast, non-blocking length check, so a
340///      request this route will reject outright doesn't pay for a timeout
341///      wrapper (which runs its target on a background thread) or engage
342///      any other layer at all
343/// A route's mirror: a declared upstream's live backends, or its own list.
344fn build_mirror(route: &str, config: &crate::proxy_config::MirrorConfig, lives: &HashMap<String, Arc<RwLock<Vec<String>>>>) -> crate::proxy::mirror::Mirror {
345    let name = if config.upstream.is_empty() { format!("{route}-mirror") } else { config.upstream.clone() };
346    let mirror = match lives.get(&config.upstream) {
347        Some(live) if !config.upstream.is_empty() => crate::proxy::mirror::Mirror::with_live_backends(name, Arc::clone(live)),
348        _ if !config.upstream.is_empty() => crate::proxy::mirror::Mirror::new(name, vec![config.upstream.clone()]),
349        _ => crate::proxy::mirror::Mirror::new(name, config.backends.clone()),
350    };
351    mirror.percent(if config.percent.is_finite() { config.percent } else { 0.0 })
352}
353
354pub(crate) fn apply_middleware(
355    handler: Arc<dyn crate::application::Application + Send + Sync>,
356    mw: &MiddlewareConfig,
357) -> Arc<dyn crate::application::Application + Send + Sync> {
358    apply_middleware_named(handler, mw, "middleware", &mut Vec::new())
359}
360
361/// [`apply_middleware`], recording the route's cache (if any) as `name`
362/// in `caches` for the admin API's registry.
363pub(crate) fn apply_middleware_named(
364    handler: Arc<dyn crate::application::Application + Send + Sync>,
365    mw: &MiddlewareConfig,
366    name: &str,
367    caches: &mut Vec<(String, crate::cache::CacheLayer)>,
368) -> Arc<dyn crate::application::Application + Send + Sync> {
369    // Wrap the Arc<dyn Application> in an ArcApp adapter so it implements
370    // Application itself (needed for WithMiddleware::new).
371    let mut app: Arc<dyn crate::application::Application + Send + Sync> = handler;
372
373    // ── Cache (applied first so it's the innermost middleware) ─────────────
374    if let Some(ref cache_cfg) = mw.cache {
375        let mut layer = if cache_cfg.dir.is_empty() {
376            crate::cache::CacheLayer::memory(1000)
377        } else {
378            let max = crate::proxy_config::cache_max_bytes(&cache_cfg.max_size).unwrap_or(256 * 1024 * 1024);
379            crate::cache::CacheLayer::disk(&cache_cfg.dir, max).unwrap_or_else(|e| {
380                eprintln!("[proxy_config] ERROR: cache dir {}: {e} — this route caches in memory instead", cache_cfg.dir);
381                crate::cache::CacheLayer::memory(1000)
382            })
383        }
384        .ttl(cache_cfg.ttl_secs);
385        if !cache_cfg.purge_token_env.is_empty() {
386            layer = layer.purge_token(std::env::var(&cache_cfg.purge_token_env).unwrap_or_default().trim());
387        }
388        caches.push((name.to_string(), layer.clone()));
389        for vh in &cache_cfg.vary_by {
390            layer = layer.vary_by_header(vh.as_str());
391        }
392        app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(layer));
393    }
394
395    // ── Rewrite ────────────────────────────────────────────────────────────
396    if !mw.rewrite_request.is_empty() || !mw.rewrite_response.is_empty() {
397        let mut layer = crate::rewrite::RewriteLayer::new();
398        for rule in &mw.rewrite_request {
399            layer = apply_request_rewrite_rule(layer, rule);
400        }
401        for rule in &mw.rewrite_response {
402            layer = apply_response_rewrite_rule(layer, rule);
403        }
404        app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(layer));
405    }
406
407    // ── WebAssembly plugin (inside auth: it sees authenticated requests) ──
408    if let Some(ref wasm) = mw.wasm {
409        #[cfg(feature = "wasm-plugins")]
410        let layer = crate::wasm_plugin::WasmLayer::from_file(&wasm.module)
411            .map(|l| l.fuel(wasm.fuel).memory_mb(wasm.memory_mb).config(wasm.config.clone()));
412        #[cfg(not(feature = "wasm-plugins"))]
413        let layer: Result<crate::proxy_config::PluginMisconfigured, String> =
414            Err(format!("plugin {}: this rws binary was built without the wasm-plugins feature", wasm.module));
415        app = match layer {
416            Ok(layer) => arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(layer)),
417            Err(e) => {
418                eprintln!("[proxy_config] ERROR: {e} — this route answers 503 until the plugin is fixed.");
419                arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(crate::proxy_config::PluginMisconfigured))
420            }
421        };
422    }
423
424    // ── Authorization (inside auth, which sets the caller) ─────────────────
425    if let Some(ref rule) = mw.authz {
426        let layer = match crate::authz::config::current() {
427            Some(Ok(authorizer)) => crate::authz::AuthzLayer::new(authorizer, &rule.relation, &rule.object),
428            Some(Err(e)) => Err(format!("[authz] couldn't be built: {e}")),
429            None => Err("the route has [route.middleware.authz] but there's no [authz] section".to_string()),
430        };
431        app = match layer {
432            Ok(layer) => arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(layer)),
433            Err(e) => {
434                eprintln!("[proxy_config] ERROR: authz: {e} — this route answers 503 until it's fixed.");
435                arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(AuthMisconfigured))
436            }
437        };
438    }
439
440    // ── Auth ───────────────────────────────────────────────────────────────
441    // Fails closed: an auth setting that can't be honoured (secret unset,
442    // htpasswd missing, feature not compiled in) puts AuthMisconfigured in
443    // front of the route instead of silently serving it unauthenticated.
444    if let Some(ref auth_cfg) = mw.auth {
445        let deny = |reason: String| -> Arc<dyn crate::application::Application + Send + Sync> {
446            eprintln!(
447                "[proxy_config] ERROR: {reason} — this route answers 503 until the auth configuration is fixed."
448            );
449            arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(AuthMisconfigured))
450        };
451        app = match auth_cfg {
452            AuthConfig::Bearer { token_env } => {
453                let token = std::env::var(token_env).unwrap_or_default();
454                if token.is_empty() {
455                    deny(format!("bearer auth: env var '{token_env}' is unset or empty"))
456                } else {
457                    arc_app(
458                        WithMiddleware::new(ArcApp(Arc::clone(&app)))
459                            .wrap(BearerAuthMiddleware { token: Arc::new(token) }),
460                    )
461                }
462            }
463            #[cfg(feature = "auth")]
464            AuthConfig::Jwt { secret_env } => {
465                let secret = std::env::var(secret_env).unwrap_or_default();
466                if secret.is_empty() {
467                    deny(format!("JWT auth: env var '{secret_env}' is unset or empty"))
468                } else {
469                    arc_app(
470                        WithMiddleware::new(ArcApp(Arc::clone(&app)))
471                            .wrap(crate::auth::JwtLayer::new(secret)),
472                    )
473                }
474            }
475            #[cfg(not(feature = "auth"))]
476            AuthConfig::Jwt { .. } => {
477                deny("JWT auth requires the 'auth' feature, which this build doesn't include".to_string())
478            }
479            #[cfg(feature = "auth")]
480            AuthConfig::Basic { htpasswd_file } => {
481                match crate::auth::BasicAuthLayer::from_htpasswd_file(htpasswd_file) {
482                    Ok(layer) => arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(layer)),
483                    Err(e) => deny(format!("basic auth: {e}")),
484                }
485            }
486            #[cfg(not(feature = "auth"))]
487            AuthConfig::Basic { .. } => {
488                deny("basic auth requires the 'auth' feature, which this build doesn't include".to_string())
489            }
490        };
491    }
492
493    // ── Rate limit ─────────────────────────────────────────────────────────
494    if let Some(ref rl_cfg) = mw.rate_limit {
495        let limiter = Arc::new(crate::rate_limit::RateLimiter::new(
496            rl_cfg.max_requests,
497            rl_cfg.window_secs,
498        ));
499        app = arc_app(
500            WithMiddleware::new(ArcApp(Arc::clone(&app)))
501                .wrap(PerRouteRateLimit(limiter)),
502        );
503    }
504
505    // ── IP filter ────────────────────────────────────────────────────────────
506    if !mw.ip_allow.is_empty() {
507        let filter = crate::ip_filter::IpFilter::allow(mw.ip_allow.iter().map(|s| s.as_str()));
508        app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(filter));
509    } else if !mw.ip_deny.is_empty() {
510        let filter = crate::ip_filter::IpFilter::deny(mw.ip_deny.iter().map(|s| s.as_str()));
511        app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(filter));
512    }
513
514    // ── Timeout (bounds this route's total time, including every other
515    // middleware above) ─────────────────────────────────────────────────────
516    if let Some(timeout_ms) = mw.timeout_ms {
517        app = arc_app(crate::timeout::TimeoutLayer::from_arc(
518            app,
519            std::time::Duration::from_millis(timeout_ms),
520        ));
521    }
522
523    // ── Max body size (outermost — see PerRouteMaxBodySize's docs for why
524    // this is not a substitute for RWS_CONFIG_MAX_BODY_SIZE_IN_BYTES) ──────
525    if let Some(max_body_size) = mw.max_body_size {
526        app = arc_app(
527            WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(PerRouteMaxBodySize(max_body_size)),
528        );
529    }
530
531    app
532}
533
534fn apply_request_rewrite_rule(
535    layer: crate::rewrite::RewriteLayer,
536    rule: &crate::proxy_config::RewriteRuleConfig,
537) -> crate::rewrite::RewriteLayer {
538    match rule.type_.as_str() {
539        "header_set" => {
540            if let (Some(name), Some(value)) = (&rule.name, &rule.value) {
541                return layer.request_header_set(name, value);
542            }
543        }
544        "header_remove" => {
545            if let Some(name) = &rule.name {
546                return layer.request_header_remove(name);
547            }
548        }
549        "uri_set" => {
550            if let Some(value) = &rule.value {
551                return layer.request_uri_set(value);
552            }
553        }
554        "uri_strip_prefix" | "strip_prefix" => {
555            if let Some(prefix) = rule.prefix.as_ref().or(rule.value.as_ref()) {
556                return layer.request_uri_strip_prefix(prefix);
557            }
558        }
559        "uri_add_prefix" | "add_prefix" => {
560            if let Some(prefix) = rule.prefix.as_ref().or(rule.value.as_ref()) {
561                return layer.request_uri_add_prefix(prefix);
562            }
563        }
564        _ => {}
565    }
566    layer
567}
568
569fn apply_response_rewrite_rule(
570    layer: crate::rewrite::RewriteLayer,
571    rule: &crate::proxy_config::RewriteRuleConfig,
572) -> crate::rewrite::RewriteLayer {
573    match rule.type_.as_str() {
574        "header_set" => {
575            if let (Some(name), Some(value)) = (&rule.name, &rule.value) {
576                return layer.response_header_set(name, value);
577            }
578        }
579        "header_remove" => {
580            if let Some(name) = &rule.name {
581                return layer.response_header_remove(name);
582            }
583        }
584        "status" => {
585            if let (Some(code), Some(reason)) = (&rule.code, &rule.reason) {
586                return layer.response_status(*code as i16, reason);
587            }
588        }
589        "body_replace" => {
590            if let (Some(from), Some(to)) = (&rule.from, &rule.to) {
591                return layer.response_body_replace(from, to);
592            }
593        }
594        _ => {}
595    }
596    layer
597}
598
599// ── ArcApp adapter ─────────────────────────────────────────────────────────────
600
601/// Wraps `Arc<dyn Application + Send + Sync>` to implement `Application`
602/// (needed because you can't implement a foreign trait on a foreign type).
603pub(crate) struct ArcApp(pub(crate) Arc<dyn crate::application::Application + Send + Sync>);
604
605impl crate::application::Application for ArcApp {
606    fn execute(
607        &self,
608        request: &crate::request::Request,
609        conn: &crate::server::ConnectionInfo,
610    ) -> Result<crate::response::Response, String> {
611        self.0.execute(request, conn)
612    }
613
614    fn streams_body(&self, request: &crate::request::Request, conn: &crate::server::ConnectionInfo) -> bool {
615        self.0.streams_body(request, conn)
616    }
617}
618
619impl Clone for ArcApp {
620    fn clone(&self) -> Self {
621        ArcApp(Arc::clone(&self.0))
622    }
623}
624
625/// The authorizer `[authz]` describes: its schema, the process-wide tuple
626/// store, the seed tuples written. `None` without `[authz]`.
627pub(crate) fn config_authorizer(config: &ProxyConfig) -> Option<Result<crate::authz::Authorizer, String>> {
628    config_authorizer_with(config, None)
629}
630
631/// [`config_authorizer`], or over `check_store` whatever `store` says — a
632/// throwaway store for `rws --check`, which must not connect or write.
633pub(crate) fn config_authorizer_with(
634    config: &ProxyConfig,
635    check_store: Option<Arc<dyn crate::authz::TupleStore>>,
636) -> Option<Result<crate::authz::Authorizer, String>> {
637    let authz = config.authz.as_ref()?;
638    Some((|| {
639        let mut schema = crate::authz::Schema::new();
640        for (kind, relations) in &authz.types {
641            schema = if relations.is_empty() { schema.object_type(kind) } else { Ok(schema) }.map_err(|e| e.to_string())?;
642            for (relation, definition) in relations {
643                schema = schema.relation(kind, relation, definition).map_err(|e| format!("type {kind}, relation {relation}: {e}"))?;
644            }
645        }
646        let store = match (authz.store.as_str(), check_store) {
647            ("" | "memory" | "sql", Some(store)) => store,
648            ("" | "memory", None) => crate::authz::config::memory_store(),
649            #[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
650            ("sql", None) => crate::authz::sql::config_store()?,
651            #[cfg(not(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql")))]
652            ("sql", None) => return Err("store = \"sql\" needs a build with a model-sqlite / model-postgres / model-mysql feature".to_string()),
653            (other, _) => return Err(format!("store = \"{other}\" isn't supported (memory or sql)")),
654        };
655        let authorizer = crate::authz::Authorizer::with_store(schema, store).map_err(|e| e.to_string())?;
656        for tuple in &authz.tuples {
657            authorizer.write_str(tuple).map_err(|e| format!("tuple {tuple:?}: {e}"))?;
658        }
659        Ok(authorizer)
660    })())
661}