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