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}
94
95#[derive(Debug, Clone)]
96pub struct HealthCheckConfig {
97 pub path: String,
98 pub interval_secs: u64,
99 pub timeout_ms: u64,
100 pub healthy_threshold: u32,
101 pub unhealthy_threshold: u32,
102}
103
104#[derive(Debug, Clone)]
105pub struct RouteConfig {
106 pub name: String,
107 pub match_: MatchConfig,
108 pub action: ActionConfig,
109 pub middleware: MiddlewareConfig,
110}
111
112#[derive(Debug, Clone, Default)]
113pub struct MatchConfig {
114 pub host: Option<String>,
115 pub path: Option<String>,
116 pub method: Option<String>,
117 pub content_type: Option<String>,
118}
119
120#[derive(Debug, Clone)]
121pub enum ActionConfig {
122 Proxy {
123 upstream: String,
124 connect_timeout_ms: u64,
125 read_timeout_ms: u64,
126 strip_path_prefix: Option<String>,
127 add_path_prefix: Option<String>,
128 },
129 Grpc {
130 upstream: String,
131 connect_timeout_ms: u64,
132 read_timeout_ms: u64,
133 },
134 Static {
135 root: String,
136 index: Vec<String>,
137 },
138 Redirect {
139 location: String,
140 status: u16,
141 },
142 Respond {
143 status: u16,
144 body: String,
145 content_type: String,
146 },
147 Mcp(Box<mcp_gateway::McpGatewayConfig>),
149 Llm(Box<llm::LlmConfig>),
151 Unknown(String),
152}
153
154#[derive(Debug, Clone, Default)]
155pub struct MiddlewareConfig {
156 pub rate_limit: Option<RateLimitConfig>,
157 pub cache: Option<CacheConfig>,
158 pub auth: Option<AuthConfig>,
159 pub rewrite_request: Vec<RewriteRuleConfig>,
160 pub rewrite_response: Vec<RewriteRuleConfig>,
161 pub ip_allow: Vec<String>,
162 pub ip_deny: Vec<String>,
163 pub timeout_ms: Option<u64>,
168 pub max_body_size: Option<u64>,
184 pub wasm: Option<WasmConfig>,
187 pub mirror: Option<MirrorConfig>,
190}
191
192#[derive(Debug, Clone, Default, PartialEq)]
194pub struct MirrorConfig {
195 pub upstream: String,
197 pub backends: Vec<String>,
199 pub percent: f64,
201}
202
203#[derive(Debug, Clone, Default, PartialEq)]
205pub struct WasmConfig {
206 pub module: String,
208 pub fuel: u64,
209 pub memory_mb: u64,
210 pub config: String,
212}
213
214#[derive(Debug, Clone)]
215pub struct RateLimitConfig {
216 pub max_requests: u32,
217 pub window_secs: u64,
218}
219
220#[derive(Debug, Clone, Default)]
221pub struct CacheConfig {
222 pub ttl_secs: u64,
223 pub vary_by: Vec<String>,
224 pub dir: String,
226 pub max_size: String,
228 pub purge_token_env: String,
231}
232
233#[derive(Debug, Clone)]
234pub enum AuthConfig {
235 Basic { htpasswd_file: String },
240 Jwt { secret_env: String },
243 Bearer { token_env: String },
244}
245
246#[derive(Debug, Clone, Default)]
247pub struct RewriteRuleConfig {
248 pub type_: String,
249 pub name: Option<String>,
250 pub value: Option<String>,
251 pub prefix: Option<String>,
252 pub from: Option<String>,
253 pub to: Option<String>,
254 pub code: Option<u16>,
255 pub reason: Option<String>,
256}
257
258#[derive(Debug, Clone)]
259pub struct TcpProxyConfig {
260 pub name: String,
261 pub listen: String,
262 pub backends: Vec<String>,
263 pub connect_timeout_ms: u64,
264}
265
266#[derive(Debug, Clone)]
267pub struct UdpProxyConfig {
268 pub name: String,
269 pub listen: String,
270 pub backends: Vec<String>,
271 pub reply_timeout_ms: u64,
272 pub buffer_size: usize,
273}
274
275#[derive(Debug, Clone)]
276pub struct WsProxyConfig {
277 pub name: String,
278 pub listen: String,
279 pub backends: Vec<String>,
280 pub connect_timeout_ms: u64,
281 pub read_timeout_ms: u64,
282 pub health_check: Option<HealthCheckConfig>,
289}
290
291fn merge_action_keys(map: &mut parser::SectionMap, action_section: &str, action_type: &str) {
300 if action_type.is_empty() {
301 return;
302 }
303 let short_form: Vec<(String, String)> = match map.get(action_section) {
304 Some(entries) => entries.iter().filter(|(k, _)| k != "type").cloned().collect(),
305 None => return,
306 };
307 let nested = map.entry(format!("{action_section}.{action_type}")).or_default();
308 for (key, value) in short_form {
309 if !nested.iter().any(|(k, _)| *k == key) {
310 nested.push((key, value));
311 }
312 }
313}
314
315impl ProxyConfig {
316 pub fn is_proxy_mode() -> bool {
320 let path = config_file_path();
321 match std::fs::read_to_string(&path) {
322 Ok(contents) => {
323 contents.contains("[[route]]") || contents.contains("[[upstream]]")
324 }
325 Err(_) => false,
326 }
327 }
328
329 pub fn load() -> Self {
331 let path = config_file_path();
332 let contents = std::fs::read_to_string(&path).unwrap_or_default();
333 Self::from_str(&contents)
334 }
335
336 pub fn from_str(toml: &str) -> Self {
338 use parser::{get_array, get_str, get_u32, get_u64, section_exists};
339
340 let mut map = parser::parse(toml);
341
342 let mut upstreams = Vec::new();
344 let mut i = 0;
345 loop {
346 let sec = format!("upstream[{}]", i);
347 if !section_exists(&map, &sec) {
348 break;
349 }
350 let name = get_str(&map, &sec, "name");
351 let backends = get_array(&map, &sec, "backends");
352 let strategy = {
353 let s = get_str(&map, &sec, "strategy");
354 if s.is_empty() { "round_robin".to_string() } else { s }
355 };
356 let hc_sec = format!("{}.health_check", sec);
357 let health_check = if section_exists(&map, &hc_sec) {
358 Some(HealthCheckConfig {
359 path: {
360 let p = get_str(&map, &hc_sec, "path");
361 if p.is_empty() { "/health".to_string() } else { p }
362 },
363 interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
364 timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
365 healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
366 unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
367 })
368 } else {
369 None
370 };
371 let tls = backends.iter().any(|b| ["https://", "h2s://", "grpcs://"].iter().any(|s| b.starts_with(s)));
372 let tls_ca_file = Some(get_str(&map, &sec, "tls_ca_file")).filter(|v| !v.is_empty());
373 let tls_client_cert_file = Some(get_str(&map, &sec, "tls_client_cert_file")).filter(|v| !v.is_empty());
374 let tls_client_key_file = Some(get_str(&map, &sec, "tls_client_key_file")).filter(|v| !v.is_empty());
375 let weights: Vec<u32> = get_array(&map, &sec, "weights").iter().map(|w| w.trim().parse().unwrap_or(0)).collect();
376 let sticky_sec = format!("{}.sticky", sec);
377 let sticky = section_exists(&map, &sticky_sec).then(|| StickyConfig {
378 cookie: Some(get_str(&map, &sticky_sec, "cookie")).filter(|c| !c.is_empty()).unwrap_or_else(|| "rws_backend".to_string()),
379 max_age_secs: Some(get_u64(&map, &sticky_sec, "max_age_secs", 0)).filter(|s| *s > 0),
380 });
381 let cb_sec = format!("{}.circuit_breaker", sec);
382 let circuit_breaker = section_exists(&map, &cb_sec).then(|| BreakerConfig {
383 failure_threshold: get_u32(&map, &cb_sec, "failure_threshold", 5),
384 recovery_secs: get_u64(&map, &cb_sec, "recovery_secs", 30),
385 half_open_probes: get_u32(&map, &cb_sec, "half_open_probes", 1),
386 });
387 let retry_sec = format!("{}.retry", sec);
388 let retry = section_exists(&map, &retry_sec).then(|| {
389 let status: Vec<i16> = get_array(&map, &retry_sec, "status").iter().filter_map(|s| s.trim().parse().ok()).collect();
390 RetryConfig { attempts: get_u32(&map, &retry_sec, "attempts", 1), status: if status.is_empty() { vec![502, 503, 504] } else { status } }
391 });
392 upstreams.push(UpstreamConfig {
393 name, backends, strategy, health_check, tls, tls_ca_file, tls_client_cert_file, tls_client_key_file,
394 weights, sticky, circuit_breaker, retry,
395 });
396 i += 1;
397 }
398
399 let mut routes = Vec::new();
401 let mut i = 0;
402 loop {
403 let sec = format!("route[{}]", i);
404 if !section_exists(&map, &sec) {
405 break;
406 }
407 let name = get_str(&map, &sec, "name");
408
409 let m_sec = format!("{}.match", sec);
411 let match_ = MatchConfig {
412 host: {
413 let h = get_str(&map, &m_sec, "host");
414 if h.is_empty() { None } else { Some(h) }
415 },
416 path: {
417 let p = get_str(&map, &m_sec, "path");
418 if p.is_empty() { None } else { Some(p) }
419 },
420 method: {
421 let m = get_str(&map, &m_sec, "method");
422 if m.is_empty() { None } else { Some(m.to_uppercase()) }
423 },
424 content_type: {
425 let c = get_str(&map, &m_sec, "content_type");
426 if c.is_empty() { None } else { Some(c) }
427 },
428 };
429
430 let a_sec = format!("{}.action", sec);
432 let action_type = get_str(&map, &a_sec, "type");
433 merge_action_keys(&mut map, &a_sec, &action_type);
434 let action = match action_type.as_str() {
435 "proxy" => {
436 let p_sec = format!("{}.action.proxy", sec);
437 ActionConfig::Proxy {
438 upstream: get_str(&map, &p_sec, "upstream"),
439 connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
440 read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
441 strip_path_prefix: {
442 let v = get_str(&map, &p_sec, "strip_path_prefix");
443 if v.is_empty() { None } else { Some(v) }
444 },
445 add_path_prefix: {
446 let v = get_str(&map, &p_sec, "add_path_prefix");
447 if v.is_empty() { None } else { Some(v) }
448 },
449 }
450 }
451 "grpc" => {
452 let p_sec = format!("{}.action.grpc", sec);
453 ActionConfig::Grpc {
454 upstream: get_str(&map, &p_sec, "upstream"),
455 connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
456 read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
457 }
458 }
459 "static" => {
460 let s_sec = format!("{}.action.static", sec);
461 ActionConfig::Static {
462 root: get_str(&map, &s_sec, "root"),
463 index: get_array(&map, &s_sec, "index"),
464 }
465 }
466 "redirect" => {
467 let r_sec = format!("{}.action.redirect", sec);
468 ActionConfig::Redirect {
469 location: get_str(&map, &r_sec, "location"),
470 status: get_u64(&map, &r_sec, "status", 301) as u16,
471 }
472 }
473 "respond" => {
474 let r_sec = format!("{}.action.respond", sec);
475 ActionConfig::Respond {
476 status: get_u64(&map, &r_sec, "status", 200) as u16,
477 body: get_str(&map, &r_sec, "body"),
478 content_type: {
479 let ct = get_str(&map, &r_sec, "content_type");
480 if ct.is_empty() { "text/plain".to_string() } else { ct }
481 },
482 }
483 }
484 "mcp" => ActionConfig::Mcp(Box::new(mcp_gateway::parse(&map, &a_sec))),
485 "llm" => ActionConfig::Llm(Box::new(llm::parse(&map, &a_sec))),
486 other => ActionConfig::Unknown(other.to_string()),
487 };
488
489 let mw_sec = format!("{}.middleware", sec);
491 let middleware = parse_middleware_config(&map, &mw_sec, i);
492
493 routes.push(RouteConfig { name, match_, action, middleware });
494 i += 1;
495 }
496
497 let mut tcp_proxies = Vec::new();
499 let mut i = 0;
500 loop {
501 let sec = format!("tcp_proxy[{}]", i);
502 if !section_exists(&map, &sec) {
503 break;
504 }
505 tcp_proxies.push(TcpProxyConfig {
506 name: get_str(&map, &sec, "name"),
507 listen: get_str(&map, &sec, "listen"),
508 backends: get_array(&map, &sec, "backends"),
509 connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
510 });
511 i += 1;
512 }
513
514 let mut udp_proxies = Vec::new();
516 let mut i = 0;
517 loop {
518 let sec = format!("udp_proxy[{}]", i);
519 if !section_exists(&map, &sec) {
520 break;
521 }
522 udp_proxies.push(UdpProxyConfig {
523 name: get_str(&map, &sec, "name"),
524 listen: get_str(&map, &sec, "listen"),
525 backends: get_array(&map, &sec, "backends"),
526 reply_timeout_ms: get_u64(&map, &sec, "reply_timeout_ms", 5000),
527 buffer_size: get_u64(&map, &sec, "buffer_size", 65536) as usize,
528 });
529 i += 1;
530 }
531
532 let mut ws_proxies = Vec::new();
534 let mut i = 0;
535 loop {
536 let sec = format!("ws_proxy[{}]", i);
537 if !section_exists(&map, &sec) {
538 break;
539 }
540 let hc_sec = format!("{}.health_check", sec);
541 let health_check = if section_exists(&map, &hc_sec) {
542 Some(HealthCheckConfig {
543 path: {
544 let p = get_str(&map, &hc_sec, "path");
545 if p.is_empty() { "/health".to_string() } else { p }
546 },
547 interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
548 timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
549 healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
550 unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
551 })
552 } else {
553 None
554 };
555 ws_proxies.push(WsProxyConfig {
556 name: get_str(&map, &sec, "name"),
557 listen: get_str(&map, &sec, "listen"),
558 backends: get_array(&map, &sec, "backends"),
559 connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
560 read_timeout_ms: get_u64(&map, &sec, "read_timeout_ms", 30000),
561 health_check,
562 });
563 i += 1;
564 }
565
566 let global_middleware = parse_middleware_config(&map, "middleware", usize::MAX);
568
569 ProxyConfig {
570 upstreams,
571 routes,
572 tcp_proxies,
573 udp_proxies,
574 ws_proxies,
575 global_middleware,
576 }
577 }
578}
579
580fn parse_middleware_config(
583 map: &parser::SectionMap,
584 mw_sec: &str,
585 route_idx: usize,
586) -> MiddlewareConfig {
587 use parser::{get_array, get_str, get_u32, get_u64, section_exists};
588
589 let rl_sec = format!("{}.rate_limit", mw_sec);
590 let rate_limit = if section_exists(map, &rl_sec) {
591 Some(RateLimitConfig {
592 max_requests: get_u32(map, &rl_sec, "max_requests", 1000),
593 window_secs: get_u64(map, &rl_sec, "window_secs", 60),
594 })
595 } else {
596 None
597 };
598
599 let c_sec = format!("{}.cache", mw_sec);
600 let cache = if section_exists(map, &c_sec) {
601 Some(CacheConfig {
602 ttl_secs: get_u64(map, &c_sec, "ttl_secs", 60),
603 vary_by: get_array(map, &c_sec, "vary_by"),
604 dir: get_str(map, &c_sec, "dir"),
605 max_size: get_str(map, &c_sec, "max_size"),
606 purge_token_env: get_str(map, &c_sec, "purge_token_env"),
607 })
608 } else {
609 None
610 };
611
612 let a_sec = format!("{}.auth", mw_sec);
613 let auth = if section_exists(map, &a_sec) {
614 let auth_type = get_str(map, &a_sec, "type");
615 match auth_type.as_str() {
616 "bearer" => Some(AuthConfig::Bearer {
617 token_env: get_str(map, &a_sec, "token_env"),
618 }),
619 "jwt" => Some(AuthConfig::Jwt {
620 secret_env: get_str(map, &a_sec, "secret_env"),
621 }),
622 "basic" => Some(AuthConfig::Basic {
623 htpasswd_file: get_str(map, &a_sec, "htpasswd_file"),
624 }),
625 _ => None,
626 }
627 } else {
628 None
629 };
630
631 let rewrite_request = collect_rewrite_rules(map, mw_sec, "request");
637 let rewrite_response = collect_rewrite_rules(map, mw_sec, "response");
638
639 let ip_sec = format!("{}.ip_filter", mw_sec);
640 let ip_allow = if section_exists(map, &ip_sec) {
641 get_array(map, &ip_sec, "allow")
642 } else {
643 vec![]
644 };
645 let ip_deny = if section_exists(map, &ip_sec) {
646 get_array(map, &ip_sec, "deny")
647 } else {
648 vec![]
649 };
650
651 let _ = route_idx; let timeout_ms = match get_u64(map, mw_sec, "timeout_ms", 0) {
657 0 => None,
658 ms => Some(ms),
659 };
660
661 let max_body_size = match get_u64(map, mw_sec, "max_body_size", 0) {
665 0 => None,
666 n => Some(n),
667 };
668
669 let w_sec = format!("{}.wasm", mw_sec);
670 let wasm = section_exists(map, &w_sec).then(|| WasmConfig {
671 module: get_str(map, &w_sec, "module"),
672 fuel: get_u64(map, &w_sec, "fuel", 50_000_000),
673 memory_mb: get_u64(map, &w_sec, "memory_mb", 16),
674 config: get_str(map, &w_sec, "config"),
675 });
676
677 let m_sec = format!("{}.mirror", mw_sec);
678 let mirror = section_exists(map, &m_sec).then(|| MirrorConfig {
679 upstream: get_str(map, &m_sec, "upstream"),
680 backends: get_array(map, &m_sec, "backends"),
681 percent: {
682 let raw = get_str(map, &m_sec, "percent");
683 if raw.is_empty() { 100.0 } else { raw.parse::<f64>().unwrap_or(f64::NAN) }
684 },
685 });
686
687 MiddlewareConfig {
688 rate_limit, cache, auth, rewrite_request, rewrite_response, ip_allow, ip_deny,
689 timeout_ms, max_body_size, wasm, mirror,
690 }
691}
692
693fn collect_rewrite_rules(
695 map: &parser::SectionMap,
696 mw_sec: &str,
697 direction: &str,
698) -> Vec<RewriteRuleConfig> {
699 use parser::{get_str, get_u64};
700
701 let mut rules = Vec::new();
702 let mut j = 0;
703 loop {
704 let rsec = format!("{}.rewrite.{}[{}]", mw_sec, direction, j);
705 if !parser::section_exists(map, &rsec) {
706 break;
707 }
708 let code_val = get_u64(map, &rsec, "code", 0);
709 rules.push(RewriteRuleConfig {
710 type_: get_str(map, &rsec, "type"),
711 name: {
712 let v = get_str(map, &rsec, "name");
713 if v.is_empty() { None } else { Some(v) }
714 },
715 value: {
716 let v = get_str(map, &rsec, "value");
717 if v.is_empty() { None } else { Some(v) }
718 },
719 prefix: {
720 let v = get_str(map, &rsec, "prefix");
721 if v.is_empty() { None } else { Some(v) }
722 },
723 from: {
724 let v = get_str(map, &rsec, "from");
725 if v.is_empty() { None } else { Some(v) }
726 },
727 to: {
728 let v = get_str(map, &rsec, "to");
729 if v.is_empty() { None } else { Some(v) }
730 },
731 code: if code_val == 0 { None } else { Some(code_val as u16) },
732 reason: {
733 let v = get_str(map, &rsec, "reason");
734 if v.is_empty() { None } else { Some(v) }
735 },
736 });
737 j += 1;
738 }
739 rules
740}
741
742pub(crate) fn config_file_path() -> String {
743 std::env::var("RWS_CONFIG_FILE").unwrap_or_else(|_| "rws.config.toml".to_string())
744}
745
746pub(crate) struct CompiledRoute {
750 pub(crate) matcher: RouteMatcher,
751 pub(crate) handler: Arc<dyn Application + Send + Sync>,
753 #[cfg_attr(not(feature = "http2"), allow(dead_code))]
757 pub(crate) async_proxy: Option<Arc<DynamicProxy>>,
758}
759
760#[derive(Clone, Default)]
762pub(crate) struct RouteMatcher {
763 pub(crate) host: Option<String>,
765 pub(crate) path_prefix: Option<String>,
767 pub(crate) path_exact: Option<String>,
769 pub(crate) method: Option<String>,
771 pub(crate) content_type_prefix: Option<String>,
773}
774
775impl RouteMatcher {
776 pub(crate) fn from_match_config(cfg: &MatchConfig) -> Self {
777 let (path_prefix, path_exact) = match &cfg.path {
778 Some(p) if p.ends_with('*') => {
779 let stripped = p.trim_end_matches('*').to_string();
781 (Some(stripped), None)
782 }
783 Some(p) => (None, Some(p.clone())),
784 None => (None, None),
785 };
786 let content_type_prefix = cfg.content_type.as_ref().map(|ct| {
787 if ct.ends_with('*') {
788 ct.trim_end_matches('*').to_string()
789 } else {
790 ct.clone()
791 }
792 });
793 RouteMatcher {
794 host: cfg.host.clone(),
795 path_prefix,
796 path_exact,
797 method: cfg.method.clone(),
798 content_type_prefix,
799 }
800 }
801
802 pub(crate) fn matches(&self, request: &Request, conn: &ConnectionInfo) -> bool {
804 if let Some(ref expected_host) = self.host {
806 let actual_host = conn
807 .sni_hostname
808 .as_deref()
809 .or_else(|| {
810 request
811 .headers
812 .iter()
813 .find(|h| h.name.eq_ignore_ascii_case("host"))
814 .map(|h| h.value.as_ref())
815 })
816 .unwrap_or("");
817 if !host_matches(expected_host, actual_host) {
818 return false;
819 }
820 }
821
822 if let Some(ref m) = self.method {
824 if request.method.to_uppercase() != m.as_str() {
825 return false;
826 }
827 }
828
829 let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
831 if let Some(ref prefix) = self.path_prefix {
832 if !path.starts_with(prefix.as_str()) {
833 return false;
834 }
835 } else if let Some(ref exact) = self.path_exact {
836 if path != exact.as_str() {
837 return false;
838 }
839 }
840
841 if let Some(ref ct_prefix) = self.content_type_prefix {
843 let actual_ct = request
844 .headers
845 .iter()
846 .find(|h| h.name.eq_ignore_ascii_case("content-type"))
847 .map(|h| h.value.as_ref())
848 .unwrap_or("");
849 if !actual_ct.starts_with(ct_prefix.as_str()) {
850 return false;
851 }
852 }
853
854 true
855 }
856}
857
858#[derive(Clone)]
865pub struct ConfigDrivenApp {
866 current: Arc<RwLock<Arc<Generation>>>,
867 reload_lock: Arc<std::sync::Mutex<()>>,
869}
870
871struct Generation {
873 dispatch: RouteDispatch,
874 entry: Option<Arc<dyn Application + Send + Sync>>,
877 config: ProxyConfig,
878 source: Option<String>,
880 health_stop: Arc<AtomicBool>,
882}
883
884impl Generation {
885 fn new(routes: Arc<Vec<CompiledRoute>>, fallback: App, config: ProxyConfig, health_stop: Arc<AtomicBool>, source: Option<String>) -> Self {
886 let dispatch = RouteDispatch { routes, fallback };
887 let entry = if config.global_middleware.is_empty() {
888 None
889 } else {
890 Some(builder::apply_middleware(arc_app(dispatch.clone()), &config.global_middleware))
891 };
892 Generation { dispatch, entry, config, source, health_stop }
893 }
894}
895
896#[derive(Clone)]
898struct RouteDispatch {
899 routes: Arc<Vec<CompiledRoute>>,
900 fallback: App,
905}
906
907impl Application for RouteDispatch {
908 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
909 for route in self.routes.iter() {
910 if route.matcher.matches(request, conn) {
911 return route.handler.execute(request, conn);
912 }
913 }
914 self.fallback.execute(request, conn)
915 }
916
917 fn streams_body(&self, request: &Request, conn: &ConnectionInfo) -> bool {
918 match self.routes.iter().find(|r| r.matcher.matches(request, conn)) {
919 Some(route) => route.handler.streams_body(request, conn),
920 None => self.fallback.streams_body(request, conn),
921 }
922 }
923}
924
925impl MiddlewareConfig {
926 pub fn is_empty(&self) -> bool {
930 self.rate_limit.is_none() && self.cache.is_none() && self.auth.is_none()
931 && self.rewrite_request.is_empty() && self.rewrite_response.is_empty()
932 && self.ip_allow.is_empty() && self.ip_deny.is_empty()
933 && self.timeout_ms.is_none() && self.max_body_size.is_none() && self.wasm.is_none()
934 }
935}
936
937#[derive(Debug, Clone, PartialEq)]
939pub enum ReloadOutcome {
940 Unchanged,
942 Reloaded { routes: usize, warnings: Vec<String> },
945}
946
947impl ConfigDrivenApp {
948 pub(crate) fn new(routes: Vec<CompiledRoute>, config: ProxyConfig, health_stop: Arc<AtomicBool>, source: Option<String>) -> Self {
949 use crate::core::New;
950 let generation = Generation::new(Arc::new(routes), App::new(), config, health_stop, source);
951 ConfigDrivenApp {
952 current: Arc::new(RwLock::new(Arc::new(generation))),
953 reload_lock: Arc::new(std::sync::Mutex::new(())),
954 }
955 }
956
957 fn current(&self) -> Arc<Generation> {
958 Arc::clone(&self.current.read().unwrap_or_else(|e| e.into_inner()))
959 }
960
961 pub fn with_config(self, config: ServerConfig) -> Self {
978 {
979 let mut slot = self.current.write().unwrap_or_else(|e| e.into_inner());
980 let old = Arc::clone(&slot);
981 *slot = Arc::new(Generation::new(
982 Arc::clone(&old.dispatch.routes),
983 App::with_config(config),
984 old.config.clone(),
985 Arc::clone(&old.health_stop),
986 old.source.clone(),
987 ));
988 }
989 self
990 }
991
992 pub fn reload_from_str(&self, toml: &str) -> Result<ReloadOutcome, crate::config_check::ConfigReport> {
1002 let _serialized = self.reload_lock.lock().unwrap_or_else(|e| e.into_inner());
1003 let old = self.current();
1004 if old.source.as_deref() == Some(toml) {
1005 return Ok(ReloadOutcome::Unchanged);
1006 }
1007 let config = ProxyConfig::from_str(toml);
1008 let mut report = crate::config_check::ConfigReport::default();
1009 validate::check_unknown_keys(toml, &mut report);
1010 config.validate(&mut report);
1011 if !report.is_ok() {
1012 return Err(report);
1013 }
1014
1015 let mut warnings = report.warnings;
1016 let listeners = |c: &ProxyConfig| format!("{:?}{:?}{:?}", c.tcp_proxies, c.udp_proxies, c.ws_proxies);
1017 if listeners(&config) != listeners(&old.config) {
1018 warnings.push("[[tcp_proxy]] / [[udp_proxy]] / [[ws_proxy]] changes need a restart; the old listeners keep running".to_string());
1019 }
1020
1021 let health_stop = Arc::new(AtomicBool::new(false));
1022 let routes = builder::compile_routes(&config, &health_stop);
1023 let route_count = routes.len();
1024 let generation = Generation::new(Arc::new(routes), old.dispatch.fallback.clone(), config, health_stop, Some(toml.to_string()));
1025 let replaced = {
1026 let mut slot = self.current.write().unwrap_or_else(|e| e.into_inner());
1027 std::mem::replace(&mut *slot, Arc::new(generation))
1028 };
1029 replaced.health_stop.store(true, Ordering::Relaxed);
1030 Ok(ReloadOutcome::Reloaded { routes: route_count, warnings })
1031 }
1032
1033 pub fn reload_from_file(&self) {
1038 let path = config_file_path();
1039 let toml = match std::fs::read_to_string(&path) {
1040 Ok(toml) => toml,
1041 Err(e) => {
1042 eprintln!("[proxy_config] reload: cannot read {path}: {e} — keeping the running routes");
1043 crate::config_reload::report_error(format!("cannot read {path}: {e}"));
1044 return;
1045 }
1046 };
1047 match self.reload_from_str(&toml) {
1048 Ok(ReloadOutcome::Unchanged) => {}
1049 Ok(ReloadOutcome::Reloaded { routes, warnings }) => {
1050 for w in &warnings {
1051 eprintln!("[proxy_config] Warning: {w}");
1052 }
1053 println!("[proxy_config] reloaded {path}: {routes} route{}", if routes == 1 { "" } else { "s" });
1054 }
1055 Err(report) => {
1056 for e in &report.errors {
1057 eprintln!("[proxy_config] reload ERROR: {e}");
1058 crate::config_reload::report_error(e.clone());
1059 }
1060 eprintln!("[proxy_config] {path} has errors — keeping the running routes");
1061 }
1062 }
1063 }
1064}
1065
1066fn normalized(request: &Request) -> std::borrow::Cow<'_, Request> {
1069 match normalize_request_target(&request.request_uri) {
1070 std::borrow::Cow::Borrowed(_) => std::borrow::Cow::Borrowed(request),
1071 std::borrow::Cow::Owned(target) => {
1072 let mut owned = request.clone();
1073 owned.request_uri = target;
1074 std::borrow::Cow::Owned(owned)
1075 }
1076 }
1077}
1078
1079pub(crate) fn normalize_request_target(target: &str) -> std::borrow::Cow<'_, str> {
1087 let (path, query) = match target.find('?') {
1088 Some(i) => (&target[..i], &target[i..]),
1089 None => (target, ""),
1090 };
1091 if !path.starts_with('/') || !(path.contains('%') || path.contains("//") || path.contains("/.")) {
1092 return std::borrow::Cow::Borrowed(target);
1093 }
1094 let bytes = path.as_bytes();
1095 let mut decoded = String::with_capacity(path.len());
1096 let mut i = 0;
1097 while i < bytes.len() {
1098 let hex = |b: u8| (b as char).to_digit(16);
1099 if bytes[i] == b'%' && i + 2 < bytes.len() {
1100 if let (Some(h), Some(l)) = (hex(bytes[i + 1]), hex(bytes[i + 2])) {
1101 let value = (h * 16 + l) as u8;
1102 if value.is_ascii_alphanumeric() || matches!(value, b'-' | b'.' | b'_' | b'~') {
1103 decoded.push(value as char);
1104 } else {
1105 decoded.push('%');
1106 decoded.push(bytes[i + 1].to_ascii_uppercase() as char);
1107 decoded.push(bytes[i + 2].to_ascii_uppercase() as char);
1108 }
1109 i += 3;
1110 continue;
1111 }
1112 }
1113 if bytes[i] == b'%' {
1117 decoded.push_str("%25");
1118 i += 1;
1119 continue;
1120 }
1121 let ch = path[i..].chars().next().unwrap_or('\u{FFFD}');
1123 decoded.push(ch);
1124 i += ch.len_utf8().max(1);
1125 }
1126 let mut segments: Vec<&str> = Vec::new();
1127 let parts: Vec<&str> = decoded.split('/').collect();
1128 let last = parts.len().saturating_sub(1);
1129 let mut trailing_slash = false;
1130 for (n, segment) in parts.iter().enumerate() {
1131 match *segment {
1132 "" => trailing_slash = n == last,
1133 "." => trailing_slash = n == last,
1134 ".." => {
1135 segments.pop();
1136 trailing_slash = n == last;
1137 }
1138 s => {
1139 segments.push(s);
1140 trailing_slash = false;
1141 }
1142 }
1143 }
1144 let mut out = String::with_capacity(decoded.len() + query.len());
1145 out.push('/');
1146 out.push_str(&segments.join("/"));
1147 if trailing_slash && !segments.is_empty() {
1148 out.push('/');
1149 }
1150 out.push_str(query);
1151 if out == target {
1152 std::borrow::Cow::Borrowed(target)
1153 } else {
1154 std::borrow::Cow::Owned(out)
1155 }
1156}
1157
1158fn host_matches(expected: &str, actual: &str) -> bool {
1161 let actual = if expected.contains(':') {
1162 actual
1163 } else if let Some(rest) = actual.strip_prefix('[') {
1164 rest.split(']').next().unwrap_or(rest)
1166 } else {
1167 actual.rsplit_once(':').map_or(actual, |(host, port)| if port.bytes().all(|b| b.is_ascii_digit()) { host } else { actual })
1168 };
1169 let expected = expected.trim_start_matches('[').trim_end_matches(']');
1170 actual.eq_ignore_ascii_case(expected)
1171}
1172
1173impl Application for ConfigDrivenApp {
1174 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1175 let request = normalized(request);
1176 let generation = self.current();
1177 match &generation.entry {
1178 Some(entry) => entry.execute(&request, conn),
1179 None => generation.dispatch.execute(&request, conn),
1180 }
1181 }
1182
1183 fn streams_body(&self, request: &Request, conn: &ConnectionInfo) -> bool {
1186 let request = normalized(request);
1187 let generation = self.current();
1188 match &generation.entry {
1189 Some(entry) => entry.streams_body(&request, conn),
1190 None => generation.dispatch.streams_body(&request, conn),
1191 }
1192 }
1193
1194 fn runs_inline(&self, request: &Request, conn: &ConnectionInfo) -> bool {
1198 let request = normalized(request);
1199 let generation = self.current();
1200 generation.entry.is_none()
1201 && !generation.dispatch.routes.iter().any(|r| r.matcher.matches(&request, conn))
1202 && App::is_inline_request(&request)
1203 }
1204
1205 #[cfg(feature = "http2")]
1209 fn execute_async<'a>(&'a self, request: &'a Request, conn: &'a ConnectionInfo) -> Option<crate::application::AsyncResponse<'a>> {
1210 let generation = self.current();
1211 if generation.entry.is_some() {
1212 return None;
1213 }
1214 let request = normalized(request);
1215 let route = generation.dispatch.routes.iter().find(|r| r.matcher.matches(&request, conn))?;
1216 let proxy = Arc::clone(route.async_proxy.as_ref()?);
1217 Some(Box::pin(async move { proxy.execute_async(&request, conn).await }))
1218 }
1219}
1220
1221#[cfg(feature = "http2")]
1227pub(crate) struct H2Route {
1228 live: Arc<RwLock<Vec<String>>>,
1229 tls: bool,
1230 trust: Option<Arc<crate::proxy::UpstreamTls>>,
1231 connect_timeout: std::time::Duration,
1232 read_timeout: std::time::Duration,
1233 counter: std::sync::atomic::AtomicUsize,
1234}
1235
1236#[cfg(feature = "http2")]
1237impl H2Route {
1238 pub(crate) fn new(
1239 live: Arc<RwLock<Vec<String>>>,
1240 tls: bool,
1241 trust: Option<Arc<crate::proxy::UpstreamTls>>,
1242 connect_timeout_ms: u64,
1243 read_timeout_ms: u64,
1244 ) -> H2Route {
1245 H2Route {
1246 live,
1247 tls,
1248 trust,
1249 connect_timeout: std::time::Duration::from_millis(connect_timeout_ms),
1250 read_timeout: std::time::Duration::from_millis(read_timeout_ms),
1251 counter: std::sync::atomic::AtomicUsize::new(0),
1252 }
1253 }
1254}
1255
1256#[cfg(feature = "http2")]
1257impl Application for H2Route {
1258 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1259 use crate::middleware::Middleware;
1260 crate::proxy::GrpcWebLayer::new().handle(request, conn, &H2Forward(self))
1261 }
1262
1263 fn streams_body(&self, _request: &Request, _conn: &ConnectionInfo) -> bool {
1264 true
1265 }
1266}
1267
1268#[cfg(feature = "http2")]
1270struct H2Forward<'a>(&'a H2Route);
1271
1272#[cfg(feature = "http2")]
1273impl Application for H2Forward<'_> {
1274 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1275 let route = self.0;
1276 let backends = route.live.read().unwrap_or_else(|e| e.into_inner()).clone();
1277 if backends.is_empty() {
1278 return Ok(crate::proxy::bad_gateway());
1279 }
1280 let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { backends.len() };
1282 let start = route.counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1283 for attempt in 0..attempts {
1284 let url = &backends[(start + attempt) % backends.len()];
1285 let result = crate::proxy::forward_h2(
1286 request, crate::proxy::Peer::of(conn), url, route.tls, route.trust.as_deref(), route.connect_timeout, route.read_timeout,
1287 );
1288 match result {
1289 Ok(response) => return Ok(response),
1290 Err(e) => eprintln!("grpc route: {e}"),
1291 }
1292 }
1293 Ok(crate::proxy::bad_gateway())
1294 }
1295}
1296
1297#[derive(Clone, Copy)]
1302pub(crate) struct NullApp;
1303
1304impl Application for NullApp {
1305 fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1306 let mut r = Response::new();
1307 r.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
1308 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
1309 Ok(r)
1310 }
1311}
1312
1313use std::collections::HashMap;
1316use std::collections::hash_map::DefaultHasher;
1317use std::hash::{Hash, Hasher};
1318use std::sync::atomic::AtomicUsize;
1319use std::time::{Duration, SystemTime, UNIX_EPOCH};
1320
1321#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1325pub(crate) enum LoadBalanceStrategy {
1326 RoundRobin,
1327 Random,
1328 IpHash,
1329 LeastConnections,
1330}
1331
1332impl LoadBalanceStrategy {
1333 fn parse(s: &str) -> Self {
1334 match s {
1335 "random" => LoadBalanceStrategy::Random,
1336 "ip_hash" => LoadBalanceStrategy::IpHash,
1337 "least_connections" => LoadBalanceStrategy::LeastConnections,
1338 _ => LoadBalanceStrategy::RoundRobin,
1339 }
1340 }
1341}
1342
1343#[derive(Clone)]
1351pub(crate) struct DynamicProxy {
1352 live: Arc<RwLock<Vec<String>>>,
1353 counter: Arc<AtomicUsize>,
1354 connect_timeout: Duration,
1355 read_timeout: Duration,
1356 strip_prefix: Option<Arc<String>>,
1357 add_prefix: Option<Arc<String>>,
1358 tls: bool,
1359 strategy: LoadBalanceStrategy,
1360 connections: Arc<RwLock<HashMap<String, Arc<AtomicUsize>>>>,
1361 trust: Option<Arc<crate::proxy::UpstreamTls>>,
1363 upstream: Arc<String>,
1365 policy: Option<Arc<upstream::UpstreamPolicy>>,
1367 mirror: Option<crate::proxy::mirror::Mirror>,
1370}
1371
1372impl DynamicProxy {
1373 pub(crate) fn with_trust(mut self, trust: Option<Arc<crate::proxy::UpstreamTls>>) -> Self {
1375 self.trust = trust;
1376 self
1377 }
1378
1379 pub(crate) fn with_mirror(mut self, mirror: Option<crate::proxy::mirror::Mirror>) -> Self {
1383 self.mirror = mirror;
1384 self
1385 }
1386
1387 pub(crate) fn with_policy(mut self, upstream: &str, policy: Option<Arc<upstream::UpstreamPolicy>>) -> Self {
1388 self.upstream = Arc::new(upstream.to_string());
1389 self.policy = policy;
1390 self
1391 }
1392
1393 pub(crate) fn new(
1394 live: Arc<RwLock<Vec<String>>>,
1395 connect_timeout_ms: u64,
1396 read_timeout_ms: u64,
1397 strip_prefix: Option<String>,
1398 add_prefix: Option<String>,
1399 tls: bool,
1400 strategy: String,
1401 ) -> Self {
1402 DynamicProxy {
1403 live,
1404 counter: Arc::new(AtomicUsize::new(0)),
1405 connect_timeout: Duration::from_millis(connect_timeout_ms),
1406 read_timeout: Duration::from_millis(read_timeout_ms),
1407 strip_prefix: strip_prefix.map(Arc::new),
1408 add_prefix: add_prefix.map(Arc::new),
1409 tls,
1410 strategy: LoadBalanceStrategy::parse(&strategy),
1411 connections: Arc::new(RwLock::new(HashMap::new())),
1412 trust: None,
1413 upstream: Arc::new(String::new()),
1414 policy: None,
1415 mirror: None,
1416 }
1417 }
1418
1419 fn pick(&self, candidates: &[String], client_ip: &str) -> String {
1421 let policy = self.policy.as_deref().filter(|p| p.weighted());
1422 let idx = match self.strategy {
1423 LoadBalanceStrategy::RoundRobin => match policy {
1424 Some(policy) => return policy.weighted_round_robin(candidates).unwrap_or_else(|| candidates[0].clone()),
1425 None => self.counter.fetch_add(1, Ordering::Relaxed) % candidates.len(),
1426 },
1427 LoadBalanceStrategy::Random => {
1428 let nanos = SystemTime::now()
1429 .duration_since(UNIX_EPOCH)
1430 .map(|d| d.subsec_nanos())
1431 .unwrap_or(0) as usize;
1432 let salt = self.counter.fetch_add(1, Ordering::Relaxed);
1433 let roll = nanos.wrapping_add(salt.wrapping_mul(2_654_435_761));
1434 match policy {
1435 Some(policy) => policy.weighted_index(candidates, roll),
1436 None => roll % candidates.len(),
1437 }
1438 }
1439 LoadBalanceStrategy::IpHash => {
1440 let mut hasher = DefaultHasher::new();
1441 client_ip.hash(&mut hasher);
1442 (hasher.finish() as usize) % candidates.len()
1443 }
1444 LoadBalanceStrategy::LeastConnections => {
1445 let connections = self.connections.read().unwrap();
1446 let load = |backend: &String| connections.get(backend).map(|c| c.load(Ordering::Relaxed)).unwrap_or(0) as u64;
1447 let weight = |backend: &String| self.policy.as_deref().map_or(1, |p| p.weight(backend)) as u64;
1448 candidates
1450 .iter()
1451 .enumerate()
1452 .min_by(|(_, a), (_, b)| (load(a) * weight(b)).cmp(&(load(b) * weight(a))))
1453 .map(|(i, _)| i)
1454 .unwrap_or(0)
1455 }
1456 };
1457 candidates[idx].clone()
1458 }
1459
1460 fn next_backend(&self, request: &Request, client_ip: &str, tried: &[String]) -> Result<String, bool> {
1465 let mut candidates: Vec<String> = self.live.read().unwrap().iter().filter(|b| !tried.contains(b)).cloned().collect();
1466 if !candidates.is_empty() {
1467 candidates.retain(|b| !upstream::is_drained(&self.upstream, b));
1468 if candidates.is_empty() {
1469 return Err(true);
1471 }
1472 }
1473 let Some(policy) = self.policy.as_deref() else {
1474 return if candidates.is_empty() { Err(false) } else { Ok(self.pick(&candidates, client_ip)) };
1475 };
1476 if let Some(sticky) = &policy.sticky {
1477 if let Some(id) = upstream::cookie_value(request, &sticky.cookie) {
1478 if let Some(backend) = candidates.iter().find(|b| upstream::backend_id(b) == id).cloned() {
1479 if policy.admits(&backend) {
1480 return Ok(backend);
1481 }
1482 candidates.retain(|b| *b != backend);
1483 }
1484 }
1485 }
1486 let ejected = policy.any_open(&candidates);
1487 while !candidates.is_empty() {
1488 let backend = self.pick(&candidates, client_ip);
1489 if policy.admits(&backend) {
1490 return Ok(backend);
1491 }
1492 candidates.retain(|b| *b != backend);
1493 }
1494 Err(ejected)
1495 }
1496
1497 fn connection_counter(&self, backend: &str) -> Arc<AtomicUsize> {
1500 if let Some(counter) = self.connections.read().unwrap().get(backend) {
1501 return Arc::clone(counter);
1502 }
1503 let mut connections = self.connections.write().unwrap();
1504 Arc::clone(
1505 connections
1506 .entry(backend.to_string())
1507 .or_insert_with(|| Arc::new(AtomicUsize::new(0))),
1508 )
1509 }
1510}
1511
1512struct ConnectionGuard {
1515 counter: Arc<AtomicUsize>,
1516}
1517
1518impl Drop for ConnectionGuard {
1519 fn drop(&mut self) {
1520 self.counter.fetch_sub(1, Ordering::Relaxed);
1521 }
1522}
1523
1524struct Prepared<'a> {
1527 backend: String,
1528 host: String,
1529 port: u16,
1530 request: std::borrow::Cow<'a, Request>,
1531 _guard: Option<ConnectionGuard>,
1532}
1533
1534enum Attempt {
1536 Done(Response),
1538 Failed(Option<Response>),
1541}
1542
1543impl DynamicProxy {
1544 fn prepare<'a>(&self, request: &'a Request, conn: &ConnectionInfo, tried: &[String]) -> Result<Prepared<'a>, Response> {
1549 let backend = self.next_backend(request, &conn.client.ip, tried).map_err(|ejected| if ejected { service_unavailable() } else { bad_gateway() })?;
1550
1551 let guard = if self.strategy == LoadBalanceStrategy::LeastConnections {
1552 let counter = self.connection_counter(&backend);
1553 counter.fetch_add(1, Ordering::Relaxed);
1554 Some(ConnectionGuard { counter })
1555 } else {
1556 None
1557 };
1558
1559 let (host, port, _) = crate::proxy_config::health::parse_backend_url(&backend).ok_or_else(bad_gateway)?;
1560
1561 let request = if self.strip_prefix.is_some() || self.add_prefix.is_some() {
1563 let mut req_clone = request.clone();
1564 if let Some(ref sp) = self.strip_prefix {
1565 if let Some(stripped) = req_clone.request_uri.strip_prefix(sp.as_str()) {
1566 req_clone.request_uri = if stripped.is_empty() || !stripped.starts_with('/') {
1567 format!("/{}", stripped)
1568 } else {
1569 stripped.to_string()
1570 };
1571 }
1572 }
1573 if let Some(ref ap) = self.add_prefix {
1574 req_clone.request_uri = format!("{}{}", ap, req_clone.request_uri);
1575 }
1576 std::borrow::Cow::Owned(req_clone)
1577 } else {
1578 std::borrow::Cow::Borrowed(request)
1579 };
1580
1581 Ok(Prepared { backend, host, port, request, _guard: guard })
1582 }
1583
1584 fn settle(&self, backend: &str, result: Result<Response, ()>, started: std::time::Instant) -> Attempt {
1586 let status = result.as_ref().ok().map(|r| r.status_code);
1587 upstream::observe(&self.upstream, backend, status, started);
1588 let failed = status.is_none_or(|s| matches!(s, 502..=504));
1589 if let Some(policy) = &self.policy {
1590 policy.record(backend, failed);
1591 }
1592 match result {
1593 Ok(response) if self.policy.as_deref().is_some_and(|p| p.retries_status(response.status_code)) => Attempt::Failed(Some(response)),
1594 Ok(response) => Attempt::Done(response),
1595 Err(()) => Attempt::Failed(None),
1596 }
1597 }
1598
1599 fn attempts(&self, request: &Request) -> u32 {
1601 match self.policy.as_deref().and_then(|p| p.retry.as_ref()) {
1602 Some(retry) if upstream::replayable(request) => 1 + retry.attempts,
1603 _ => 1,
1604 }
1605 }
1606
1607 fn stick(&self, request: &Request, backend: &str, response: &mut Response) {
1609 let Some(sticky) = self.policy.as_deref().and_then(|p| p.sticky.as_ref()) else { return };
1610 let id = upstream::backend_id(backend);
1611 if upstream::cookie_value(request, &sticky.cookie).as_deref() == Some(id.as_str()) {
1612 return;
1613 }
1614 let max_age = sticky.max_age_secs.map(|s| format!("; Max-Age={s}")).unwrap_or_default();
1615 response.headers.push(crate::header::Header {
1616 name: "Set-Cookie".into(),
1617 value: format!("{}={id}; Path=/; HttpOnly; SameSite=Lax{max_age}", sticky.cookie).into(),
1618 });
1619 }
1620
1621 #[cfg(feature = "http2")]
1624 pub(crate) async fn execute_async(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1625 if let Some(mirror) = &self.mirror {
1626 mirror.send(request, conn);
1627 }
1628 let mut tried: Vec<String> = Vec::new();
1629 let mut last: Option<Response> = None;
1630 for attempt in 0..self.attempts(request) {
1631 let prepared = match self.prepare(request, conn, &tried) {
1632 Ok(p) => p,
1633 Err(response) => return Ok(last.unwrap_or(response)),
1634 };
1635 if attempt > 0 {
1636 upstream::observe_retry(&self.upstream);
1637 }
1638 let tls = self.tls.then(|| match &self.trust {
1639 Some(trust) => trust.async_tls(),
1640 None => crate::proxy::async_upstream::AsyncTls {
1641 config: crate::proxy::tls_client_config(),
1642 pool: crate::proxy::async_upstream::default_tls_pool(),
1643 },
1644 });
1645 let started = std::time::Instant::now();
1646 let result = crate::proxy::async_upstream::forward(
1647 &prepared.request,
1648 crate::proxy::Peer::of(conn),
1649 &prepared.host,
1650 prepared.port,
1651 tls,
1652 self.connect_timeout,
1653 self.read_timeout,
1654 )
1655 .await
1656 .map_err(|_| ());
1657 match self.settle(&prepared.backend, result, started) {
1658 Attempt::Done(mut response) => {
1659 self.stick(request, &prepared.backend, &mut response);
1660 return Ok(response);
1661 }
1662 Attempt::Failed(response) => last = response,
1663 }
1664 tried.push(prepared.backend.clone());
1665 }
1666 Ok(last.unwrap_or_else(bad_gateway))
1667 }
1668
1669 pub(crate) fn supports_async(&self) -> bool {
1671 cfg!(feature = "http2")
1672 }
1673}
1674
1675impl Application for DynamicProxy {
1676 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1677 if let Some(mirror) = &self.mirror {
1678 mirror.send(request, conn);
1679 }
1680 let mut tried: Vec<String> = Vec::new();
1681 let mut last: Option<Response> = None;
1682 for attempt in 0..self.attempts(request) {
1683 let prepared = match self.prepare(request, conn, &tried) {
1684 Ok(p) => p,
1685 Err(response) => return Ok(last.unwrap_or(response)),
1686 };
1687 if attempt > 0 {
1688 upstream::observe_retry(&self.upstream);
1689 }
1690 let started = std::time::Instant::now();
1693 let result = crate::proxy::forward_upstream_trusting(
1694 crate::proxy::shared_pool(),
1695 &prepared.request,
1696 crate::proxy::Peer::of(conn),
1697 &prepared.host,
1698 prepared.port,
1699 self.tls,
1700 self.trust.as_deref(),
1701 self.connect_timeout,
1702 self.read_timeout,
1703 )
1704 .map_err(|_| ());
1705 match self.settle(&prepared.backend, result, started) {
1706 Attempt::Done(mut response) => {
1707 self.stick(request, &prepared.backend, &mut response);
1708 return Ok(response);
1709 }
1710 Attempt::Failed(response) => last = response,
1711 }
1712 tried.push(prepared.backend.clone());
1713 }
1714 Ok(last.unwrap_or_else(bad_gateway))
1715 }
1716}
1717
1718fn service_unavailable() -> Response {
1719 use crate::mime_type::MimeType;
1720 use crate::range::Range;
1721 let mut r = Response::new();
1722 r.status_code = *STATUS_CODE_REASON_PHRASE.n503_service_unavailable.status_code;
1723 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n503_service_unavailable.reason_phrase.to_string();
1724 r.headers.push(crate::header::Header { name: "Retry-After".into(), value: "5".into() });
1725 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())];
1726 r
1727}
1728
1729fn bad_gateway() -> Response {
1730 use crate::mime_type::MimeType;
1731 use crate::range::Range;
1732 let cr = Range::get_content_range(
1733 b"502 Bad Gateway".to_vec(),
1734 MimeType::TEXT_PLAIN.to_string(),
1735 );
1736 let mut r = Response::new();
1737 r.status_code = *STATUS_CODE_REASON_PHRASE.n502_bad_gateway.status_code;
1738 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n502_bad_gateway.reason_phrase.to_string();
1739 r.content_range_list = vec![cr];
1740 r
1741}
1742
1743#[derive(Clone)]
1749pub(crate) struct RedirectAdapter {
1750 location_template: Arc<String>,
1751 status: i16,
1752 reason: Arc<String>,
1753}
1754
1755impl RedirectAdapter {
1756 pub(crate) fn new(location: String, status: u16) -> Self {
1757 let (code, reason) = redirect_status(status);
1758 RedirectAdapter {
1759 location_template: Arc::new(location),
1760 status: code,
1761 reason: Arc::new(reason),
1762 }
1763 }
1764}
1765
1766fn redirect_status(code: u16) -> (i16, String) {
1767 let phrase = match code {
1768 301 => STATUS_CODE_REASON_PHRASE.n301_moved_permanently.reason_phrase,
1769 302 => STATUS_CODE_REASON_PHRASE.n302_found.reason_phrase,
1770 307 => STATUS_CODE_REASON_PHRASE.n307_temporary_redirect.reason_phrase,
1771 308 => STATUS_CODE_REASON_PHRASE.n308_permanent_redirect.reason_phrase,
1772 _ => "Redirect",
1773 };
1774 (code as i16, phrase.to_string())
1775}
1776
1777impl Application for RedirectAdapter {
1778 fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1779 let location = self
1780 .location_template
1781 .replace("$path", &request.request_uri);
1782 use crate::header::Header;
1783 let mut r = Response::new();
1784 r.status_code = self.status;
1785 r.reason_phrase = self.reason.as_ref().clone();
1786 r.headers.push(Header { name: "Location".into(), value: location.into() });
1787 Ok(r)
1788 }
1789}
1790
1791#[derive(Clone)]
1795pub(crate) struct RespondAdapter {
1796 status: i16,
1797 reason: Arc<String>,
1798 body: Arc<Vec<u8>>,
1799 content_type: Arc<String>,
1800}
1801
1802impl RespondAdapter {
1803 pub(crate) fn new(status: u16, body: String, content_type: String) -> Self {
1804 use crate::response::STATUS_CODE_REASON_PHRASE;
1805 let reason = match status {
1806 200 => STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string(),
1807 201 => STATUS_CODE_REASON_PHRASE.n201_created.reason_phrase.to_string(),
1808 204 => STATUS_CODE_REASON_PHRASE.n204_no_content.reason_phrase.to_string(),
1809 400 => STATUS_CODE_REASON_PHRASE.n400_bad_request.reason_phrase.to_string(),
1810 401 => STATUS_CODE_REASON_PHRASE.n401_unauthorized.reason_phrase.to_string(),
1811 403 => STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string(),
1812 404 => STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string(),
1813 500 => STATUS_CODE_REASON_PHRASE.n500_internal_server_error.reason_phrase.to_string(),
1814 _ => "OK".to_string(),
1815 };
1816 RespondAdapter {
1817 status: status as i16,
1818 reason: Arc::new(reason),
1819 body: Arc::new(body.into_bytes()),
1820 content_type: Arc::new(content_type),
1821 }
1822 }
1823}
1824
1825impl Application for RespondAdapter {
1826 fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1827 use crate::range::Range;
1828 let mut r = Response::new();
1829 r.status_code = self.status;
1830 r.reason_phrase = self.reason.as_ref().clone();
1831 if !self.body.is_empty() {
1832 r.content_range_list = vec![Range::get_content_range(
1833 self.body.as_ref().clone(),
1834 self.content_type.as_ref().clone(),
1835 )];
1836 }
1837 Ok(r)
1838 }
1839}
1840
1841#[derive(Clone)]
1850pub(crate) struct StaticAdapter {
1851 root: Arc<std::path::PathBuf>,
1852 index: Arc<Vec<String>>,
1853}
1854
1855impl StaticAdapter {
1856 pub(crate) fn new(root: String, index: Vec<String>) -> Self {
1857 let index = if index.is_empty() { vec!["index.html".to_string()] } else { index };
1858 StaticAdapter {
1859 root: Arc::new(std::path::PathBuf::from(root)),
1860 index: Arc::new(index),
1861 }
1862 }
1863
1864 fn resolve(&self, request_uri: &str) -> Option<std::path::PathBuf> {
1868 let raw_path = request_uri.split('?').next().unwrap_or(request_uri);
1869 let decoded = crate::url::URL::percent_decode(raw_path);
1870
1871 if decoded.split('/').any(|segment| segment == "..") {
1872 return None;
1873 }
1874
1875 let relative = decoded.trim_start_matches('/');
1876 Some(self.root.join(relative))
1877 }
1878}
1879
1880impl Application for StaticAdapter {
1881 fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1882 let mut response = Response::new();
1883
1884 let not_found = |mut response: Response| {
1885 response.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
1886 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
1887 response
1888 };
1889
1890 let candidate = match self.resolve(&request.request_uri) {
1891 Some(p) => p,
1892 None => {
1893 response.status_code = *STATUS_CODE_REASON_PHRASE.n403_forbidden.status_code;
1894 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string();
1895 return Ok(response);
1896 }
1897 };
1898
1899 let mut file_path = candidate;
1900 if file_path.is_dir() {
1901 let indexed = self
1902 .index
1903 .iter()
1904 .map(|name| file_path.join(name))
1905 .find(|p| p.is_file());
1906
1907 file_path = match indexed {
1908 Some(p) => p,
1909 None => return Ok(not_found(response)),
1910 };
1911 }
1912
1913 if !file_path.is_file() {
1914 return Ok(not_found(response));
1915 }
1916
1917 if let (Ok(root_canon), Ok(file_canon)) =
1920 (self.root.canonicalize(), file_path.canonicalize())
1921 {
1922 if !file_canon.starts_with(&root_canon) {
1923 return Ok(not_found(response));
1924 }
1925 }
1926
1927 let path_str = match file_path.to_str() {
1928 Some(s) => s,
1929 None => return Ok(not_found(response)),
1930 };
1931
1932 match crate::range::Range::get_content_range_of_a_file(path_str) {
1933 Ok(content_range) => {
1934 response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
1935 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
1936 response.content_range_list = vec![content_range];
1937 Ok(response)
1938 }
1939 Err(_) => Ok(not_found(response)),
1940 }
1941 }
1942}
1943
1944pub(crate) struct PerRouteRateLimit(pub(crate) Arc<crate::rate_limit::RateLimiter>);
1948
1949impl crate::middleware::Middleware for PerRouteRateLimit {
1950 fn handle(
1951 &self,
1952 request: &Request,
1953 conn: &ConnectionInfo,
1954 next: &dyn Application,
1955 ) -> Result<Response, String> {
1956 use crate::error::{AppError, IntoResponse};
1957 if self.0.check(&conn.client.ip) {
1958 next.execute(request, conn)
1959 } else {
1960 let mut response = AppError::TooManyRequests.into_response();
1961 let retry_after = self.0.retry_after(&conn.client.ip);
1962 response.headers.push(Header {
1963 name: Header::_RETRY_AFTER.into(),
1964 value: retry_after.to_string().into(),
1965 });
1966 Ok(response)
1967 }
1968 }
1969}
1970
1971pub(crate) struct PerRouteMaxBodySize(pub(crate) u64);
1986
1987impl crate::middleware::Middleware for PerRouteMaxBodySize {
1988 fn handle(
1989 &self,
1990 request: &Request,
1991 conn: &ConnectionInfo,
1992 next: &dyn Application,
1993 ) -> Result<Response, String> {
1994 use crate::error::{AppError, IntoResponse};
1995 if request.body.len() as u64 > self.0 {
1996 Ok(AppError::PayloadTooLarge(format!(
1997 "request body of {} bytes exceeds this route's {}-byte limit",
1998 request.body.len(),
1999 self.0,
2000 ))
2001 .into_response())
2002 } else {
2003 next.execute(request, conn)
2004 }
2005 }
2006}
2007
2008pub(crate) struct BearerAuthMiddleware {
2012 pub(crate) token: Arc<String>,
2013}
2014
2015impl crate::middleware::Middleware for BearerAuthMiddleware {
2016 fn handle(
2017 &self,
2018 request: &Request,
2019 conn: &ConnectionInfo,
2020 next: &dyn Application,
2021 ) -> Result<Response, String> {
2022 use crate::error::{AppError, IntoResponse};
2023 let expected = format!("Bearer {}", self.token);
2024 let authorized = request
2025 .headers
2026 .iter()
2027 .any(|h| h.name.eq_ignore_ascii_case("authorization") && constant_time_eq(h.value.as_bytes(), expected.as_bytes()));
2028 if authorized {
2029 next.execute(request, conn)
2030 } else {
2031 Ok(AppError::Unauthorized.into_response())
2032 }
2033 }
2034}
2035
2036pub(crate) fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
2039 if a.len() != b.len() {
2040 return false;
2041 }
2042 a.iter().zip(b).fold(0u8, |diff, (x, y)| diff | (x ^ y)) == 0
2043}
2044
2045pub(crate) struct AuthMisconfigured;
2048
2049pub(crate) fn cache_max_bytes(max_size: &str) -> Option<u64> {
2051 if max_size.trim().is_empty() {
2052 Some(256 * 1024 * 1024)
2053 } else {
2054 crate::log::access_sink::parse_size(max_size.trim()).filter(|n| *n > 0)
2055 }
2056}
2057
2058pub(crate) struct PluginMisconfigured;
2061
2062impl crate::middleware::Middleware for PluginMisconfigured {
2063 fn handle(&self, _request: &Request, _conn: &ConnectionInfo, _next: &dyn Application) -> Result<Response, String> {
2064 Ok(Response::text(
2065 STATUS_CODE_REASON_PHRASE.n503_service_unavailable,
2066 "503 Service Unavailable: this route's plugin is misconfigured",
2067 ))
2068 }
2069}
2070
2071impl crate::middleware::Middleware for AuthMisconfigured {
2072 fn handle(
2073 &self,
2074 _request: &Request,
2075 _conn: &ConnectionInfo,
2076 _next: &dyn Application,
2077 ) -> Result<Response, String> {
2078 Ok(Response::text(
2079 STATUS_CODE_REASON_PHRASE.n503_service_unavailable,
2080 "503 Service Unavailable: authentication for this route is misconfigured",
2081 ))
2082 }
2083}
2084
2085pub(crate) fn arc_app<A: Application + Send + Sync + 'static>(
2089 a: A,
2090) -> Arc<dyn Application + Send + Sync> {
2091 Arc::new(a)
2092}
2093
2094pub fn build_from_file() -> (ConfigDrivenApp, Vec<std::thread::JoinHandle<()>>) {
2099 builder::build_from_file()
2100}