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