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
227impl ProxyConfig {
230 pub fn is_proxy_mode() -> bool {
234 let path = config_file_path();
235 match std::fs::read_to_string(&path) {
236 Ok(contents) => {
237 contents.contains("[[route]]") || contents.contains("[[upstream]]")
238 }
239 Err(_) => false,
240 }
241 }
242
243 pub fn load() -> Self {
245 let path = config_file_path();
246 let contents = std::fs::read_to_string(&path).unwrap_or_default();
247 Self::from_str(&contents)
248 }
249
250 pub fn from_str(toml: &str) -> Self {
252 use parser::{get_array, get_str, get_u32, get_u64, section_exists};
253
254 let map = parser::parse(toml);
255
256 let mut upstreams = Vec::new();
258 let mut i = 0;
259 loop {
260 let sec = format!("upstream[{}]", i);
261 if !section_exists(&map, &sec) {
262 break;
263 }
264 let name = get_str(&map, &sec, "name");
265 let backends = get_array(&map, &sec, "backends");
266 let strategy = {
267 let s = get_str(&map, &sec, "strategy");
268 if s.is_empty() { "round_robin".to_string() } else { s }
269 };
270 let hc_sec = format!("{}.health_check", sec);
271 let health_check = if section_exists(&map, &hc_sec) {
272 Some(HealthCheckConfig {
273 path: {
274 let p = get_str(&map, &hc_sec, "path");
275 if p.is_empty() { "/health".to_string() } else { p }
276 },
277 interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
278 timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
279 healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
280 unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
281 })
282 } else {
283 None
284 };
285 let tls = backends.iter().any(|b| b.starts_with("https://"));
286 upstreams.push(UpstreamConfig { name, backends, strategy, health_check, tls });
287 i += 1;
288 }
289
290 let mut routes = Vec::new();
292 let mut i = 0;
293 loop {
294 let sec = format!("route[{}]", i);
295 if !section_exists(&map, &sec) {
296 break;
297 }
298 let name = get_str(&map, &sec, "name");
299
300 let m_sec = format!("{}.match", sec);
302 let match_ = MatchConfig {
303 host: {
304 let h = get_str(&map, &m_sec, "host");
305 if h.is_empty() { None } else { Some(h) }
306 },
307 path: {
308 let p = get_str(&map, &m_sec, "path");
309 if p.is_empty() { None } else { Some(p) }
310 },
311 method: {
312 let m = get_str(&map, &m_sec, "method");
313 if m.is_empty() { None } else { Some(m.to_uppercase()) }
314 },
315 content_type: {
316 let c = get_str(&map, &m_sec, "content_type");
317 if c.is_empty() { None } else { Some(c) }
318 },
319 };
320
321 let a_sec = format!("{}.action", sec);
323 let action_type = get_str(&map, &a_sec, "type");
324 let action = match action_type.as_str() {
325 "proxy" => {
326 let p_sec = format!("{}.action.proxy", sec);
327 ActionConfig::Proxy {
328 upstream: get_str(&map, &p_sec, "upstream"),
329 connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
330 read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
331 strip_path_prefix: {
332 let v = get_str(&map, &p_sec, "strip_path_prefix");
333 if v.is_empty() { None } else { Some(v) }
334 },
335 add_path_prefix: {
336 let v = get_str(&map, &p_sec, "add_path_prefix");
337 if v.is_empty() { None } else { Some(v) }
338 },
339 }
340 }
341 "grpc" => {
342 let p_sec = format!("{}.action.grpc", sec);
343 ActionConfig::Grpc {
344 upstream: get_str(&map, &p_sec, "upstream"),
345 connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
346 read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
347 }
348 }
349 "static" => {
350 let s_sec = format!("{}.action.static", sec);
351 ActionConfig::Static {
352 root: get_str(&map, &s_sec, "root"),
353 index: get_array(&map, &s_sec, "index"),
354 }
355 }
356 "redirect" => {
357 let r_sec = format!("{}.action.redirect", sec);
358 ActionConfig::Redirect {
359 location: get_str(&map, &r_sec, "location"),
360 status: get_u64(&map, &r_sec, "status", 301) as u16,
361 }
362 }
363 "respond" => {
364 let r_sec = format!("{}.action.respond", sec);
365 ActionConfig::Respond {
366 status: get_u64(&map, &r_sec, "status", 200) as u16,
367 body: get_str(&map, &r_sec, "body"),
368 content_type: {
369 let ct = get_str(&map, &r_sec, "content_type");
370 if ct.is_empty() { "text/plain".to_string() } else { ct }
371 },
372 }
373 }
374 "mcp" => ActionConfig::Mcp,
375 other => ActionConfig::Unknown(other.to_string()),
376 };
377
378 let mw_sec = format!("{}.middleware", sec);
380 let middleware = parse_middleware_config(&map, &mw_sec, i);
381
382 routes.push(RouteConfig { name, match_, action, middleware });
383 i += 1;
384 }
385
386 let mut tcp_proxies = Vec::new();
388 let mut i = 0;
389 loop {
390 let sec = format!("tcp_proxy[{}]", i);
391 if !section_exists(&map, &sec) {
392 break;
393 }
394 tcp_proxies.push(TcpProxyConfig {
395 name: get_str(&map, &sec, "name"),
396 listen: get_str(&map, &sec, "listen"),
397 backends: get_array(&map, &sec, "backends"),
398 connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
399 });
400 i += 1;
401 }
402
403 let mut udp_proxies = Vec::new();
405 let mut i = 0;
406 loop {
407 let sec = format!("udp_proxy[{}]", i);
408 if !section_exists(&map, &sec) {
409 break;
410 }
411 udp_proxies.push(UdpProxyConfig {
412 name: get_str(&map, &sec, "name"),
413 listen: get_str(&map, &sec, "listen"),
414 backends: get_array(&map, &sec, "backends"),
415 reply_timeout_ms: get_u64(&map, &sec, "reply_timeout_ms", 5000),
416 buffer_size: get_u64(&map, &sec, "buffer_size", 65536) as usize,
417 });
418 i += 1;
419 }
420
421 let mut ws_proxies = Vec::new();
423 let mut i = 0;
424 loop {
425 let sec = format!("ws_proxy[{}]", i);
426 if !section_exists(&map, &sec) {
427 break;
428 }
429 let hc_sec = format!("{}.health_check", sec);
430 let health_check = if section_exists(&map, &hc_sec) {
431 Some(HealthCheckConfig {
432 path: {
433 let p = get_str(&map, &hc_sec, "path");
434 if p.is_empty() { "/health".to_string() } else { p }
435 },
436 interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
437 timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
438 healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
439 unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
440 })
441 } else {
442 None
443 };
444 ws_proxies.push(WsProxyConfig {
445 name: get_str(&map, &sec, "name"),
446 listen: get_str(&map, &sec, "listen"),
447 backends: get_array(&map, &sec, "backends"),
448 connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
449 read_timeout_ms: get_u64(&map, &sec, "read_timeout_ms", 30000),
450 health_check,
451 });
452 i += 1;
453 }
454
455 let global_middleware = parse_middleware_config(&map, "middleware", usize::MAX);
457
458 ProxyConfig {
459 upstreams,
460 routes,
461 tcp_proxies,
462 udp_proxies,
463 ws_proxies,
464 global_middleware,
465 }
466 }
467}
468
469fn parse_middleware_config(
472 map: &parser::SectionMap,
473 mw_sec: &str,
474 route_idx: usize,
475) -> MiddlewareConfig {
476 use parser::{get_array, get_str, get_u32, get_u64, section_exists};
477
478 let rl_sec = format!("{}.rate_limit", mw_sec);
479 let rate_limit = if section_exists(map, &rl_sec) {
480 Some(RateLimitConfig {
481 max_requests: get_u32(map, &rl_sec, "max_requests", 1000),
482 window_secs: get_u64(map, &rl_sec, "window_secs", 60),
483 })
484 } else {
485 None
486 };
487
488 let c_sec = format!("{}.cache", mw_sec);
489 let cache = if section_exists(map, &c_sec) {
490 Some(CacheConfig {
491 ttl_secs: get_u64(map, &c_sec, "ttl_secs", 60),
492 vary_by: get_array(map, &c_sec, "vary_by"),
493 })
494 } else {
495 None
496 };
497
498 let a_sec = format!("{}.auth", mw_sec);
499 let auth = if section_exists(map, &a_sec) {
500 let auth_type = get_str(map, &a_sec, "type");
501 match auth_type.as_str() {
502 "bearer" => Some(AuthConfig::Bearer {
503 token_env: get_str(map, &a_sec, "token_env"),
504 }),
505 "jwt" => Some(AuthConfig::Jwt {
506 secret_env: get_str(map, &a_sec, "secret_env"),
507 }),
508 "basic" => Some(AuthConfig::Basic {
509 htpasswd_file: get_str(map, &a_sec, "htpasswd_file"),
510 }),
511 _ => None,
512 }
513 } else {
514 None
515 };
516
517 let rewrite_request = collect_rewrite_rules(map, mw_sec, "request");
523 let rewrite_response = collect_rewrite_rules(map, mw_sec, "response");
524
525 let ip_sec = format!("{}.ip_filter", mw_sec);
526 let ip_allow = if section_exists(map, &ip_sec) {
527 get_array(map, &ip_sec, "allow")
528 } else {
529 vec![]
530 };
531 let ip_deny = if section_exists(map, &ip_sec) {
532 get_array(map, &ip_sec, "deny")
533 } else {
534 vec![]
535 };
536
537 let _ = route_idx; let timeout_ms = match get_u64(map, mw_sec, "timeout_ms", 0) {
543 0 => None,
544 ms => Some(ms),
545 };
546
547 let max_body_size = match get_u64(map, mw_sec, "max_body_size", 0) {
551 0 => None,
552 n => Some(n),
553 };
554
555 MiddlewareConfig {
556 rate_limit, cache, auth, rewrite_request, rewrite_response, ip_allow, ip_deny,
557 timeout_ms, max_body_size,
558 }
559}
560
561fn collect_rewrite_rules(
563 map: &parser::SectionMap,
564 mw_sec: &str,
565 direction: &str,
566) -> Vec<RewriteRuleConfig> {
567 use parser::{get_str, get_u64};
568
569 let mut rules = Vec::new();
570 let mut j = 0;
571 loop {
572 let rsec = format!("{}.rewrite.{}[{}]", mw_sec, direction, j);
573 if !parser::section_exists(map, &rsec) {
574 break;
575 }
576 let code_val = get_u64(map, &rsec, "code", 0);
577 rules.push(RewriteRuleConfig {
578 type_: get_str(map, &rsec, "type"),
579 name: {
580 let v = get_str(map, &rsec, "name");
581 if v.is_empty() { None } else { Some(v) }
582 },
583 value: {
584 let v = get_str(map, &rsec, "value");
585 if v.is_empty() { None } else { Some(v) }
586 },
587 prefix: {
588 let v = get_str(map, &rsec, "prefix");
589 if v.is_empty() { None } else { Some(v) }
590 },
591 from: {
592 let v = get_str(map, &rsec, "from");
593 if v.is_empty() { None } else { Some(v) }
594 },
595 to: {
596 let v = get_str(map, &rsec, "to");
597 if v.is_empty() { None } else { Some(v) }
598 },
599 code: if code_val == 0 { None } else { Some(code_val as u16) },
600 reason: {
601 let v = get_str(map, &rsec, "reason");
602 if v.is_empty() { None } else { Some(v) }
603 },
604 });
605 j += 1;
606 }
607 rules
608}
609
610fn config_file_path() -> String {
611 std::env::var("RWS_CONFIG_FILE").unwrap_or_else(|_| "rws.config.toml".to_string())
612}
613
614pub(crate) struct CompiledRoute {
618 pub(crate) matcher: RouteMatcher,
619 pub(crate) handler: Arc<dyn Application + Send + Sync>,
621}
622
623#[derive(Clone, Default)]
625pub(crate) struct RouteMatcher {
626 pub(crate) host: Option<String>,
628 pub(crate) path_prefix: Option<String>,
630 pub(crate) path_exact: Option<String>,
632 pub(crate) method: Option<String>,
634 pub(crate) content_type_prefix: Option<String>,
636}
637
638impl RouteMatcher {
639 pub(crate) fn from_match_config(cfg: &MatchConfig) -> Self {
640 let (path_prefix, path_exact) = match &cfg.path {
641 Some(p) if p.ends_with('*') => {
642 let stripped = p.trim_end_matches('*').to_string();
644 (Some(stripped), None)
645 }
646 Some(p) => (None, Some(p.clone())),
647 None => (None, None),
648 };
649 let content_type_prefix = cfg.content_type.as_ref().map(|ct| {
650 if ct.ends_with('*') {
651 ct.trim_end_matches('*').to_string()
652 } else {
653 ct.clone()
654 }
655 });
656 RouteMatcher {
657 host: cfg.host.clone(),
658 path_prefix,
659 path_exact,
660 method: cfg.method.clone(),
661 content_type_prefix,
662 }
663 }
664
665 pub(crate) fn matches(&self, request: &Request, conn: &ConnectionInfo) -> bool {
667 if let Some(ref expected_host) = self.host {
669 let actual_host = conn
670 .sni_hostname
671 .as_deref()
672 .or_else(|| {
673 request
674 .headers
675 .iter()
676 .find(|h| h.name.eq_ignore_ascii_case("host"))
677 .map(|h| h.value.as_str())
678 })
679 .unwrap_or("");
680 if actual_host != expected_host.as_str() {
681 return false;
682 }
683 }
684
685 if let Some(ref m) = self.method {
687 if request.method.to_uppercase() != m.as_str() {
688 return false;
689 }
690 }
691
692 let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
694 if let Some(ref prefix) = self.path_prefix {
695 if !path.starts_with(prefix.as_str()) {
696 return false;
697 }
698 } else if let Some(ref exact) = self.path_exact {
699 if path != exact.as_str() {
700 return false;
701 }
702 }
703
704 if let Some(ref ct_prefix) = self.content_type_prefix {
706 let actual_ct = request
707 .headers
708 .iter()
709 .find(|h| h.name.eq_ignore_ascii_case("content-type"))
710 .map(|h| h.value.as_str())
711 .unwrap_or("");
712 if !actual_ct.starts_with(ct_prefix.as_str()) {
713 return false;
714 }
715 }
716
717 true
718 }
719}
720
721#[derive(Clone)]
726pub struct ConfigDrivenApp {
727 routes: Arc<Vec<CompiledRoute>>,
728 fallback: App,
733}
734
735impl ConfigDrivenApp {
736 pub(crate) fn new(routes: Vec<CompiledRoute>) -> Self {
737 use crate::core::New;
738 ConfigDrivenApp {
739 routes: Arc::new(routes),
740 fallback: App::new(),
741 }
742 }
743
744 pub fn with_config(mut self, config: ServerConfig) -> Self {
760 self.fallback = App::with_config(config);
761 self
762 }
763}
764
765impl Application for ConfigDrivenApp {
766 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
767 for route in self.routes.iter() {
768 if route.matcher.matches(request, conn) {
769 return route.handler.execute(request, conn);
770 }
771 }
772 self.fallback.execute(request, conn)
773 }
774}
775
776#[derive(Clone, Copy)]
781pub(crate) struct NullApp;
782
783impl Application for NullApp {
784 fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
785 let mut r = Response::new();
786 r.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
787 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
788 Ok(r)
789 }
790}
791
792use std::collections::HashMap;
795use std::collections::hash_map::DefaultHasher;
796use std::hash::{Hash, Hasher};
797use std::sync::atomic::{AtomicUsize, Ordering};
798use std::sync::RwLock;
799use std::time::{Duration, SystemTime, UNIX_EPOCH};
800
801#[derive(Clone, Copy, Debug, PartialEq, Eq)]
805pub(crate) enum LoadBalanceStrategy {
806 RoundRobin,
807 Random,
808 IpHash,
809 LeastConnections,
810}
811
812impl LoadBalanceStrategy {
813 fn parse(s: &str) -> Self {
814 match s {
815 "random" => LoadBalanceStrategy::Random,
816 "ip_hash" => LoadBalanceStrategy::IpHash,
817 "least_connections" => LoadBalanceStrategy::LeastConnections,
818 _ => LoadBalanceStrategy::RoundRobin,
819 }
820 }
821}
822
823#[derive(Clone)]
829pub(crate) struct DynamicProxy {
830 live: Arc<RwLock<Vec<String>>>,
831 counter: Arc<AtomicUsize>,
832 connect_timeout: Duration,
833 read_timeout: Duration,
834 strip_prefix: Option<Arc<String>>,
835 add_prefix: Option<Arc<String>>,
836 tls: bool,
837 strategy: LoadBalanceStrategy,
838 connections: Arc<RwLock<HashMap<String, Arc<AtomicUsize>>>>,
839}
840
841impl DynamicProxy {
842 pub(crate) fn new(
843 live: Arc<RwLock<Vec<String>>>,
844 connect_timeout_ms: u64,
845 read_timeout_ms: u64,
846 strip_prefix: Option<String>,
847 add_prefix: Option<String>,
848 tls: bool,
849 strategy: String,
850 ) -> Self {
851 DynamicProxy {
852 live,
853 counter: Arc::new(AtomicUsize::new(0)),
854 connect_timeout: Duration::from_millis(connect_timeout_ms),
855 read_timeout: Duration::from_millis(read_timeout_ms),
856 strip_prefix: strip_prefix.map(Arc::new),
857 add_prefix: add_prefix.map(Arc::new),
858 tls,
859 strategy: LoadBalanceStrategy::parse(&strategy),
860 connections: Arc::new(RwLock::new(HashMap::new())),
861 }
862 }
863
864 fn next_backend(&self, client_ip: &str) -> Option<String> {
865 let live = self.live.read().unwrap();
866 if live.is_empty() {
867 return None;
868 }
869
870 let idx = match self.strategy {
871 LoadBalanceStrategy::RoundRobin => {
872 self.counter.fetch_add(1, Ordering::Relaxed) % live.len()
873 }
874 LoadBalanceStrategy::Random => {
875 let nanos = SystemTime::now()
876 .duration_since(UNIX_EPOCH)
877 .map(|d| d.subsec_nanos())
878 .unwrap_or(0) as usize;
879 let salt = self.counter.fetch_add(1, Ordering::Relaxed);
880 nanos.wrapping_add(salt) % live.len()
881 }
882 LoadBalanceStrategy::IpHash => {
883 let mut hasher = DefaultHasher::new();
884 client_ip.hash(&mut hasher);
885 (hasher.finish() as usize) % live.len()
886 }
887 LoadBalanceStrategy::LeastConnections => {
888 let connections = self.connections.read().unwrap();
889 live.iter()
890 .enumerate()
891 .min_by_key(|(_, backend)| {
892 connections
893 .get(*backend)
894 .map(|c| c.load(Ordering::Relaxed))
895 .unwrap_or(0)
896 })
897 .map(|(i, _)| i)
898 .unwrap_or(0)
899 }
900 };
901
902 Some(live[idx].clone())
903 }
904
905 fn connection_counter(&self, backend: &str) -> Arc<AtomicUsize> {
908 if let Some(counter) = self.connections.read().unwrap().get(backend) {
909 return Arc::clone(counter);
910 }
911 let mut connections = self.connections.write().unwrap();
912 Arc::clone(
913 connections
914 .entry(backend.to_string())
915 .or_insert_with(|| Arc::new(AtomicUsize::new(0))),
916 )
917 }
918}
919
920struct ConnectionGuard {
923 counter: Arc<AtomicUsize>,
924}
925
926impl Drop for ConnectionGuard {
927 fn drop(&mut self) {
928 self.counter.fetch_sub(1, Ordering::Relaxed);
929 }
930}
931
932impl Application for DynamicProxy {
933 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
934 let backend = match self.next_backend(&conn.client.ip) {
935 Some(b) => b,
936 None => {
937 return Ok(bad_gateway());
938 }
939 };
940
941 let _connection_guard = if self.strategy == LoadBalanceStrategy::LeastConnections {
942 let counter = self.connection_counter(&backend);
943 counter.fetch_add(1, Ordering::Relaxed);
944 Some(ConnectionGuard { counter })
945 } else {
946 None
947 };
948
949 let (host, port, _) = match crate::proxy_config::health::parse_backend_url(&backend) {
950 Some(t) => t,
951 None => return Ok(bad_gateway()),
952 };
953
954 let mut req_clone;
956 let effective_request = if self.strip_prefix.is_some() || self.add_prefix.is_some() {
957 req_clone = request.clone();
958 if let Some(ref sp) = self.strip_prefix {
959 if let Some(stripped) = req_clone.request_uri.strip_prefix(sp.as_str()) {
960 req_clone.request_uri = if stripped.is_empty() || !stripped.starts_with('/') {
961 format!("/{}", stripped)
962 } else {
963 stripped.to_string()
964 };
965 }
966 }
967 if let Some(ref ap) = self.add_prefix {
968 req_clone.request_uri = format!("{}{}", ap, req_clone.request_uri);
969 }
970 &req_clone
971 } else {
972 request
973 };
974
975 let result = if self.tls {
976 #[cfg(any(feature = "http-client", feature = "http2"))]
977 {
978 crate::proxy::proxy_https1(
979 effective_request,
980 &conn.client.ip,
981 &host,
982 port,
983 self.connect_timeout,
984 self.read_timeout,
985 )
986 }
987 #[cfg(not(any(feature = "http-client", feature = "http2")))]
988 {
989 eprintln!("[proxy] HTTPS upstream requires http-client or http2 feature");
990 Err("TLS upstream not supported in this build".to_string())
991 }
992 } else {
993 crate::proxy::proxy_http1(
994 effective_request,
995 &conn.client.ip,
996 &host,
997 port,
998 self.connect_timeout,
999 self.read_timeout,
1000 )
1001 };
1002
1003 match result {
1004 Ok(r) => Ok(r),
1005 Err(_) => Ok(bad_gateway()),
1006 }
1007 }
1008}
1009
1010fn bad_gateway() -> Response {
1011 use crate::mime_type::MimeType;
1012 use crate::range::Range;
1013 let cr = Range::get_content_range(
1014 b"502 Bad Gateway".to_vec(),
1015 MimeType::TEXT_PLAIN.to_string(),
1016 );
1017 let mut r = Response::new();
1018 r.status_code = *STATUS_CODE_REASON_PHRASE.n502_bad_gateway.status_code;
1019 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n502_bad_gateway.reason_phrase.to_string();
1020 r.content_range_list = vec![cr];
1021 r
1022}
1023
1024#[derive(Clone)]
1030pub(crate) struct RedirectAdapter {
1031 location_template: Arc<String>,
1032 status: i16,
1033 reason: Arc<String>,
1034}
1035
1036impl RedirectAdapter {
1037 pub(crate) fn new(location: String, status: u16) -> Self {
1038 let (code, reason) = redirect_status(status);
1039 RedirectAdapter {
1040 location_template: Arc::new(location),
1041 status: code,
1042 reason: Arc::new(reason),
1043 }
1044 }
1045}
1046
1047fn redirect_status(code: u16) -> (i16, String) {
1048 let phrase = match code {
1049 301 => STATUS_CODE_REASON_PHRASE.n301_moved_permanently.reason_phrase,
1050 302 => STATUS_CODE_REASON_PHRASE.n302_found.reason_phrase,
1051 307 => STATUS_CODE_REASON_PHRASE.n307_temporary_redirect.reason_phrase,
1052 308 => STATUS_CODE_REASON_PHRASE.n308_permanent_redirect.reason_phrase,
1053 _ => "Redirect",
1054 };
1055 (code as i16, phrase.to_string())
1056}
1057
1058impl Application for RedirectAdapter {
1059 fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1060 let location = self
1061 .location_template
1062 .replace("$path", &request.request_uri);
1063 use crate::header::Header;
1064 let mut r = Response::new();
1065 r.status_code = self.status;
1066 r.reason_phrase = self.reason.as_ref().clone();
1067 r.headers.push(Header { name: "Location".to_string(), value: location });
1068 Ok(r)
1069 }
1070}
1071
1072#[derive(Clone)]
1076pub(crate) struct RespondAdapter {
1077 status: i16,
1078 reason: Arc<String>,
1079 body: Arc<Vec<u8>>,
1080 content_type: Arc<String>,
1081}
1082
1083impl RespondAdapter {
1084 pub(crate) fn new(status: u16, body: String, content_type: String) -> Self {
1085 use crate::response::STATUS_CODE_REASON_PHRASE;
1086 let reason = match status {
1087 200 => STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string(),
1088 201 => STATUS_CODE_REASON_PHRASE.n201_created.reason_phrase.to_string(),
1089 204 => STATUS_CODE_REASON_PHRASE.n204_no_content.reason_phrase.to_string(),
1090 400 => STATUS_CODE_REASON_PHRASE.n400_bad_request.reason_phrase.to_string(),
1091 401 => STATUS_CODE_REASON_PHRASE.n401_unauthorized.reason_phrase.to_string(),
1092 403 => STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string(),
1093 404 => STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string(),
1094 500 => STATUS_CODE_REASON_PHRASE.n500_internal_server_error.reason_phrase.to_string(),
1095 _ => "OK".to_string(),
1096 };
1097 RespondAdapter {
1098 status: status as i16,
1099 reason: Arc::new(reason),
1100 body: Arc::new(body.into_bytes()),
1101 content_type: Arc::new(content_type),
1102 }
1103 }
1104}
1105
1106impl Application for RespondAdapter {
1107 fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1108 use crate::range::Range;
1109 let mut r = Response::new();
1110 r.status_code = self.status;
1111 r.reason_phrase = self.reason.as_ref().clone();
1112 if !self.body.is_empty() {
1113 r.content_range_list = vec![Range::get_content_range(
1114 self.body.as_ref().clone(),
1115 self.content_type.as_ref().clone(),
1116 )];
1117 }
1118 Ok(r)
1119 }
1120}
1121
1122#[derive(Clone)]
1131pub(crate) struct StaticAdapter {
1132 root: Arc<std::path::PathBuf>,
1133 index: Arc<Vec<String>>,
1134}
1135
1136impl StaticAdapter {
1137 pub(crate) fn new(root: String, index: Vec<String>) -> Self {
1138 let index = if index.is_empty() { vec!["index.html".to_string()] } else { index };
1139 StaticAdapter {
1140 root: Arc::new(std::path::PathBuf::from(root)),
1141 index: Arc::new(index),
1142 }
1143 }
1144
1145 fn resolve(&self, request_uri: &str) -> Option<std::path::PathBuf> {
1149 let raw_path = request_uri.split('?').next().unwrap_or(request_uri);
1150 let decoded = crate::url::URL::percent_decode(raw_path);
1151
1152 if decoded.split('/').any(|segment| segment == "..") {
1153 return None;
1154 }
1155
1156 let relative = decoded.trim_start_matches('/');
1157 Some(self.root.join(relative))
1158 }
1159}
1160
1161impl Application for StaticAdapter {
1162 fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1163 let mut response = Response::new();
1164
1165 let not_found = |mut response: Response| {
1166 response.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
1167 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
1168 response
1169 };
1170
1171 let candidate = match self.resolve(&request.request_uri) {
1172 Some(p) => p,
1173 None => {
1174 response.status_code = *STATUS_CODE_REASON_PHRASE.n403_forbidden.status_code;
1175 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string();
1176 return Ok(response);
1177 }
1178 };
1179
1180 let mut file_path = candidate;
1181 if file_path.is_dir() {
1182 let indexed = self
1183 .index
1184 .iter()
1185 .map(|name| file_path.join(name))
1186 .find(|p| p.is_file());
1187
1188 file_path = match indexed {
1189 Some(p) => p,
1190 None => return Ok(not_found(response)),
1191 };
1192 }
1193
1194 if !file_path.is_file() {
1195 return Ok(not_found(response));
1196 }
1197
1198 if let (Ok(root_canon), Ok(file_canon)) =
1201 (self.root.canonicalize(), file_path.canonicalize())
1202 {
1203 if !file_canon.starts_with(&root_canon) {
1204 return Ok(not_found(response));
1205 }
1206 }
1207
1208 let path_str = match file_path.to_str() {
1209 Some(s) => s,
1210 None => return Ok(not_found(response)),
1211 };
1212
1213 match crate::range::Range::get_content_range_of_a_file(path_str) {
1214 Ok(content_range) => {
1215 response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
1216 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
1217 response.content_range_list = vec![content_range];
1218 Ok(response)
1219 }
1220 Err(_) => Ok(not_found(response)),
1221 }
1222 }
1223}
1224
1225pub(crate) struct PerRouteRateLimit(pub(crate) Arc<crate::rate_limit::RateLimiter>);
1229
1230impl crate::middleware::Middleware for PerRouteRateLimit {
1231 fn handle(
1232 &self,
1233 request: &Request,
1234 conn: &ConnectionInfo,
1235 next: &dyn Application,
1236 ) -> Result<Response, String> {
1237 use crate::error::{AppError, IntoResponse};
1238 if self.0.check(&conn.client.ip) {
1239 next.execute(request, conn)
1240 } else {
1241 Ok(AppError::TooManyRequests.into_response())
1242 }
1243 }
1244}
1245
1246pub(crate) struct PerRouteMaxBodySize(pub(crate) u64);
1261
1262impl crate::middleware::Middleware for PerRouteMaxBodySize {
1263 fn handle(
1264 &self,
1265 request: &Request,
1266 conn: &ConnectionInfo,
1267 next: &dyn Application,
1268 ) -> Result<Response, String> {
1269 use crate::error::{AppError, IntoResponse};
1270 if request.body.len() as u64 > self.0 {
1271 Ok(AppError::PayloadTooLarge(format!(
1272 "request body of {} bytes exceeds this route's {}-byte limit",
1273 request.body.len(),
1274 self.0,
1275 ))
1276 .into_response())
1277 } else {
1278 next.execute(request, conn)
1279 }
1280 }
1281}
1282
1283pub(crate) struct BearerAuthMiddleware {
1287 pub(crate) token: Arc<String>,
1288}
1289
1290impl crate::middleware::Middleware for BearerAuthMiddleware {
1291 fn handle(
1292 &self,
1293 request: &Request,
1294 conn: &ConnectionInfo,
1295 next: &dyn Application,
1296 ) -> Result<Response, String> {
1297 use crate::error::{AppError, IntoResponse};
1298 let expected = format!("Bearer {}", self.token);
1299 let authorized = request
1300 .headers
1301 .iter()
1302 .any(|h| h.name.eq_ignore_ascii_case("authorization") && h.value == expected);
1303 if authorized {
1304 next.execute(request, conn)
1305 } else {
1306 Ok(AppError::Unauthorized.into_response())
1307 }
1308 }
1309}
1310
1311pub(crate) fn arc_app<A: Application + Send + Sync + 'static>(
1315 a: A,
1316) -> Arc<dyn Application + Send + Sync> {
1317 Arc::new(a)
1318}
1319
1320pub fn build_from_file() -> (ConfigDrivenApp, Vec<std::thread::JoinHandle<()>>) {
1325 builder::build_from_file()
1326}