1pub mod parser;
29pub mod health;
30pub mod builder;
31pub mod validate;
32
33#[cfg(test)]
34mod tests;
35
36use std::sync::atomic::{AtomicBool, Ordering};
37use std::sync::{Arc, RwLock};
38
39use crate::app::App;
40use crate::application::Application;
41use crate::core::New;
42use crate::request::Request;
43use crate::header::Header;
44use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
45use crate::server::ConnectionInfo;
46use crate::server_config::ServerConfig;
47
48#[derive(Debug, Clone)]
51pub struct ProxyConfig {
52 pub upstreams: Vec<UpstreamConfig>,
53 pub routes: Vec<RouteConfig>,
54 pub tcp_proxies: Vec<TcpProxyConfig>,
55 pub udp_proxies: Vec<UdpProxyConfig>,
56 pub ws_proxies: Vec<WsProxyConfig>,
57 pub global_middleware: MiddlewareConfig,
58}
59
60#[derive(Debug, Clone)]
61pub struct UpstreamConfig {
62 pub name: String,
63 pub backends: Vec<String>,
64 pub strategy: String, pub health_check: Option<HealthCheckConfig>,
66 pub tls: bool,
70}
71
72#[derive(Debug, Clone)]
73pub struct HealthCheckConfig {
74 pub path: String,
75 pub interval_secs: u64,
76 pub timeout_ms: u64,
77 pub healthy_threshold: u32,
78 pub unhealthy_threshold: u32,
79}
80
81#[derive(Debug, Clone)]
82pub struct RouteConfig {
83 pub name: String,
84 pub match_: MatchConfig,
85 pub action: ActionConfig,
86 pub middleware: MiddlewareConfig,
87}
88
89#[derive(Debug, Clone, Default)]
90pub struct MatchConfig {
91 pub host: Option<String>,
92 pub path: Option<String>,
93 pub method: Option<String>,
94 pub content_type: Option<String>,
95}
96
97#[derive(Debug, Clone)]
98pub enum ActionConfig {
99 Proxy {
100 upstream: String,
101 connect_timeout_ms: u64,
102 read_timeout_ms: u64,
103 strip_path_prefix: Option<String>,
104 add_path_prefix: Option<String>,
105 },
106 Grpc {
107 upstream: String,
108 connect_timeout_ms: u64,
109 read_timeout_ms: u64,
110 },
111 Static {
112 root: String,
113 index: Vec<String>,
114 },
115 Redirect {
116 location: String,
117 status: u16,
118 },
119 Respond {
120 status: u16,
121 body: String,
122 content_type: String,
123 },
124 Mcp,
125 Unknown(String),
126}
127
128#[derive(Debug, Clone, Default)]
129pub struct MiddlewareConfig {
130 pub rate_limit: Option<RateLimitConfig>,
131 pub cache: Option<CacheConfig>,
132 pub auth: Option<AuthConfig>,
133 pub rewrite_request: Vec<RewriteRuleConfig>,
134 pub rewrite_response: Vec<RewriteRuleConfig>,
135 pub ip_allow: Vec<String>,
136 pub ip_deny: Vec<String>,
137 pub timeout_ms: Option<u64>,
142 pub max_body_size: Option<u64>,
158}
159
160#[derive(Debug, Clone)]
161pub struct RateLimitConfig {
162 pub max_requests: u32,
163 pub window_secs: u64,
164}
165
166#[derive(Debug, Clone)]
167pub struct CacheConfig {
168 pub ttl_secs: u64,
169 pub vary_by: Vec<String>,
170}
171
172#[derive(Debug, Clone)]
173pub enum AuthConfig {
174 Basic { htpasswd_file: String },
179 Jwt { secret_env: String },
182 Bearer { token_env: String },
183}
184
185#[derive(Debug, Clone, Default)]
186pub struct RewriteRuleConfig {
187 pub type_: String,
188 pub name: Option<String>,
189 pub value: Option<String>,
190 pub prefix: Option<String>,
191 pub from: Option<String>,
192 pub to: Option<String>,
193 pub code: Option<u16>,
194 pub reason: Option<String>,
195}
196
197#[derive(Debug, Clone)]
198pub struct TcpProxyConfig {
199 pub name: String,
200 pub listen: String,
201 pub backends: Vec<String>,
202 pub connect_timeout_ms: u64,
203}
204
205#[derive(Debug, Clone)]
206pub struct UdpProxyConfig {
207 pub name: String,
208 pub listen: String,
209 pub backends: Vec<String>,
210 pub reply_timeout_ms: u64,
211 pub buffer_size: usize,
212}
213
214#[derive(Debug, Clone)]
215pub struct WsProxyConfig {
216 pub name: String,
217 pub listen: String,
218 pub backends: Vec<String>,
219 pub connect_timeout_ms: u64,
220 pub read_timeout_ms: u64,
221 pub health_check: Option<HealthCheckConfig>,
228}
229
230fn merge_action_keys(map: &mut parser::SectionMap, action_section: &str, action_type: &str) {
239 if action_type.is_empty() {
240 return;
241 }
242 let short_form: Vec<(String, String)> = match map.get(action_section) {
243 Some(entries) => entries.iter().filter(|(k, _)| k != "type").cloned().collect(),
244 None => return,
245 };
246 let nested = map.entry(format!("{action_section}.{action_type}")).or_default();
247 for (key, value) in short_form {
248 if !nested.iter().any(|(k, _)| *k == key) {
249 nested.push((key, value));
250 }
251 }
252}
253
254impl ProxyConfig {
255 pub fn is_proxy_mode() -> bool {
259 let path = config_file_path();
260 match std::fs::read_to_string(&path) {
261 Ok(contents) => {
262 contents.contains("[[route]]") || contents.contains("[[upstream]]")
263 }
264 Err(_) => false,
265 }
266 }
267
268 pub fn load() -> Self {
270 let path = config_file_path();
271 let contents = std::fs::read_to_string(&path).unwrap_or_default();
272 Self::from_str(&contents)
273 }
274
275 pub fn from_str(toml: &str) -> Self {
277 use parser::{get_array, get_str, get_u32, get_u64, section_exists};
278
279 let mut map = parser::parse(toml);
280
281 let mut upstreams = Vec::new();
283 let mut i = 0;
284 loop {
285 let sec = format!("upstream[{}]", i);
286 if !section_exists(&map, &sec) {
287 break;
288 }
289 let name = get_str(&map, &sec, "name");
290 let backends = get_array(&map, &sec, "backends");
291 let strategy = {
292 let s = get_str(&map, &sec, "strategy");
293 if s.is_empty() { "round_robin".to_string() } else { s }
294 };
295 let hc_sec = format!("{}.health_check", sec);
296 let health_check = if section_exists(&map, &hc_sec) {
297 Some(HealthCheckConfig {
298 path: {
299 let p = get_str(&map, &hc_sec, "path");
300 if p.is_empty() { "/health".to_string() } else { p }
301 },
302 interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
303 timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
304 healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
305 unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
306 })
307 } else {
308 None
309 };
310 let tls = backends.iter().any(|b| b.starts_with("https://"));
311 upstreams.push(UpstreamConfig { name, backends, strategy, health_check, tls });
312 i += 1;
313 }
314
315 let mut routes = Vec::new();
317 let mut i = 0;
318 loop {
319 let sec = format!("route[{}]", i);
320 if !section_exists(&map, &sec) {
321 break;
322 }
323 let name = get_str(&map, &sec, "name");
324
325 let m_sec = format!("{}.match", sec);
327 let match_ = MatchConfig {
328 host: {
329 let h = get_str(&map, &m_sec, "host");
330 if h.is_empty() { None } else { Some(h) }
331 },
332 path: {
333 let p = get_str(&map, &m_sec, "path");
334 if p.is_empty() { None } else { Some(p) }
335 },
336 method: {
337 let m = get_str(&map, &m_sec, "method");
338 if m.is_empty() { None } else { Some(m.to_uppercase()) }
339 },
340 content_type: {
341 let c = get_str(&map, &m_sec, "content_type");
342 if c.is_empty() { None } else { Some(c) }
343 },
344 };
345
346 let a_sec = format!("{}.action", sec);
348 let action_type = get_str(&map, &a_sec, "type");
349 merge_action_keys(&mut map, &a_sec, &action_type);
350 let action = match action_type.as_str() {
351 "proxy" => {
352 let p_sec = format!("{}.action.proxy", sec);
353 ActionConfig::Proxy {
354 upstream: get_str(&map, &p_sec, "upstream"),
355 connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
356 read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
357 strip_path_prefix: {
358 let v = get_str(&map, &p_sec, "strip_path_prefix");
359 if v.is_empty() { None } else { Some(v) }
360 },
361 add_path_prefix: {
362 let v = get_str(&map, &p_sec, "add_path_prefix");
363 if v.is_empty() { None } else { Some(v) }
364 },
365 }
366 }
367 "grpc" => {
368 let p_sec = format!("{}.action.grpc", sec);
369 ActionConfig::Grpc {
370 upstream: get_str(&map, &p_sec, "upstream"),
371 connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
372 read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
373 }
374 }
375 "static" => {
376 let s_sec = format!("{}.action.static", sec);
377 ActionConfig::Static {
378 root: get_str(&map, &s_sec, "root"),
379 index: get_array(&map, &s_sec, "index"),
380 }
381 }
382 "redirect" => {
383 let r_sec = format!("{}.action.redirect", sec);
384 ActionConfig::Redirect {
385 location: get_str(&map, &r_sec, "location"),
386 status: get_u64(&map, &r_sec, "status", 301) as u16,
387 }
388 }
389 "respond" => {
390 let r_sec = format!("{}.action.respond", sec);
391 ActionConfig::Respond {
392 status: get_u64(&map, &r_sec, "status", 200) as u16,
393 body: get_str(&map, &r_sec, "body"),
394 content_type: {
395 let ct = get_str(&map, &r_sec, "content_type");
396 if ct.is_empty() { "text/plain".to_string() } else { ct }
397 },
398 }
399 }
400 "mcp" => ActionConfig::Mcp,
401 other => ActionConfig::Unknown(other.to_string()),
402 };
403
404 let mw_sec = format!("{}.middleware", sec);
406 let middleware = parse_middleware_config(&map, &mw_sec, i);
407
408 routes.push(RouteConfig { name, match_, action, middleware });
409 i += 1;
410 }
411
412 let mut tcp_proxies = Vec::new();
414 let mut i = 0;
415 loop {
416 let sec = format!("tcp_proxy[{}]", i);
417 if !section_exists(&map, &sec) {
418 break;
419 }
420 tcp_proxies.push(TcpProxyConfig {
421 name: get_str(&map, &sec, "name"),
422 listen: get_str(&map, &sec, "listen"),
423 backends: get_array(&map, &sec, "backends"),
424 connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
425 });
426 i += 1;
427 }
428
429 let mut udp_proxies = Vec::new();
431 let mut i = 0;
432 loop {
433 let sec = format!("udp_proxy[{}]", i);
434 if !section_exists(&map, &sec) {
435 break;
436 }
437 udp_proxies.push(UdpProxyConfig {
438 name: get_str(&map, &sec, "name"),
439 listen: get_str(&map, &sec, "listen"),
440 backends: get_array(&map, &sec, "backends"),
441 reply_timeout_ms: get_u64(&map, &sec, "reply_timeout_ms", 5000),
442 buffer_size: get_u64(&map, &sec, "buffer_size", 65536) as usize,
443 });
444 i += 1;
445 }
446
447 let mut ws_proxies = Vec::new();
449 let mut i = 0;
450 loop {
451 let sec = format!("ws_proxy[{}]", i);
452 if !section_exists(&map, &sec) {
453 break;
454 }
455 let hc_sec = format!("{}.health_check", sec);
456 let health_check = if section_exists(&map, &hc_sec) {
457 Some(HealthCheckConfig {
458 path: {
459 let p = get_str(&map, &hc_sec, "path");
460 if p.is_empty() { "/health".to_string() } else { p }
461 },
462 interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
463 timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
464 healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
465 unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
466 })
467 } else {
468 None
469 };
470 ws_proxies.push(WsProxyConfig {
471 name: get_str(&map, &sec, "name"),
472 listen: get_str(&map, &sec, "listen"),
473 backends: get_array(&map, &sec, "backends"),
474 connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
475 read_timeout_ms: get_u64(&map, &sec, "read_timeout_ms", 30000),
476 health_check,
477 });
478 i += 1;
479 }
480
481 let global_middleware = parse_middleware_config(&map, "middleware", usize::MAX);
483
484 ProxyConfig {
485 upstreams,
486 routes,
487 tcp_proxies,
488 udp_proxies,
489 ws_proxies,
490 global_middleware,
491 }
492 }
493}
494
495fn parse_middleware_config(
498 map: &parser::SectionMap,
499 mw_sec: &str,
500 route_idx: usize,
501) -> MiddlewareConfig {
502 use parser::{get_array, get_str, get_u32, get_u64, section_exists};
503
504 let rl_sec = format!("{}.rate_limit", mw_sec);
505 let rate_limit = if section_exists(map, &rl_sec) {
506 Some(RateLimitConfig {
507 max_requests: get_u32(map, &rl_sec, "max_requests", 1000),
508 window_secs: get_u64(map, &rl_sec, "window_secs", 60),
509 })
510 } else {
511 None
512 };
513
514 let c_sec = format!("{}.cache", mw_sec);
515 let cache = if section_exists(map, &c_sec) {
516 Some(CacheConfig {
517 ttl_secs: get_u64(map, &c_sec, "ttl_secs", 60),
518 vary_by: get_array(map, &c_sec, "vary_by"),
519 })
520 } else {
521 None
522 };
523
524 let a_sec = format!("{}.auth", mw_sec);
525 let auth = if section_exists(map, &a_sec) {
526 let auth_type = get_str(map, &a_sec, "type");
527 match auth_type.as_str() {
528 "bearer" => Some(AuthConfig::Bearer {
529 token_env: get_str(map, &a_sec, "token_env"),
530 }),
531 "jwt" => Some(AuthConfig::Jwt {
532 secret_env: get_str(map, &a_sec, "secret_env"),
533 }),
534 "basic" => Some(AuthConfig::Basic {
535 htpasswd_file: get_str(map, &a_sec, "htpasswd_file"),
536 }),
537 _ => None,
538 }
539 } else {
540 None
541 };
542
543 let rewrite_request = collect_rewrite_rules(map, mw_sec, "request");
549 let rewrite_response = collect_rewrite_rules(map, mw_sec, "response");
550
551 let ip_sec = format!("{}.ip_filter", mw_sec);
552 let ip_allow = if section_exists(map, &ip_sec) {
553 get_array(map, &ip_sec, "allow")
554 } else {
555 vec![]
556 };
557 let ip_deny = if section_exists(map, &ip_sec) {
558 get_array(map, &ip_sec, "deny")
559 } else {
560 vec![]
561 };
562
563 let _ = route_idx; let timeout_ms = match get_u64(map, mw_sec, "timeout_ms", 0) {
569 0 => None,
570 ms => Some(ms),
571 };
572
573 let max_body_size = match get_u64(map, mw_sec, "max_body_size", 0) {
577 0 => None,
578 n => Some(n),
579 };
580
581 MiddlewareConfig {
582 rate_limit, cache, auth, rewrite_request, rewrite_response, ip_allow, ip_deny,
583 timeout_ms, max_body_size,
584 }
585}
586
587fn collect_rewrite_rules(
589 map: &parser::SectionMap,
590 mw_sec: &str,
591 direction: &str,
592) -> Vec<RewriteRuleConfig> {
593 use parser::{get_str, get_u64};
594
595 let mut rules = Vec::new();
596 let mut j = 0;
597 loop {
598 let rsec = format!("{}.rewrite.{}[{}]", mw_sec, direction, j);
599 if !parser::section_exists(map, &rsec) {
600 break;
601 }
602 let code_val = get_u64(map, &rsec, "code", 0);
603 rules.push(RewriteRuleConfig {
604 type_: get_str(map, &rsec, "type"),
605 name: {
606 let v = get_str(map, &rsec, "name");
607 if v.is_empty() { None } else { Some(v) }
608 },
609 value: {
610 let v = get_str(map, &rsec, "value");
611 if v.is_empty() { None } else { Some(v) }
612 },
613 prefix: {
614 let v = get_str(map, &rsec, "prefix");
615 if v.is_empty() { None } else { Some(v) }
616 },
617 from: {
618 let v = get_str(map, &rsec, "from");
619 if v.is_empty() { None } else { Some(v) }
620 },
621 to: {
622 let v = get_str(map, &rsec, "to");
623 if v.is_empty() { None } else { Some(v) }
624 },
625 code: if code_val == 0 { None } else { Some(code_val as u16) },
626 reason: {
627 let v = get_str(map, &rsec, "reason");
628 if v.is_empty() { None } else { Some(v) }
629 },
630 });
631 j += 1;
632 }
633 rules
634}
635
636pub(crate) fn config_file_path() -> String {
637 std::env::var("RWS_CONFIG_FILE").unwrap_or_else(|_| "rws.config.toml".to_string())
638}
639
640pub(crate) struct CompiledRoute {
644 pub(crate) matcher: RouteMatcher,
645 pub(crate) handler: Arc<dyn Application + Send + Sync>,
647 #[cfg_attr(not(feature = "http2"), allow(dead_code))]
651 pub(crate) async_proxy: Option<Arc<DynamicProxy>>,
652}
653
654#[derive(Clone, Default)]
656pub(crate) struct RouteMatcher {
657 pub(crate) host: Option<String>,
659 pub(crate) path_prefix: Option<String>,
661 pub(crate) path_exact: Option<String>,
663 pub(crate) method: Option<String>,
665 pub(crate) content_type_prefix: Option<String>,
667}
668
669impl RouteMatcher {
670 pub(crate) fn from_match_config(cfg: &MatchConfig) -> Self {
671 let (path_prefix, path_exact) = match &cfg.path {
672 Some(p) if p.ends_with('*') => {
673 let stripped = p.trim_end_matches('*').to_string();
675 (Some(stripped), None)
676 }
677 Some(p) => (None, Some(p.clone())),
678 None => (None, None),
679 };
680 let content_type_prefix = cfg.content_type.as_ref().map(|ct| {
681 if ct.ends_with('*') {
682 ct.trim_end_matches('*').to_string()
683 } else {
684 ct.clone()
685 }
686 });
687 RouteMatcher {
688 host: cfg.host.clone(),
689 path_prefix,
690 path_exact,
691 method: cfg.method.clone(),
692 content_type_prefix,
693 }
694 }
695
696 pub(crate) fn matches(&self, request: &Request, conn: &ConnectionInfo) -> bool {
698 if let Some(ref expected_host) = self.host {
700 let actual_host = conn
701 .sni_hostname
702 .as_deref()
703 .or_else(|| {
704 request
705 .headers
706 .iter()
707 .find(|h| h.name.eq_ignore_ascii_case("host"))
708 .map(|h| h.value.as_str())
709 })
710 .unwrap_or("");
711 if actual_host != expected_host.as_str() {
712 return false;
713 }
714 }
715
716 if let Some(ref m) = self.method {
718 if request.method.to_uppercase() != m.as_str() {
719 return false;
720 }
721 }
722
723 let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
725 if let Some(ref prefix) = self.path_prefix {
726 if !path.starts_with(prefix.as_str()) {
727 return false;
728 }
729 } else if let Some(ref exact) = self.path_exact {
730 if path != exact.as_str() {
731 return false;
732 }
733 }
734
735 if let Some(ref ct_prefix) = self.content_type_prefix {
737 let actual_ct = request
738 .headers
739 .iter()
740 .find(|h| h.name.eq_ignore_ascii_case("content-type"))
741 .map(|h| h.value.as_str())
742 .unwrap_or("");
743 if !actual_ct.starts_with(ct_prefix.as_str()) {
744 return false;
745 }
746 }
747
748 true
749 }
750}
751
752#[derive(Clone)]
759pub struct ConfigDrivenApp {
760 current: Arc<RwLock<Arc<Generation>>>,
761 reload_lock: Arc<std::sync::Mutex<()>>,
763}
764
765struct Generation {
767 dispatch: RouteDispatch,
768 entry: Option<Arc<dyn Application + Send + Sync>>,
771 config: ProxyConfig,
772 source: Option<String>,
774 health_stop: Arc<AtomicBool>,
776}
777
778impl Generation {
779 fn new(routes: Arc<Vec<CompiledRoute>>, fallback: App, config: ProxyConfig, health_stop: Arc<AtomicBool>, source: Option<String>) -> Self {
780 let dispatch = RouteDispatch { routes, fallback };
781 let entry = if config.global_middleware.is_empty() {
782 None
783 } else {
784 Some(builder::apply_middleware(arc_app(dispatch.clone()), &config.global_middleware))
785 };
786 Generation { dispatch, entry, config, source, health_stop }
787 }
788}
789
790#[derive(Clone)]
792struct RouteDispatch {
793 routes: Arc<Vec<CompiledRoute>>,
794 fallback: App,
799}
800
801impl Application for RouteDispatch {
802 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
803 for route in self.routes.iter() {
804 if route.matcher.matches(request, conn) {
805 return route.handler.execute(request, conn);
806 }
807 }
808 self.fallback.execute(request, conn)
809 }
810}
811
812impl MiddlewareConfig {
813 pub fn is_empty(&self) -> bool {
815 self.rate_limit.is_none() && self.cache.is_none() && self.auth.is_none()
816 && self.rewrite_request.is_empty() && self.rewrite_response.is_empty()
817 && self.ip_allow.is_empty() && self.ip_deny.is_empty()
818 && self.timeout_ms.is_none() && self.max_body_size.is_none()
819 }
820}
821
822#[derive(Debug, Clone, PartialEq)]
824pub enum ReloadOutcome {
825 Unchanged,
827 Reloaded { routes: usize, warnings: Vec<String> },
830}
831
832impl ConfigDrivenApp {
833 pub(crate) fn new(routes: Vec<CompiledRoute>, config: ProxyConfig, health_stop: Arc<AtomicBool>, source: Option<String>) -> Self {
834 use crate::core::New;
835 let generation = Generation::new(Arc::new(routes), App::new(), config, health_stop, source);
836 ConfigDrivenApp {
837 current: Arc::new(RwLock::new(Arc::new(generation))),
838 reload_lock: Arc::new(std::sync::Mutex::new(())),
839 }
840 }
841
842 fn current(&self) -> Arc<Generation> {
843 Arc::clone(&self.current.read().unwrap_or_else(|e| e.into_inner()))
844 }
845
846 pub fn with_config(self, config: ServerConfig) -> Self {
863 {
864 let mut slot = self.current.write().unwrap_or_else(|e| e.into_inner());
865 let old = Arc::clone(&slot);
866 *slot = Arc::new(Generation::new(
867 Arc::clone(&old.dispatch.routes),
868 App::with_config(config),
869 old.config.clone(),
870 Arc::clone(&old.health_stop),
871 old.source.clone(),
872 ));
873 }
874 self
875 }
876
877 pub fn reload_from_str(&self, toml: &str) -> Result<ReloadOutcome, crate::config_check::ConfigReport> {
887 let _serialized = self.reload_lock.lock().unwrap_or_else(|e| e.into_inner());
888 let old = self.current();
889 if old.source.as_deref() == Some(toml) {
890 return Ok(ReloadOutcome::Unchanged);
891 }
892 let config = ProxyConfig::from_str(toml);
893 let mut report = crate::config_check::ConfigReport::default();
894 validate::check_unknown_keys(toml, &mut report);
895 config.validate(&mut report);
896 if !report.is_ok() {
897 return Err(report);
898 }
899
900 let mut warnings = report.warnings;
901 let listeners = |c: &ProxyConfig| format!("{:?}{:?}{:?}", c.tcp_proxies, c.udp_proxies, c.ws_proxies);
902 if listeners(&config) != listeners(&old.config) {
903 warnings.push("[[tcp_proxy]] / [[udp_proxy]] / [[ws_proxy]] changes need a restart; the old listeners keep running".to_string());
904 }
905
906 let health_stop = Arc::new(AtomicBool::new(false));
907 let routes = builder::compile_routes(&config, &health_stop);
908 let route_count = routes.len();
909 let generation = Generation::new(Arc::new(routes), old.dispatch.fallback.clone(), config, health_stop, Some(toml.to_string()));
910 let replaced = {
911 let mut slot = self.current.write().unwrap_or_else(|e| e.into_inner());
912 std::mem::replace(&mut *slot, Arc::new(generation))
913 };
914 replaced.health_stop.store(true, Ordering::Relaxed);
915 Ok(ReloadOutcome::Reloaded { routes: route_count, warnings })
916 }
917
918 pub fn reload_from_file(&self) {
923 let path = config_file_path();
924 let toml = match std::fs::read_to_string(&path) {
925 Ok(toml) => toml,
926 Err(e) => {
927 eprintln!("[proxy_config] reload: cannot read {path}: {e} — keeping the running routes");
928 return;
929 }
930 };
931 match self.reload_from_str(&toml) {
932 Ok(ReloadOutcome::Unchanged) => {}
933 Ok(ReloadOutcome::Reloaded { routes, warnings }) => {
934 for w in &warnings {
935 eprintln!("[proxy_config] Warning: {w}");
936 }
937 println!("[proxy_config] reloaded {path}: {routes} route{}", if routes == 1 { "" } else { "s" });
938 }
939 Err(report) => {
940 for e in &report.errors {
941 eprintln!("[proxy_config] reload ERROR: {e}");
942 }
943 eprintln!("[proxy_config] {path} has errors — keeping the running routes");
944 }
945 }
946 }
947}
948
949impl Application for ConfigDrivenApp {
950 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
951 let generation = self.current();
952 match &generation.entry {
953 Some(entry) => entry.execute(request, conn),
954 None => generation.dispatch.execute(request, conn),
955 }
956 }
957
958 fn runs_inline(&self, request: &Request, conn: &ConnectionInfo) -> bool {
962 let generation = self.current();
963 generation.entry.is_none()
964 && !generation.dispatch.routes.iter().any(|r| r.matcher.matches(request, conn))
965 && App::is_inline_request(request)
966 }
967
968 #[cfg(feature = "http2")]
972 fn execute_async<'a>(&'a self, request: &'a Request, conn: &'a ConnectionInfo) -> Option<crate::application::AsyncResponse<'a>> {
973 let generation = self.current();
974 if generation.entry.is_some() {
975 return None;
976 }
977 let route = generation.dispatch.routes.iter().find(|r| r.matcher.matches(request, conn))?;
978 let proxy = Arc::clone(route.async_proxy.as_ref()?);
979 Some(Box::pin(async move { proxy.execute_async(request, conn).await }))
980 }
981}
982
983#[derive(Clone, Copy)]
988pub(crate) struct NullApp;
989
990impl Application for NullApp {
991 fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
992 let mut r = Response::new();
993 r.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
994 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
995 Ok(r)
996 }
997}
998
999use std::collections::HashMap;
1002use std::collections::hash_map::DefaultHasher;
1003use std::hash::{Hash, Hasher};
1004use std::sync::atomic::AtomicUsize;
1005use std::time::{Duration, SystemTime, UNIX_EPOCH};
1006
1007#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1011pub(crate) enum LoadBalanceStrategy {
1012 RoundRobin,
1013 Random,
1014 IpHash,
1015 LeastConnections,
1016}
1017
1018impl LoadBalanceStrategy {
1019 fn parse(s: &str) -> Self {
1020 match s {
1021 "random" => LoadBalanceStrategy::Random,
1022 "ip_hash" => LoadBalanceStrategy::IpHash,
1023 "least_connections" => LoadBalanceStrategy::LeastConnections,
1024 _ => LoadBalanceStrategy::RoundRobin,
1025 }
1026 }
1027}
1028
1029#[derive(Clone)]
1035pub(crate) struct DynamicProxy {
1036 live: Arc<RwLock<Vec<String>>>,
1037 counter: Arc<AtomicUsize>,
1038 connect_timeout: Duration,
1039 read_timeout: Duration,
1040 strip_prefix: Option<Arc<String>>,
1041 add_prefix: Option<Arc<String>>,
1042 tls: bool,
1043 strategy: LoadBalanceStrategy,
1044 connections: Arc<RwLock<HashMap<String, Arc<AtomicUsize>>>>,
1045}
1046
1047impl DynamicProxy {
1048 pub(crate) fn new(
1049 live: Arc<RwLock<Vec<String>>>,
1050 connect_timeout_ms: u64,
1051 read_timeout_ms: u64,
1052 strip_prefix: Option<String>,
1053 add_prefix: Option<String>,
1054 tls: bool,
1055 strategy: String,
1056 ) -> Self {
1057 DynamicProxy {
1058 live,
1059 counter: Arc::new(AtomicUsize::new(0)),
1060 connect_timeout: Duration::from_millis(connect_timeout_ms),
1061 read_timeout: Duration::from_millis(read_timeout_ms),
1062 strip_prefix: strip_prefix.map(Arc::new),
1063 add_prefix: add_prefix.map(Arc::new),
1064 tls,
1065 strategy: LoadBalanceStrategy::parse(&strategy),
1066 connections: Arc::new(RwLock::new(HashMap::new())),
1067 }
1068 }
1069
1070 fn next_backend(&self, client_ip: &str) -> Option<String> {
1071 let live = self.live.read().unwrap();
1072 if live.is_empty() {
1073 return None;
1074 }
1075
1076 let idx = match self.strategy {
1077 LoadBalanceStrategy::RoundRobin => {
1078 self.counter.fetch_add(1, Ordering::Relaxed) % live.len()
1079 }
1080 LoadBalanceStrategy::Random => {
1081 let nanos = SystemTime::now()
1082 .duration_since(UNIX_EPOCH)
1083 .map(|d| d.subsec_nanos())
1084 .unwrap_or(0) as usize;
1085 let salt = self.counter.fetch_add(1, Ordering::Relaxed);
1086 nanos.wrapping_add(salt) % live.len()
1087 }
1088 LoadBalanceStrategy::IpHash => {
1089 let mut hasher = DefaultHasher::new();
1090 client_ip.hash(&mut hasher);
1091 (hasher.finish() as usize) % live.len()
1092 }
1093 LoadBalanceStrategy::LeastConnections => {
1094 let connections = self.connections.read().unwrap();
1095 live.iter()
1096 .enumerate()
1097 .min_by_key(|(_, backend)| {
1098 connections
1099 .get(*backend)
1100 .map(|c| c.load(Ordering::Relaxed))
1101 .unwrap_or(0)
1102 })
1103 .map(|(i, _)| i)
1104 .unwrap_or(0)
1105 }
1106 };
1107
1108 Some(live[idx].clone())
1109 }
1110
1111 fn connection_counter(&self, backend: &str) -> Arc<AtomicUsize> {
1114 if let Some(counter) = self.connections.read().unwrap().get(backend) {
1115 return Arc::clone(counter);
1116 }
1117 let mut connections = self.connections.write().unwrap();
1118 Arc::clone(
1119 connections
1120 .entry(backend.to_string())
1121 .or_insert_with(|| Arc::new(AtomicUsize::new(0))),
1122 )
1123 }
1124}
1125
1126struct ConnectionGuard {
1129 counter: Arc<AtomicUsize>,
1130}
1131
1132impl Drop for ConnectionGuard {
1133 fn drop(&mut self) {
1134 self.counter.fetch_sub(1, Ordering::Relaxed);
1135 }
1136}
1137
1138struct Prepared<'a> {
1141 host: String,
1142 port: u16,
1143 request: std::borrow::Cow<'a, Request>,
1144 _guard: Option<ConnectionGuard>,
1145}
1146
1147impl DynamicProxy {
1148 fn prepare<'a>(&self, request: &'a Request, conn: &ConnectionInfo) -> Result<Prepared<'a>, Response> {
1151 let backend = self.next_backend(&conn.client.ip).ok_or_else(bad_gateway)?;
1152
1153 let guard = if self.strategy == LoadBalanceStrategy::LeastConnections {
1154 let counter = self.connection_counter(&backend);
1155 counter.fetch_add(1, Ordering::Relaxed);
1156 Some(ConnectionGuard { counter })
1157 } else {
1158 None
1159 };
1160
1161 let (host, port, _) = crate::proxy_config::health::parse_backend_url(&backend).ok_or_else(bad_gateway)?;
1162
1163 let request = if self.strip_prefix.is_some() || self.add_prefix.is_some() {
1165 let mut req_clone = request.clone();
1166 if let Some(ref sp) = self.strip_prefix {
1167 if let Some(stripped) = req_clone.request_uri.strip_prefix(sp.as_str()) {
1168 req_clone.request_uri = if stripped.is_empty() || !stripped.starts_with('/') {
1169 format!("/{}", stripped)
1170 } else {
1171 stripped.to_string()
1172 };
1173 }
1174 }
1175 if let Some(ref ap) = self.add_prefix {
1176 req_clone.request_uri = format!("{}{}", ap, req_clone.request_uri);
1177 }
1178 std::borrow::Cow::Owned(req_clone)
1179 } else {
1180 std::borrow::Cow::Borrowed(request)
1181 };
1182
1183 Ok(Prepared { host, port, request, _guard: guard })
1184 }
1185
1186 #[cfg(feature = "http2")]
1189 pub(crate) async fn execute_async(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1190 let prepared = match self.prepare(request, conn) {
1191 Ok(p) => p,
1192 Err(response) => return Ok(response),
1193 };
1194 let result = crate::proxy::async_upstream::forward(
1195 &prepared.request,
1196 &conn.client.ip,
1197 &prepared.host,
1198 prepared.port,
1199 self.connect_timeout,
1200 self.read_timeout,
1201 )
1202 .await;
1203 Ok(result.unwrap_or_else(|_| bad_gateway()))
1204 }
1205
1206 pub(crate) fn supports_async(&self) -> bool {
1208 cfg!(feature = "http2") && !self.tls
1209 }
1210}
1211
1212impl Application for DynamicProxy {
1213 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
1214 let prepared = match self.prepare(request, conn) {
1215 Ok(p) => p,
1216 Err(response) => return Ok(response),
1217 };
1218
1219 let result = crate::proxy::forward_upstream(
1222 crate::proxy::shared_pool(),
1223 &prepared.request,
1224 &conn.client.ip,
1225 &prepared.host,
1226 prepared.port,
1227 self.tls,
1228 self.connect_timeout,
1229 self.read_timeout,
1230 );
1231
1232 match result {
1233 Ok(r) => Ok(r),
1234 Err(_) => Ok(bad_gateway()),
1235 }
1236 }
1237}
1238
1239fn bad_gateway() -> Response {
1240 use crate::mime_type::MimeType;
1241 use crate::range::Range;
1242 let cr = Range::get_content_range(
1243 b"502 Bad Gateway".to_vec(),
1244 MimeType::TEXT_PLAIN.to_string(),
1245 );
1246 let mut r = Response::new();
1247 r.status_code = *STATUS_CODE_REASON_PHRASE.n502_bad_gateway.status_code;
1248 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n502_bad_gateway.reason_phrase.to_string();
1249 r.content_range_list = vec![cr];
1250 r
1251}
1252
1253#[derive(Clone)]
1259pub(crate) struct RedirectAdapter {
1260 location_template: Arc<String>,
1261 status: i16,
1262 reason: Arc<String>,
1263}
1264
1265impl RedirectAdapter {
1266 pub(crate) fn new(location: String, status: u16) -> Self {
1267 let (code, reason) = redirect_status(status);
1268 RedirectAdapter {
1269 location_template: Arc::new(location),
1270 status: code,
1271 reason: Arc::new(reason),
1272 }
1273 }
1274}
1275
1276fn redirect_status(code: u16) -> (i16, String) {
1277 let phrase = match code {
1278 301 => STATUS_CODE_REASON_PHRASE.n301_moved_permanently.reason_phrase,
1279 302 => STATUS_CODE_REASON_PHRASE.n302_found.reason_phrase,
1280 307 => STATUS_CODE_REASON_PHRASE.n307_temporary_redirect.reason_phrase,
1281 308 => STATUS_CODE_REASON_PHRASE.n308_permanent_redirect.reason_phrase,
1282 _ => "Redirect",
1283 };
1284 (code as i16, phrase.to_string())
1285}
1286
1287impl Application for RedirectAdapter {
1288 fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1289 let location = self
1290 .location_template
1291 .replace("$path", &request.request_uri);
1292 use crate::header::Header;
1293 let mut r = Response::new();
1294 r.status_code = self.status;
1295 r.reason_phrase = self.reason.as_ref().clone();
1296 r.headers.push(Header { name: "Location".to_string(), value: location });
1297 Ok(r)
1298 }
1299}
1300
1301#[derive(Clone)]
1305pub(crate) struct RespondAdapter {
1306 status: i16,
1307 reason: Arc<String>,
1308 body: Arc<Vec<u8>>,
1309 content_type: Arc<String>,
1310}
1311
1312impl RespondAdapter {
1313 pub(crate) fn new(status: u16, body: String, content_type: String) -> Self {
1314 use crate::response::STATUS_CODE_REASON_PHRASE;
1315 let reason = match status {
1316 200 => STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string(),
1317 201 => STATUS_CODE_REASON_PHRASE.n201_created.reason_phrase.to_string(),
1318 204 => STATUS_CODE_REASON_PHRASE.n204_no_content.reason_phrase.to_string(),
1319 400 => STATUS_CODE_REASON_PHRASE.n400_bad_request.reason_phrase.to_string(),
1320 401 => STATUS_CODE_REASON_PHRASE.n401_unauthorized.reason_phrase.to_string(),
1321 403 => STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string(),
1322 404 => STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string(),
1323 500 => STATUS_CODE_REASON_PHRASE.n500_internal_server_error.reason_phrase.to_string(),
1324 _ => "OK".to_string(),
1325 };
1326 RespondAdapter {
1327 status: status as i16,
1328 reason: Arc::new(reason),
1329 body: Arc::new(body.into_bytes()),
1330 content_type: Arc::new(content_type),
1331 }
1332 }
1333}
1334
1335impl Application for RespondAdapter {
1336 fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1337 use crate::range::Range;
1338 let mut r = Response::new();
1339 r.status_code = self.status;
1340 r.reason_phrase = self.reason.as_ref().clone();
1341 if !self.body.is_empty() {
1342 r.content_range_list = vec![Range::get_content_range(
1343 self.body.as_ref().clone(),
1344 self.content_type.as_ref().clone(),
1345 )];
1346 }
1347 Ok(r)
1348 }
1349}
1350
1351#[derive(Clone)]
1360pub(crate) struct StaticAdapter {
1361 root: Arc<std::path::PathBuf>,
1362 index: Arc<Vec<String>>,
1363}
1364
1365impl StaticAdapter {
1366 pub(crate) fn new(root: String, index: Vec<String>) -> Self {
1367 let index = if index.is_empty() { vec!["index.html".to_string()] } else { index };
1368 StaticAdapter {
1369 root: Arc::new(std::path::PathBuf::from(root)),
1370 index: Arc::new(index),
1371 }
1372 }
1373
1374 fn resolve(&self, request_uri: &str) -> Option<std::path::PathBuf> {
1378 let raw_path = request_uri.split('?').next().unwrap_or(request_uri);
1379 let decoded = crate::url::URL::percent_decode(raw_path);
1380
1381 if decoded.split('/').any(|segment| segment == "..") {
1382 return None;
1383 }
1384
1385 let relative = decoded.trim_start_matches('/');
1386 Some(self.root.join(relative))
1387 }
1388}
1389
1390impl Application for StaticAdapter {
1391 fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1392 let mut response = Response::new();
1393
1394 let not_found = |mut response: Response| {
1395 response.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
1396 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
1397 response
1398 };
1399
1400 let candidate = match self.resolve(&request.request_uri) {
1401 Some(p) => p,
1402 None => {
1403 response.status_code = *STATUS_CODE_REASON_PHRASE.n403_forbidden.status_code;
1404 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string();
1405 return Ok(response);
1406 }
1407 };
1408
1409 let mut file_path = candidate;
1410 if file_path.is_dir() {
1411 let indexed = self
1412 .index
1413 .iter()
1414 .map(|name| file_path.join(name))
1415 .find(|p| p.is_file());
1416
1417 file_path = match indexed {
1418 Some(p) => p,
1419 None => return Ok(not_found(response)),
1420 };
1421 }
1422
1423 if !file_path.is_file() {
1424 return Ok(not_found(response));
1425 }
1426
1427 if let (Ok(root_canon), Ok(file_canon)) =
1430 (self.root.canonicalize(), file_path.canonicalize())
1431 {
1432 if !file_canon.starts_with(&root_canon) {
1433 return Ok(not_found(response));
1434 }
1435 }
1436
1437 let path_str = match file_path.to_str() {
1438 Some(s) => s,
1439 None => return Ok(not_found(response)),
1440 };
1441
1442 match crate::range::Range::get_content_range_of_a_file(path_str) {
1443 Ok(content_range) => {
1444 response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
1445 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
1446 response.content_range_list = vec![content_range];
1447 Ok(response)
1448 }
1449 Err(_) => Ok(not_found(response)),
1450 }
1451 }
1452}
1453
1454pub(crate) struct PerRouteRateLimit(pub(crate) Arc<crate::rate_limit::RateLimiter>);
1458
1459impl crate::middleware::Middleware for PerRouteRateLimit {
1460 fn handle(
1461 &self,
1462 request: &Request,
1463 conn: &ConnectionInfo,
1464 next: &dyn Application,
1465 ) -> Result<Response, String> {
1466 use crate::error::{AppError, IntoResponse};
1467 if self.0.check(&conn.client.ip) {
1468 next.execute(request, conn)
1469 } else {
1470 let mut response = AppError::TooManyRequests.into_response();
1471 let retry_after = self.0.retry_after(&conn.client.ip);
1472 response.headers.push(Header {
1473 name: Header::_RETRY_AFTER.to_string(),
1474 value: retry_after.to_string(),
1475 });
1476 Ok(response)
1477 }
1478 }
1479}
1480
1481pub(crate) struct PerRouteMaxBodySize(pub(crate) u64);
1496
1497impl crate::middleware::Middleware for PerRouteMaxBodySize {
1498 fn handle(
1499 &self,
1500 request: &Request,
1501 conn: &ConnectionInfo,
1502 next: &dyn Application,
1503 ) -> Result<Response, String> {
1504 use crate::error::{AppError, IntoResponse};
1505 if request.body.len() as u64 > self.0 {
1506 Ok(AppError::PayloadTooLarge(format!(
1507 "request body of {} bytes exceeds this route's {}-byte limit",
1508 request.body.len(),
1509 self.0,
1510 ))
1511 .into_response())
1512 } else {
1513 next.execute(request, conn)
1514 }
1515 }
1516}
1517
1518pub(crate) struct BearerAuthMiddleware {
1522 pub(crate) token: Arc<String>,
1523}
1524
1525impl crate::middleware::Middleware for BearerAuthMiddleware {
1526 fn handle(
1527 &self,
1528 request: &Request,
1529 conn: &ConnectionInfo,
1530 next: &dyn Application,
1531 ) -> Result<Response, String> {
1532 use crate::error::{AppError, IntoResponse};
1533 let expected = format!("Bearer {}", self.token);
1534 let authorized = request
1535 .headers
1536 .iter()
1537 .any(|h| h.name.eq_ignore_ascii_case("authorization") && constant_time_eq(h.value.as_bytes(), expected.as_bytes()));
1538 if authorized {
1539 next.execute(request, conn)
1540 } else {
1541 Ok(AppError::Unauthorized.into_response())
1542 }
1543 }
1544}
1545
1546pub(crate) fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
1549 if a.len() != b.len() {
1550 return false;
1551 }
1552 a.iter().zip(b).fold(0u8, |diff, (x, y)| diff | (x ^ y)) == 0
1553}
1554
1555pub(crate) struct AuthMisconfigured;
1558
1559impl crate::middleware::Middleware for AuthMisconfigured {
1560 fn handle(
1561 &self,
1562 _request: &Request,
1563 _conn: &ConnectionInfo,
1564 _next: &dyn Application,
1565 ) -> Result<Response, String> {
1566 Ok(Response::text(
1567 STATUS_CODE_REASON_PHRASE.n503_service_unavailable,
1568 "503 Service Unavailable: authentication for this route is misconfigured",
1569 ))
1570 }
1571}
1572
1573pub(crate) fn arc_app<A: Application + Send + Sync + 'static>(
1577 a: A,
1578) -> Arc<dyn Application + Send + Sync> {
1579 Arc::new(a)
1580}
1581
1582pub fn build_from_file() -> (ConfigDrivenApp, Vec<std::thread::JoinHandle<()>>) {
1587 builder::build_from_file()
1588}