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