rust_web_server/proxy_config/
builder.rs1use 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
15pub 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
38pub 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
50pub(crate) fn compile_routes(config: &ProxyConfig, health_stop: &Arc<AtomicBool>) -> Vec<CompiledRoute> {
54 let mut upstream_lives: HashMap<String, Arc<RwLock<Vec<String>>>> = HashMap::new();
56 let mut upstream_trust: HashMap<String, Arc<crate::proxy::UpstreamTls>> = HashMap::new();
60 let mut upstream_policies: HashMap<String, Arc<crate::proxy_config::upstream::UpstreamPolicy>> = HashMap::new();
63
64 for upstream in &config.upstreams {
65 let live = Arc::new(RwLock::new(upstream.backends.clone()));
66 upstream_lives.insert(upstream.name.clone(), Arc::clone(&live));
67 upstream_policies.insert(
68 upstream.name.clone(),
69 Arc::new(crate::proxy_config::upstream::UpstreamPolicy::new(
70 &upstream.name,
71 Arc::clone(&live),
72 &upstream.backends,
73 &upstream.weights,
74 upstream.sticky.clone(),
75 upstream.circuit_breaker.as_ref(),
76 upstream.retry.clone(),
77 )),
78 );
79
80 let client = upstream.tls_client_cert_file.as_deref().zip(upstream.tls_client_key_file.as_deref());
81 let configured = upstream.tls_ca_file.is_some() || upstream.tls_client_cert_file.is_some() || upstream.tls_client_key_file.is_some();
82 let trust = configured.then(|| {
83 Arc::new(crate::proxy::UpstreamTls::new(upstream.tls_ca_file.as_deref(), client).unwrap_or_else(|e| {
84 eprintln!("[proxy_config] ERROR: upstream '{}': {e} — its TLS backends answer 502 until this is fixed", upstream.name);
85 crate::proxy::UpstreamTls::reject_all(&upstream.name)
86 }))
87 });
88 if let Some(trust) = &trust {
89 upstream_trust.insert(upstream.name.clone(), Arc::clone(trust));
90 }
91
92 if let Some(ref hc) = upstream.health_check {
93 crate::proxy_config::health::start_health_checker(
94 upstream.name.clone(),
95 upstream.backends.clone(),
96 Arc::clone(&live),
97 hc.clone(),
98 Arc::clone(health_stop),
99 trust,
100 );
101 }
102 }
103
104 crate::proxy_config::upstream::register(upstream_policies.values().cloned().collect());
105
106 let mut compiled: Vec<CompiledRoute> = Vec::new();
108
109 for route in &config.routes {
110 let matcher = RouteMatcher::from_match_config(&route.match_);
111
112 let mut async_proxy: Option<Arc<DynamicProxy>> = None;
113
114 let base_handler: Arc<dyn crate::application::Application + Send + Sync> =
116 match &route.action {
117 ActionConfig::Proxy {
118 upstream,
119 connect_timeout_ms,
120 read_timeout_ms,
121 strip_path_prefix,
122 add_path_prefix,
123 } => {
124 let live = upstream_lives
125 .get(upstream.as_str())
126 .cloned()
127 .unwrap_or_else(|| {
128 Arc::new(RwLock::new(vec![upstream.clone()]))
130 });
131 let upstream_cfg = config.upstreams.iter().find(|u| u.name == *upstream);
132 let upstream_tls = upstream_cfg.map_or_else(|| upstream.starts_with("https://"), |u| u.tls);
133 let upstream_strategy = upstream_cfg
134 .map(|u| u.strategy.clone())
135 .unwrap_or_else(|| "round_robin".to_string());
136 let proxy = Arc::new(DynamicProxy::new(
137 live,
138 *connect_timeout_ms,
139 *read_timeout_ms,
140 strip_path_prefix.clone(),
141 add_path_prefix.clone(),
142 upstream_tls,
143 upstream_strategy,
144 )
145 .with_trust(upstream_trust.get(upstream.as_str()).cloned())
146 .with_policy(upstream, upstream_policies.get(upstream.as_str()).cloned()));
147 if proxy.supports_async() && route.middleware.is_empty() {
148 async_proxy = Some(Arc::clone(&proxy));
149 }
150 proxy
151 }
152
153 ActionConfig::Redirect { location, status } => {
154 arc_app(RedirectAdapter::new(location.clone(), *status))
155 }
156
157 ActionConfig::Respond { status, body, content_type } => {
158 arc_app(RespondAdapter::new(*status, body.clone(), content_type.clone()))
159 }
160
161 ActionConfig::Static { root, index } => {
162 arc_app(StaticAdapter::new(root.clone(), index.clone()))
163 }
164
165 ActionConfig::Grpc { upstream, connect_timeout_ms, read_timeout_ms } => {
168 let live = upstream_lives
169 .get(upstream.as_str())
170 .cloned()
171 .unwrap_or_else(|| Arc::new(RwLock::new(vec![upstream.clone()])));
172 let upstream_cfg = config.upstreams.iter().find(|u| u.name == *upstream);
173 let upstream_tls = upstream_cfg.map_or_else(|| upstream.starts_with("https://"), |u| u.tls);
174 #[cfg(feature = "http2")]
175 {
176 let _ = upstream_cfg;
177 arc_app(crate::proxy_config::H2Route::new(
178 live,
179 upstream_tls,
180 upstream_trust.get(upstream.as_str()).cloned(),
181 *connect_timeout_ms,
182 *read_timeout_ms,
183 ))
184 }
185 #[cfg(not(feature = "http2"))]
186 {
187 let upstream_strategy = upstream_cfg
188 .map(|u| u.strategy.clone())
189 .unwrap_or_else(|| "round_robin".to_string());
190 arc_app(
191 DynamicProxy::new(live, *connect_timeout_ms, *read_timeout_ms, None, None, upstream_tls, upstream_strategy)
192 .with_trust(upstream_trust.get(upstream.as_str()).cloned())
193 .with_policy(upstream, upstream_policies.get(upstream.as_str()).cloned()),
194 )
195 }
196 }
197
198 ActionConfig::Mcp(gateway) => arc_app(crate::proxy_config::mcp_gateway::GatewayAdapter::start(
199 (**gateway).clone(),
200 route.match_.path.clone().unwrap_or_else(|| "/mcp".to_string()),
201 )),
202 ActionConfig::Llm(llm) => arc_app(crate::proxy_config::llm::build(llm)),
203 ActionConfig::Unknown(_) => {
204 arc_app(crate::proxy_config::NullApp)
206 }
207 };
208
209 let handler = apply_middleware(base_handler, &route.middleware);
211
212 compiled.push(CompiledRoute { matcher, handler, async_proxy });
213 }
214 compiled
215}
216
217fn spawn_listeners(config: &ProxyConfig) -> Vec<std::thread::JoinHandle<()>> {
221 let mut handles: Vec<std::thread::JoinHandle<()>> = Vec::new();
222
223 for tcp_cfg in &config.tcp_proxies {
224 let listen = tcp_cfg.listen.clone();
225 let backends = tcp_cfg.backends.clone();
226 let timeout_ms = tcp_cfg.connect_timeout_ms;
227 let name = tcp_cfg.name.clone();
228 let h = std::thread::Builder::new()
229 .name(format!("tcp-proxy-{}", name))
230 .spawn(move || {
231 let proxy = crate::tcp_proxy::TcpProxy::new(backends)
232 .connect_timeout_ms(timeout_ms);
233 if let Err(e) = proxy.bind(&listen) {
234 eprintln!("[tcp_proxy:{}] {}", name, e);
235 }
236 })
237 .expect("failed to spawn tcp proxy thread");
238 handles.push(h);
239 }
240
241 for udp_cfg in &config.udp_proxies {
242 let listen = udp_cfg.listen.clone();
243 let backends = udp_cfg.backends.clone();
244 let reply_timeout_ms = udp_cfg.reply_timeout_ms;
245 let buffer_size = udp_cfg.buffer_size;
246 let name = udp_cfg.name.clone();
247 let h = std::thread::Builder::new()
248 .name(format!("udp-proxy-{}", name))
249 .spawn(move || {
250 let proxy = crate::udp_proxy::UdpProxy::new(backends)
251 .reply_timeout_ms(reply_timeout_ms)
252 .buffer_size(buffer_size);
253 if let Err(e) = proxy.bind(&listen) {
254 eprintln!("[udp_proxy:{}] {}", name, e);
255 }
256 })
257 .expect("failed to spawn udp proxy thread");
258 handles.push(h);
259 }
260
261 for ws_cfg in &config.ws_proxies {
262 let listen = ws_cfg.listen.clone();
263 let backends = ws_cfg.backends.clone();
264 let connect_timeout_ms = ws_cfg.connect_timeout_ms;
265 let read_timeout_ms = ws_cfg.read_timeout_ms;
266 let name = ws_cfg.name.clone();
267
268 let live = Arc::new(RwLock::new(backends.clone()));
272 if let Some(ref hc) = ws_cfg.health_check {
273 crate::proxy_config::health::start_health_checker(
274 name.clone(),
275 backends.clone(),
276 Arc::clone(&live),
277 hc.clone(),
278 Arc::new(AtomicBool::new(false)),
279 None,
280 );
281 }
282
283 let h = std::thread::Builder::new()
284 .name(format!("ws-proxy-{}", name))
285 .spawn(move || {
286 let proxy = crate::ws_proxy::WsProxy::with_live_backends(backends, live)
287 .connect_timeout_ms(connect_timeout_ms)
288 .read_timeout_ms(read_timeout_ms);
289 if let Err(e) = proxy.bind(&listen) {
290 eprintln!("[ws_proxy:{}] {}", name, e);
291 }
292 })
293 .expect("failed to spawn ws proxy thread");
294 handles.push(h);
295 }
296
297 handles
298}
299
300pub(crate) fn apply_middleware(
314 handler: Arc<dyn crate::application::Application + Send + Sync>,
315 mw: &MiddlewareConfig,
316) -> Arc<dyn crate::application::Application + Send + Sync> {
317 let mut app: Arc<dyn crate::application::Application + Send + Sync> = handler;
320
321 if let Some(ref cache_cfg) = mw.cache {
323 let mut layer = crate::cache::CacheLayer::memory(1000).ttl(cache_cfg.ttl_secs);
324 for vh in &cache_cfg.vary_by {
325 layer = layer.vary_by_header(vh.as_str());
326 }
327 app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(layer));
328 }
329
330 if !mw.rewrite_request.is_empty() || !mw.rewrite_response.is_empty() {
332 let mut layer = crate::rewrite::RewriteLayer::new();
333 for rule in &mw.rewrite_request {
334 layer = apply_request_rewrite_rule(layer, rule);
335 }
336 for rule in &mw.rewrite_response {
337 layer = apply_response_rewrite_rule(layer, rule);
338 }
339 app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(layer));
340 }
341
342 if let Some(ref auth_cfg) = mw.auth {
347 let deny = |reason: String| -> Arc<dyn crate::application::Application + Send + Sync> {
348 eprintln!(
349 "[proxy_config] ERROR: {reason} — this route answers 503 until the auth configuration is fixed."
350 );
351 arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(AuthMisconfigured))
352 };
353 app = match auth_cfg {
354 AuthConfig::Bearer { token_env } => {
355 let token = std::env::var(token_env).unwrap_or_default();
356 if token.is_empty() {
357 deny(format!("bearer auth: env var '{token_env}' is unset or empty"))
358 } else {
359 arc_app(
360 WithMiddleware::new(ArcApp(Arc::clone(&app)))
361 .wrap(BearerAuthMiddleware { token: Arc::new(token) }),
362 )
363 }
364 }
365 #[cfg(feature = "auth")]
366 AuthConfig::Jwt { secret_env } => {
367 let secret = std::env::var(secret_env).unwrap_or_default();
368 if secret.is_empty() {
369 deny(format!("JWT auth: env var '{secret_env}' is unset or empty"))
370 } else {
371 arc_app(
372 WithMiddleware::new(ArcApp(Arc::clone(&app)))
373 .wrap(crate::auth::JwtLayer::new(secret)),
374 )
375 }
376 }
377 #[cfg(not(feature = "auth"))]
378 AuthConfig::Jwt { .. } => {
379 deny("JWT auth requires the 'auth' feature, which this build doesn't include".to_string())
380 }
381 #[cfg(feature = "auth")]
382 AuthConfig::Basic { htpasswd_file } => {
383 match crate::auth::BasicAuthLayer::from_htpasswd_file(htpasswd_file) {
384 Ok(layer) => arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(layer)),
385 Err(e) => deny(format!("basic auth: {e}")),
386 }
387 }
388 #[cfg(not(feature = "auth"))]
389 AuthConfig::Basic { .. } => {
390 deny("basic auth requires the 'auth' feature, which this build doesn't include".to_string())
391 }
392 };
393 }
394
395 if let Some(ref rl_cfg) = mw.rate_limit {
397 let limiter = Arc::new(crate::rate_limit::RateLimiter::new(
398 rl_cfg.max_requests,
399 rl_cfg.window_secs,
400 ));
401 app = arc_app(
402 WithMiddleware::new(ArcApp(Arc::clone(&app)))
403 .wrap(PerRouteRateLimit(limiter)),
404 );
405 }
406
407 if !mw.ip_allow.is_empty() {
409 let filter = crate::ip_filter::IpFilter::allow(mw.ip_allow.iter().map(|s| s.as_str()));
410 app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(filter));
411 } else if !mw.ip_deny.is_empty() {
412 let filter = crate::ip_filter::IpFilter::deny(mw.ip_deny.iter().map(|s| s.as_str()));
413 app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(filter));
414 }
415
416 if let Some(timeout_ms) = mw.timeout_ms {
419 app = arc_app(crate::timeout::TimeoutLayer::from_arc(
420 app,
421 std::time::Duration::from_millis(timeout_ms),
422 ));
423 }
424
425 if let Some(max_body_size) = mw.max_body_size {
428 app = arc_app(
429 WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(PerRouteMaxBodySize(max_body_size)),
430 );
431 }
432
433 app
434}
435
436fn apply_request_rewrite_rule(
437 layer: crate::rewrite::RewriteLayer,
438 rule: &crate::proxy_config::RewriteRuleConfig,
439) -> crate::rewrite::RewriteLayer {
440 match rule.type_.as_str() {
441 "header_set" => {
442 if let (Some(name), Some(value)) = (&rule.name, &rule.value) {
443 return layer.request_header_set(name, value);
444 }
445 }
446 "header_remove" => {
447 if let Some(name) = &rule.name {
448 return layer.request_header_remove(name);
449 }
450 }
451 "uri_set" => {
452 if let Some(value) = &rule.value {
453 return layer.request_uri_set(value);
454 }
455 }
456 "uri_strip_prefix" | "strip_prefix" => {
457 if let Some(prefix) = rule.prefix.as_ref().or(rule.value.as_ref()) {
458 return layer.request_uri_strip_prefix(prefix);
459 }
460 }
461 "uri_add_prefix" | "add_prefix" => {
462 if let Some(prefix) = rule.prefix.as_ref().or(rule.value.as_ref()) {
463 return layer.request_uri_add_prefix(prefix);
464 }
465 }
466 _ => {}
467 }
468 layer
469}
470
471fn apply_response_rewrite_rule(
472 layer: crate::rewrite::RewriteLayer,
473 rule: &crate::proxy_config::RewriteRuleConfig,
474) -> crate::rewrite::RewriteLayer {
475 match rule.type_.as_str() {
476 "header_set" => {
477 if let (Some(name), Some(value)) = (&rule.name, &rule.value) {
478 return layer.response_header_set(name, value);
479 }
480 }
481 "header_remove" => {
482 if let Some(name) = &rule.name {
483 return layer.response_header_remove(name);
484 }
485 }
486 "status" => {
487 if let (Some(code), Some(reason)) = (&rule.code, &rule.reason) {
488 return layer.response_status(*code as i16, reason);
489 }
490 }
491 "body_replace" => {
492 if let (Some(from), Some(to)) = (&rule.from, &rule.to) {
493 return layer.response_body_replace(from, to);
494 }
495 }
496 _ => {}
497 }
498 layer
499}
500
501struct ArcApp(Arc<dyn crate::application::Application + Send + Sync>);
506
507impl crate::application::Application for ArcApp {
508 fn execute(
509 &self,
510 request: &crate::request::Request,
511 conn: &crate::server::ConnectionInfo,
512 ) -> Result<crate::response::Response, String> {
513 self.0.execute(request, conn)
514 }
515
516 fn streams_body(&self, request: &crate::request::Request, conn: &crate::server::ConnectionInfo) -> bool {
517 self.0.streams_body(request, conn)
518 }
519}
520
521impl Clone for ArcApp {
522 fn clone(&self) -> Self {
523 ArcApp(Arc::clone(&self.0))
524 }
525}