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    BearerAuthMiddleware, 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        let h = std::thread::Builder::new()
166            .name(format!("ws-proxy-{}", name))
167            .spawn(move || {
168                let proxy = crate::ws_proxy::WsProxy::new(backends)
169                    .connect_timeout_ms(connect_timeout_ms)
170                    .read_timeout_ms(read_timeout_ms);
171                if let Err(e) = proxy.bind(&listen) {
172                    eprintln!("[ws_proxy:{}] {}", name, e);
173                }
174            })
175            .expect("failed to spawn ws proxy thread");
176        handles.push(h);
177    }
178
179    (ConfigDrivenApp::new(compiled), handles)
180}
181
182/// Wrap `handler` with the middleware layers described by `mw`.
183/// Layers are applied innermost → outermost in the order:
184///   1. IP filter (outermost — rejects early)
185///   2. Rate limit
186///   3. Auth
187///   4. Rewrite (request + response rules combined)
188///   5. Cache (innermost middleware — closest to handler)
189fn apply_middleware(
190    handler: Arc<dyn crate::application::Application + Send + Sync>,
191    mw: &MiddlewareConfig,
192) -> Arc<dyn crate::application::Application + Send + Sync> {
193    // Wrap the Arc<dyn Application> in an ArcApp adapter so it implements
194    // Application itself (needed for WithMiddleware::new).
195    let mut app: Arc<dyn crate::application::Application + Send + Sync> = handler;
196
197    // ── Cache (applied first so it's the innermost middleware) ─────────────
198    if let Some(ref cache_cfg) = mw.cache {
199        let mut layer = crate::cache::CacheLayer::memory(1000).ttl(cache_cfg.ttl_secs);
200        for vh in &cache_cfg.vary_by {
201            layer = layer.vary_by_header(vh.as_str());
202        }
203        app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(layer));
204    }
205
206    // ── Rewrite ────────────────────────────────────────────────────────────
207    if !mw.rewrite_request.is_empty() || !mw.rewrite_response.is_empty() {
208        let mut layer = crate::rewrite::RewriteLayer::new();
209        for rule in &mw.rewrite_request {
210            layer = apply_request_rewrite_rule(layer, rule);
211        }
212        for rule in &mw.rewrite_response {
213            layer = apply_response_rewrite_rule(layer, rule);
214        }
215        app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(layer));
216    }
217
218    // ── Auth ───────────────────────────────────────────────────────────────
219    if let Some(ref auth_cfg) = mw.auth {
220        match auth_cfg {
221            AuthConfig::Bearer { token_env } => {
222                let token = std::env::var(token_env).unwrap_or_default();
223                if !token.is_empty() {
224                    app = arc_app(
225                        WithMiddleware::new(ArcApp(Arc::clone(&app)))
226                            .wrap(BearerAuthMiddleware { token: Arc::new(token) }),
227                    );
228                }
229            }
230            #[cfg(feature = "auth")]
231            AuthConfig::Jwt { secret_env } => {
232                let _secret = std::env::var(secret_env).unwrap_or_default();
233                // JWT verification would be wired in here when auth feature is enabled.
234                // For now this is a no-op placeholder.
235            }
236            #[cfg(not(feature = "auth"))]
237            AuthConfig::Jwt { .. } => {
238                eprintln!("[proxy_config] JWT auth requires the 'auth' feature; skipping.");
239            }
240            AuthConfig::Basic { .. } => {
241                eprintln!("[proxy_config] Basic auth is not yet implemented; skipping.");
242            }
243        }
244    }
245
246    // ── Rate limit ─────────────────────────────────────────────────────────
247    if let Some(ref rl_cfg) = mw.rate_limit {
248        let limiter = Arc::new(crate::rate_limit::RateLimiter::new(
249            rl_cfg.max_requests,
250            rl_cfg.window_secs,
251        ));
252        app = arc_app(
253            WithMiddleware::new(ArcApp(Arc::clone(&app)))
254                .wrap(PerRouteRateLimit(limiter)),
255        );
256    }
257
258    // ── IP filter (outermost) ──────────────────────────────────────────────
259    if !mw.ip_allow.is_empty() {
260        let filter = crate::ip_filter::IpFilter::allow(mw.ip_allow.iter().map(|s| s.as_str()));
261        app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(filter));
262    } else if !mw.ip_deny.is_empty() {
263        let filter = crate::ip_filter::IpFilter::deny(mw.ip_deny.iter().map(|s| s.as_str()));
264        app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(filter));
265    }
266
267    app
268}
269
270fn apply_request_rewrite_rule(
271    layer: crate::rewrite::RewriteLayer,
272    rule: &crate::proxy_config::RewriteRuleConfig,
273) -> crate::rewrite::RewriteLayer {
274    match rule.type_.as_str() {
275        "header_set" => {
276            if let (Some(name), Some(value)) = (&rule.name, &rule.value) {
277                return layer.request_header_set(name, value);
278            }
279        }
280        "header_remove" => {
281            if let Some(name) = &rule.name {
282                return layer.request_header_remove(name);
283            }
284        }
285        "uri_set" => {
286            if let Some(value) = &rule.value {
287                return layer.request_uri_set(value);
288            }
289        }
290        "uri_strip_prefix" | "strip_prefix" => {
291            if let Some(prefix) = rule.prefix.as_ref().or(rule.value.as_ref()) {
292                return layer.request_uri_strip_prefix(prefix);
293            }
294        }
295        "uri_add_prefix" | "add_prefix" => {
296            if let Some(prefix) = rule.prefix.as_ref().or(rule.value.as_ref()) {
297                return layer.request_uri_add_prefix(prefix);
298            }
299        }
300        _ => {}
301    }
302    layer
303}
304
305fn apply_response_rewrite_rule(
306    layer: crate::rewrite::RewriteLayer,
307    rule: &crate::proxy_config::RewriteRuleConfig,
308) -> crate::rewrite::RewriteLayer {
309    match rule.type_.as_str() {
310        "header_set" => {
311            if let (Some(name), Some(value)) = (&rule.name, &rule.value) {
312                return layer.response_header_set(name, value);
313            }
314        }
315        "header_remove" => {
316            if let Some(name) = &rule.name {
317                return layer.response_header_remove(name);
318            }
319        }
320        "status" => {
321            if let (Some(code), Some(reason)) = (&rule.code, &rule.reason) {
322                return layer.response_status(*code as i16, reason);
323            }
324        }
325        "body_replace" => {
326            if let (Some(from), Some(to)) = (&rule.from, &rule.to) {
327                return layer.response_body_replace(from, to);
328            }
329        }
330        _ => {}
331    }
332    layer
333}
334
335// ── ArcApp adapter ─────────────────────────────────────────────────────────────
336
337/// Wraps `Arc<dyn Application + Send + Sync>` to implement `Application`
338/// (needed because you can't implement a foreign trait on a foreign type).
339struct ArcApp(Arc<dyn crate::application::Application + Send + Sync>);
340
341impl crate::application::Application for ArcApp {
342    fn execute(
343        &self,
344        request: &crate::request::Request,
345        conn: &crate::server::ConnectionInfo,
346    ) -> Result<crate::response::Response, String> {
347        self.0.execute(request, conn)
348    }
349}
350
351impl Clone for ArcApp {
352    fn clone(&self) -> Self {
353        ArcApp(Arc::clone(&self.0))
354    }
355}