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