1pub mod parser;
29pub mod health;
30pub mod builder;
31
32#[cfg(test)]
33mod tests;
34
35use std::sync::Arc;
36
37use crate::app::App;
38use crate::application::Application;
39use crate::core::New;
40use crate::request::Request;
41use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
42use crate::server::ConnectionInfo;
43use crate::server_config::ServerConfig;
44
45#[derive(Debug, Clone)]
48pub struct ProxyConfig {
49 pub upstreams: Vec<UpstreamConfig>,
50 pub routes: Vec<RouteConfig>,
51 pub tcp_proxies: Vec<TcpProxyConfig>,
52 pub udp_proxies: Vec<UdpProxyConfig>,
53 pub ws_proxies: Vec<WsProxyConfig>,
54 pub global_middleware: MiddlewareConfig,
55}
56
57#[derive(Debug, Clone)]
58pub struct UpstreamConfig {
59 pub name: String,
60 pub backends: Vec<String>,
61 pub strategy: String, pub health_check: Option<HealthCheckConfig>,
63 pub tls: bool,
67}
68
69#[derive(Debug, Clone)]
70pub struct HealthCheckConfig {
71 pub path: String,
72 pub interval_secs: u64,
73 pub timeout_ms: u64,
74 pub healthy_threshold: u32,
75 pub unhealthy_threshold: u32,
76}
77
78#[derive(Debug, Clone)]
79pub struct RouteConfig {
80 pub name: String,
81 pub match_: MatchConfig,
82 pub action: ActionConfig,
83 pub middleware: MiddlewareConfig,
84}
85
86#[derive(Debug, Clone, Default)]
87pub struct MatchConfig {
88 pub host: Option<String>,
89 pub path: Option<String>,
90 pub method: Option<String>,
91 pub content_type: Option<String>,
92}
93
94#[derive(Debug, Clone)]
95pub enum ActionConfig {
96 Proxy {
97 upstream: String,
98 connect_timeout_ms: u64,
99 read_timeout_ms: u64,
100 strip_path_prefix: Option<String>,
101 add_path_prefix: Option<String>,
102 },
103 Grpc {
104 upstream: String,
105 connect_timeout_ms: u64,
106 read_timeout_ms: u64,
107 },
108 Static {
109 root: String,
110 index: Vec<String>,
111 },
112 Redirect {
113 location: String,
114 status: u16,
115 },
116 Respond {
117 status: u16,
118 body: String,
119 content_type: String,
120 },
121 Mcp,
122 Unknown(String),
123}
124
125#[derive(Debug, Clone, Default)]
126pub struct MiddlewareConfig {
127 pub rate_limit: Option<RateLimitConfig>,
128 pub cache: Option<CacheConfig>,
129 pub auth: Option<AuthConfig>,
130 pub rewrite_request: Vec<RewriteRuleConfig>,
131 pub rewrite_response: Vec<RewriteRuleConfig>,
132 pub ip_allow: Vec<String>,
133 pub ip_deny: Vec<String>,
134 pub timeout_ms: Option<u64>,
139 pub max_body_size: Option<u64>,
155}
156
157#[derive(Debug, Clone)]
158pub struct RateLimitConfig {
159 pub max_requests: u32,
160 pub window_secs: u64,
161}
162
163#[derive(Debug, Clone)]
164pub struct CacheConfig {
165 pub ttl_secs: u64,
166 pub vary_by: Vec<String>,
167}
168
169#[derive(Debug, Clone)]
170pub enum AuthConfig {
171 Basic { htpasswd_file: String },
176 Jwt { secret_env: String },
179 Bearer { token_env: String },
180}
181
182#[derive(Debug, Clone, Default)]
183pub struct RewriteRuleConfig {
184 pub type_: String,
185 pub name: Option<String>,
186 pub value: Option<String>,
187 pub prefix: Option<String>,
188 pub from: Option<String>,
189 pub to: Option<String>,
190 pub code: Option<u16>,
191 pub reason: Option<String>,
192}
193
194#[derive(Debug, Clone)]
195pub struct TcpProxyConfig {
196 pub name: String,
197 pub listen: String,
198 pub backends: Vec<String>,
199 pub connect_timeout_ms: u64,
200}
201
202#[derive(Debug, Clone)]
203pub struct UdpProxyConfig {
204 pub name: String,
205 pub listen: String,
206 pub backends: Vec<String>,
207 pub reply_timeout_ms: u64,
208 pub buffer_size: usize,
209}
210
211#[derive(Debug, Clone)]
212pub struct WsProxyConfig {
213 pub name: String,
214 pub listen: String,
215 pub backends: Vec<String>,
216 pub connect_timeout_ms: u64,
217 pub read_timeout_ms: u64,
218 pub health_check: Option<HealthCheckConfig>,
225}
226
227fn merge_action_keys(map: &mut parser::SectionMap, action_section: &str, action_type: &str) {
236 if action_type.is_empty() {
237 return;
238 }
239 let short_form: Vec<(String, String)> = match map.get(action_section) {
240 Some(entries) => entries.iter().filter(|(k, _)| k != "type").cloned().collect(),
241 None => return,
242 };
243 let nested = map.entry(format!("{action_section}.{action_type}")).or_default();
244 for (key, value) in short_form {
245 if !nested.iter().any(|(k, _)| *k == key) {
246 nested.push((key, value));
247 }
248 }
249}
250
251impl ProxyConfig {
252 pub fn is_proxy_mode() -> bool {
256 let path = config_file_path();
257 match std::fs::read_to_string(&path) {
258 Ok(contents) => {
259 contents.contains("[[route]]") || contents.contains("[[upstream]]")
260 }
261 Err(_) => false,
262 }
263 }
264
265 pub fn load() -> Self {
267 let path = config_file_path();
268 let contents = std::fs::read_to_string(&path).unwrap_or_default();
269 Self::from_str(&contents)
270 }
271
272 pub fn from_str(toml: &str) -> Self {
274 use parser::{get_array, get_str, get_u32, get_u64, section_exists};
275
276 let mut map = parser::parse(toml);
277
278 let mut upstreams = Vec::new();
280 let mut i = 0;
281 loop {
282 let sec = format!("upstream[{}]", i);
283 if !section_exists(&map, &sec) {
284 break;
285 }
286 let name = get_str(&map, &sec, "name");
287 let backends = get_array(&map, &sec, "backends");
288 let strategy = {
289 let s = get_str(&map, &sec, "strategy");
290 if s.is_empty() { "round_robin".to_string() } else { s }
291 };
292 let hc_sec = format!("{}.health_check", sec);
293 let health_check = if section_exists(&map, &hc_sec) {
294 Some(HealthCheckConfig {
295 path: {
296 let p = get_str(&map, &hc_sec, "path");
297 if p.is_empty() { "/health".to_string() } else { p }
298 },
299 interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
300 timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
301 healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
302 unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
303 })
304 } else {
305 None
306 };
307 let tls = backends.iter().any(|b| b.starts_with("https://"));
308 upstreams.push(UpstreamConfig { name, backends, strategy, health_check, tls });
309 i += 1;
310 }
311
312 let mut routes = Vec::new();
314 let mut i = 0;
315 loop {
316 let sec = format!("route[{}]", i);
317 if !section_exists(&map, &sec) {
318 break;
319 }
320 let name = get_str(&map, &sec, "name");
321
322 let m_sec = format!("{}.match", sec);
324 let match_ = MatchConfig {
325 host: {
326 let h = get_str(&map, &m_sec, "host");
327 if h.is_empty() { None } else { Some(h) }
328 },
329 path: {
330 let p = get_str(&map, &m_sec, "path");
331 if p.is_empty() { None } else { Some(p) }
332 },
333 method: {
334 let m = get_str(&map, &m_sec, "method");
335 if m.is_empty() { None } else { Some(m.to_uppercase()) }
336 },
337 content_type: {
338 let c = get_str(&map, &m_sec, "content_type");
339 if c.is_empty() { None } else { Some(c) }
340 },
341 };
342
343 let a_sec = format!("{}.action", sec);
345 let action_type = get_str(&map, &a_sec, "type");
346 merge_action_keys(&mut map, &a_sec, &action_type);
347 let action = match action_type.as_str() {
348 "proxy" => {
349 let p_sec = format!("{}.action.proxy", sec);
350 ActionConfig::Proxy {
351 upstream: get_str(&map, &p_sec, "upstream"),
352 connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
353 read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
354 strip_path_prefix: {
355 let v = get_str(&map, &p_sec, "strip_path_prefix");
356 if v.is_empty() { None } else { Some(v) }
357 },
358 add_path_prefix: {
359 let v = get_str(&map, &p_sec, "add_path_prefix");
360 if v.is_empty() { None } else { Some(v) }
361 },
362 }
363 }
364 "grpc" => {
365 let p_sec = format!("{}.action.grpc", sec);
366 ActionConfig::Grpc {
367 upstream: get_str(&map, &p_sec, "upstream"),
368 connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
369 read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
370 }
371 }
372 "static" => {
373 let s_sec = format!("{}.action.static", sec);
374 ActionConfig::Static {
375 root: get_str(&map, &s_sec, "root"),
376 index: get_array(&map, &s_sec, "index"),
377 }
378 }
379 "redirect" => {
380 let r_sec = format!("{}.action.redirect", sec);
381 ActionConfig::Redirect {
382 location: get_str(&map, &r_sec, "location"),
383 status: get_u64(&map, &r_sec, "status", 301) as u16,
384 }
385 }
386 "respond" => {
387 let r_sec = format!("{}.action.respond", sec);
388 ActionConfig::Respond {
389 status: get_u64(&map, &r_sec, "status", 200) as u16,
390 body: get_str(&map, &r_sec, "body"),
391 content_type: {
392 let ct = get_str(&map, &r_sec, "content_type");
393 if ct.is_empty() { "text/plain".to_string() } else { ct }
394 },
395 }
396 }
397 "mcp" => ActionConfig::Mcp,
398 other => ActionConfig::Unknown(other.to_string()),
399 };
400
401 let mw_sec = format!("{}.middleware", sec);
403 let middleware = parse_middleware_config(&map, &mw_sec, i);
404
405 routes.push(RouteConfig { name, match_, action, middleware });
406 i += 1;
407 }
408
409 let mut tcp_proxies = Vec::new();
411 let mut i = 0;
412 loop {
413 let sec = format!("tcp_proxy[{}]", i);
414 if !section_exists(&map, &sec) {
415 break;
416 }
417 tcp_proxies.push(TcpProxyConfig {
418 name: get_str(&map, &sec, "name"),
419 listen: get_str(&map, &sec, "listen"),
420 backends: get_array(&map, &sec, "backends"),
421 connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
422 });
423 i += 1;
424 }
425
426 let mut udp_proxies = Vec::new();
428 let mut i = 0;
429 loop {
430 let sec = format!("udp_proxy[{}]", i);
431 if !section_exists(&map, &sec) {
432 break;
433 }
434 udp_proxies.push(UdpProxyConfig {
435 name: get_str(&map, &sec, "name"),
436 listen: get_str(&map, &sec, "listen"),
437 backends: get_array(&map, &sec, "backends"),
438 reply_timeout_ms: get_u64(&map, &sec, "reply_timeout_ms", 5000),
439 buffer_size: get_u64(&map, &sec, "buffer_size", 65536) as usize,
440 });
441 i += 1;
442 }
443
444 let mut ws_proxies = Vec::new();
446 let mut i = 0;
447 loop {
448 let sec = format!("ws_proxy[{}]", i);
449 if !section_exists(&map, &sec) {
450 break;
451 }
452 let hc_sec = format!("{}.health_check", sec);
453 let health_check = if section_exists(&map, &hc_sec) {
454 Some(HealthCheckConfig {
455 path: {
456 let p = get_str(&map, &hc_sec, "path");
457 if p.is_empty() { "/health".to_string() } else { p }
458 },
459 interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
460 timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
461 healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
462 unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
463 })
464 } else {
465 None
466 };
467 ws_proxies.push(WsProxyConfig {
468 name: get_str(&map, &sec, "name"),
469 listen: get_str(&map, &sec, "listen"),
470 backends: get_array(&map, &sec, "backends"),
471 connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
472 read_timeout_ms: get_u64(&map, &sec, "read_timeout_ms", 30000),
473 health_check,
474 });
475 i += 1;
476 }
477
478 let global_middleware = parse_middleware_config(&map, "middleware", usize::MAX);
480
481 ProxyConfig {
482 upstreams,
483 routes,
484 tcp_proxies,
485 udp_proxies,
486 ws_proxies,
487 global_middleware,
488 }
489 }
490}
491
492fn parse_middleware_config(
495 map: &parser::SectionMap,
496 mw_sec: &str,
497 route_idx: usize,
498) -> MiddlewareConfig {
499 use parser::{get_array, get_str, get_u32, get_u64, section_exists};
500
501 let rl_sec = format!("{}.rate_limit", mw_sec);
502 let rate_limit = if section_exists(map, &rl_sec) {
503 Some(RateLimitConfig {
504 max_requests: get_u32(map, &rl_sec, "max_requests", 1000),
505 window_secs: get_u64(map, &rl_sec, "window_secs", 60),
506 })
507 } else {
508 None
509 };
510
511 let c_sec = format!("{}.cache", mw_sec);
512 let cache = if section_exists(map, &c_sec) {
513 Some(CacheConfig {
514 ttl_secs: get_u64(map, &c_sec, "ttl_secs", 60),
515 vary_by: get_array(map, &c_sec, "vary_by"),
516 })
517 } else {
518 None
519 };
520
521 let a_sec = format!("{}.auth", mw_sec);
522 let auth = if section_exists(map, &a_sec) {
523 let auth_type = get_str(map, &a_sec, "type");
524 match auth_type.as_str() {
525 "bearer" => Some(AuthConfig::Bearer {
526 token_env: get_str(map, &a_sec, "token_env"),
527 }),
528 "jwt" => Some(AuthConfig::Jwt {
529 secret_env: get_str(map, &a_sec, "secret_env"),
530 }),
531 "basic" => Some(AuthConfig::Basic {
532 htpasswd_file: get_str(map, &a_sec, "htpasswd_file"),
533 }),
534 _ => None,
535 }
536 } else {
537 None
538 };
539
540 let rewrite_request = collect_rewrite_rules(map, mw_sec, "request");
546 let rewrite_response = collect_rewrite_rules(map, mw_sec, "response");
547
548 let ip_sec = format!("{}.ip_filter", mw_sec);
549 let ip_allow = if section_exists(map, &ip_sec) {
550 get_array(map, &ip_sec, "allow")
551 } else {
552 vec![]
553 };
554 let ip_deny = if section_exists(map, &ip_sec) {
555 get_array(map, &ip_sec, "deny")
556 } else {
557 vec![]
558 };
559
560 let _ = route_idx; let timeout_ms = match get_u64(map, mw_sec, "timeout_ms", 0) {
566 0 => None,
567 ms => Some(ms),
568 };
569
570 let max_body_size = match get_u64(map, mw_sec, "max_body_size", 0) {
574 0 => None,
575 n => Some(n),
576 };
577
578 MiddlewareConfig {
579 rate_limit, cache, auth, rewrite_request, rewrite_response, ip_allow, ip_deny,
580 timeout_ms, max_body_size,
581 }
582}
583
584fn collect_rewrite_rules(
586 map: &parser::SectionMap,
587 mw_sec: &str,
588 direction: &str,
589) -> Vec<RewriteRuleConfig> {
590 use parser::{get_str, get_u64};
591
592 let mut rules = Vec::new();
593 let mut j = 0;
594 loop {
595 let rsec = format!("{}.rewrite.{}[{}]", mw_sec, direction, j);
596 if !parser::section_exists(map, &rsec) {
597 break;
598 }
599 let code_val = get_u64(map, &rsec, "code", 0);
600 rules.push(RewriteRuleConfig {
601 type_: get_str(map, &rsec, "type"),
602 name: {
603 let v = get_str(map, &rsec, "name");
604 if v.is_empty() { None } else { Some(v) }
605 },
606 value: {
607 let v = get_str(map, &rsec, "value");
608 if v.is_empty() { None } else { Some(v) }
609 },
610 prefix: {
611 let v = get_str(map, &rsec, "prefix");
612 if v.is_empty() { None } else { Some(v) }
613 },
614 from: {
615 let v = get_str(map, &rsec, "from");
616 if v.is_empty() { None } else { Some(v) }
617 },
618 to: {
619 let v = get_str(map, &rsec, "to");
620 if v.is_empty() { None } else { Some(v) }
621 },
622 code: if code_val == 0 { None } else { Some(code_val as u16) },
623 reason: {
624 let v = get_str(map, &rsec, "reason");
625 if v.is_empty() { None } else { Some(v) }
626 },
627 });
628 j += 1;
629 }
630 rules
631}
632
633fn config_file_path() -> String {
634 std::env::var("RWS_CONFIG_FILE").unwrap_or_else(|_| "rws.config.toml".to_string())
635}
636
637pub(crate) struct CompiledRoute {
641 pub(crate) matcher: RouteMatcher,
642 pub(crate) handler: Arc<dyn Application + Send + Sync>,
644}
645
646#[derive(Clone, Default)]
648pub(crate) struct RouteMatcher {
649 pub(crate) host: Option<String>,
651 pub(crate) path_prefix: Option<String>,
653 pub(crate) path_exact: Option<String>,
655 pub(crate) method: Option<String>,
657 pub(crate) content_type_prefix: Option<String>,
659}
660
661impl RouteMatcher {
662 pub(crate) fn from_match_config(cfg: &MatchConfig) -> Self {
663 let (path_prefix, path_exact) = match &cfg.path {
664 Some(p) if p.ends_with('*') => {
665 let stripped = p.trim_end_matches('*').to_string();
667 (Some(stripped), None)
668 }
669 Some(p) => (None, Some(p.clone())),
670 None => (None, None),
671 };
672 let content_type_prefix = cfg.content_type.as_ref().map(|ct| {
673 if ct.ends_with('*') {
674 ct.trim_end_matches('*').to_string()
675 } else {
676 ct.clone()
677 }
678 });
679 RouteMatcher {
680 host: cfg.host.clone(),
681 path_prefix,
682 path_exact,
683 method: cfg.method.clone(),
684 content_type_prefix,
685 }
686 }
687
688 pub(crate) fn matches(&self, request: &Request, conn: &ConnectionInfo) -> bool {
690 if let Some(ref expected_host) = self.host {
692 let actual_host = conn
693 .sni_hostname
694 .as_deref()
695 .or_else(|| {
696 request
697 .headers
698 .iter()
699 .find(|h| h.name.eq_ignore_ascii_case("host"))
700 .map(|h| h.value.as_str())
701 })
702 .unwrap_or("");
703 if actual_host != expected_host.as_str() {
704 return false;
705 }
706 }
707
708 if let Some(ref m) = self.method {
710 if request.method.to_uppercase() != m.as_str() {
711 return false;
712 }
713 }
714
715 let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
717 if let Some(ref prefix) = self.path_prefix {
718 if !path.starts_with(prefix.as_str()) {
719 return false;
720 }
721 } else if let Some(ref exact) = self.path_exact {
722 if path != exact.as_str() {
723 return false;
724 }
725 }
726
727 if let Some(ref ct_prefix) = self.content_type_prefix {
729 let actual_ct = request
730 .headers
731 .iter()
732 .find(|h| h.name.eq_ignore_ascii_case("content-type"))
733 .map(|h| h.value.as_str())
734 .unwrap_or("");
735 if !actual_ct.starts_with(ct_prefix.as_str()) {
736 return false;
737 }
738 }
739
740 true
741 }
742}
743
744#[derive(Clone)]
749pub struct ConfigDrivenApp {
750 routes: Arc<Vec<CompiledRoute>>,
751 fallback: App,
756}
757
758impl ConfigDrivenApp {
759 pub(crate) fn new(routes: Vec<CompiledRoute>) -> Self {
760 use crate::core::New;
761 ConfigDrivenApp {
762 routes: Arc::new(routes),
763 fallback: App::new(),
764 }
765 }
766
767 pub fn with_config(mut self, config: ServerConfig) -> Self {
783 self.fallback = App::with_config(config);
784 self
785 }
786}
787
788impl Application for ConfigDrivenApp {
789 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
790 for route in self.routes.iter() {
791 if route.matcher.matches(request, conn) {
792 return route.handler.execute(request, conn);
793 }
794 }
795 self.fallback.execute(request, conn)
796 }
797}
798
799#[derive(Clone, Copy)]
804pub(crate) struct NullApp;
805
806impl Application for NullApp {
807 fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
808 let mut r = Response::new();
809 r.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
810 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
811 Ok(r)
812 }
813}
814
815use std::collections::HashMap;
818use std::collections::hash_map::DefaultHasher;
819use std::hash::{Hash, Hasher};
820use std::sync::atomic::{AtomicUsize, Ordering};
821use std::sync::RwLock;
822use std::time::{Duration, SystemTime, UNIX_EPOCH};
823
824#[derive(Clone, Copy, Debug, PartialEq, Eq)]
828pub(crate) enum LoadBalanceStrategy {
829 RoundRobin,
830 Random,
831 IpHash,
832 LeastConnections,
833}
834
835impl LoadBalanceStrategy {
836 fn parse(s: &str) -> Self {
837 match s {
838 "random" => LoadBalanceStrategy::Random,
839 "ip_hash" => LoadBalanceStrategy::IpHash,
840 "least_connections" => LoadBalanceStrategy::LeastConnections,
841 _ => LoadBalanceStrategy::RoundRobin,
842 }
843 }
844}
845
846#[derive(Clone)]
852pub(crate) struct DynamicProxy {
853 live: Arc<RwLock<Vec<String>>>,
854 counter: Arc<AtomicUsize>,
855 connect_timeout: Duration,
856 read_timeout: Duration,
857 strip_prefix: Option<Arc<String>>,
858 add_prefix: Option<Arc<String>>,
859 tls: bool,
860 strategy: LoadBalanceStrategy,
861 connections: Arc<RwLock<HashMap<String, Arc<AtomicUsize>>>>,
862}
863
864impl DynamicProxy {
865 pub(crate) fn new(
866 live: Arc<RwLock<Vec<String>>>,
867 connect_timeout_ms: u64,
868 read_timeout_ms: u64,
869 strip_prefix: Option<String>,
870 add_prefix: Option<String>,
871 tls: bool,
872 strategy: String,
873 ) -> Self {
874 DynamicProxy {
875 live,
876 counter: Arc::new(AtomicUsize::new(0)),
877 connect_timeout: Duration::from_millis(connect_timeout_ms),
878 read_timeout: Duration::from_millis(read_timeout_ms),
879 strip_prefix: strip_prefix.map(Arc::new),
880 add_prefix: add_prefix.map(Arc::new),
881 tls,
882 strategy: LoadBalanceStrategy::parse(&strategy),
883 connections: Arc::new(RwLock::new(HashMap::new())),
884 }
885 }
886
887 fn next_backend(&self, client_ip: &str) -> Option<String> {
888 let live = self.live.read().unwrap();
889 if live.is_empty() {
890 return None;
891 }
892
893 let idx = match self.strategy {
894 LoadBalanceStrategy::RoundRobin => {
895 self.counter.fetch_add(1, Ordering::Relaxed) % live.len()
896 }
897 LoadBalanceStrategy::Random => {
898 let nanos = SystemTime::now()
899 .duration_since(UNIX_EPOCH)
900 .map(|d| d.subsec_nanos())
901 .unwrap_or(0) as usize;
902 let salt = self.counter.fetch_add(1, Ordering::Relaxed);
903 nanos.wrapping_add(salt) % live.len()
904 }
905 LoadBalanceStrategy::IpHash => {
906 let mut hasher = DefaultHasher::new();
907 client_ip.hash(&mut hasher);
908 (hasher.finish() as usize) % live.len()
909 }
910 LoadBalanceStrategy::LeastConnections => {
911 let connections = self.connections.read().unwrap();
912 live.iter()
913 .enumerate()
914 .min_by_key(|(_, backend)| {
915 connections
916 .get(*backend)
917 .map(|c| c.load(Ordering::Relaxed))
918 .unwrap_or(0)
919 })
920 .map(|(i, _)| i)
921 .unwrap_or(0)
922 }
923 };
924
925 Some(live[idx].clone())
926 }
927
928 fn connection_counter(&self, backend: &str) -> Arc<AtomicUsize> {
931 if let Some(counter) = self.connections.read().unwrap().get(backend) {
932 return Arc::clone(counter);
933 }
934 let mut connections = self.connections.write().unwrap();
935 Arc::clone(
936 connections
937 .entry(backend.to_string())
938 .or_insert_with(|| Arc::new(AtomicUsize::new(0))),
939 )
940 }
941}
942
943struct ConnectionGuard {
946 counter: Arc<AtomicUsize>,
947}
948
949impl Drop for ConnectionGuard {
950 fn drop(&mut self) {
951 self.counter.fetch_sub(1, Ordering::Relaxed);
952 }
953}
954
955impl Application for DynamicProxy {
956 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
957 let backend = match self.next_backend(&conn.client.ip) {
958 Some(b) => b,
959 None => {
960 return Ok(bad_gateway());
961 }
962 };
963
964 let _connection_guard = if self.strategy == LoadBalanceStrategy::LeastConnections {
965 let counter = self.connection_counter(&backend);
966 counter.fetch_add(1, Ordering::Relaxed);
967 Some(ConnectionGuard { counter })
968 } else {
969 None
970 };
971
972 let (host, port, _) = match crate::proxy_config::health::parse_backend_url(&backend) {
973 Some(t) => t,
974 None => return Ok(bad_gateway()),
975 };
976
977 let mut req_clone;
979 let effective_request = if self.strip_prefix.is_some() || self.add_prefix.is_some() {
980 req_clone = request.clone();
981 if let Some(ref sp) = self.strip_prefix {
982 if let Some(stripped) = req_clone.request_uri.strip_prefix(sp.as_str()) {
983 req_clone.request_uri = if stripped.is_empty() || !stripped.starts_with('/') {
984 format!("/{}", stripped)
985 } else {
986 stripped.to_string()
987 };
988 }
989 }
990 if let Some(ref ap) = self.add_prefix {
991 req_clone.request_uri = format!("{}{}", ap, req_clone.request_uri);
992 }
993 &req_clone
994 } else {
995 request
996 };
997
998 let result = if self.tls {
999 #[cfg(any(feature = "http-client", feature = "http2"))]
1000 {
1001 crate::proxy::proxy_https1(
1002 effective_request,
1003 &conn.client.ip,
1004 &host,
1005 port,
1006 self.connect_timeout,
1007 self.read_timeout,
1008 )
1009 }
1010 #[cfg(not(any(feature = "http-client", feature = "http2")))]
1011 {
1012 eprintln!("[proxy] HTTPS upstream requires http-client or http2 feature");
1013 Err("TLS upstream not supported in this build".to_string())
1014 }
1015 } else {
1016 crate::proxy::proxy_http1(
1017 effective_request,
1018 &conn.client.ip,
1019 &host,
1020 port,
1021 self.connect_timeout,
1022 self.read_timeout,
1023 )
1024 };
1025
1026 match result {
1027 Ok(r) => Ok(r),
1028 Err(_) => Ok(bad_gateway()),
1029 }
1030 }
1031}
1032
1033fn bad_gateway() -> Response {
1034 use crate::mime_type::MimeType;
1035 use crate::range::Range;
1036 let cr = Range::get_content_range(
1037 b"502 Bad Gateway".to_vec(),
1038 MimeType::TEXT_PLAIN.to_string(),
1039 );
1040 let mut r = Response::new();
1041 r.status_code = *STATUS_CODE_REASON_PHRASE.n502_bad_gateway.status_code;
1042 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n502_bad_gateway.reason_phrase.to_string();
1043 r.content_range_list = vec![cr];
1044 r
1045}
1046
1047#[derive(Clone)]
1053pub(crate) struct RedirectAdapter {
1054 location_template: Arc<String>,
1055 status: i16,
1056 reason: Arc<String>,
1057}
1058
1059impl RedirectAdapter {
1060 pub(crate) fn new(location: String, status: u16) -> Self {
1061 let (code, reason) = redirect_status(status);
1062 RedirectAdapter {
1063 location_template: Arc::new(location),
1064 status: code,
1065 reason: Arc::new(reason),
1066 }
1067 }
1068}
1069
1070fn redirect_status(code: u16) -> (i16, String) {
1071 let phrase = match code {
1072 301 => STATUS_CODE_REASON_PHRASE.n301_moved_permanently.reason_phrase,
1073 302 => STATUS_CODE_REASON_PHRASE.n302_found.reason_phrase,
1074 307 => STATUS_CODE_REASON_PHRASE.n307_temporary_redirect.reason_phrase,
1075 308 => STATUS_CODE_REASON_PHRASE.n308_permanent_redirect.reason_phrase,
1076 _ => "Redirect",
1077 };
1078 (code as i16, phrase.to_string())
1079}
1080
1081impl Application for RedirectAdapter {
1082 fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1083 let location = self
1084 .location_template
1085 .replace("$path", &request.request_uri);
1086 use crate::header::Header;
1087 let mut r = Response::new();
1088 r.status_code = self.status;
1089 r.reason_phrase = self.reason.as_ref().clone();
1090 r.headers.push(Header { name: "Location".to_string(), value: location });
1091 Ok(r)
1092 }
1093}
1094
1095#[derive(Clone)]
1099pub(crate) struct RespondAdapter {
1100 status: i16,
1101 reason: Arc<String>,
1102 body: Arc<Vec<u8>>,
1103 content_type: Arc<String>,
1104}
1105
1106impl RespondAdapter {
1107 pub(crate) fn new(status: u16, body: String, content_type: String) -> Self {
1108 use crate::response::STATUS_CODE_REASON_PHRASE;
1109 let reason = match status {
1110 200 => STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string(),
1111 201 => STATUS_CODE_REASON_PHRASE.n201_created.reason_phrase.to_string(),
1112 204 => STATUS_CODE_REASON_PHRASE.n204_no_content.reason_phrase.to_string(),
1113 400 => STATUS_CODE_REASON_PHRASE.n400_bad_request.reason_phrase.to_string(),
1114 401 => STATUS_CODE_REASON_PHRASE.n401_unauthorized.reason_phrase.to_string(),
1115 403 => STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string(),
1116 404 => STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string(),
1117 500 => STATUS_CODE_REASON_PHRASE.n500_internal_server_error.reason_phrase.to_string(),
1118 _ => "OK".to_string(),
1119 };
1120 RespondAdapter {
1121 status: status as i16,
1122 reason: Arc::new(reason),
1123 body: Arc::new(body.into_bytes()),
1124 content_type: Arc::new(content_type),
1125 }
1126 }
1127}
1128
1129impl Application for RespondAdapter {
1130 fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1131 use crate::range::Range;
1132 let mut r = Response::new();
1133 r.status_code = self.status;
1134 r.reason_phrase = self.reason.as_ref().clone();
1135 if !self.body.is_empty() {
1136 r.content_range_list = vec![Range::get_content_range(
1137 self.body.as_ref().clone(),
1138 self.content_type.as_ref().clone(),
1139 )];
1140 }
1141 Ok(r)
1142 }
1143}
1144
1145#[derive(Clone)]
1154pub(crate) struct StaticAdapter {
1155 root: Arc<std::path::PathBuf>,
1156 index: Arc<Vec<String>>,
1157}
1158
1159impl StaticAdapter {
1160 pub(crate) fn new(root: String, index: Vec<String>) -> Self {
1161 let index = if index.is_empty() { vec!["index.html".to_string()] } else { index };
1162 StaticAdapter {
1163 root: Arc::new(std::path::PathBuf::from(root)),
1164 index: Arc::new(index),
1165 }
1166 }
1167
1168 fn resolve(&self, request_uri: &str) -> Option<std::path::PathBuf> {
1172 let raw_path = request_uri.split('?').next().unwrap_or(request_uri);
1173 let decoded = crate::url::URL::percent_decode(raw_path);
1174
1175 if decoded.split('/').any(|segment| segment == "..") {
1176 return None;
1177 }
1178
1179 let relative = decoded.trim_start_matches('/');
1180 Some(self.root.join(relative))
1181 }
1182}
1183
1184impl Application for StaticAdapter {
1185 fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1186 let mut response = Response::new();
1187
1188 let not_found = |mut response: Response| {
1189 response.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
1190 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
1191 response
1192 };
1193
1194 let candidate = match self.resolve(&request.request_uri) {
1195 Some(p) => p,
1196 None => {
1197 response.status_code = *STATUS_CODE_REASON_PHRASE.n403_forbidden.status_code;
1198 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string();
1199 return Ok(response);
1200 }
1201 };
1202
1203 let mut file_path = candidate;
1204 if file_path.is_dir() {
1205 let indexed = self
1206 .index
1207 .iter()
1208 .map(|name| file_path.join(name))
1209 .find(|p| p.is_file());
1210
1211 file_path = match indexed {
1212 Some(p) => p,
1213 None => return Ok(not_found(response)),
1214 };
1215 }
1216
1217 if !file_path.is_file() {
1218 return Ok(not_found(response));
1219 }
1220
1221 if let (Ok(root_canon), Ok(file_canon)) =
1224 (self.root.canonicalize(), file_path.canonicalize())
1225 {
1226 if !file_canon.starts_with(&root_canon) {
1227 return Ok(not_found(response));
1228 }
1229 }
1230
1231 let path_str = match file_path.to_str() {
1232 Some(s) => s,
1233 None => return Ok(not_found(response)),
1234 };
1235
1236 match crate::range::Range::get_content_range_of_a_file(path_str) {
1237 Ok(content_range) => {
1238 response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
1239 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
1240 response.content_range_list = vec![content_range];
1241 Ok(response)
1242 }
1243 Err(_) => Ok(not_found(response)),
1244 }
1245 }
1246}
1247
1248pub(crate) struct PerRouteRateLimit(pub(crate) Arc<crate::rate_limit::RateLimiter>);
1252
1253impl crate::middleware::Middleware for PerRouteRateLimit {
1254 fn handle(
1255 &self,
1256 request: &Request,
1257 conn: &ConnectionInfo,
1258 next: &dyn Application,
1259 ) -> Result<Response, String> {
1260 use crate::error::{AppError, IntoResponse};
1261 if self.0.check(&conn.client.ip) {
1262 next.execute(request, conn)
1263 } else {
1264 Ok(AppError::TooManyRequests.into_response())
1265 }
1266 }
1267}
1268
1269pub(crate) struct PerRouteMaxBodySize(pub(crate) u64);
1284
1285impl crate::middleware::Middleware for PerRouteMaxBodySize {
1286 fn handle(
1287 &self,
1288 request: &Request,
1289 conn: &ConnectionInfo,
1290 next: &dyn Application,
1291 ) -> Result<Response, String> {
1292 use crate::error::{AppError, IntoResponse};
1293 if request.body.len() as u64 > self.0 {
1294 Ok(AppError::PayloadTooLarge(format!(
1295 "request body of {} bytes exceeds this route's {}-byte limit",
1296 request.body.len(),
1297 self.0,
1298 ))
1299 .into_response())
1300 } else {
1301 next.execute(request, conn)
1302 }
1303 }
1304}
1305
1306pub(crate) struct BearerAuthMiddleware {
1310 pub(crate) token: Arc<String>,
1311}
1312
1313impl crate::middleware::Middleware for BearerAuthMiddleware {
1314 fn handle(
1315 &self,
1316 request: &Request,
1317 conn: &ConnectionInfo,
1318 next: &dyn Application,
1319 ) -> Result<Response, String> {
1320 use crate::error::{AppError, IntoResponse};
1321 let expected = format!("Bearer {}", self.token);
1322 let authorized = request
1323 .headers
1324 .iter()
1325 .any(|h| h.name.eq_ignore_ascii_case("authorization") && constant_time_eq(h.value.as_bytes(), expected.as_bytes()));
1326 if authorized {
1327 next.execute(request, conn)
1328 } else {
1329 Ok(AppError::Unauthorized.into_response())
1330 }
1331 }
1332}
1333
1334pub(crate) fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
1337 if a.len() != b.len() {
1338 return false;
1339 }
1340 a.iter().zip(b).fold(0u8, |diff, (x, y)| diff | (x ^ y)) == 0
1341}
1342
1343pub(crate) struct AuthMisconfigured;
1346
1347impl crate::middleware::Middleware for AuthMisconfigured {
1348 fn handle(
1349 &self,
1350 _request: &Request,
1351 _conn: &ConnectionInfo,
1352 _next: &dyn Application,
1353 ) -> Result<Response, String> {
1354 Ok(Response::text(
1355 STATUS_CODE_REASON_PHRASE.n503_service_unavailable,
1356 "503 Service Unavailable: authentication for this route is misconfigured",
1357 ))
1358 }
1359}
1360
1361pub(crate) fn arc_app<A: Application + Send + Sync + 'static>(
1365 a: A,
1366) -> Arc<dyn Application + Send + Sync> {
1367 Arc::new(a)
1368}
1369
1370pub fn build_from_file() -> (ConfigDrivenApp, Vec<std::thread::JoinHandle<()>>) {
1375 builder::build_from_file()
1376}