Skip to main content

rust_web_server/proxy_config/
builder.rs

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