1pub mod parser;
29pub mod health;
30pub(crate) mod upstream;
31
32pub use upstream::{BreakerConfig, RetryConfig, StickyConfig};
33pub mod builder;
34pub mod validate;
35pub mod mcp_gateway;
36pub mod llm;
37
38#[cfg(test)]
39mod tests;
40
41use std::sync::atomic::{AtomicBool, Ordering};
42use std::sync::{Arc, RwLock};
43
44use crate::app::App;
45use crate::application::Application;
46use crate::core::New;
47use crate::request::Request;
48use crate::header::Header;
49use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
50use crate::server::ConnectionInfo;
51use crate::server_config::ServerConfig;
52
53#[derive(Debug, Clone)]
56pub struct ProxyConfig {
57 pub upstreams: Vec<UpstreamConfig>,
58 pub routes: Vec<RouteConfig>,
59 pub tcp_proxies: Vec<TcpProxyConfig>,
60 pub udp_proxies: Vec<UdpProxyConfig>,
61 pub ws_proxies: Vec<WsProxyConfig>,
62 pub global_middleware: MiddlewareConfig,
63 pub authz: Option<AuthzConfig>,
65}
66
67#[derive(Debug, Clone, Default)]
69pub struct AuthzConfig {
70 pub store: String,
72 pub tuples: Vec<String>,
74 pub types: Vec<(String, Vec<(String, String)>)>,
76}
77
78#[derive(Debug, Clone)]
80pub struct RouteAuthzConfig {
81 pub relation: String,
82 pub object: String,
84}
85
86#[derive(Debug, Clone)]
87pub struct UpstreamConfig {
88 pub name: String,
89 pub backends: Vec<String>,
90 pub strategy: String, pub health_check: Option<HealthCheckConfig>,
92 pub tls: bool,
96 pub tls_ca_file: Option<String>,
100 pub tls_client_cert_file: Option<String>,
104 pub tls_client_key_file: Option<String>,
105 pub weights: Vec<u32>,
107 pub sticky: Option<StickyConfig>,
109 pub circuit_breaker: Option<BreakerConfig>,
112 pub retry: Option<RetryConfig>,
114 pub protocol: String,
119}
120
121#[derive(Debug, Clone)]
122pub struct HealthCheckConfig {
123 pub path: String,
124 pub interval_secs: u64,
125 pub timeout_ms: u64,
126 pub healthy_threshold: u32,
127 pub unhealthy_threshold: u32,
128}
129
130#[derive(Debug, Clone)]
131pub struct RouteConfig {
132 pub name: String,
133 pub match_: MatchConfig,
134 pub action: ActionConfig,
135 pub middleware: MiddlewareConfig,
136}
137
138#[derive(Debug, Clone, Default)]
139pub struct MatchConfig {
140 pub host: Option<String>,
141 pub path: Option<String>,
142 pub method: Option<String>,
143 pub content_type: Option<String>,
144}
145
146#[derive(Debug, Clone)]
147pub enum ActionConfig {
148 Proxy {
149 upstream: String,
150 connect_timeout_ms: u64,
151 read_timeout_ms: u64,
152 strip_path_prefix: Option<String>,
153 add_path_prefix: Option<String>,
154 },
155 Grpc {
156 upstream: String,
157 connect_timeout_ms: u64,
158 read_timeout_ms: u64,
159 },
160 Static {
161 root: String,
162 index: Vec<String>,
163 },
164 Redirect {
165 location: String,
166 status: u16,
167 },
168 Respond {
169 status: u16,
170 body: String,
171 content_type: String,
172 },
173 Mcp(Box<mcp_gateway::McpGatewayConfig>),
175 Llm(Box<llm::LlmConfig>),
177 Unknown(String),
178}
179
180#[derive(Debug, Clone, Default)]
181pub struct MiddlewareConfig {
182 pub rate_limit: Option<RateLimitConfig>,
183 pub cache: Option<CacheConfig>,
184 pub auth: Option<AuthConfig>,
185 pub rewrite_request: Vec<RewriteRuleConfig>,
186 pub rewrite_response: Vec<RewriteRuleConfig>,
187 pub ip_allow: Vec<String>,
188 pub ip_deny: Vec<String>,
189 pub timeout_ms: Option<u64>,
194 pub max_body_size: Option<u64>,
210 pub wasm: Option<WasmConfig>,
213 pub early_hints: Vec<String>,
216 pub authz: Option<RouteAuthzConfig>,
219 pub mirror: Option<MirrorConfig>,
222}
223
224#[derive(Debug, Clone, Default, PartialEq)]
226pub struct MirrorConfig {
227 pub upstream: String,
229 pub backends: Vec<String>,
231 pub percent: f64,
233}
234
235#[derive(Debug, Clone, Default, PartialEq)]
237pub struct WasmConfig {
238 pub module: String,
240 pub fuel: u64,
241 pub memory_mb: u64,
242 pub config: String,
244}
245
246#[derive(Debug, Clone)]
247pub struct RateLimitConfig {
248 pub max_requests: u32,
249 pub window_secs: u64,
250}
251
252#[derive(Debug, Clone, Default)]
253pub struct CacheConfig {
254 pub ttl_secs: u64,
255 pub vary_by: Vec<String>,
256 pub dir: String,
258 pub max_size: String,
260 pub purge_token_env: String,
263}
264
265#[derive(Debug, Clone)]
266pub enum AuthConfig {
267 Basic { htpasswd_file: String },
272 Jwt { secret_env: String },
275 Bearer { token_env: String },
276}
277
278#[derive(Debug, Clone, Default)]
279pub struct RewriteRuleConfig {
280 pub type_: String,
281 pub name: Option<String>,
282 pub value: Option<String>,
283 pub prefix: Option<String>,
284 pub from: Option<String>,
285 pub to: Option<String>,
286 pub code: Option<u16>,
287 pub reason: Option<String>,
288}
289
290#[derive(Debug, Clone)]
291pub struct TcpProxyConfig {
292 pub name: String,
293 pub listen: String,
294 pub backends: Vec<String>,
295 pub connect_timeout_ms: u64,
296}
297
298#[derive(Debug, Clone)]
299pub struct UdpProxyConfig {
300 pub name: String,
301 pub listen: String,
302 pub backends: Vec<String>,
303 pub reply_timeout_ms: u64,
304 pub buffer_size: usize,
305}
306
307#[derive(Debug, Clone)]
308pub struct WsProxyConfig {
309 pub name: String,
310 pub listen: String,
311 pub backends: Vec<String>,
312 pub connect_timeout_ms: u64,
313 pub read_timeout_ms: u64,
314 pub health_check: Option<HealthCheckConfig>,
321}
322
323fn merge_action_keys(map: &mut parser::SectionMap, action_section: &str, action_type: &str) {
332 if action_type.is_empty() {
333 return;
334 }
335 let short_form: Vec<(String, String)> = match map.get(action_section) {
336 Some(entries) => entries.iter().filter(|(k, _)| k != "type").cloned().collect(),
337 None => return,
338 };
339 let nested = map.entry(format!("{action_section}.{action_type}")).or_default();
340 for (key, value) in short_form {
341 if !nested.iter().any(|(k, _)| *k == key) {
342 nested.push((key, value));
343 }
344 }
345}
346
347impl ProxyConfig {
348 pub fn is_proxy_mode() -> bool {
352 let path = config_file_path();
353 match std::fs::read_to_string(&path) {
354 Ok(contents) => {
355 contents.contains("[[route]]") || contents.contains("[[upstream]]")
356 }
357 Err(_) => false,
358 }
359 }
360
361 pub fn load() -> Self {
363 let path = config_file_path();
364 let contents = std::fs::read_to_string(&path).unwrap_or_default();
365 Self::from_str(&contents)
366 }
367
368 pub fn from_str(toml: &str) -> Self {
370 use parser::{get_array, get_str, get_u32, get_u64, section_exists};
371
372 let mut map = parser::parse(toml);
373
374 let mut upstreams = Vec::new();
376 let mut i = 0;
377 loop {
378 let sec = format!("upstream[{}]", i);
379 if !section_exists(&map, &sec) {
380 break;
381 }
382 let name = get_str(&map, &sec, "name");
383 let backends = get_array(&map, &sec, "backends");
384 let strategy = {
385 let s = get_str(&map, &sec, "strategy");
386 if s.is_empty() { "round_robin".to_string() } else { s }
387 };
388 let hc_sec = format!("{}.health_check", sec);
389 let health_check = if section_exists(&map, &hc_sec) {
390 Some(HealthCheckConfig {
391 path: {
392 let p = get_str(&map, &hc_sec, "path");
393 if p.is_empty() { "/health".to_string() } else { p }
394 },
395 interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
396 timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
397 healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
398 unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
399 })
400 } else {
401 None
402 };
403 let tls = backends.iter().any(|b| ["https://", "h2s://", "grpcs://"].iter().any(|s| b.starts_with(s)));
404 let tls_ca_file = Some(get_str(&map, &sec, "tls_ca_file")).filter(|v| !v.is_empty());
405 let tls_client_cert_file = Some(get_str(&map, &sec, "tls_client_cert_file")).filter(|v| !v.is_empty());
406 let tls_client_key_file = Some(get_str(&map, &sec, "tls_client_key_file")).filter(|v| !v.is_empty());
407 let weights: Vec<u32> = get_array(&map, &sec, "weights").iter().map(|w| w.trim().parse().unwrap_or(0)).collect();
408 let sticky_sec = format!("{}.sticky", sec);
409 let sticky = section_exists(&map, &sticky_sec).then(|| StickyConfig {
410 cookie: Some(get_str(&map, &sticky_sec, "cookie")).filter(|c| !c.is_empty()).unwrap_or_else(|| "rws_backend".to_string()),
411 max_age_secs: Some(get_u64(&map, &sticky_sec, "max_age_secs", 0)).filter(|s| *s > 0),
412 });
413 let cb_sec = format!("{}.circuit_breaker", sec);
414 let circuit_breaker = section_exists(&map, &cb_sec).then(|| BreakerConfig {
415 failure_threshold: get_u32(&map, &cb_sec, "failure_threshold", 5),
416 recovery_secs: get_u64(&map, &cb_sec, "recovery_secs", 30),
417 half_open_probes: get_u32(&map, &cb_sec, "half_open_probes", 1),
418 });
419 let retry_sec = format!("{}.retry", sec);
420 let retry = section_exists(&map, &retry_sec).then(|| {
421 let status: Vec<i16> = get_array(&map, &retry_sec, "status").iter().filter_map(|s| s.trim().parse().ok()).collect();
422 RetryConfig { attempts: get_u32(&map, &retry_sec, "attempts", 1), status: if status.is_empty() { vec![502, 503, 504] } else { status } }
423 });
424 upstreams.push(UpstreamConfig {
425 name, backends, strategy, health_check, tls, tls_ca_file, tls_client_cert_file, tls_client_key_file,
426 weights, sticky, circuit_breaker, retry,
427 protocol: get_str(&map, &sec, "protocol"),
428 });
429 i += 1;
430 }
431
432 let mut routes = Vec::new();
434 let mut i = 0;
435 loop {
436 let sec = format!("route[{}]", i);
437 if !section_exists(&map, &sec) {
438 break;
439 }
440 let name = get_str(&map, &sec, "name");
441
442 let m_sec = format!("{}.match", sec);
444 let match_ = MatchConfig {
445 host: {
446 let h = get_str(&map, &m_sec, "host");
447 if h.is_empty() { None } else { Some(h) }
448 },
449 path: {
450 let p = get_str(&map, &m_sec, "path");
451 if p.is_empty() { None } else { Some(p) }
452 },
453 method: {
454 let m = get_str(&map, &m_sec, "method");
455 if m.is_empty() { None } else { Some(m.to_uppercase()) }
456 },
457 content_type: {
458 let c = get_str(&map, &m_sec, "content_type");
459 if c.is_empty() { None } else { Some(c) }
460 },
461 };
462
463 let a_sec = format!("{}.action", sec);
465 let action_type = get_str(&map, &a_sec, "type");
466 merge_action_keys(&mut map, &a_sec, &action_type);
467 let action = match action_type.as_str() {
468 "proxy" => {
469 let p_sec = format!("{}.action.proxy", sec);
470 ActionConfig::Proxy {
471 upstream: get_str(&map, &p_sec, "upstream"),
472 connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
473 read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
474 strip_path_prefix: {
475 let v = get_str(&map, &p_sec, "strip_path_prefix");
476 if v.is_empty() { None } else { Some(v) }
477 },
478 add_path_prefix: {
479 let v = get_str(&map, &p_sec, "add_path_prefix");
480 if v.is_empty() { None } else { Some(v) }
481 },
482 }
483 }
484 "grpc" => {
485 let p_sec = format!("{}.action.grpc", sec);
486 ActionConfig::Grpc {
487 upstream: get_str(&map, &p_sec, "upstream"),
488 connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
489 read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
490 }
491 }
492 "static" => {
493 let s_sec = format!("{}.action.static", sec);
494 ActionConfig::Static {
495 root: get_str(&map, &s_sec, "root"),
496 index: get_array(&map, &s_sec, "index"),
497 }
498 }
499 "redirect" => {
500 let r_sec = format!("{}.action.redirect", sec);
501 ActionConfig::Redirect {
502 location: get_str(&map, &r_sec, "location"),
503 status: get_u64(&map, &r_sec, "status", 301) as u16,
504 }
505 }
506 "respond" => {
507 let r_sec = format!("{}.action.respond", sec);
508 ActionConfig::Respond {
509 status: get_u64(&map, &r_sec, "status", 200) as u16,
510 body: get_str(&map, &r_sec, "body"),
511 content_type: {
512 let ct = get_str(&map, &r_sec, "content_type");
513 if ct.is_empty() { "text/plain".to_string() } else { ct }
514 },
515 }
516 }
517 "mcp" => ActionConfig::Mcp(Box::new(mcp_gateway::parse(&map, &a_sec))),
518 "llm" => ActionConfig::Llm(Box::new(llm::parse(&map, &a_sec))),
519 other => ActionConfig::Unknown(other.to_string()),
520 };
521
522 let mw_sec = format!("{}.middleware", sec);
524 let middleware = parse_middleware_config(&map, &mw_sec, i);
525
526 routes.push(RouteConfig { name, match_, action, middleware });
527 i += 1;
528 }
529
530 let mut tcp_proxies = Vec::new();
532 let mut i = 0;
533 loop {
534 let sec = format!("tcp_proxy[{}]", i);
535 if !section_exists(&map, &sec) {
536 break;
537 }
538 tcp_proxies.push(TcpProxyConfig {
539 name: get_str(&map, &sec, "name"),
540 listen: get_str(&map, &sec, "listen"),
541 backends: get_array(&map, &sec, "backends"),
542 connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
543 });
544 i += 1;
545 }
546
547 let mut udp_proxies = Vec::new();
549 let mut i = 0;
550 loop {
551 let sec = format!("udp_proxy[{}]", i);
552 if !section_exists(&map, &sec) {
553 break;
554 }
555 udp_proxies.push(UdpProxyConfig {
556 name: get_str(&map, &sec, "name"),
557 listen: get_str(&map, &sec, "listen"),
558 backends: get_array(&map, &sec, "backends"),
559 reply_timeout_ms: get_u64(&map, &sec, "reply_timeout_ms", 5000),
560 buffer_size: get_u64(&map, &sec, "buffer_size", 65536) as usize,
561 });
562 i += 1;
563 }
564
565 let mut ws_proxies = Vec::new();
567 let mut i = 0;
568 loop {
569 let sec = format!("ws_proxy[{}]", i);
570 if !section_exists(&map, &sec) {
571 break;
572 }
573 let hc_sec = format!("{}.health_check", sec);
574 let health_check = if section_exists(&map, &hc_sec) {
575 Some(HealthCheckConfig {
576 path: {
577 let p = get_str(&map, &hc_sec, "path");
578 if p.is_empty() { "/health".to_string() } else { p }
579 },
580 interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
581 timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
582 healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
583 unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
584 })
585 } else {
586 None
587 };
588 ws_proxies.push(WsProxyConfig {
589 name: get_str(&map, &sec, "name"),
590 listen: get_str(&map, &sec, "listen"),
591 backends: get_array(&map, &sec, "backends"),
592 connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
593 read_timeout_ms: get_u64(&map, &sec, "read_timeout_ms", 30000),
594 health_check,
595 });
596 i += 1;
597 }
598
599 let global_middleware = parse_middleware_config(&map, "middleware", usize::MAX);
601
602 let authz = map.keys().any(|k| k == "authz" || k.starts_with("authz.")).then(|| {
604 let mut type_sections: Vec<&String> = map.keys().filter(|k| k.starts_with("authz.type.")).collect();
605 type_sections.sort();
606 AuthzConfig {
607 store: get_str(&map, "authz", "store"),
608 tuples: get_array(&map, "authz", "tuples"),
609 types: type_sections
610 .into_iter()
611 .map(|sec| {
612 let relations = map[sec].iter().map(|(k, _)| (k.clone(), get_str(&map, sec, k))).collect();
613 (sec["authz.type.".len()..].to_string(), relations)
614 })
615 .collect(),
616 }
617 });
618
619 ProxyConfig {
620 upstreams,
621 routes,
622 tcp_proxies,
623 udp_proxies,
624 ws_proxies,
625 global_middleware,
626 authz,
627 }
628 }
629}
630
631fn parse_middleware_config(
634 map: &parser::SectionMap,
635 mw_sec: &str,
636 route_idx: usize,
637) -> MiddlewareConfig {
638 use parser::{get_array, get_str, get_u32, get_u64, section_exists};
639
640 let rl_sec = format!("{}.rate_limit", mw_sec);
641 let rate_limit = if section_exists(map, &rl_sec) {
642 Some(RateLimitConfig {
643 max_requests: get_u32(map, &rl_sec, "max_requests", 1000),
644 window_secs: get_u64(map, &rl_sec, "window_secs", 60),
645 })
646 } else {
647 None
648 };
649
650 let c_sec = format!("{}.cache", mw_sec);
651 let cache = if section_exists(map, &c_sec) {
652 Some(CacheConfig {
653 ttl_secs: get_u64(map, &c_sec, "ttl_secs", 60),
654 vary_by: get_array(map, &c_sec, "vary_by"),
655 dir: get_str(map, &c_sec, "dir"),
656 max_size: get_str(map, &c_sec, "max_size"),
657 purge_token_env: get_str(map, &c_sec, "purge_token_env"),
658 })
659 } else {
660 None
661 };
662
663 let a_sec = format!("{}.auth", mw_sec);
664 let auth = if section_exists(map, &a_sec) {
665 let auth_type = get_str(map, &a_sec, "type");
666 match auth_type.as_str() {
667 "bearer" => Some(AuthConfig::Bearer {
668 token_env: get_str(map, &a_sec, "token_env"),
669 }),
670 "jwt" => Some(AuthConfig::Jwt {
671 secret_env: get_str(map, &a_sec, "secret_env"),
672 }),
673 "basic" => Some(AuthConfig::Basic {
674 htpasswd_file: get_str(map, &a_sec, "htpasswd_file"),
675 }),
676 _ => None,
677 }
678 } else {
679 None
680 };
681
682 let rewrite_request = collect_rewrite_rules(map, mw_sec, "request");
688 let rewrite_response = collect_rewrite_rules(map, mw_sec, "response");
689
690 let ip_sec = format!("{}.ip_filter", mw_sec);
691 let ip_allow = if section_exists(map, &ip_sec) {
692 get_array(map, &ip_sec, "allow")
693 } else {
694 vec![]
695 };
696 let ip_deny = if section_exists(map, &ip_sec) {
697 get_array(map, &ip_sec, "deny")
698 } else {
699 vec![]
700 };
701
702 let _ = route_idx; let timeout_ms = match get_u64(map, mw_sec, "timeout_ms", 0) {
708 0 => None,
709 ms => Some(ms),
710 };
711
712 let max_body_size = match get_u64(map, mw_sec, "max_body_size", 0) {
716 0 => None,
717 n => Some(n),
718 };
719
720 let w_sec = format!("{}.wasm", mw_sec);
721 let wasm = section_exists(map, &w_sec).then(|| WasmConfig {
722 module: get_str(map, &w_sec, "module"),
723 fuel: get_u64(map, &w_sec, "fuel", 50_000_000),
724 memory_mb: get_u64(map, &w_sec, "memory_mb", 16),
725 config: get_str(map, &w_sec, "config"),
726 });
727
728 let m_sec = format!("{}.mirror", mw_sec);
729 let mirror = section_exists(map, &m_sec).then(|| MirrorConfig {
730 upstream: get_str(map, &m_sec, "upstream"),
731 backends: get_array(map, &m_sec, "backends"),
732 percent: {
733 let raw = get_str(map, &m_sec, "percent");
734 if raw.is_empty() { 100.0 } else { raw.parse::<f64>().unwrap_or(f64::NAN) }
735 },
736 });
737
738 MiddlewareConfig {
739 rate_limit, cache, auth, rewrite_request, rewrite_response, ip_allow, ip_deny,
740 timeout_ms, max_body_size, wasm, mirror,
741 early_hints: get_array(map, mw_sec, "early_hints"),
742 authz: section_exists(map, &format!("{mw_sec}.authz")).then(|| RouteAuthzConfig {
743 relation: get_str(map, &format!("{mw_sec}.authz"), "relation"),
744 object: get_str(map, &format!("{mw_sec}.authz"), "object"),
745 }),
746 }
747}
748
749fn collect_rewrite_rules(
751 map: &parser::SectionMap,
752 mw_sec: &str,
753 direction: &str,
754) -> Vec<RewriteRuleConfig> {
755 use parser::{get_str, get_u64};
756
757 let mut rules = Vec::new();
758 let mut j = 0;
759 loop {
760 let rsec = format!("{}.rewrite.{}[{}]", mw_sec, direction, j);
761 if !parser::section_exists(map, &rsec) {
762 break;
763 }
764 let code_val = get_u64(map, &rsec, "code", 0);
765 rules.push(RewriteRuleConfig {
766 type_: get_str(map, &rsec, "type"),
767 name: {
768 let v = get_str(map, &rsec, "name");
769 if v.is_empty() { None } else { Some(v) }
770 },
771 value: {
772 let v = get_str(map, &rsec, "value");
773 if v.is_empty() { None } else { Some(v) }
774 },
775 prefix: {
776 let v = get_str(map, &rsec, "prefix");
777 if v.is_empty() { None } else { Some(v) }
778 },
779 from: {
780 let v = get_str(map, &rsec, "from");
781 if v.is_empty() { None } else { Some(v) }
782 },
783 to: {
784 let v = get_str(map, &rsec, "to");
785 if v.is_empty() { None } else { Some(v) }
786 },
787 code: if code_val == 0 { None } else { Some(code_val as u16) },
788 reason: {
789 let v = get_str(map, &rsec, "reason");
790 if v.is_empty() { None } else { Some(v) }
791 },
792 });
793 j += 1;
794 }
795 rules
796}
797
798pub(crate) fn config_file_path() -> String {
799 std::env::var("RWS_CONFIG_FILE").unwrap_or_else(|_| "rws.config.toml".to_string())
800}
801
802pub(crate) struct CompiledRoute {
806 pub(crate) matcher: RouteMatcher,
807 pub(crate) handler: Arc<dyn Application + Send + Sync>,
809 #[cfg_attr(not(feature = "http2"), allow(dead_code))]
813 pub(crate) async_proxy: Option<Arc<DynamicProxy>>,
814}
815
816#[derive(Clone, Default)]
818pub(crate) struct RouteMatcher {
819 pub(crate) host: Option<String>,
821 pub(crate) path_prefix: Option<String>,
823 pub(crate) path_exact: Option<String>,
825 pub(crate) method: Option<String>,
827 pub(crate) content_type_prefix: Option<String>,
829}
830
831impl RouteMatcher {
832 pub(crate) fn from_match_config(cfg: &MatchConfig) -> Self {
833 let (path_prefix, path_exact) = match &cfg.path {
834 Some(p) if p.ends_with('*') => {
835 let stripped = p.trim_end_matches('*').to_string();
837 (Some(stripped), None)
838 }
839 Some(p) => (None, Some(p.clone())),
840 None => (None, None),
841 };
842 let content_type_prefix = cfg.content_type.as_ref().map(|ct| {
843 if ct.ends_with('*') {
844 ct.trim_end_matches('*').to_string()
845 } else {
846 ct.clone()
847 }
848 });
849 RouteMatcher {
850 host: cfg.host.clone(),
851 path_prefix,
852 path_exact,
853 method: cfg.method.clone(),
854 content_type_prefix,
855 }
856 }
857
858 pub(crate) fn matches(&self, request: &Request, conn: &ConnectionInfo) -> bool {
860 if let Some(ref expected_host) = self.host {
862 let actual_host = conn
863 .sni_hostname
864 .as_deref()
865 .or_else(|| {
866 request
867 .headers
868 .iter()
869 .find(|h| h.name.eq_ignore_ascii_case("host"))
870 .map(|h| h.value.as_ref())
871 })
872 .unwrap_or("");
873 if !host_matches(expected_host, actual_host) {
874 return false;
875 }
876 }
877
878 if let Some(ref m) = self.method {
880 if request.method.to_uppercase() != m.as_str() {
881 return false;
882 }
883 }
884
885 let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
887 if let Some(ref prefix) = self.path_prefix {
888 if !path.starts_with(prefix.as_str()) {
889 return false;
890 }
891 } else if let Some(ref exact) = self.path_exact {
892 if path != exact.as_str() {
893 return false;
894 }
895 }
896
897 if let Some(ref ct_prefix) = self.content_type_prefix {
899 let actual_ct = request
900 .headers
901 .iter()
902 .find(|h| h.name.eq_ignore_ascii_case("content-type"))
903 .map(|h| h.value.as_ref())
904 .unwrap_or("");
905 if !actual_ct.starts_with(ct_prefix.as_str()) {
906 return false;
907 }
908 }
909
910 true
911 }
912}
913
914#[derive(Clone)]
921pub struct ConfigDrivenApp {
922 current: Arc<RwLock<Arc<Generation>>>,
923 reload_lock: Arc<std::sync::Mutex<()>>,
925}
926
927struct Generation {
929 dispatch: RouteDispatch,
930 entry: Option<Arc<dyn Application + Send + Sync>>,
933 config: ProxyConfig,
934 source: Option<String>,
936 health_stop: Arc<AtomicBool>,
938}
939
940impl Generation {
941 fn new(routes: Arc<Vec<CompiledRoute>>, fallback: App, config: ProxyConfig, health_stop: Arc<AtomicBool>, source: Option<String>) -> Self {
942 let dispatch = RouteDispatch { routes, fallback };
943 let global = &config.global_middleware;
944 let entry = if global.is_empty() && global.early_hints.is_empty() {
945 None
946 } else {
947 let mut entry = builder::apply_middleware(arc_app(dispatch.clone()), global);
948 if !global.early_hints.is_empty() {
951 let layer = crate::early_hints::EarlyHintsLayer::new(global.early_hints.iter().cloned());
952 entry = arc_app(crate::middleware::WithMiddleware::new(builder::ArcApp(entry)).wrap(layer));
953 }
954 Some(entry)
955 };
956 Generation { dispatch, entry, config, source, health_stop }
957 }
958}
959
960#[derive(Clone)]
962struct RouteDispatch {
963 routes: Arc<Vec<CompiledRoute>>,
964 fallback: App,
969}
970
971impl Application for RouteDispatch {
972 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
973 for route in self.routes.iter() {
974 if route.matcher.matches(request, conn) {
975 return route.handler.execute(request, conn);
976 }
977 }
978 self.fallback.execute(request, conn)
979 }
980
981 fn streams_body(&self, request: &Request, conn: &ConnectionInfo) -> bool {
982 match self.routes.iter().find(|r| r.matcher.matches(request, conn)) {
983 Some(route) => route.handler.streams_body(request, conn),
984 None => self.fallback.streams_body(request, conn),
985 }
986 }
987}
988
989impl MiddlewareConfig {
990 pub fn is_empty(&self) -> bool {
994 self.rate_limit.is_none() && self.cache.is_none() && self.auth.is_none()
995 && self.rewrite_request.is_empty() && self.rewrite_response.is_empty()
996 && self.ip_allow.is_empty() && self.ip_deny.is_empty()
997 && self.timeout_ms.is_none() && self.max_body_size.is_none() && self.wasm.is_none()
998 && self.authz.is_none()
999 }
1000}
1001
1002#[derive(Debug, Clone, PartialEq)]
1004pub enum ReloadOutcome {
1005 Unchanged,
1007 Reloaded { routes: usize, warnings: Vec<String> },
1010}
1011
1012impl ConfigDrivenApp {
1013 pub(crate) fn new(routes: Vec<CompiledRoute>, config: ProxyConfig, health_stop: Arc<AtomicBool>, source: Option<String>) -> Self {
1014 use crate::core::New;
1015 let generation = Generation::new(Arc::new(routes), App::new(), config, health_stop, source);
1016 ConfigDrivenApp {
1017 current: Arc::new(RwLock::new(Arc::new(generation))),
1018 reload_lock: Arc::new(std::sync::Mutex::new(())),
1019 }
1020 }
1021
1022 fn current(&self) -> Arc<Generation> {
1023 Arc::clone(&self.current.read().unwrap_or_else(|e| e.into_inner()))
1024 }
1025
1026 pub fn with_config(self, config: ServerConfig) -> Self {
1043 {
1044 let mut slot = self.current.write().unwrap_or_else(|e| e.into_inner());
1045 let old = Arc::clone(&slot);
1046 *slot = Arc::new(Generation::new(
1047 Arc::clone(&old.dispatch.routes),
1048 App::with_config(config),
1049 old.config.clone(),
1050 Arc::clone(&old.health_stop),
1051 old.source.clone(),
1052 ));
1053 }
1054 self
1055 }
1056
1057 pub fn reload_from_str(&self, toml: &str) -> Result<ReloadOutcome, crate::config_check::ConfigReport> {
1067 let _serialized = self.reload_lock.lock().unwrap_or_else(|e| e.into_inner());
1068 let old = self.current();
1069 if old.source.as_deref() == Some(toml) {
1070 return Ok(ReloadOutcome::Unchanged);
1071 }
1072 let config = ProxyConfig::from_str(toml);
1073 let mut report = crate::config_check::ConfigReport::default();
1074 validate::check_unknown_keys(toml, &mut report);
1075 config.validate(&mut report);
1076 if !report.is_ok() {
1077 return Err(report);
1078 }
1079
1080 let mut warnings = report.warnings;
1081 let listeners = |c: &ProxyConfig| format!("{:?}{:?}{:?}", c.tcp_proxies, c.udp_proxies, c.ws_proxies);
1082 if listeners(&config) != listeners(&old.config) {
1083 warnings.push("[[tcp_proxy]] / [[udp_proxy]] / [[ws_proxy]] changes need a restart; the old listeners keep running".to_string());
1084 }
1085
1086 let health_stop = Arc::new(AtomicBool::new(false));
1087 let routes = builder::compile_routes(&config, &health_stop);
1088 let route_count = routes.len();
1089 let generation = Generation::new(Arc::new(routes), old.dispatch.fallback.clone(), config, health_stop, Some(toml.to_string()));
1090 let replaced = {
1091 let mut slot = self.current.write().unwrap_or_else(|e| e.into_inner());
1092 std::mem::replace(&mut *slot, Arc::new(generation))
1093 };
1094 replaced.health_stop.store(true, Ordering::Relaxed);
1095 Ok(ReloadOutcome::Reloaded { routes: route_count, warnings })
1096 }
1097
1098 pub fn reload_from_file(&self) {
1103 let path = config_file_path();
1104 let toml = match std::fs::read_to_string(&path) {
1105 Ok(toml) => toml,
1106 Err(e) => {
1107 eprintln!("[proxy_config] reload: cannot read {path}: {e} — keeping the running routes");
1108 crate::config_reload::report_error(format!("cannot read {path}: {e}"));
1109 return;
1110 }
1111 };
1112 match self.reload_from_str(&toml) {
1113 Ok(ReloadOutcome::Unchanged) => {}
1114 Ok(ReloadOutcome::Reloaded { routes, warnings }) => {
1115 for w in &warnings {
1116 eprintln!("[proxy_config] Warning: {w}");
1117 }
1118 println!("[proxy_config] reloaded {path}: {routes} route{}", if routes == 1 { "" } else { "s" });
1119 }
1120 Err(report) => {
1121 for e in &report.errors {
1122 eprintln!("[proxy_config] reload ERROR: {e}");
1123 crate::config_reload::report_error(e.clone());
1124 }
1125 eprintln!("[proxy_config] {path} has errors — keeping the running routes");
1126 }
1127 }
1128 }
1129}
1130
1131fn normalized(request: &Request) -> std::borrow::Cow<'_, Request> {
1134 match normalize_request_target(&request.request_uri) {
1135 std::borrow::Cow::Borrowed(_) => std::borrow::Cow::Borrowed(request),
1136 std::borrow::Cow::Owned(target) => {
1137 let mut owned = request.clone();
1138 owned.request_uri = target;
1139 std::borrow::Cow::Owned(owned)
1140 }
1141 }
1142}
1143
1144pub(crate) fn normalize_request_target(target: &str) -> std::borrow::Cow<'_, str> {
1152 let (path, query) = match target.find('?') {
1153 Some(i) => (&target[..i], &target[i..]),
1154 None => (target, ""),
1155 };
1156 if !path.starts_with('/') || !(path.contains('%') || path.contains("//") || path.contains("/.")) {
1157 return std::borrow::Cow::Borrowed(target);
1158 }
1159 let bytes = path.as_bytes();
1160 let mut decoded = String::with_capacity(path.len());
1161 let mut i = 0;
1162 while i < bytes.len() {
1163 let hex = |b: u8| (b as char).to_digit(16);
1164 if bytes[i] == b'%' && i + 2 < bytes.len() {
1165 if let (Some(h), Some(l)) = (hex(bytes[i + 1]), hex(bytes[i + 2])) {
1166 let value = (h * 16 + l) as u8;
1167 if value.is_ascii_alphanumeric() || matches!(value, b'-' | b'.' | b'_' | b'~') {
1168 decoded.push(value as char);
1169 } else {
1170 decoded.push('%');
1171 decoded.push(bytes[i + 1].to_ascii_uppercase() as char);
1172 decoded.push(bytes[i + 2].to_ascii_uppercase() as char);
1173 }
1174 i += 3;
1175 continue;
1176 }
1177 }
1178 if bytes[i] == b'%' {
1182 decoded.push_str("%25");
1183 i += 1;
1184 continue;
1185 }
1186 let ch = path[i..].chars().next().unwrap_or('\u{FFFD}');
1188 decoded.push(ch);
1189 i += ch.len_utf8().max(1);
1190 }
1191 let mut segments: Vec<&str> = Vec::new();
1192 let parts: Vec<&str> = decoded.split('/').collect();
1193 let last = parts.len().saturating_sub(1);
1194 let mut trailing_slash = false;
1195 for (n, segment) in parts.iter().enumerate() {
1196 match *segment {
1197 "" => trailing_slash = n == last,
1198 "." => trailing_slash = n == last,
1199 ".." => {
1200 segments.pop();
1201 trailing_slash = n == last;
1202 }
1203 s => {
1204 segments.push(s);
1205 trailing_slash = false;
1206 }
1207 }
1208 }
1209 let mut out = String::with_capacity(decoded.len() + query.len());
1210 out.push('/');
1211 out.push_str(&segments.join("/"));
1212 if trailing_slash && !segments.is_empty() {
1213 out.push('/');
1214 }
1215 out.push_str(query);
1216 if out == target {
1217 std::borrow::Cow::Borrowed(target)
1218 } else {
1219 std::borrow::Cow::Owned(out)
1220 }
1221}
1222
1223fn host_matches(expected: &str, actual: &str) -> bool {
1226 let actual = if expected.contains(':') {
1227 actual
1228 } else if let Some(rest) = actual.strip_prefix('[') {
1229 rest.split(']').next().unwrap_or(rest)
1231 } else {
1232 actual.rsplit_once(':').map_or(actual, |(host, port)| if port.bytes().all(|b| b.is_ascii_digit()) { host } else { actual })
1233 };
1234 let expected = expected.trim_start_matches('[').trim_end_matches(']');
1235 actual.eq_ignore_ascii_case(expected)
1236}
1237
1238impl Application for ConfigDrivenApp {
1239 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1240 let request = normalized(request);
1241 let generation = self.current();
1242 match &generation.entry {
1243 Some(entry) => entry.execute(&request, conn),
1244 None => generation.dispatch.execute(&request, conn),
1245 }
1246 }
1247
1248 fn streams_body(&self, request: &Request, conn: &ConnectionInfo) -> bool {
1251 let request = normalized(request);
1252 let generation = self.current();
1253 match &generation.entry {
1254 Some(entry) => entry.streams_body(&request, conn),
1255 None => generation.dispatch.streams_body(&request, conn),
1256 }
1257 }
1258
1259 fn runs_inline(&self, request: &Request, conn: &ConnectionInfo) -> bool {
1263 let request = normalized(request);
1264 let generation = self.current();
1265 generation.entry.is_none()
1266 && !generation.dispatch.routes.iter().any(|r| r.matcher.matches(&request, conn))
1267 && App::is_inline_request(&request)
1268 }
1269
1270 #[cfg(feature = "http2")]
1274 fn execute_async<'a>(&'a self, request: &'a Request, conn: &'a ConnectionInfo) -> Option<crate::application::AsyncResponse<'a>> {
1275 let generation = self.current();
1276 if generation.entry.is_some() {
1277 return None;
1278 }
1279 let request = normalized(request);
1280 let route = generation.dispatch.routes.iter().find(|r| r.matcher.matches(&request, conn))?;
1281 let proxy = Arc::clone(route.async_proxy.as_ref()?);
1282 Some(Box::pin(async move { proxy.execute_async(&request, conn).await }))
1283 }
1284}
1285
1286#[cfg(feature = "http2")]
1292pub(crate) struct H2Route {
1293 live: Arc<RwLock<Vec<String>>>,
1294 tls: bool,
1295 trust: Option<Arc<crate::proxy::UpstreamTls>>,
1296 connect_timeout: std::time::Duration,
1297 read_timeout: std::time::Duration,
1298 counter: std::sync::atomic::AtomicUsize,
1299}
1300
1301#[cfg(feature = "http2")]
1302impl H2Route {
1303 pub(crate) fn new(
1304 live: Arc<RwLock<Vec<String>>>,
1305 tls: bool,
1306 trust: Option<Arc<crate::proxy::UpstreamTls>>,
1307 connect_timeout_ms: u64,
1308 read_timeout_ms: u64,
1309 ) -> H2Route {
1310 H2Route {
1311 live,
1312 tls,
1313 trust,
1314 connect_timeout: std::time::Duration::from_millis(connect_timeout_ms),
1315 read_timeout: std::time::Duration::from_millis(read_timeout_ms),
1316 counter: std::sync::atomic::AtomicUsize::new(0),
1317 }
1318 }
1319}
1320
1321#[cfg(feature = "http2")]
1322impl Application for H2Route {
1323 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1324 use crate::middleware::Middleware;
1325 crate::proxy::GrpcWebLayer::new().handle(request, conn, &H2Forward(self))
1326 }
1327
1328 fn streams_body(&self, _request: &Request, _conn: &ConnectionInfo) -> bool {
1329 true
1330 }
1331}
1332
1333#[cfg(feature = "http2")]
1335struct H2Forward<'a>(&'a H2Route);
1336
1337#[cfg(feature = "http2")]
1338impl Application for H2Forward<'_> {
1339 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1340 let route = self.0;
1341 let backends = route.live.read().unwrap_or_else(|e| e.into_inner()).clone();
1342 if backends.is_empty() {
1343 return Ok(crate::proxy::bad_gateway());
1344 }
1345 let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { backends.len() };
1347 let start = route.counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1348 for attempt in 0..attempts {
1349 let url = &backends[(start + attempt) % backends.len()];
1350 let result = crate::proxy::forward_h2(
1351 request, crate::proxy::Peer::of(conn), url, route.tls, route.trust.as_deref(), route.connect_timeout, route.read_timeout,
1352 );
1353 match result {
1354 Ok(response) => return Ok(response),
1355 Err(e) => eprintln!("grpc route: {e}"),
1356 }
1357 }
1358 Ok(crate::proxy::bad_gateway())
1359 }
1360}
1361
1362#[derive(Clone, Copy)]
1367pub(crate) struct NullApp;
1368
1369impl Application for NullApp {
1370 fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1371 let mut r = Response::new();
1372 r.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
1373 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
1374 Ok(r)
1375 }
1376}
1377
1378use std::collections::HashMap;
1381use std::collections::hash_map::DefaultHasher;
1382use std::hash::{Hash, Hasher};
1383use std::sync::atomic::AtomicUsize;
1384use std::time::{Duration, SystemTime, UNIX_EPOCH};
1385
1386#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1390pub(crate) enum LoadBalanceStrategy {
1391 RoundRobin,
1392 Random,
1393 IpHash,
1394 LeastConnections,
1395}
1396
1397impl LoadBalanceStrategy {
1398 fn parse(s: &str) -> Self {
1399 match s {
1400 "random" => LoadBalanceStrategy::Random,
1401 "ip_hash" => LoadBalanceStrategy::IpHash,
1402 "least_connections" => LoadBalanceStrategy::LeastConnections,
1403 _ => LoadBalanceStrategy::RoundRobin,
1404 }
1405 }
1406}
1407
1408#[derive(Clone)]
1416pub(crate) struct DynamicProxy {
1417 live: Arc<RwLock<Vec<String>>>,
1418 counter: Arc<AtomicUsize>,
1419 connect_timeout: Duration,
1420 read_timeout: Duration,
1421 strip_prefix: Option<Arc<String>>,
1422 add_prefix: Option<Arc<String>>,
1423 tls: bool,
1424 strategy: LoadBalanceStrategy,
1425 connections: Arc<RwLock<HashMap<String, Arc<AtomicUsize>>>>,
1426 trust: Option<Arc<crate::proxy::UpstreamTls>>,
1428 upstream: Arc<String>,
1430 policy: Option<Arc<upstream::UpstreamPolicy>>,
1432 mirror: Option<crate::proxy::mirror::Mirror>,
1435 early_hints: Option<Arc<Vec<String>>>,
1437 #[cfg(feature = "http3")]
1440 h3: Option<Arc<std::sync::Mutex<HashMap<String, std::time::Instant>>>>,
1441}
1442
1443impl DynamicProxy {
1444 pub(crate) fn with_trust(mut self, trust: Option<Arc<crate::proxy::UpstreamTls>>) -> Self {
1446 self.trust = trust;
1447 self
1448 }
1449
1450 pub(crate) fn with_early_hints(mut self, links: &[String]) -> Self {
1455 self.early_hints = (!links.is_empty()).then(|| Arc::new(links.to_vec()));
1456 self
1457 }
1458
1459 #[cfg_attr(not(feature = "http3"), allow(unused_mut))]
1463 pub(crate) fn with_h3(mut self, enabled: bool) -> Self {
1464 #[cfg(feature = "http3")]
1465 {
1466 self.h3 = (enabled && self.tls).then(Default::default);
1467 }
1468 #[cfg(not(feature = "http3"))]
1469 let _ = enabled;
1470 self
1471 }
1472
1473 #[cfg(feature = "http3")]
1476 fn h3_target(&self, backend: &str) -> Option<Arc<rustls::ClientConfig>> {
1477 let tcp_until = self.h3.as_ref()?;
1478 let mut tcp_until = tcp_until.lock().unwrap_or_else(|e| e.into_inner());
1479 if let Some(until) = tcp_until.get(backend) {
1480 if std::time::Instant::now() < *until {
1481 return None;
1482 }
1483 tcp_until.remove(backend);
1484 }
1485 Some(match &self.trust {
1486 Some(trust) => trust.client_config(),
1487 None => crate::proxy::tls_client_config(),
1488 })
1489 }
1490
1491 #[cfg(feature = "http3")]
1493 fn fall_back_to_tcp(&self, backend: &str, error: &str) {
1494 if let Some(tcp_until) = &self.h3 {
1495 tcp_until.lock().unwrap_or_else(|e| e.into_inner()).insert(backend.to_string(), std::time::Instant::now() + H3_FALLBACK);
1496 }
1497 upstream::observe_h3_fallback(&self.upstream);
1498 eprintln!("upstream {}: HTTP/3 to {backend} failed ({error}); using TCP for {} s", self.upstream, H3_FALLBACK.as_secs());
1499 }
1500
1501 pub(crate) fn with_mirror(mut self, mirror: Option<crate::proxy::mirror::Mirror>) -> Self {
1503 self.mirror = mirror;
1504 self
1505 }
1506
1507 pub(crate) fn with_policy(mut self, upstream: &str, policy: Option<Arc<upstream::UpstreamPolicy>>) -> Self {
1508 self.upstream = Arc::new(upstream.to_string());
1509 self.policy = policy;
1510 self
1511 }
1512
1513 pub(crate) fn new(
1514 live: Arc<RwLock<Vec<String>>>,
1515 connect_timeout_ms: u64,
1516 read_timeout_ms: u64,
1517 strip_prefix: Option<String>,
1518 add_prefix: Option<String>,
1519 tls: bool,
1520 strategy: String,
1521 ) -> Self {
1522 DynamicProxy {
1523 live,
1524 counter: Arc::new(AtomicUsize::new(0)),
1525 connect_timeout: Duration::from_millis(connect_timeout_ms),
1526 read_timeout: Duration::from_millis(read_timeout_ms),
1527 strip_prefix: strip_prefix.map(Arc::new),
1528 add_prefix: add_prefix.map(Arc::new),
1529 tls,
1530 strategy: LoadBalanceStrategy::parse(&strategy),
1531 connections: Arc::new(RwLock::new(HashMap::new())),
1532 trust: None,
1533 upstream: Arc::new(String::new()),
1534 policy: None,
1535 mirror: None,
1536 early_hints: None,
1537 #[cfg(feature = "http3")]
1538 h3: None,
1539 }
1540 }
1541
1542 fn pick(&self, candidates: &[String], client_ip: &str) -> String {
1544 let policy = self.policy.as_deref().filter(|p| p.weighted());
1545 let idx = match self.strategy {
1546 LoadBalanceStrategy::RoundRobin => match policy {
1547 Some(policy) => return policy.weighted_round_robin(candidates).unwrap_or_else(|| candidates[0].clone()),
1548 None => self.counter.fetch_add(1, Ordering::Relaxed) % candidates.len(),
1549 },
1550 LoadBalanceStrategy::Random => {
1551 let nanos = SystemTime::now()
1552 .duration_since(UNIX_EPOCH)
1553 .map(|d| d.subsec_nanos())
1554 .unwrap_or(0) as usize;
1555 let salt = self.counter.fetch_add(1, Ordering::Relaxed);
1556 let roll = nanos.wrapping_add(salt.wrapping_mul(2_654_435_761));
1557 match policy {
1558 Some(policy) => policy.weighted_index(candidates, roll),
1559 None => roll % candidates.len(),
1560 }
1561 }
1562 LoadBalanceStrategy::IpHash => {
1563 let mut hasher = DefaultHasher::new();
1564 client_ip.hash(&mut hasher);
1565 (hasher.finish() as usize) % candidates.len()
1566 }
1567 LoadBalanceStrategy::LeastConnections => {
1568 let connections = self.connections.read().unwrap();
1569 let load = |backend: &String| connections.get(backend).map(|c| c.load(Ordering::Relaxed)).unwrap_or(0) as u64;
1570 let weight = |backend: &String| self.policy.as_deref().map_or(1, |p| p.weight(backend)) as u64;
1571 candidates
1573 .iter()
1574 .enumerate()
1575 .min_by(|(_, a), (_, b)| (load(a) * weight(b)).cmp(&(load(b) * weight(a))))
1576 .map(|(i, _)| i)
1577 .unwrap_or(0)
1578 }
1579 };
1580 candidates[idx].clone()
1581 }
1582
1583 fn next_backend(&self, request: &Request, client_ip: &str, tried: &[String]) -> Result<String, bool> {
1588 let mut candidates: Vec<String> = self.live.read().unwrap().iter().filter(|b| !tried.contains(b)).cloned().collect();
1589 if !candidates.is_empty() {
1590 candidates.retain(|b| !upstream::is_drained(&self.upstream, b));
1591 if candidates.is_empty() {
1592 return Err(true);
1594 }
1595 }
1596 let Some(policy) = self.policy.as_deref() else {
1597 return if candidates.is_empty() { Err(false) } else { Ok(self.pick(&candidates, client_ip)) };
1598 };
1599 if let Some(sticky) = &policy.sticky {
1600 if let Some(id) = upstream::cookie_value(request, &sticky.cookie) {
1601 if let Some(backend) = candidates.iter().find(|b| upstream::backend_id(b) == id).cloned() {
1602 if policy.admits(&backend) {
1603 return Ok(backend);
1604 }
1605 candidates.retain(|b| *b != backend);
1606 }
1607 }
1608 }
1609 let ejected = policy.any_open(&candidates);
1610 while !candidates.is_empty() {
1611 let backend = self.pick(&candidates, client_ip);
1612 if policy.admits(&backend) {
1613 return Ok(backend);
1614 }
1615 candidates.retain(|b| *b != backend);
1616 }
1617 Err(ejected)
1618 }
1619
1620 fn connection_counter(&self, backend: &str) -> Arc<AtomicUsize> {
1623 if let Some(counter) = self.connections.read().unwrap().get(backend) {
1624 return Arc::clone(counter);
1625 }
1626 let mut connections = self.connections.write().unwrap();
1627 Arc::clone(
1628 connections
1629 .entry(backend.to_string())
1630 .or_insert_with(|| Arc::new(AtomicUsize::new(0))),
1631 )
1632 }
1633}
1634
1635struct ConnectionGuard {
1638 counter: Arc<AtomicUsize>,
1639}
1640
1641impl Drop for ConnectionGuard {
1642 fn drop(&mut self) {
1643 self.counter.fetch_sub(1, Ordering::Relaxed);
1644 }
1645}
1646
1647struct Prepared<'a> {
1650 backend: String,
1651 host: String,
1652 port: u16,
1653 request: std::borrow::Cow<'a, Request>,
1654 _guard: Option<ConnectionGuard>,
1655}
1656
1657enum Attempt {
1659 Done(Response),
1661 Failed(Option<Response>),
1664}
1665
1666impl DynamicProxy {
1667 fn prepare<'a>(&self, request: &'a Request, conn: &ConnectionInfo, tried: &[String]) -> Result<Prepared<'a>, Response> {
1672 let backend = self.next_backend(request, &conn.client.ip, tried).map_err(|ejected| if ejected { service_unavailable() } else { bad_gateway() })?;
1673
1674 let guard = if self.strategy == LoadBalanceStrategy::LeastConnections {
1675 let counter = self.connection_counter(&backend);
1676 counter.fetch_add(1, Ordering::Relaxed);
1677 Some(ConnectionGuard { counter })
1678 } else {
1679 None
1680 };
1681
1682 let (host, port, _) = crate::proxy_config::health::parse_backend_url(&backend).ok_or_else(bad_gateway)?;
1683
1684 let request = if self.strip_prefix.is_some() || self.add_prefix.is_some() {
1686 let mut req_clone = request.clone();
1687 if let Some(ref sp) = self.strip_prefix {
1688 if let Some(stripped) = req_clone.request_uri.strip_prefix(sp.as_str()) {
1689 req_clone.request_uri = if stripped.is_empty() || !stripped.starts_with('/') {
1690 format!("/{}", stripped)
1691 } else {
1692 stripped.to_string()
1693 };
1694 }
1695 }
1696 if let Some(ref ap) = self.add_prefix {
1697 req_clone.request_uri = format!("{}{}", ap, req_clone.request_uri);
1698 }
1699 std::borrow::Cow::Owned(req_clone)
1700 } else {
1701 std::borrow::Cow::Borrowed(request)
1702 };
1703
1704 Ok(Prepared { backend, host, port, request, _guard: guard })
1705 }
1706
1707 fn settle(&self, backend: &str, result: Result<Response, ()>, started: std::time::Instant) -> Attempt {
1709 let status = result.as_ref().ok().map(|r| r.status_code);
1710 upstream::observe(&self.upstream, backend, status, started);
1711 let failed = status.is_none_or(|s| matches!(s, 502..=504));
1712 if let Some(policy) = &self.policy {
1713 policy.record(backend, failed);
1714 }
1715 match result {
1716 Ok(response) if self.policy.as_deref().is_some_and(|p| p.retries_status(response.status_code)) => Attempt::Failed(Some(response)),
1717 Ok(response) => Attempt::Done(response),
1718 Err(()) => Attempt::Failed(None),
1719 }
1720 }
1721
1722 fn attempts(&self, request: &Request) -> u32 {
1724 match self.policy.as_deref().and_then(|p| p.retry.as_ref()) {
1725 Some(retry) if upstream::replayable(request) => 1 + retry.attempts,
1726 _ => 1,
1727 }
1728 }
1729
1730 fn stick(&self, request: &Request, backend: &str, response: &mut Response) {
1732 let Some(sticky) = self.policy.as_deref().and_then(|p| p.sticky.as_ref()) else { return };
1733 let id = upstream::backend_id(backend);
1734 if upstream::cookie_value(request, &sticky.cookie).as_deref() == Some(id.as_str()) {
1735 return;
1736 }
1737 let max_age = sticky.max_age_secs.map(|s| format!("; Max-Age={s}")).unwrap_or_default();
1738 response.headers.push(crate::header::Header {
1739 name: "Set-Cookie".into(),
1740 value: format!("{}={id}; Path=/; HttpOnly; SameSite=Lax{max_age}", sticky.cookie).into(),
1741 });
1742 }
1743
1744 #[cfg(feature = "http2")]
1747 pub(crate) async fn execute_async(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1748 if let Some(mirror) = &self.mirror {
1749 mirror.send(request, conn);
1750 }
1751 if let Some(links) = &self.early_hints {
1752 crate::early_hints::send_from(crate::early_hints::Source::Configured, links);
1753 }
1754 let mut tried: Vec<String> = Vec::new();
1755 let mut last: Option<Response> = None;
1756 for attempt in 0..self.attempts(request) {
1757 let prepared = match self.prepare(request, conn, &tried) {
1758 Ok(p) => p,
1759 Err(response) => return Ok(last.unwrap_or(response)),
1760 };
1761 if attempt > 0 {
1762 upstream::observe_retry(&self.upstream);
1763 }
1764 let tls = self.tls.then(|| match &self.trust {
1765 Some(trust) => trust.async_tls(),
1766 None => crate::proxy::async_upstream::AsyncTls {
1767 config: crate::proxy::tls_client_config(),
1768 pool: crate::proxy::async_upstream::default_tls_pool(),
1769 },
1770 });
1771 let started = std::time::Instant::now();
1772 #[cfg(feature = "http3")]
1773 if let Some(config) = self.h3_target(&prepared.backend) {
1774 let target = crate::proxy::h3_upstream::Target {
1775 host: &prepared.host,
1776 port: prepared.port,
1777 tls: &config,
1778 connect_timeout: self.connect_timeout,
1779 read_timeout: self.read_timeout,
1780 };
1781 match crate::proxy::h3_upstream::forward_async(&prepared.request, crate::proxy::Peer::of(conn), &target).await {
1782 Err(crate::proxy::h3_upstream::H3Error::Connect(e)) => self.fall_back_to_tcp(&prepared.backend, &e),
1783 result => {
1784 match self.settle(&prepared.backend, result.map_err(|_| ()), started) {
1785 Attempt::Done(mut response) => {
1786 self.stick(request, &prepared.backend, &mut response);
1787 return Ok(response);
1788 }
1789 Attempt::Failed(response) => last = response,
1790 }
1791 tried.push(prepared.backend.clone());
1792 continue;
1793 }
1794 }
1795 }
1796 let result = crate::proxy::async_upstream::forward(
1797 &prepared.request,
1798 crate::proxy::Peer::of(conn),
1799 &prepared.host,
1800 prepared.port,
1801 tls,
1802 self.connect_timeout,
1803 self.read_timeout,
1804 )
1805 .await
1806 .map_err(|_| ());
1807 match self.settle(&prepared.backend, result, started) {
1808 Attempt::Done(mut response) => {
1809 self.stick(request, &prepared.backend, &mut response);
1810 return Ok(response);
1811 }
1812 Attempt::Failed(response) => last = response,
1813 }
1814 tried.push(prepared.backend.clone());
1815 }
1816 Ok(last.unwrap_or_else(bad_gateway))
1817 }
1818
1819 pub(crate) fn supports_async(&self) -> bool {
1821 cfg!(feature = "http2")
1822 }
1823}
1824
1825impl Application for DynamicProxy {
1826 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1827 if let Some(mirror) = &self.mirror {
1828 mirror.send(request, conn);
1829 }
1830 if let Some(links) = &self.early_hints {
1831 crate::early_hints::send_from(crate::early_hints::Source::Configured, links);
1832 }
1833 let mut tried: Vec<String> = Vec::new();
1834 let mut last: Option<Response> = None;
1835 for attempt in 0..self.attempts(request) {
1836 let prepared = match self.prepare(request, conn, &tried) {
1837 Ok(p) => p,
1838 Err(response) => return Ok(last.unwrap_or(response)),
1839 };
1840 if attempt > 0 {
1841 upstream::observe_retry(&self.upstream);
1842 }
1843 let started = std::time::Instant::now();
1846 #[cfg(feature = "http3")]
1847 if let Some(config) = self.h3_target(&prepared.backend) {
1848 let target = crate::proxy::h3_upstream::Target {
1849 host: &prepared.host,
1850 port: prepared.port,
1851 tls: &config,
1852 connect_timeout: self.connect_timeout,
1853 read_timeout: self.read_timeout,
1854 };
1855 match crate::proxy::h3_upstream::forward(&prepared.request, crate::proxy::Peer::of(conn), &target) {
1856 Err(crate::proxy::h3_upstream::H3Error::Connect(e)) => self.fall_back_to_tcp(&prepared.backend, &e),
1857 result => {
1858 match self.settle(&prepared.backend, result.map_err(|_| ()), started) {
1859 Attempt::Done(mut response) => {
1860 self.stick(request, &prepared.backend, &mut response);
1861 return Ok(response);
1862 }
1863 Attempt::Failed(response) => last = response,
1864 }
1865 tried.push(prepared.backend.clone());
1866 continue;
1867 }
1868 }
1869 }
1870 let result = crate::proxy::forward_upstream_trusting(
1871 crate::proxy::shared_pool(),
1872 &prepared.request,
1873 crate::proxy::Peer::of(conn),
1874 &prepared.host,
1875 prepared.port,
1876 self.tls,
1877 self.trust.as_deref(),
1878 self.connect_timeout,
1879 self.read_timeout,
1880 )
1881 .map_err(|_| ());
1882 match self.settle(&prepared.backend, result, started) {
1883 Attempt::Done(mut response) => {
1884 self.stick(request, &prepared.backend, &mut response);
1885 return Ok(response);
1886 }
1887 Attempt::Failed(response) => last = response,
1888 }
1889 tried.push(prepared.backend.clone());
1890 }
1891 Ok(last.unwrap_or_else(bad_gateway))
1892 }
1893}
1894
1895#[cfg(feature = "http3")]
1897pub(crate) const H3_FALLBACK: Duration = Duration::from_secs(300);
1898
1899fn service_unavailable() -> Response {
1900 use crate::mime_type::MimeType;
1901 use crate::range::Range;
1902 let mut r = Response::new();
1903 r.status_code = *STATUS_CODE_REASON_PHRASE.n503_service_unavailable.status_code;
1904 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n503_service_unavailable.reason_phrase.to_string();
1905 r.headers.push(crate::header::Header { name: "Retry-After".into(), value: "5".into() });
1906 r.content_range_list = vec![Range::get_content_range(b"503 Service Unavailable: every backend is out of rotation".to_vec(), MimeType::TEXT_PLAIN.to_string())];
1907 r
1908}
1909
1910fn bad_gateway() -> Response {
1911 use crate::mime_type::MimeType;
1912 use crate::range::Range;
1913 let cr = Range::get_content_range(
1914 b"502 Bad Gateway".to_vec(),
1915 MimeType::TEXT_PLAIN.to_string(),
1916 );
1917 let mut r = Response::new();
1918 r.status_code = *STATUS_CODE_REASON_PHRASE.n502_bad_gateway.status_code;
1919 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n502_bad_gateway.reason_phrase.to_string();
1920 r.content_range_list = vec![cr];
1921 r
1922}
1923
1924#[derive(Clone)]
1930pub(crate) struct RedirectAdapter {
1931 location_template: Arc<String>,
1932 status: i16,
1933 reason: Arc<String>,
1934}
1935
1936impl RedirectAdapter {
1937 pub(crate) fn new(location: String, status: u16) -> Self {
1938 let (code, reason) = redirect_status(status);
1939 RedirectAdapter {
1940 location_template: Arc::new(location),
1941 status: code,
1942 reason: Arc::new(reason),
1943 }
1944 }
1945}
1946
1947fn redirect_status(code: u16) -> (i16, String) {
1948 let phrase = match code {
1949 301 => STATUS_CODE_REASON_PHRASE.n301_moved_permanently.reason_phrase,
1950 302 => STATUS_CODE_REASON_PHRASE.n302_found.reason_phrase,
1951 307 => STATUS_CODE_REASON_PHRASE.n307_temporary_redirect.reason_phrase,
1952 308 => STATUS_CODE_REASON_PHRASE.n308_permanent_redirect.reason_phrase,
1953 _ => "Redirect",
1954 };
1955 (code as i16, phrase.to_string())
1956}
1957
1958impl Application for RedirectAdapter {
1959 fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1960 let location = self
1961 .location_template
1962 .replace("$path", &request.request_uri);
1963 use crate::header::Header;
1964 let mut r = Response::new();
1965 r.status_code = self.status;
1966 r.reason_phrase = self.reason.as_ref().clone();
1967 r.headers.push(Header { name: "Location".into(), value: location.into() });
1968 Ok(r)
1969 }
1970}
1971
1972#[derive(Clone)]
1976pub(crate) struct RespondAdapter {
1977 status: i16,
1978 reason: Arc<String>,
1979 body: Arc<Vec<u8>>,
1980 content_type: Arc<String>,
1981}
1982
1983impl RespondAdapter {
1984 pub(crate) fn new(status: u16, body: String, content_type: String) -> Self {
1985 use crate::response::STATUS_CODE_REASON_PHRASE;
1986 let reason = match status {
1987 200 => STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string(),
1988 201 => STATUS_CODE_REASON_PHRASE.n201_created.reason_phrase.to_string(),
1989 204 => STATUS_CODE_REASON_PHRASE.n204_no_content.reason_phrase.to_string(),
1990 400 => STATUS_CODE_REASON_PHRASE.n400_bad_request.reason_phrase.to_string(),
1991 401 => STATUS_CODE_REASON_PHRASE.n401_unauthorized.reason_phrase.to_string(),
1992 403 => STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string(),
1993 404 => STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string(),
1994 500 => STATUS_CODE_REASON_PHRASE.n500_internal_server_error.reason_phrase.to_string(),
1995 _ => "OK".to_string(),
1996 };
1997 RespondAdapter {
1998 status: status as i16,
1999 reason: Arc::new(reason),
2000 body: Arc::new(body.into_bytes()),
2001 content_type: Arc::new(content_type),
2002 }
2003 }
2004}
2005
2006impl Application for RespondAdapter {
2007 fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
2008 use crate::range::Range;
2009 let mut r = Response::new();
2010 r.status_code = self.status;
2011 r.reason_phrase = self.reason.as_ref().clone();
2012 if !self.body.is_empty() {
2013 r.content_range_list = vec![Range::get_content_range(
2014 self.body.as_ref().clone(),
2015 self.content_type.as_ref().clone(),
2016 )];
2017 }
2018 Ok(r)
2019 }
2020}
2021
2022#[derive(Clone)]
2031pub(crate) struct StaticAdapter {
2032 root: Arc<std::path::PathBuf>,
2033 index: Arc<Vec<String>>,
2034}
2035
2036impl StaticAdapter {
2037 pub(crate) fn new(root: String, index: Vec<String>) -> Self {
2038 let index = if index.is_empty() { vec!["index.html".to_string()] } else { index };
2039 StaticAdapter {
2040 root: Arc::new(std::path::PathBuf::from(root)),
2041 index: Arc::new(index),
2042 }
2043 }
2044
2045 fn resolve(&self, request_uri: &str) -> Option<std::path::PathBuf> {
2049 let raw_path = request_uri.split('?').next().unwrap_or(request_uri);
2050 let decoded = crate::url::URL::percent_decode(raw_path);
2051
2052 if decoded.split('/').any(|segment| segment == "..") {
2053 return None;
2054 }
2055
2056 let relative = decoded.trim_start_matches('/');
2057 Some(self.root.join(relative))
2058 }
2059}
2060
2061impl Application for StaticAdapter {
2062 fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
2063 let mut response = Response::new();
2064
2065 let not_found = |mut response: Response| {
2066 response.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
2067 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
2068 response
2069 };
2070
2071 let candidate = match self.resolve(&request.request_uri) {
2072 Some(p) => p,
2073 None => {
2074 response.status_code = *STATUS_CODE_REASON_PHRASE.n403_forbidden.status_code;
2075 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string();
2076 return Ok(response);
2077 }
2078 };
2079
2080 let mut file_path = candidate;
2081 if file_path.is_dir() {
2082 let indexed = self
2083 .index
2084 .iter()
2085 .map(|name| file_path.join(name))
2086 .find(|p| p.is_file());
2087
2088 file_path = match indexed {
2089 Some(p) => p,
2090 None => return Ok(not_found(response)),
2091 };
2092 }
2093
2094 if !file_path.is_file() {
2095 return Ok(not_found(response));
2096 }
2097
2098 if let (Ok(root_canon), Ok(file_canon)) =
2101 (self.root.canonicalize(), file_path.canonicalize())
2102 {
2103 if !file_canon.starts_with(&root_canon) {
2104 return Ok(not_found(response));
2105 }
2106 }
2107
2108 let path_str = match file_path.to_str() {
2109 Some(s) => s,
2110 None => return Ok(not_found(response)),
2111 };
2112
2113 match crate::range::Range::get_content_range_of_a_file(path_str) {
2114 Ok(content_range) => {
2115 response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
2116 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
2117 response.content_range_list = vec![content_range];
2118 Ok(response)
2119 }
2120 Err(_) => Ok(not_found(response)),
2121 }
2122 }
2123}
2124
2125pub(crate) struct PerRouteRateLimit(pub(crate) Arc<crate::rate_limit::RateLimiter>);
2129
2130impl crate::middleware::Middleware for PerRouteRateLimit {
2131 fn handle(
2132 &self,
2133 request: &Request,
2134 conn: &ConnectionInfo,
2135 next: &dyn Application,
2136 ) -> Result<Response, String> {
2137 use crate::error::{AppError, IntoResponse};
2138 if self.0.check(&conn.client.ip) {
2139 next.execute(request, conn)
2140 } else {
2141 let mut response = AppError::TooManyRequests.into_response();
2142 let retry_after = self.0.retry_after(&conn.client.ip);
2143 response.headers.push(Header {
2144 name: Header::_RETRY_AFTER.into(),
2145 value: retry_after.to_string().into(),
2146 });
2147 Ok(response)
2148 }
2149 }
2150}
2151
2152pub(crate) struct PerRouteMaxBodySize(pub(crate) u64);
2167
2168impl crate::middleware::Middleware for PerRouteMaxBodySize {
2169 fn handle(
2170 &self,
2171 request: &Request,
2172 conn: &ConnectionInfo,
2173 next: &dyn Application,
2174 ) -> Result<Response, String> {
2175 use crate::error::{AppError, IntoResponse};
2176 if request.body.len() as u64 > self.0 {
2177 Ok(AppError::PayloadTooLarge(format!(
2178 "request body of {} bytes exceeds this route's {}-byte limit",
2179 request.body.len(),
2180 self.0,
2181 ))
2182 .into_response())
2183 } else {
2184 next.execute(request, conn)
2185 }
2186 }
2187}
2188
2189pub(crate) struct BearerAuthMiddleware {
2193 pub(crate) token: Arc<String>,
2194}
2195
2196impl crate::middleware::Middleware for BearerAuthMiddleware {
2197 fn handle(
2198 &self,
2199 request: &Request,
2200 conn: &ConnectionInfo,
2201 next: &dyn Application,
2202 ) -> Result<Response, String> {
2203 use crate::error::{AppError, IntoResponse};
2204 let expected = format!("Bearer {}", self.token);
2205 let authorized = request
2206 .headers
2207 .iter()
2208 .any(|h| h.name.eq_ignore_ascii_case("authorization") && constant_time_eq(h.value.as_bytes(), expected.as_bytes()));
2209 if authorized {
2210 next.execute(request, conn)
2211 } else {
2212 Ok(AppError::Unauthorized.into_response())
2213 }
2214 }
2215}
2216
2217pub(crate) fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
2220 if a.len() != b.len() {
2221 return false;
2222 }
2223 a.iter().zip(b).fold(0u8, |diff, (x, y)| diff | (x ^ y)) == 0
2224}
2225
2226pub(crate) struct AuthMisconfigured;
2229
2230pub(crate) fn cache_max_bytes(max_size: &str) -> Option<u64> {
2232 if max_size.trim().is_empty() {
2233 Some(256 * 1024 * 1024)
2234 } else {
2235 crate::log::access_sink::parse_size(max_size.trim()).filter(|n| *n > 0)
2236 }
2237}
2238
2239pub(crate) struct PluginMisconfigured;
2242
2243impl crate::middleware::Middleware for PluginMisconfigured {
2244 fn handle(&self, _request: &Request, _conn: &ConnectionInfo, _next: &dyn Application) -> Result<Response, String> {
2245 Ok(Response::text(
2246 STATUS_CODE_REASON_PHRASE.n503_service_unavailable,
2247 "503 Service Unavailable: this route's plugin is misconfigured",
2248 ))
2249 }
2250}
2251
2252impl crate::middleware::Middleware for AuthMisconfigured {
2253 fn handle(
2254 &self,
2255 _request: &Request,
2256 _conn: &ConnectionInfo,
2257 _next: &dyn Application,
2258 ) -> Result<Response, String> {
2259 Ok(Response::text(
2260 STATUS_CODE_REASON_PHRASE.n503_service_unavailable,
2261 "503 Service Unavailable: authentication for this route is misconfigured",
2262 ))
2263 }
2264}
2265
2266pub(crate) fn arc_app<A: Application + Send + Sync + 'static>(
2270 a: A,
2271) -> Arc<dyn Application + Send + Sync> {
2272 Arc::new(a)
2273}
2274
2275pub fn build_from_file() -> (ConfigDrivenApp, Vec<std::thread::JoinHandle<()>>) {
2280 builder::build_from_file()
2281}