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}
188
189#[derive(Debug, Clone, Default, PartialEq)]
191pub struct WasmConfig {
192 pub module: String,
194 pub fuel: u64,
195 pub memory_mb: u64,
196 pub config: String,
198}
199
200#[derive(Debug, Clone)]
201pub struct RateLimitConfig {
202 pub max_requests: u32,
203 pub window_secs: u64,
204}
205
206#[derive(Debug, Clone)]
207pub struct CacheConfig {
208 pub ttl_secs: u64,
209 pub vary_by: Vec<String>,
210}
211
212#[derive(Debug, Clone)]
213pub enum AuthConfig {
214 Basic { htpasswd_file: String },
219 Jwt { secret_env: String },
222 Bearer { token_env: String },
223}
224
225#[derive(Debug, Clone, Default)]
226pub struct RewriteRuleConfig {
227 pub type_: String,
228 pub name: Option<String>,
229 pub value: Option<String>,
230 pub prefix: Option<String>,
231 pub from: Option<String>,
232 pub to: Option<String>,
233 pub code: Option<u16>,
234 pub reason: Option<String>,
235}
236
237#[derive(Debug, Clone)]
238pub struct TcpProxyConfig {
239 pub name: String,
240 pub listen: String,
241 pub backends: Vec<String>,
242 pub connect_timeout_ms: u64,
243}
244
245#[derive(Debug, Clone)]
246pub struct UdpProxyConfig {
247 pub name: String,
248 pub listen: String,
249 pub backends: Vec<String>,
250 pub reply_timeout_ms: u64,
251 pub buffer_size: usize,
252}
253
254#[derive(Debug, Clone)]
255pub struct WsProxyConfig {
256 pub name: String,
257 pub listen: String,
258 pub backends: Vec<String>,
259 pub connect_timeout_ms: u64,
260 pub read_timeout_ms: u64,
261 pub health_check: Option<HealthCheckConfig>,
268}
269
270fn merge_action_keys(map: &mut parser::SectionMap, action_section: &str, action_type: &str) {
279 if action_type.is_empty() {
280 return;
281 }
282 let short_form: Vec<(String, String)> = match map.get(action_section) {
283 Some(entries) => entries.iter().filter(|(k, _)| k != "type").cloned().collect(),
284 None => return,
285 };
286 let nested = map.entry(format!("{action_section}.{action_type}")).or_default();
287 for (key, value) in short_form {
288 if !nested.iter().any(|(k, _)| *k == key) {
289 nested.push((key, value));
290 }
291 }
292}
293
294impl ProxyConfig {
295 pub fn is_proxy_mode() -> bool {
299 let path = config_file_path();
300 match std::fs::read_to_string(&path) {
301 Ok(contents) => {
302 contents.contains("[[route]]") || contents.contains("[[upstream]]")
303 }
304 Err(_) => false,
305 }
306 }
307
308 pub fn load() -> Self {
310 let path = config_file_path();
311 let contents = std::fs::read_to_string(&path).unwrap_or_default();
312 Self::from_str(&contents)
313 }
314
315 pub fn from_str(toml: &str) -> Self {
317 use parser::{get_array, get_str, get_u32, get_u64, section_exists};
318
319 let mut map = parser::parse(toml);
320
321 let mut upstreams = Vec::new();
323 let mut i = 0;
324 loop {
325 let sec = format!("upstream[{}]", i);
326 if !section_exists(&map, &sec) {
327 break;
328 }
329 let name = get_str(&map, &sec, "name");
330 let backends = get_array(&map, &sec, "backends");
331 let strategy = {
332 let s = get_str(&map, &sec, "strategy");
333 if s.is_empty() { "round_robin".to_string() } else { s }
334 };
335 let hc_sec = format!("{}.health_check", sec);
336 let health_check = if section_exists(&map, &hc_sec) {
337 Some(HealthCheckConfig {
338 path: {
339 let p = get_str(&map, &hc_sec, "path");
340 if p.is_empty() { "/health".to_string() } else { p }
341 },
342 interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
343 timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
344 healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
345 unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
346 })
347 } else {
348 None
349 };
350 let tls = backends.iter().any(|b| ["https://", "h2s://", "grpcs://"].iter().any(|s| b.starts_with(s)));
351 let tls_ca_file = Some(get_str(&map, &sec, "tls_ca_file")).filter(|v| !v.is_empty());
352 let tls_client_cert_file = Some(get_str(&map, &sec, "tls_client_cert_file")).filter(|v| !v.is_empty());
353 let tls_client_key_file = Some(get_str(&map, &sec, "tls_client_key_file")).filter(|v| !v.is_empty());
354 let weights: Vec<u32> = get_array(&map, &sec, "weights").iter().map(|w| w.trim().parse().unwrap_or(0)).collect();
355 let sticky_sec = format!("{}.sticky", sec);
356 let sticky = section_exists(&map, &sticky_sec).then(|| StickyConfig {
357 cookie: Some(get_str(&map, &sticky_sec, "cookie")).filter(|c| !c.is_empty()).unwrap_or_else(|| "rws_backend".to_string()),
358 max_age_secs: Some(get_u64(&map, &sticky_sec, "max_age_secs", 0)).filter(|s| *s > 0),
359 });
360 let cb_sec = format!("{}.circuit_breaker", sec);
361 let circuit_breaker = section_exists(&map, &cb_sec).then(|| BreakerConfig {
362 failure_threshold: get_u32(&map, &cb_sec, "failure_threshold", 5),
363 recovery_secs: get_u64(&map, &cb_sec, "recovery_secs", 30),
364 half_open_probes: get_u32(&map, &cb_sec, "half_open_probes", 1),
365 });
366 let retry_sec = format!("{}.retry", sec);
367 let retry = section_exists(&map, &retry_sec).then(|| {
368 let status: Vec<i16> = get_array(&map, &retry_sec, "status").iter().filter_map(|s| s.trim().parse().ok()).collect();
369 RetryConfig { attempts: get_u32(&map, &retry_sec, "attempts", 1), status: if status.is_empty() { vec![502, 503, 504] } else { status } }
370 });
371 upstreams.push(UpstreamConfig {
372 name, backends, strategy, health_check, tls, tls_ca_file, tls_client_cert_file, tls_client_key_file,
373 weights, sticky, circuit_breaker, retry,
374 });
375 i += 1;
376 }
377
378 let mut routes = Vec::new();
380 let mut i = 0;
381 loop {
382 let sec = format!("route[{}]", i);
383 if !section_exists(&map, &sec) {
384 break;
385 }
386 let name = get_str(&map, &sec, "name");
387
388 let m_sec = format!("{}.match", sec);
390 let match_ = MatchConfig {
391 host: {
392 let h = get_str(&map, &m_sec, "host");
393 if h.is_empty() { None } else { Some(h) }
394 },
395 path: {
396 let p = get_str(&map, &m_sec, "path");
397 if p.is_empty() { None } else { Some(p) }
398 },
399 method: {
400 let m = get_str(&map, &m_sec, "method");
401 if m.is_empty() { None } else { Some(m.to_uppercase()) }
402 },
403 content_type: {
404 let c = get_str(&map, &m_sec, "content_type");
405 if c.is_empty() { None } else { Some(c) }
406 },
407 };
408
409 let a_sec = format!("{}.action", sec);
411 let action_type = get_str(&map, &a_sec, "type");
412 merge_action_keys(&mut map, &a_sec, &action_type);
413 let action = match action_type.as_str() {
414 "proxy" => {
415 let p_sec = format!("{}.action.proxy", sec);
416 ActionConfig::Proxy {
417 upstream: get_str(&map, &p_sec, "upstream"),
418 connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
419 read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
420 strip_path_prefix: {
421 let v = get_str(&map, &p_sec, "strip_path_prefix");
422 if v.is_empty() { None } else { Some(v) }
423 },
424 add_path_prefix: {
425 let v = get_str(&map, &p_sec, "add_path_prefix");
426 if v.is_empty() { None } else { Some(v) }
427 },
428 }
429 }
430 "grpc" => {
431 let p_sec = format!("{}.action.grpc", sec);
432 ActionConfig::Grpc {
433 upstream: get_str(&map, &p_sec, "upstream"),
434 connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
435 read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
436 }
437 }
438 "static" => {
439 let s_sec = format!("{}.action.static", sec);
440 ActionConfig::Static {
441 root: get_str(&map, &s_sec, "root"),
442 index: get_array(&map, &s_sec, "index"),
443 }
444 }
445 "redirect" => {
446 let r_sec = format!("{}.action.redirect", sec);
447 ActionConfig::Redirect {
448 location: get_str(&map, &r_sec, "location"),
449 status: get_u64(&map, &r_sec, "status", 301) as u16,
450 }
451 }
452 "respond" => {
453 let r_sec = format!("{}.action.respond", sec);
454 ActionConfig::Respond {
455 status: get_u64(&map, &r_sec, "status", 200) as u16,
456 body: get_str(&map, &r_sec, "body"),
457 content_type: {
458 let ct = get_str(&map, &r_sec, "content_type");
459 if ct.is_empty() { "text/plain".to_string() } else { ct }
460 },
461 }
462 }
463 "mcp" => ActionConfig::Mcp(Box::new(mcp_gateway::parse(&map, &a_sec))),
464 "llm" => ActionConfig::Llm(Box::new(llm::parse(&map, &a_sec))),
465 other => ActionConfig::Unknown(other.to_string()),
466 };
467
468 let mw_sec = format!("{}.middleware", sec);
470 let middleware = parse_middleware_config(&map, &mw_sec, i);
471
472 routes.push(RouteConfig { name, match_, action, middleware });
473 i += 1;
474 }
475
476 let mut tcp_proxies = Vec::new();
478 let mut i = 0;
479 loop {
480 let sec = format!("tcp_proxy[{}]", i);
481 if !section_exists(&map, &sec) {
482 break;
483 }
484 tcp_proxies.push(TcpProxyConfig {
485 name: get_str(&map, &sec, "name"),
486 listen: get_str(&map, &sec, "listen"),
487 backends: get_array(&map, &sec, "backends"),
488 connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
489 });
490 i += 1;
491 }
492
493 let mut udp_proxies = Vec::new();
495 let mut i = 0;
496 loop {
497 let sec = format!("udp_proxy[{}]", i);
498 if !section_exists(&map, &sec) {
499 break;
500 }
501 udp_proxies.push(UdpProxyConfig {
502 name: get_str(&map, &sec, "name"),
503 listen: get_str(&map, &sec, "listen"),
504 backends: get_array(&map, &sec, "backends"),
505 reply_timeout_ms: get_u64(&map, &sec, "reply_timeout_ms", 5000),
506 buffer_size: get_u64(&map, &sec, "buffer_size", 65536) as usize,
507 });
508 i += 1;
509 }
510
511 let mut ws_proxies = Vec::new();
513 let mut i = 0;
514 loop {
515 let sec = format!("ws_proxy[{}]", i);
516 if !section_exists(&map, &sec) {
517 break;
518 }
519 let hc_sec = format!("{}.health_check", sec);
520 let health_check = if section_exists(&map, &hc_sec) {
521 Some(HealthCheckConfig {
522 path: {
523 let p = get_str(&map, &hc_sec, "path");
524 if p.is_empty() { "/health".to_string() } else { p }
525 },
526 interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
527 timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
528 healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
529 unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
530 })
531 } else {
532 None
533 };
534 ws_proxies.push(WsProxyConfig {
535 name: get_str(&map, &sec, "name"),
536 listen: get_str(&map, &sec, "listen"),
537 backends: get_array(&map, &sec, "backends"),
538 connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
539 read_timeout_ms: get_u64(&map, &sec, "read_timeout_ms", 30000),
540 health_check,
541 });
542 i += 1;
543 }
544
545 let global_middleware = parse_middleware_config(&map, "middleware", usize::MAX);
547
548 ProxyConfig {
549 upstreams,
550 routes,
551 tcp_proxies,
552 udp_proxies,
553 ws_proxies,
554 global_middleware,
555 }
556 }
557}
558
559fn parse_middleware_config(
562 map: &parser::SectionMap,
563 mw_sec: &str,
564 route_idx: usize,
565) -> MiddlewareConfig {
566 use parser::{get_array, get_str, get_u32, get_u64, section_exists};
567
568 let rl_sec = format!("{}.rate_limit", mw_sec);
569 let rate_limit = if section_exists(map, &rl_sec) {
570 Some(RateLimitConfig {
571 max_requests: get_u32(map, &rl_sec, "max_requests", 1000),
572 window_secs: get_u64(map, &rl_sec, "window_secs", 60),
573 })
574 } else {
575 None
576 };
577
578 let c_sec = format!("{}.cache", mw_sec);
579 let cache = if section_exists(map, &c_sec) {
580 Some(CacheConfig {
581 ttl_secs: get_u64(map, &c_sec, "ttl_secs", 60),
582 vary_by: get_array(map, &c_sec, "vary_by"),
583 })
584 } else {
585 None
586 };
587
588 let a_sec = format!("{}.auth", mw_sec);
589 let auth = if section_exists(map, &a_sec) {
590 let auth_type = get_str(map, &a_sec, "type");
591 match auth_type.as_str() {
592 "bearer" => Some(AuthConfig::Bearer {
593 token_env: get_str(map, &a_sec, "token_env"),
594 }),
595 "jwt" => Some(AuthConfig::Jwt {
596 secret_env: get_str(map, &a_sec, "secret_env"),
597 }),
598 "basic" => Some(AuthConfig::Basic {
599 htpasswd_file: get_str(map, &a_sec, "htpasswd_file"),
600 }),
601 _ => None,
602 }
603 } else {
604 None
605 };
606
607 let rewrite_request = collect_rewrite_rules(map, mw_sec, "request");
613 let rewrite_response = collect_rewrite_rules(map, mw_sec, "response");
614
615 let ip_sec = format!("{}.ip_filter", mw_sec);
616 let ip_allow = if section_exists(map, &ip_sec) {
617 get_array(map, &ip_sec, "allow")
618 } else {
619 vec![]
620 };
621 let ip_deny = if section_exists(map, &ip_sec) {
622 get_array(map, &ip_sec, "deny")
623 } else {
624 vec![]
625 };
626
627 let _ = route_idx; let timeout_ms = match get_u64(map, mw_sec, "timeout_ms", 0) {
633 0 => None,
634 ms => Some(ms),
635 };
636
637 let max_body_size = match get_u64(map, mw_sec, "max_body_size", 0) {
641 0 => None,
642 n => Some(n),
643 };
644
645 let w_sec = format!("{}.wasm", mw_sec);
646 let wasm = section_exists(map, &w_sec).then(|| WasmConfig {
647 module: get_str(map, &w_sec, "module"),
648 fuel: get_u64(map, &w_sec, "fuel", 50_000_000),
649 memory_mb: get_u64(map, &w_sec, "memory_mb", 16),
650 config: get_str(map, &w_sec, "config"),
651 });
652
653 MiddlewareConfig {
654 rate_limit, cache, auth, rewrite_request, rewrite_response, ip_allow, ip_deny,
655 timeout_ms, max_body_size, wasm,
656 }
657}
658
659fn collect_rewrite_rules(
661 map: &parser::SectionMap,
662 mw_sec: &str,
663 direction: &str,
664) -> Vec<RewriteRuleConfig> {
665 use parser::{get_str, get_u64};
666
667 let mut rules = Vec::new();
668 let mut j = 0;
669 loop {
670 let rsec = format!("{}.rewrite.{}[{}]", mw_sec, direction, j);
671 if !parser::section_exists(map, &rsec) {
672 break;
673 }
674 let code_val = get_u64(map, &rsec, "code", 0);
675 rules.push(RewriteRuleConfig {
676 type_: get_str(map, &rsec, "type"),
677 name: {
678 let v = get_str(map, &rsec, "name");
679 if v.is_empty() { None } else { Some(v) }
680 },
681 value: {
682 let v = get_str(map, &rsec, "value");
683 if v.is_empty() { None } else { Some(v) }
684 },
685 prefix: {
686 let v = get_str(map, &rsec, "prefix");
687 if v.is_empty() { None } else { Some(v) }
688 },
689 from: {
690 let v = get_str(map, &rsec, "from");
691 if v.is_empty() { None } else { Some(v) }
692 },
693 to: {
694 let v = get_str(map, &rsec, "to");
695 if v.is_empty() { None } else { Some(v) }
696 },
697 code: if code_val == 0 { None } else { Some(code_val as u16) },
698 reason: {
699 let v = get_str(map, &rsec, "reason");
700 if v.is_empty() { None } else { Some(v) }
701 },
702 });
703 j += 1;
704 }
705 rules
706}
707
708pub(crate) fn config_file_path() -> String {
709 std::env::var("RWS_CONFIG_FILE").unwrap_or_else(|_| "rws.config.toml".to_string())
710}
711
712pub(crate) struct CompiledRoute {
716 pub(crate) matcher: RouteMatcher,
717 pub(crate) handler: Arc<dyn Application + Send + Sync>,
719 #[cfg_attr(not(feature = "http2"), allow(dead_code))]
723 pub(crate) async_proxy: Option<Arc<DynamicProxy>>,
724}
725
726#[derive(Clone, Default)]
728pub(crate) struct RouteMatcher {
729 pub(crate) host: Option<String>,
731 pub(crate) path_prefix: Option<String>,
733 pub(crate) path_exact: Option<String>,
735 pub(crate) method: Option<String>,
737 pub(crate) content_type_prefix: Option<String>,
739}
740
741impl RouteMatcher {
742 pub(crate) fn from_match_config(cfg: &MatchConfig) -> Self {
743 let (path_prefix, path_exact) = match &cfg.path {
744 Some(p) if p.ends_with('*') => {
745 let stripped = p.trim_end_matches('*').to_string();
747 (Some(stripped), None)
748 }
749 Some(p) => (None, Some(p.clone())),
750 None => (None, None),
751 };
752 let content_type_prefix = cfg.content_type.as_ref().map(|ct| {
753 if ct.ends_with('*') {
754 ct.trim_end_matches('*').to_string()
755 } else {
756 ct.clone()
757 }
758 });
759 RouteMatcher {
760 host: cfg.host.clone(),
761 path_prefix,
762 path_exact,
763 method: cfg.method.clone(),
764 content_type_prefix,
765 }
766 }
767
768 pub(crate) fn matches(&self, request: &Request, conn: &ConnectionInfo) -> bool {
770 if let Some(ref expected_host) = self.host {
772 let actual_host = conn
773 .sni_hostname
774 .as_deref()
775 .or_else(|| {
776 request
777 .headers
778 .iter()
779 .find(|h| h.name.eq_ignore_ascii_case("host"))
780 .map(|h| h.value.as_ref())
781 })
782 .unwrap_or("");
783 if !host_matches(expected_host, actual_host) {
784 return false;
785 }
786 }
787
788 if let Some(ref m) = self.method {
790 if request.method.to_uppercase() != m.as_str() {
791 return false;
792 }
793 }
794
795 let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
797 if let Some(ref prefix) = self.path_prefix {
798 if !path.starts_with(prefix.as_str()) {
799 return false;
800 }
801 } else if let Some(ref exact) = self.path_exact {
802 if path != exact.as_str() {
803 return false;
804 }
805 }
806
807 if let Some(ref ct_prefix) = self.content_type_prefix {
809 let actual_ct = request
810 .headers
811 .iter()
812 .find(|h| h.name.eq_ignore_ascii_case("content-type"))
813 .map(|h| h.value.as_ref())
814 .unwrap_or("");
815 if !actual_ct.starts_with(ct_prefix.as_str()) {
816 return false;
817 }
818 }
819
820 true
821 }
822}
823
824#[derive(Clone)]
831pub struct ConfigDrivenApp {
832 current: Arc<RwLock<Arc<Generation>>>,
833 reload_lock: Arc<std::sync::Mutex<()>>,
835}
836
837struct Generation {
839 dispatch: RouteDispatch,
840 entry: Option<Arc<dyn Application + Send + Sync>>,
843 config: ProxyConfig,
844 source: Option<String>,
846 health_stop: Arc<AtomicBool>,
848}
849
850impl Generation {
851 fn new(routes: Arc<Vec<CompiledRoute>>, fallback: App, config: ProxyConfig, health_stop: Arc<AtomicBool>, source: Option<String>) -> Self {
852 let dispatch = RouteDispatch { routes, fallback };
853 let entry = if config.global_middleware.is_empty() {
854 None
855 } else {
856 Some(builder::apply_middleware(arc_app(dispatch.clone()), &config.global_middleware))
857 };
858 Generation { dispatch, entry, config, source, health_stop }
859 }
860}
861
862#[derive(Clone)]
864struct RouteDispatch {
865 routes: Arc<Vec<CompiledRoute>>,
866 fallback: App,
871}
872
873impl Application for RouteDispatch {
874 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
875 for route in self.routes.iter() {
876 if route.matcher.matches(request, conn) {
877 return route.handler.execute(request, conn);
878 }
879 }
880 self.fallback.execute(request, conn)
881 }
882
883 fn streams_body(&self, request: &Request, conn: &ConnectionInfo) -> bool {
884 match self.routes.iter().find(|r| r.matcher.matches(request, conn)) {
885 Some(route) => route.handler.streams_body(request, conn),
886 None => self.fallback.streams_body(request, conn),
887 }
888 }
889}
890
891impl MiddlewareConfig {
892 pub fn is_empty(&self) -> bool {
894 self.rate_limit.is_none() && self.cache.is_none() && self.auth.is_none()
895 && self.rewrite_request.is_empty() && self.rewrite_response.is_empty()
896 && self.ip_allow.is_empty() && self.ip_deny.is_empty()
897 && self.timeout_ms.is_none() && self.max_body_size.is_none() && self.wasm.is_none()
898 }
899}
900
901#[derive(Debug, Clone, PartialEq)]
903pub enum ReloadOutcome {
904 Unchanged,
906 Reloaded { routes: usize, warnings: Vec<String> },
909}
910
911impl ConfigDrivenApp {
912 pub(crate) fn new(routes: Vec<CompiledRoute>, config: ProxyConfig, health_stop: Arc<AtomicBool>, source: Option<String>) -> Self {
913 use crate::core::New;
914 let generation = Generation::new(Arc::new(routes), App::new(), config, health_stop, source);
915 ConfigDrivenApp {
916 current: Arc::new(RwLock::new(Arc::new(generation))),
917 reload_lock: Arc::new(std::sync::Mutex::new(())),
918 }
919 }
920
921 fn current(&self) -> Arc<Generation> {
922 Arc::clone(&self.current.read().unwrap_or_else(|e| e.into_inner()))
923 }
924
925 pub fn with_config(self, config: ServerConfig) -> Self {
942 {
943 let mut slot = self.current.write().unwrap_or_else(|e| e.into_inner());
944 let old = Arc::clone(&slot);
945 *slot = Arc::new(Generation::new(
946 Arc::clone(&old.dispatch.routes),
947 App::with_config(config),
948 old.config.clone(),
949 Arc::clone(&old.health_stop),
950 old.source.clone(),
951 ));
952 }
953 self
954 }
955
956 pub fn reload_from_str(&self, toml: &str) -> Result<ReloadOutcome, crate::config_check::ConfigReport> {
966 let _serialized = self.reload_lock.lock().unwrap_or_else(|e| e.into_inner());
967 let old = self.current();
968 if old.source.as_deref() == Some(toml) {
969 return Ok(ReloadOutcome::Unchanged);
970 }
971 let config = ProxyConfig::from_str(toml);
972 let mut report = crate::config_check::ConfigReport::default();
973 validate::check_unknown_keys(toml, &mut report);
974 config.validate(&mut report);
975 if !report.is_ok() {
976 return Err(report);
977 }
978
979 let mut warnings = report.warnings;
980 let listeners = |c: &ProxyConfig| format!("{:?}{:?}{:?}", c.tcp_proxies, c.udp_proxies, c.ws_proxies);
981 if listeners(&config) != listeners(&old.config) {
982 warnings.push("[[tcp_proxy]] / [[udp_proxy]] / [[ws_proxy]] changes need a restart; the old listeners keep running".to_string());
983 }
984
985 let health_stop = Arc::new(AtomicBool::new(false));
986 let routes = builder::compile_routes(&config, &health_stop);
987 let route_count = routes.len();
988 let generation = Generation::new(Arc::new(routes), old.dispatch.fallback.clone(), config, health_stop, Some(toml.to_string()));
989 let replaced = {
990 let mut slot = self.current.write().unwrap_or_else(|e| e.into_inner());
991 std::mem::replace(&mut *slot, Arc::new(generation))
992 };
993 replaced.health_stop.store(true, Ordering::Relaxed);
994 Ok(ReloadOutcome::Reloaded { routes: route_count, warnings })
995 }
996
997 pub fn reload_from_file(&self) {
1002 let path = config_file_path();
1003 let toml = match std::fs::read_to_string(&path) {
1004 Ok(toml) => toml,
1005 Err(e) => {
1006 eprintln!("[proxy_config] reload: cannot read {path}: {e} — keeping the running routes");
1007 crate::config_reload::report_error(format!("cannot read {path}: {e}"));
1008 return;
1009 }
1010 };
1011 match self.reload_from_str(&toml) {
1012 Ok(ReloadOutcome::Unchanged) => {}
1013 Ok(ReloadOutcome::Reloaded { routes, warnings }) => {
1014 for w in &warnings {
1015 eprintln!("[proxy_config] Warning: {w}");
1016 }
1017 println!("[proxy_config] reloaded {path}: {routes} route{}", if routes == 1 { "" } else { "s" });
1018 }
1019 Err(report) => {
1020 for e in &report.errors {
1021 eprintln!("[proxy_config] reload ERROR: {e}");
1022 crate::config_reload::report_error(e.clone());
1023 }
1024 eprintln!("[proxy_config] {path} has errors — keeping the running routes");
1025 }
1026 }
1027 }
1028}
1029
1030fn normalized(request: &Request) -> std::borrow::Cow<'_, Request> {
1033 match normalize_request_target(&request.request_uri) {
1034 std::borrow::Cow::Borrowed(_) => std::borrow::Cow::Borrowed(request),
1035 std::borrow::Cow::Owned(target) => {
1036 let mut owned = request.clone();
1037 owned.request_uri = target;
1038 std::borrow::Cow::Owned(owned)
1039 }
1040 }
1041}
1042
1043pub(crate) fn normalize_request_target(target: &str) -> std::borrow::Cow<'_, str> {
1051 let (path, query) = match target.find('?') {
1052 Some(i) => (&target[..i], &target[i..]),
1053 None => (target, ""),
1054 };
1055 if !path.starts_with('/') || !(path.contains('%') || path.contains("//") || path.contains("/.")) {
1056 return std::borrow::Cow::Borrowed(target);
1057 }
1058 let bytes = path.as_bytes();
1059 let mut decoded = String::with_capacity(path.len());
1060 let mut i = 0;
1061 while i < bytes.len() {
1062 let hex = |b: u8| (b as char).to_digit(16);
1063 if bytes[i] == b'%' && i + 2 < bytes.len() {
1064 if let (Some(h), Some(l)) = (hex(bytes[i + 1]), hex(bytes[i + 2])) {
1065 let value = (h * 16 + l) as u8;
1066 if value.is_ascii_alphanumeric() || matches!(value, b'-' | b'.' | b'_' | b'~') {
1067 decoded.push(value as char);
1068 } else {
1069 decoded.push('%');
1070 decoded.push(bytes[i + 1].to_ascii_uppercase() as char);
1071 decoded.push(bytes[i + 2].to_ascii_uppercase() as char);
1072 }
1073 i += 3;
1074 continue;
1075 }
1076 }
1077 if bytes[i] == b'%' {
1081 decoded.push_str("%25");
1082 i += 1;
1083 continue;
1084 }
1085 let ch = path[i..].chars().next().unwrap_or('\u{FFFD}');
1087 decoded.push(ch);
1088 i += ch.len_utf8().max(1);
1089 }
1090 let mut segments: Vec<&str> = Vec::new();
1091 let parts: Vec<&str> = decoded.split('/').collect();
1092 let last = parts.len().saturating_sub(1);
1093 let mut trailing_slash = false;
1094 for (n, segment) in parts.iter().enumerate() {
1095 match *segment {
1096 "" => trailing_slash = n == last,
1097 "." => trailing_slash = n == last,
1098 ".." => {
1099 segments.pop();
1100 trailing_slash = n == last;
1101 }
1102 s => {
1103 segments.push(s);
1104 trailing_slash = false;
1105 }
1106 }
1107 }
1108 let mut out = String::with_capacity(decoded.len() + query.len());
1109 out.push('/');
1110 out.push_str(&segments.join("/"));
1111 if trailing_slash && !segments.is_empty() {
1112 out.push('/');
1113 }
1114 out.push_str(query);
1115 if out == target {
1116 std::borrow::Cow::Borrowed(target)
1117 } else {
1118 std::borrow::Cow::Owned(out)
1119 }
1120}
1121
1122fn host_matches(expected: &str, actual: &str) -> bool {
1125 let actual = if expected.contains(':') {
1126 actual
1127 } else if let Some(rest) = actual.strip_prefix('[') {
1128 rest.split(']').next().unwrap_or(rest)
1130 } else {
1131 actual.rsplit_once(':').map_or(actual, |(host, port)| if port.bytes().all(|b| b.is_ascii_digit()) { host } else { actual })
1132 };
1133 let expected = expected.trim_start_matches('[').trim_end_matches(']');
1134 actual.eq_ignore_ascii_case(expected)
1135}
1136
1137impl Application for ConfigDrivenApp {
1138 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1139 let request = normalized(request);
1140 let generation = self.current();
1141 match &generation.entry {
1142 Some(entry) => entry.execute(&request, conn),
1143 None => generation.dispatch.execute(&request, conn),
1144 }
1145 }
1146
1147 fn streams_body(&self, request: &Request, conn: &ConnectionInfo) -> bool {
1150 let request = normalized(request);
1151 let generation = self.current();
1152 match &generation.entry {
1153 Some(entry) => entry.streams_body(&request, conn),
1154 None => generation.dispatch.streams_body(&request, conn),
1155 }
1156 }
1157
1158 fn runs_inline(&self, request: &Request, conn: &ConnectionInfo) -> bool {
1162 let request = normalized(request);
1163 let generation = self.current();
1164 generation.entry.is_none()
1165 && !generation.dispatch.routes.iter().any(|r| r.matcher.matches(&request, conn))
1166 && App::is_inline_request(&request)
1167 }
1168
1169 #[cfg(feature = "http2")]
1173 fn execute_async<'a>(&'a self, request: &'a Request, conn: &'a ConnectionInfo) -> Option<crate::application::AsyncResponse<'a>> {
1174 let generation = self.current();
1175 if generation.entry.is_some() {
1176 return None;
1177 }
1178 let request = normalized(request);
1179 let route = generation.dispatch.routes.iter().find(|r| r.matcher.matches(&request, conn))?;
1180 let proxy = Arc::clone(route.async_proxy.as_ref()?);
1181 Some(Box::pin(async move { proxy.execute_async(&request, conn).await }))
1182 }
1183}
1184
1185#[cfg(feature = "http2")]
1191pub(crate) struct H2Route {
1192 live: Arc<RwLock<Vec<String>>>,
1193 tls: bool,
1194 trust: Option<Arc<crate::proxy::UpstreamTls>>,
1195 connect_timeout: std::time::Duration,
1196 read_timeout: std::time::Duration,
1197 counter: std::sync::atomic::AtomicUsize,
1198}
1199
1200#[cfg(feature = "http2")]
1201impl H2Route {
1202 pub(crate) fn new(
1203 live: Arc<RwLock<Vec<String>>>,
1204 tls: bool,
1205 trust: Option<Arc<crate::proxy::UpstreamTls>>,
1206 connect_timeout_ms: u64,
1207 read_timeout_ms: u64,
1208 ) -> H2Route {
1209 H2Route {
1210 live,
1211 tls,
1212 trust,
1213 connect_timeout: std::time::Duration::from_millis(connect_timeout_ms),
1214 read_timeout: std::time::Duration::from_millis(read_timeout_ms),
1215 counter: std::sync::atomic::AtomicUsize::new(0),
1216 }
1217 }
1218}
1219
1220#[cfg(feature = "http2")]
1221impl Application for H2Route {
1222 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1223 use crate::middleware::Middleware;
1224 crate::proxy::GrpcWebLayer::new().handle(request, conn, &H2Forward(self))
1225 }
1226
1227 fn streams_body(&self, _request: &Request, _conn: &ConnectionInfo) -> bool {
1228 true
1229 }
1230}
1231
1232#[cfg(feature = "http2")]
1234struct H2Forward<'a>(&'a H2Route);
1235
1236#[cfg(feature = "http2")]
1237impl Application for H2Forward<'_> {
1238 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1239 let route = self.0;
1240 let backends = route.live.read().unwrap_or_else(|e| e.into_inner()).clone();
1241 if backends.is_empty() {
1242 return Ok(crate::proxy::bad_gateway());
1243 }
1244 let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { backends.len() };
1246 let start = route.counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1247 for attempt in 0..attempts {
1248 let url = &backends[(start + attempt) % backends.len()];
1249 let result = crate::proxy::forward_h2(
1250 request, crate::proxy::Peer::of(conn), url, route.tls, route.trust.as_deref(), route.connect_timeout, route.read_timeout,
1251 );
1252 match result {
1253 Ok(response) => return Ok(response),
1254 Err(e) => eprintln!("grpc route: {e}"),
1255 }
1256 }
1257 Ok(crate::proxy::bad_gateway())
1258 }
1259}
1260
1261#[derive(Clone, Copy)]
1266pub(crate) struct NullApp;
1267
1268impl Application for NullApp {
1269 fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1270 let mut r = Response::new();
1271 r.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
1272 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
1273 Ok(r)
1274 }
1275}
1276
1277use std::collections::HashMap;
1280use std::collections::hash_map::DefaultHasher;
1281use std::hash::{Hash, Hasher};
1282use std::sync::atomic::AtomicUsize;
1283use std::time::{Duration, SystemTime, UNIX_EPOCH};
1284
1285#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1289pub(crate) enum LoadBalanceStrategy {
1290 RoundRobin,
1291 Random,
1292 IpHash,
1293 LeastConnections,
1294}
1295
1296impl LoadBalanceStrategy {
1297 fn parse(s: &str) -> Self {
1298 match s {
1299 "random" => LoadBalanceStrategy::Random,
1300 "ip_hash" => LoadBalanceStrategy::IpHash,
1301 "least_connections" => LoadBalanceStrategy::LeastConnections,
1302 _ => LoadBalanceStrategy::RoundRobin,
1303 }
1304 }
1305}
1306
1307#[derive(Clone)]
1315pub(crate) struct DynamicProxy {
1316 live: Arc<RwLock<Vec<String>>>,
1317 counter: Arc<AtomicUsize>,
1318 connect_timeout: Duration,
1319 read_timeout: Duration,
1320 strip_prefix: Option<Arc<String>>,
1321 add_prefix: Option<Arc<String>>,
1322 tls: bool,
1323 strategy: LoadBalanceStrategy,
1324 connections: Arc<RwLock<HashMap<String, Arc<AtomicUsize>>>>,
1325 trust: Option<Arc<crate::proxy::UpstreamTls>>,
1327 upstream: Arc<String>,
1329 policy: Option<Arc<upstream::UpstreamPolicy>>,
1331}
1332
1333impl DynamicProxy {
1334 pub(crate) fn with_trust(mut self, trust: Option<Arc<crate::proxy::UpstreamTls>>) -> Self {
1336 self.trust = trust;
1337 self
1338 }
1339
1340 pub(crate) fn with_policy(mut self, upstream: &str, policy: Option<Arc<upstream::UpstreamPolicy>>) -> Self {
1343 self.upstream = Arc::new(upstream.to_string());
1344 self.policy = policy;
1345 self
1346 }
1347
1348 pub(crate) fn new(
1349 live: Arc<RwLock<Vec<String>>>,
1350 connect_timeout_ms: u64,
1351 read_timeout_ms: u64,
1352 strip_prefix: Option<String>,
1353 add_prefix: Option<String>,
1354 tls: bool,
1355 strategy: String,
1356 ) -> Self {
1357 DynamicProxy {
1358 live,
1359 counter: Arc::new(AtomicUsize::new(0)),
1360 connect_timeout: Duration::from_millis(connect_timeout_ms),
1361 read_timeout: Duration::from_millis(read_timeout_ms),
1362 strip_prefix: strip_prefix.map(Arc::new),
1363 add_prefix: add_prefix.map(Arc::new),
1364 tls,
1365 strategy: LoadBalanceStrategy::parse(&strategy),
1366 connections: Arc::new(RwLock::new(HashMap::new())),
1367 trust: None,
1368 upstream: Arc::new(String::new()),
1369 policy: None,
1370 }
1371 }
1372
1373 fn pick(&self, candidates: &[String], client_ip: &str) -> String {
1375 let policy = self.policy.as_deref().filter(|p| p.weighted());
1376 let idx = match self.strategy {
1377 LoadBalanceStrategy::RoundRobin => match policy {
1378 Some(policy) => return policy.weighted_round_robin(candidates).unwrap_or_else(|| candidates[0].clone()),
1379 None => self.counter.fetch_add(1, Ordering::Relaxed) % candidates.len(),
1380 },
1381 LoadBalanceStrategy::Random => {
1382 let nanos = SystemTime::now()
1383 .duration_since(UNIX_EPOCH)
1384 .map(|d| d.subsec_nanos())
1385 .unwrap_or(0) as usize;
1386 let salt = self.counter.fetch_add(1, Ordering::Relaxed);
1387 let roll = nanos.wrapping_add(salt.wrapping_mul(2_654_435_761));
1388 match policy {
1389 Some(policy) => policy.weighted_index(candidates, roll),
1390 None => roll % candidates.len(),
1391 }
1392 }
1393 LoadBalanceStrategy::IpHash => {
1394 let mut hasher = DefaultHasher::new();
1395 client_ip.hash(&mut hasher);
1396 (hasher.finish() as usize) % candidates.len()
1397 }
1398 LoadBalanceStrategy::LeastConnections => {
1399 let connections = self.connections.read().unwrap();
1400 let load = |backend: &String| connections.get(backend).map(|c| c.load(Ordering::Relaxed)).unwrap_or(0) as u64;
1401 let weight = |backend: &String| self.policy.as_deref().map_or(1, |p| p.weight(backend)) as u64;
1402 candidates
1404 .iter()
1405 .enumerate()
1406 .min_by(|(_, a), (_, b)| (load(a) * weight(b)).cmp(&(load(b) * weight(a))))
1407 .map(|(i, _)| i)
1408 .unwrap_or(0)
1409 }
1410 };
1411 candidates[idx].clone()
1412 }
1413
1414 fn next_backend(&self, request: &Request, client_ip: &str, tried: &[String]) -> Result<String, bool> {
1419 let mut candidates: Vec<String> = self.live.read().unwrap().iter().filter(|b| !tried.contains(b)).cloned().collect();
1420 if !candidates.is_empty() {
1421 candidates.retain(|b| !upstream::is_drained(&self.upstream, b));
1422 if candidates.is_empty() {
1423 return Err(true);
1425 }
1426 }
1427 let Some(policy) = self.policy.as_deref() else {
1428 return if candidates.is_empty() { Err(false) } else { Ok(self.pick(&candidates, client_ip)) };
1429 };
1430 if let Some(sticky) = &policy.sticky {
1431 if let Some(id) = upstream::cookie_value(request, &sticky.cookie) {
1432 if let Some(backend) = candidates.iter().find(|b| upstream::backend_id(b) == id).cloned() {
1433 if policy.admits(&backend) {
1434 return Ok(backend);
1435 }
1436 candidates.retain(|b| *b != backend);
1437 }
1438 }
1439 }
1440 let ejected = policy.any_open(&candidates);
1441 while !candidates.is_empty() {
1442 let backend = self.pick(&candidates, client_ip);
1443 if policy.admits(&backend) {
1444 return Ok(backend);
1445 }
1446 candidates.retain(|b| *b != backend);
1447 }
1448 Err(ejected)
1449 }
1450
1451 fn connection_counter(&self, backend: &str) -> Arc<AtomicUsize> {
1454 if let Some(counter) = self.connections.read().unwrap().get(backend) {
1455 return Arc::clone(counter);
1456 }
1457 let mut connections = self.connections.write().unwrap();
1458 Arc::clone(
1459 connections
1460 .entry(backend.to_string())
1461 .or_insert_with(|| Arc::new(AtomicUsize::new(0))),
1462 )
1463 }
1464}
1465
1466struct ConnectionGuard {
1469 counter: Arc<AtomicUsize>,
1470}
1471
1472impl Drop for ConnectionGuard {
1473 fn drop(&mut self) {
1474 self.counter.fetch_sub(1, Ordering::Relaxed);
1475 }
1476}
1477
1478struct Prepared<'a> {
1481 backend: String,
1482 host: String,
1483 port: u16,
1484 request: std::borrow::Cow<'a, Request>,
1485 _guard: Option<ConnectionGuard>,
1486}
1487
1488enum Attempt {
1490 Done(Response),
1492 Failed(Option<Response>),
1495}
1496
1497impl DynamicProxy {
1498 fn prepare<'a>(&self, request: &'a Request, conn: &ConnectionInfo, tried: &[String]) -> Result<Prepared<'a>, Response> {
1503 let backend = self.next_backend(request, &conn.client.ip, tried).map_err(|ejected| if ejected { service_unavailable() } else { bad_gateway() })?;
1504
1505 let guard = if self.strategy == LoadBalanceStrategy::LeastConnections {
1506 let counter = self.connection_counter(&backend);
1507 counter.fetch_add(1, Ordering::Relaxed);
1508 Some(ConnectionGuard { counter })
1509 } else {
1510 None
1511 };
1512
1513 let (host, port, _) = crate::proxy_config::health::parse_backend_url(&backend).ok_or_else(bad_gateway)?;
1514
1515 let request = if self.strip_prefix.is_some() || self.add_prefix.is_some() {
1517 let mut req_clone = request.clone();
1518 if let Some(ref sp) = self.strip_prefix {
1519 if let Some(stripped) = req_clone.request_uri.strip_prefix(sp.as_str()) {
1520 req_clone.request_uri = if stripped.is_empty() || !stripped.starts_with('/') {
1521 format!("/{}", stripped)
1522 } else {
1523 stripped.to_string()
1524 };
1525 }
1526 }
1527 if let Some(ref ap) = self.add_prefix {
1528 req_clone.request_uri = format!("{}{}", ap, req_clone.request_uri);
1529 }
1530 std::borrow::Cow::Owned(req_clone)
1531 } else {
1532 std::borrow::Cow::Borrowed(request)
1533 };
1534
1535 Ok(Prepared { backend, host, port, request, _guard: guard })
1536 }
1537
1538 fn settle(&self, backend: &str, result: Result<Response, ()>, started: std::time::Instant) -> Attempt {
1540 let status = result.as_ref().ok().map(|r| r.status_code);
1541 upstream::observe(&self.upstream, backend, status, started);
1542 let failed = status.is_none_or(|s| matches!(s, 502..=504));
1543 if let Some(policy) = &self.policy {
1544 policy.record(backend, failed);
1545 }
1546 match result {
1547 Ok(response) if self.policy.as_deref().is_some_and(|p| p.retries_status(response.status_code)) => Attempt::Failed(Some(response)),
1548 Ok(response) => Attempt::Done(response),
1549 Err(()) => Attempt::Failed(None),
1550 }
1551 }
1552
1553 fn attempts(&self, request: &Request) -> u32 {
1555 match self.policy.as_deref().and_then(|p| p.retry.as_ref()) {
1556 Some(retry) if upstream::replayable(request) => 1 + retry.attempts,
1557 _ => 1,
1558 }
1559 }
1560
1561 fn stick(&self, request: &Request, backend: &str, response: &mut Response) {
1563 let Some(sticky) = self.policy.as_deref().and_then(|p| p.sticky.as_ref()) else { return };
1564 let id = upstream::backend_id(backend);
1565 if upstream::cookie_value(request, &sticky.cookie).as_deref() == Some(id.as_str()) {
1566 return;
1567 }
1568 let max_age = sticky.max_age_secs.map(|s| format!("; Max-Age={s}")).unwrap_or_default();
1569 response.headers.push(crate::header::Header {
1570 name: "Set-Cookie".into(),
1571 value: format!("{}={id}; Path=/; HttpOnly; SameSite=Lax{max_age}", sticky.cookie).into(),
1572 });
1573 }
1574
1575 #[cfg(feature = "http2")]
1578 pub(crate) async fn execute_async(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1579 let mut tried: Vec<String> = Vec::new();
1580 let mut last: Option<Response> = None;
1581 for attempt in 0..self.attempts(request) {
1582 let prepared = match self.prepare(request, conn, &tried) {
1583 Ok(p) => p,
1584 Err(response) => return Ok(last.unwrap_or(response)),
1585 };
1586 if attempt > 0 {
1587 upstream::observe_retry(&self.upstream);
1588 }
1589 let tls = self.tls.then(|| match &self.trust {
1590 Some(trust) => trust.async_tls(),
1591 None => crate::proxy::async_upstream::AsyncTls {
1592 config: crate::proxy::tls_client_config(),
1593 pool: crate::proxy::async_upstream::default_tls_pool(),
1594 },
1595 });
1596 let started = std::time::Instant::now();
1597 let result = crate::proxy::async_upstream::forward(
1598 &prepared.request,
1599 crate::proxy::Peer::of(conn),
1600 &prepared.host,
1601 prepared.port,
1602 tls,
1603 self.connect_timeout,
1604 self.read_timeout,
1605 )
1606 .await
1607 .map_err(|_| ());
1608 match self.settle(&prepared.backend, result, started) {
1609 Attempt::Done(mut response) => {
1610 self.stick(request, &prepared.backend, &mut response);
1611 return Ok(response);
1612 }
1613 Attempt::Failed(response) => last = response,
1614 }
1615 tried.push(prepared.backend.clone());
1616 }
1617 Ok(last.unwrap_or_else(bad_gateway))
1618 }
1619
1620 pub(crate) fn supports_async(&self) -> bool {
1622 cfg!(feature = "http2")
1623 }
1624}
1625
1626impl Application for DynamicProxy {
1627 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
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 started = std::time::Instant::now();
1641 let result = crate::proxy::forward_upstream_trusting(
1642 crate::proxy::shared_pool(),
1643 &prepared.request,
1644 crate::proxy::Peer::of(conn),
1645 &prepared.host,
1646 prepared.port,
1647 self.tls,
1648 self.trust.as_deref(),
1649 self.connect_timeout,
1650 self.read_timeout,
1651 )
1652 .map_err(|_| ());
1653 match self.settle(&prepared.backend, result, started) {
1654 Attempt::Done(mut response) => {
1655 self.stick(request, &prepared.backend, &mut response);
1656 return Ok(response);
1657 }
1658 Attempt::Failed(response) => last = response,
1659 }
1660 tried.push(prepared.backend.clone());
1661 }
1662 Ok(last.unwrap_or_else(bad_gateway))
1663 }
1664}
1665
1666fn service_unavailable() -> Response {
1667 use crate::mime_type::MimeType;
1668 use crate::range::Range;
1669 let mut r = Response::new();
1670 r.status_code = *STATUS_CODE_REASON_PHRASE.n503_service_unavailable.status_code;
1671 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n503_service_unavailable.reason_phrase.to_string();
1672 r.headers.push(crate::header::Header { name: "Retry-After".into(), value: "5".into() });
1673 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())];
1674 r
1675}
1676
1677fn bad_gateway() -> Response {
1678 use crate::mime_type::MimeType;
1679 use crate::range::Range;
1680 let cr = Range::get_content_range(
1681 b"502 Bad Gateway".to_vec(),
1682 MimeType::TEXT_PLAIN.to_string(),
1683 );
1684 let mut r = Response::new();
1685 r.status_code = *STATUS_CODE_REASON_PHRASE.n502_bad_gateway.status_code;
1686 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n502_bad_gateway.reason_phrase.to_string();
1687 r.content_range_list = vec![cr];
1688 r
1689}
1690
1691#[derive(Clone)]
1697pub(crate) struct RedirectAdapter {
1698 location_template: Arc<String>,
1699 status: i16,
1700 reason: Arc<String>,
1701}
1702
1703impl RedirectAdapter {
1704 pub(crate) fn new(location: String, status: u16) -> Self {
1705 let (code, reason) = redirect_status(status);
1706 RedirectAdapter {
1707 location_template: Arc::new(location),
1708 status: code,
1709 reason: Arc::new(reason),
1710 }
1711 }
1712}
1713
1714fn redirect_status(code: u16) -> (i16, String) {
1715 let phrase = match code {
1716 301 => STATUS_CODE_REASON_PHRASE.n301_moved_permanently.reason_phrase,
1717 302 => STATUS_CODE_REASON_PHRASE.n302_found.reason_phrase,
1718 307 => STATUS_CODE_REASON_PHRASE.n307_temporary_redirect.reason_phrase,
1719 308 => STATUS_CODE_REASON_PHRASE.n308_permanent_redirect.reason_phrase,
1720 _ => "Redirect",
1721 };
1722 (code as i16, phrase.to_string())
1723}
1724
1725impl Application for RedirectAdapter {
1726 fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1727 let location = self
1728 .location_template
1729 .replace("$path", &request.request_uri);
1730 use crate::header::Header;
1731 let mut r = Response::new();
1732 r.status_code = self.status;
1733 r.reason_phrase = self.reason.as_ref().clone();
1734 r.headers.push(Header { name: "Location".into(), value: location.into() });
1735 Ok(r)
1736 }
1737}
1738
1739#[derive(Clone)]
1743pub(crate) struct RespondAdapter {
1744 status: i16,
1745 reason: Arc<String>,
1746 body: Arc<Vec<u8>>,
1747 content_type: Arc<String>,
1748}
1749
1750impl RespondAdapter {
1751 pub(crate) fn new(status: u16, body: String, content_type: String) -> Self {
1752 use crate::response::STATUS_CODE_REASON_PHRASE;
1753 let reason = match status {
1754 200 => STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string(),
1755 201 => STATUS_CODE_REASON_PHRASE.n201_created.reason_phrase.to_string(),
1756 204 => STATUS_CODE_REASON_PHRASE.n204_no_content.reason_phrase.to_string(),
1757 400 => STATUS_CODE_REASON_PHRASE.n400_bad_request.reason_phrase.to_string(),
1758 401 => STATUS_CODE_REASON_PHRASE.n401_unauthorized.reason_phrase.to_string(),
1759 403 => STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string(),
1760 404 => STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string(),
1761 500 => STATUS_CODE_REASON_PHRASE.n500_internal_server_error.reason_phrase.to_string(),
1762 _ => "OK".to_string(),
1763 };
1764 RespondAdapter {
1765 status: status as i16,
1766 reason: Arc::new(reason),
1767 body: Arc::new(body.into_bytes()),
1768 content_type: Arc::new(content_type),
1769 }
1770 }
1771}
1772
1773impl Application for RespondAdapter {
1774 fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1775 use crate::range::Range;
1776 let mut r = Response::new();
1777 r.status_code = self.status;
1778 r.reason_phrase = self.reason.as_ref().clone();
1779 if !self.body.is_empty() {
1780 r.content_range_list = vec![Range::get_content_range(
1781 self.body.as_ref().clone(),
1782 self.content_type.as_ref().clone(),
1783 )];
1784 }
1785 Ok(r)
1786 }
1787}
1788
1789#[derive(Clone)]
1798pub(crate) struct StaticAdapter {
1799 root: Arc<std::path::PathBuf>,
1800 index: Arc<Vec<String>>,
1801}
1802
1803impl StaticAdapter {
1804 pub(crate) fn new(root: String, index: Vec<String>) -> Self {
1805 let index = if index.is_empty() { vec!["index.html".to_string()] } else { index };
1806 StaticAdapter {
1807 root: Arc::new(std::path::PathBuf::from(root)),
1808 index: Arc::new(index),
1809 }
1810 }
1811
1812 fn resolve(&self, request_uri: &str) -> Option<std::path::PathBuf> {
1816 let raw_path = request_uri.split('?').next().unwrap_or(request_uri);
1817 let decoded = crate::url::URL::percent_decode(raw_path);
1818
1819 if decoded.split('/').any(|segment| segment == "..") {
1820 return None;
1821 }
1822
1823 let relative = decoded.trim_start_matches('/');
1824 Some(self.root.join(relative))
1825 }
1826}
1827
1828impl Application for StaticAdapter {
1829 fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1830 let mut response = Response::new();
1831
1832 let not_found = |mut response: Response| {
1833 response.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
1834 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
1835 response
1836 };
1837
1838 let candidate = match self.resolve(&request.request_uri) {
1839 Some(p) => p,
1840 None => {
1841 response.status_code = *STATUS_CODE_REASON_PHRASE.n403_forbidden.status_code;
1842 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string();
1843 return Ok(response);
1844 }
1845 };
1846
1847 let mut file_path = candidate;
1848 if file_path.is_dir() {
1849 let indexed = self
1850 .index
1851 .iter()
1852 .map(|name| file_path.join(name))
1853 .find(|p| p.is_file());
1854
1855 file_path = match indexed {
1856 Some(p) => p,
1857 None => return Ok(not_found(response)),
1858 };
1859 }
1860
1861 if !file_path.is_file() {
1862 return Ok(not_found(response));
1863 }
1864
1865 if let (Ok(root_canon), Ok(file_canon)) =
1868 (self.root.canonicalize(), file_path.canonicalize())
1869 {
1870 if !file_canon.starts_with(&root_canon) {
1871 return Ok(not_found(response));
1872 }
1873 }
1874
1875 let path_str = match file_path.to_str() {
1876 Some(s) => s,
1877 None => return Ok(not_found(response)),
1878 };
1879
1880 match crate::range::Range::get_content_range_of_a_file(path_str) {
1881 Ok(content_range) => {
1882 response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
1883 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
1884 response.content_range_list = vec![content_range];
1885 Ok(response)
1886 }
1887 Err(_) => Ok(not_found(response)),
1888 }
1889 }
1890}
1891
1892pub(crate) struct PerRouteRateLimit(pub(crate) Arc<crate::rate_limit::RateLimiter>);
1896
1897impl crate::middleware::Middleware for PerRouteRateLimit {
1898 fn handle(
1899 &self,
1900 request: &Request,
1901 conn: &ConnectionInfo,
1902 next: &dyn Application,
1903 ) -> Result<Response, String> {
1904 use crate::error::{AppError, IntoResponse};
1905 if self.0.check(&conn.client.ip) {
1906 next.execute(request, conn)
1907 } else {
1908 let mut response = AppError::TooManyRequests.into_response();
1909 let retry_after = self.0.retry_after(&conn.client.ip);
1910 response.headers.push(Header {
1911 name: Header::_RETRY_AFTER.into(),
1912 value: retry_after.to_string().into(),
1913 });
1914 Ok(response)
1915 }
1916 }
1917}
1918
1919pub(crate) struct PerRouteMaxBodySize(pub(crate) u64);
1934
1935impl crate::middleware::Middleware for PerRouteMaxBodySize {
1936 fn handle(
1937 &self,
1938 request: &Request,
1939 conn: &ConnectionInfo,
1940 next: &dyn Application,
1941 ) -> Result<Response, String> {
1942 use crate::error::{AppError, IntoResponse};
1943 if request.body.len() as u64 > self.0 {
1944 Ok(AppError::PayloadTooLarge(format!(
1945 "request body of {} bytes exceeds this route's {}-byte limit",
1946 request.body.len(),
1947 self.0,
1948 ))
1949 .into_response())
1950 } else {
1951 next.execute(request, conn)
1952 }
1953 }
1954}
1955
1956pub(crate) struct BearerAuthMiddleware {
1960 pub(crate) token: Arc<String>,
1961}
1962
1963impl crate::middleware::Middleware for BearerAuthMiddleware {
1964 fn handle(
1965 &self,
1966 request: &Request,
1967 conn: &ConnectionInfo,
1968 next: &dyn Application,
1969 ) -> Result<Response, String> {
1970 use crate::error::{AppError, IntoResponse};
1971 let expected = format!("Bearer {}", self.token);
1972 let authorized = request
1973 .headers
1974 .iter()
1975 .any(|h| h.name.eq_ignore_ascii_case("authorization") && constant_time_eq(h.value.as_bytes(), expected.as_bytes()));
1976 if authorized {
1977 next.execute(request, conn)
1978 } else {
1979 Ok(AppError::Unauthorized.into_response())
1980 }
1981 }
1982}
1983
1984pub(crate) fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
1987 if a.len() != b.len() {
1988 return false;
1989 }
1990 a.iter().zip(b).fold(0u8, |diff, (x, y)| diff | (x ^ y)) == 0
1991}
1992
1993pub(crate) struct AuthMisconfigured;
1996
1997pub(crate) struct PluginMisconfigured;
2000
2001impl crate::middleware::Middleware for PluginMisconfigured {
2002 fn handle(&self, _request: &Request, _conn: &ConnectionInfo, _next: &dyn Application) -> Result<Response, String> {
2003 Ok(Response::text(
2004 STATUS_CODE_REASON_PHRASE.n503_service_unavailable,
2005 "503 Service Unavailable: this route's plugin is misconfigured",
2006 ))
2007 }
2008}
2009
2010impl crate::middleware::Middleware for AuthMisconfigured {
2011 fn handle(
2012 &self,
2013 _request: &Request,
2014 _conn: &ConnectionInfo,
2015 _next: &dyn Application,
2016 ) -> Result<Response, String> {
2017 Ok(Response::text(
2018 STATUS_CODE_REASON_PHRASE.n503_service_unavailable,
2019 "503 Service Unavailable: authentication for this route is misconfigured",
2020 ))
2021 }
2022}
2023
2024pub(crate) fn arc_app<A: Application + Send + Sync + 'static>(
2028 a: A,
2029) -> Arc<dyn Application + Send + Sync> {
2030 Arc::new(a)
2031}
2032
2033pub fn build_from_file() -> (ConfigDrivenApp, Vec<std::thread::JoinHandle<()>>) {
2038 builder::build_from_file()
2039}