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