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;
43
44#[derive(Debug, Clone)]
47pub struct ProxyConfig {
48 pub upstreams: Vec<UpstreamConfig>,
49 pub routes: Vec<RouteConfig>,
50 pub tcp_proxies: Vec<TcpProxyConfig>,
51 pub udp_proxies: Vec<UdpProxyConfig>,
52 pub ws_proxies: Vec<WsProxyConfig>,
53 pub global_middleware: MiddlewareConfig,
54}
55
56#[derive(Debug, Clone)]
57pub struct UpstreamConfig {
58 pub name: String,
59 pub backends: Vec<String>,
60 pub strategy: String, pub health_check: Option<HealthCheckConfig>,
62 pub tls: bool,
66}
67
68#[derive(Debug, Clone)]
69pub struct HealthCheckConfig {
70 pub path: String,
71 pub interval_secs: u64,
72 pub timeout_ms: u64,
73 pub healthy_threshold: u32,
74 pub unhealthy_threshold: u32,
75}
76
77#[derive(Debug, Clone)]
78pub struct RouteConfig {
79 pub name: String,
80 pub match_: MatchConfig,
81 pub action: ActionConfig,
82 pub middleware: MiddlewareConfig,
83}
84
85#[derive(Debug, Clone, Default)]
86pub struct MatchConfig {
87 pub host: Option<String>,
88 pub path: Option<String>,
89 pub method: Option<String>,
90 pub content_type: Option<String>,
91}
92
93#[derive(Debug, Clone)]
94pub enum ActionConfig {
95 Proxy {
96 upstream: String,
97 connect_timeout_ms: u64,
98 read_timeout_ms: u64,
99 strip_path_prefix: Option<String>,
100 add_path_prefix: Option<String>,
101 },
102 Grpc {
103 upstream: String,
104 connect_timeout_ms: u64,
105 read_timeout_ms: u64,
106 },
107 Static {
108 root: String,
109 index: Vec<String>,
110 },
111 Redirect {
112 location: String,
113 status: u16,
114 },
115 Respond {
116 status: u16,
117 body: String,
118 content_type: String,
119 },
120 Mcp,
121 Unknown(String),
122}
123
124#[derive(Debug, Clone, Default)]
125pub struct MiddlewareConfig {
126 pub rate_limit: Option<RateLimitConfig>,
127 pub cache: Option<CacheConfig>,
128 pub auth: Option<AuthConfig>,
129 pub rewrite_request: Vec<RewriteRuleConfig>,
130 pub rewrite_response: Vec<RewriteRuleConfig>,
131 pub ip_allow: Vec<String>,
132 pub ip_deny: Vec<String>,
133}
134
135#[derive(Debug, Clone)]
136pub struct RateLimitConfig {
137 pub max_requests: u32,
138 pub window_secs: u64,
139}
140
141#[derive(Debug, Clone)]
142pub struct CacheConfig {
143 pub ttl_secs: u64,
144 pub vary_by: Vec<String>,
145}
146
147#[derive(Debug, Clone)]
148pub enum AuthConfig {
149 Basic { users_file: String },
150 Jwt { secret_env: String },
151 Bearer { token_env: String },
152}
153
154#[derive(Debug, Clone, Default)]
155pub struct RewriteRuleConfig {
156 pub type_: String,
157 pub name: Option<String>,
158 pub value: Option<String>,
159 pub prefix: Option<String>,
160 pub from: Option<String>,
161 pub to: Option<String>,
162 pub code: Option<u16>,
163 pub reason: Option<String>,
164}
165
166#[derive(Debug, Clone)]
167pub struct TcpProxyConfig {
168 pub name: String,
169 pub listen: String,
170 pub backends: Vec<String>,
171 pub connect_timeout_ms: u64,
172}
173
174#[derive(Debug, Clone)]
175pub struct UdpProxyConfig {
176 pub name: String,
177 pub listen: String,
178 pub backends: Vec<String>,
179 pub reply_timeout_ms: u64,
180 pub buffer_size: usize,
181}
182
183#[derive(Debug, Clone)]
184pub struct WsProxyConfig {
185 pub name: String,
186 pub listen: String,
187 pub backends: Vec<String>,
188 pub connect_timeout_ms: u64,
189 pub read_timeout_ms: u64,
190}
191
192impl ProxyConfig {
195 pub fn is_proxy_mode() -> bool {
199 let path = config_file_path();
200 match std::fs::read_to_string(&path) {
201 Ok(contents) => {
202 contents.contains("[[route]]") || contents.contains("[[upstream]]")
203 }
204 Err(_) => false,
205 }
206 }
207
208 pub fn load() -> Self {
210 let path = config_file_path();
211 let contents = std::fs::read_to_string(&path).unwrap_or_default();
212 Self::from_str(&contents)
213 }
214
215 pub fn from_str(toml: &str) -> Self {
217 use parser::{get_array, get_str, get_u32, get_u64, section_exists};
218
219 let map = parser::parse(toml);
220
221 let mut upstreams = Vec::new();
223 let mut i = 0;
224 loop {
225 let sec = format!("upstream[{}]", i);
226 if !section_exists(&map, &sec) {
227 break;
228 }
229 let name = get_str(&map, &sec, "name");
230 let backends = get_array(&map, &sec, "backends");
231 let strategy = {
232 let s = get_str(&map, &sec, "strategy");
233 if s.is_empty() { "round_robin".to_string() } else { s }
234 };
235 let hc_sec = format!("{}.health_check", sec);
236 let health_check = if section_exists(&map, &hc_sec) {
237 Some(HealthCheckConfig {
238 path: {
239 let p = get_str(&map, &hc_sec, "path");
240 if p.is_empty() { "/health".to_string() } else { p }
241 },
242 interval_secs: get_u64(&map, &hc_sec, "interval_secs", 30),
243 timeout_ms: get_u64(&map, &hc_sec, "timeout_ms", 5000),
244 healthy_threshold: get_u32(&map, &hc_sec, "healthy_threshold", 2),
245 unhealthy_threshold: get_u32(&map, &hc_sec, "unhealthy_threshold", 3),
246 })
247 } else {
248 None
249 };
250 let tls = backends.iter().any(|b| b.starts_with("https://"));
251 upstreams.push(UpstreamConfig { name, backends, strategy, health_check, tls });
252 i += 1;
253 }
254
255 let mut routes = Vec::new();
257 let mut i = 0;
258 loop {
259 let sec = format!("route[{}]", i);
260 if !section_exists(&map, &sec) {
261 break;
262 }
263 let name = get_str(&map, &sec, "name");
264
265 let m_sec = format!("{}.match", sec);
267 let match_ = MatchConfig {
268 host: {
269 let h = get_str(&map, &m_sec, "host");
270 if h.is_empty() { None } else { Some(h) }
271 },
272 path: {
273 let p = get_str(&map, &m_sec, "path");
274 if p.is_empty() { None } else { Some(p) }
275 },
276 method: {
277 let m = get_str(&map, &m_sec, "method");
278 if m.is_empty() { None } else { Some(m.to_uppercase()) }
279 },
280 content_type: {
281 let c = get_str(&map, &m_sec, "content_type");
282 if c.is_empty() { None } else { Some(c) }
283 },
284 };
285
286 let a_sec = format!("{}.action", sec);
288 let action_type = get_str(&map, &a_sec, "type");
289 let action = match action_type.as_str() {
290 "proxy" => {
291 let p_sec = format!("{}.action.proxy", sec);
292 ActionConfig::Proxy {
293 upstream: get_str(&map, &p_sec, "upstream"),
294 connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
295 read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
296 strip_path_prefix: {
297 let v = get_str(&map, &p_sec, "strip_path_prefix");
298 if v.is_empty() { None } else { Some(v) }
299 },
300 add_path_prefix: {
301 let v = get_str(&map, &p_sec, "add_path_prefix");
302 if v.is_empty() { None } else { Some(v) }
303 },
304 }
305 }
306 "grpc" => {
307 let p_sec = format!("{}.action.grpc", sec);
308 ActionConfig::Grpc {
309 upstream: get_str(&map, &p_sec, "upstream"),
310 connect_timeout_ms: get_u64(&map, &p_sec, "connect_timeout_ms", 5000),
311 read_timeout_ms: get_u64(&map, &p_sec, "read_timeout_ms", 30000),
312 }
313 }
314 "static" => {
315 let s_sec = format!("{}.action.static", sec);
316 ActionConfig::Static {
317 root: get_str(&map, &s_sec, "root"),
318 index: get_array(&map, &s_sec, "index"),
319 }
320 }
321 "redirect" => {
322 let r_sec = format!("{}.action.redirect", sec);
323 ActionConfig::Redirect {
324 location: get_str(&map, &r_sec, "location"),
325 status: get_u64(&map, &r_sec, "status", 301) as u16,
326 }
327 }
328 "respond" => {
329 let r_sec = format!("{}.action.respond", sec);
330 ActionConfig::Respond {
331 status: get_u64(&map, &r_sec, "status", 200) as u16,
332 body: get_str(&map, &r_sec, "body"),
333 content_type: {
334 let ct = get_str(&map, &r_sec, "content_type");
335 if ct.is_empty() { "text/plain".to_string() } else { ct }
336 },
337 }
338 }
339 "mcp" => ActionConfig::Mcp,
340 other => ActionConfig::Unknown(other.to_string()),
341 };
342
343 let mw_sec = format!("{}.middleware", sec);
345 let middleware = parse_middleware_config(&map, &mw_sec, i);
346
347 routes.push(RouteConfig { name, match_, action, middleware });
348 i += 1;
349 }
350
351 let mut tcp_proxies = Vec::new();
353 let mut i = 0;
354 loop {
355 let sec = format!("tcp_proxy[{}]", i);
356 if !section_exists(&map, &sec) {
357 break;
358 }
359 tcp_proxies.push(TcpProxyConfig {
360 name: get_str(&map, &sec, "name"),
361 listen: get_str(&map, &sec, "listen"),
362 backends: get_array(&map, &sec, "backends"),
363 connect_timeout_ms: get_u64(&map, &sec, "connect_timeout_ms", 5000),
364 });
365 i += 1;
366 }
367
368 let mut udp_proxies = Vec::new();
370 let mut i = 0;
371 loop {
372 let sec = format!("udp_proxy[{}]", i);
373 if !section_exists(&map, &sec) {
374 break;
375 }
376 udp_proxies.push(UdpProxyConfig {
377 name: get_str(&map, &sec, "name"),
378 listen: get_str(&map, &sec, "listen"),
379 backends: get_array(&map, &sec, "backends"),
380 reply_timeout_ms: get_u64(&map, &sec, "reply_timeout_ms", 5000),
381 buffer_size: get_u64(&map, &sec, "buffer_size", 65536) as usize,
382 });
383 i += 1;
384 }
385
386 let mut ws_proxies = Vec::new();
388 let mut i = 0;
389 loop {
390 let sec = format!("ws_proxy[{}]", i);
391 if !section_exists(&map, &sec) {
392 break;
393 }
394 ws_proxies.push(WsProxyConfig {
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 read_timeout_ms: get_u64(&map, &sec, "read_timeout_ms", 30000),
400 });
401 i += 1;
402 }
403
404 let global_middleware = parse_middleware_config(&map, "middleware", usize::MAX);
406
407 ProxyConfig {
408 upstreams,
409 routes,
410 tcp_proxies,
411 udp_proxies,
412 ws_proxies,
413 global_middleware,
414 }
415 }
416}
417
418fn parse_middleware_config(
421 map: &parser::SectionMap,
422 mw_sec: &str,
423 route_idx: usize,
424) -> MiddlewareConfig {
425 use parser::{get_array, get_str, get_u32, get_u64, section_exists};
426
427 let rl_sec = format!("{}.rate_limit", mw_sec);
428 let rate_limit = if section_exists(map, &rl_sec) {
429 Some(RateLimitConfig {
430 max_requests: get_u32(map, &rl_sec, "max_requests", 1000),
431 window_secs: get_u64(map, &rl_sec, "window_secs", 60),
432 })
433 } else {
434 None
435 };
436
437 let c_sec = format!("{}.cache", mw_sec);
438 let cache = if section_exists(map, &c_sec) {
439 Some(CacheConfig {
440 ttl_secs: get_u64(map, &c_sec, "ttl_secs", 60),
441 vary_by: get_array(map, &c_sec, "vary_by"),
442 })
443 } else {
444 None
445 };
446
447 let a_sec = format!("{}.auth", mw_sec);
448 let auth = if section_exists(map, &a_sec) {
449 let auth_type = get_str(map, &a_sec, "type");
450 match auth_type.as_str() {
451 "bearer" => Some(AuthConfig::Bearer {
452 token_env: get_str(map, &a_sec, "token_env"),
453 }),
454 "jwt" => Some(AuthConfig::Jwt {
455 secret_env: get_str(map, &a_sec, "secret_env"),
456 }),
457 "basic" => Some(AuthConfig::Basic {
458 users_file: get_str(map, &a_sec, "users_file"),
459 }),
460 _ => None,
461 }
462 } else {
463 None
464 };
465
466 let rewrite_request = collect_rewrite_rules(map, mw_sec, "request");
472 let rewrite_response = collect_rewrite_rules(map, mw_sec, "response");
473
474 let ip_sec = format!("{}.ip_filter", mw_sec);
475 let ip_allow = if section_exists(map, &ip_sec) {
476 get_array(map, &ip_sec, "allow")
477 } else {
478 vec![]
479 };
480 let ip_deny = if section_exists(map, &ip_sec) {
481 get_array(map, &ip_sec, "deny")
482 } else {
483 vec![]
484 };
485
486 let _ = route_idx; MiddlewareConfig { rate_limit, cache, auth, rewrite_request, rewrite_response, ip_allow, ip_deny }
489}
490
491fn collect_rewrite_rules(
493 map: &parser::SectionMap,
494 mw_sec: &str,
495 direction: &str,
496) -> Vec<RewriteRuleConfig> {
497 use parser::{get_str, get_u64};
498
499 let mut rules = Vec::new();
500 let mut j = 0;
501 loop {
502 let rsec = format!("{}.rewrite.{}[{}]", mw_sec, direction, j);
503 if !parser::section_exists(map, &rsec) {
504 break;
505 }
506 let code_val = get_u64(map, &rsec, "code", 0);
507 rules.push(RewriteRuleConfig {
508 type_: get_str(map, &rsec, "type"),
509 name: {
510 let v = get_str(map, &rsec, "name");
511 if v.is_empty() { None } else { Some(v) }
512 },
513 value: {
514 let v = get_str(map, &rsec, "value");
515 if v.is_empty() { None } else { Some(v) }
516 },
517 prefix: {
518 let v = get_str(map, &rsec, "prefix");
519 if v.is_empty() { None } else { Some(v) }
520 },
521 from: {
522 let v = get_str(map, &rsec, "from");
523 if v.is_empty() { None } else { Some(v) }
524 },
525 to: {
526 let v = get_str(map, &rsec, "to");
527 if v.is_empty() { None } else { Some(v) }
528 },
529 code: if code_val == 0 { None } else { Some(code_val as u16) },
530 reason: {
531 let v = get_str(map, &rsec, "reason");
532 if v.is_empty() { None } else { Some(v) }
533 },
534 });
535 j += 1;
536 }
537 rules
538}
539
540fn config_file_path() -> String {
541 std::env::var("RWS_CONFIG_FILE").unwrap_or_else(|_| "rws.config.toml".to_string())
542}
543
544pub(crate) struct CompiledRoute {
548 pub(crate) matcher: RouteMatcher,
549 pub(crate) handler: Arc<dyn Application + Send + Sync>,
551}
552
553#[derive(Clone, Default)]
555pub(crate) struct RouteMatcher {
556 pub(crate) host: Option<String>,
558 pub(crate) path_prefix: Option<String>,
560 pub(crate) path_exact: Option<String>,
562 pub(crate) method: Option<String>,
564 pub(crate) content_type_prefix: Option<String>,
566}
567
568impl RouteMatcher {
569 pub(crate) fn from_match_config(cfg: &MatchConfig) -> Self {
570 let (path_prefix, path_exact) = match &cfg.path {
571 Some(p) if p.ends_with('*') => {
572 let stripped = p.trim_end_matches('*').to_string();
574 (Some(stripped), None)
575 }
576 Some(p) => (None, Some(p.clone())),
577 None => (None, None),
578 };
579 let content_type_prefix = cfg.content_type.as_ref().map(|ct| {
580 if ct.ends_with('*') {
581 ct.trim_end_matches('*').to_string()
582 } else {
583 ct.clone()
584 }
585 });
586 RouteMatcher {
587 host: cfg.host.clone(),
588 path_prefix,
589 path_exact,
590 method: cfg.method.clone(),
591 content_type_prefix,
592 }
593 }
594
595 pub(crate) fn matches(&self, request: &Request, conn: &ConnectionInfo) -> bool {
597 if let Some(ref expected_host) = self.host {
599 let actual_host = conn
600 .sni_hostname
601 .as_deref()
602 .or_else(|| {
603 request
604 .headers
605 .iter()
606 .find(|h| h.name.eq_ignore_ascii_case("host"))
607 .map(|h| h.value.as_str())
608 })
609 .unwrap_or("");
610 if actual_host != expected_host.as_str() {
611 return false;
612 }
613 }
614
615 if let Some(ref m) = self.method {
617 if request.method.to_uppercase() != m.as_str() {
618 return false;
619 }
620 }
621
622 let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
624 if let Some(ref prefix) = self.path_prefix {
625 if !path.starts_with(prefix.as_str()) {
626 return false;
627 }
628 } else if let Some(ref exact) = self.path_exact {
629 if path != exact.as_str() {
630 return false;
631 }
632 }
633
634 if let Some(ref ct_prefix) = self.content_type_prefix {
636 let actual_ct = request
637 .headers
638 .iter()
639 .find(|h| h.name.eq_ignore_ascii_case("content-type"))
640 .map(|h| h.value.as_str())
641 .unwrap_or("");
642 if !actual_ct.starts_with(ct_prefix.as_str()) {
643 return false;
644 }
645 }
646
647 true
648 }
649}
650
651#[derive(Clone)]
656pub struct ConfigDrivenApp {
657 routes: Arc<Vec<CompiledRoute>>,
658 fallback: App,
661}
662
663impl ConfigDrivenApp {
664 pub(crate) fn new(routes: Vec<CompiledRoute>) -> Self {
665 use crate::core::New;
666 ConfigDrivenApp {
667 routes: Arc::new(routes),
668 fallback: App::new(),
669 }
670 }
671}
672
673impl Application for ConfigDrivenApp {
674 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
675 for route in self.routes.iter() {
676 if route.matcher.matches(request, conn) {
677 return route.handler.execute(request, conn);
678 }
679 }
680 self.fallback.execute(request, conn)
681 }
682}
683
684#[derive(Clone, Copy)]
689pub(crate) struct NullApp;
690
691impl Application for NullApp {
692 fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
693 let mut r = Response::new();
694 r.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
695 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
696 Ok(r)
697 }
698}
699
700use std::collections::HashMap;
703use std::collections::hash_map::DefaultHasher;
704use std::hash::{Hash, Hasher};
705use std::sync::atomic::{AtomicUsize, Ordering};
706use std::sync::RwLock;
707use std::time::{Duration, SystemTime, UNIX_EPOCH};
708
709#[derive(Clone, Copy, Debug, PartialEq, Eq)]
713pub(crate) enum LoadBalanceStrategy {
714 RoundRobin,
715 Random,
716 IpHash,
717 LeastConnections,
718}
719
720impl LoadBalanceStrategy {
721 fn parse(s: &str) -> Self {
722 match s {
723 "random" => LoadBalanceStrategy::Random,
724 "ip_hash" => LoadBalanceStrategy::IpHash,
725 "least_connections" => LoadBalanceStrategy::LeastConnections,
726 _ => LoadBalanceStrategy::RoundRobin,
727 }
728 }
729}
730
731#[derive(Clone)]
737pub(crate) struct DynamicProxy {
738 live: Arc<RwLock<Vec<String>>>,
739 counter: Arc<AtomicUsize>,
740 connect_timeout: Duration,
741 read_timeout: Duration,
742 strip_prefix: Option<Arc<String>>,
743 add_prefix: Option<Arc<String>>,
744 tls: bool,
745 strategy: LoadBalanceStrategy,
746 connections: Arc<RwLock<HashMap<String, Arc<AtomicUsize>>>>,
747}
748
749impl DynamicProxy {
750 pub(crate) fn new(
751 live: Arc<RwLock<Vec<String>>>,
752 connect_timeout_ms: u64,
753 read_timeout_ms: u64,
754 strip_prefix: Option<String>,
755 add_prefix: Option<String>,
756 tls: bool,
757 strategy: String,
758 ) -> Self {
759 DynamicProxy {
760 live,
761 counter: Arc::new(AtomicUsize::new(0)),
762 connect_timeout: Duration::from_millis(connect_timeout_ms),
763 read_timeout: Duration::from_millis(read_timeout_ms),
764 strip_prefix: strip_prefix.map(Arc::new),
765 add_prefix: add_prefix.map(Arc::new),
766 tls,
767 strategy: LoadBalanceStrategy::parse(&strategy),
768 connections: Arc::new(RwLock::new(HashMap::new())),
769 }
770 }
771
772 fn next_backend(&self, client_ip: &str) -> Option<String> {
773 let live = self.live.read().unwrap();
774 if live.is_empty() {
775 return None;
776 }
777
778 let idx = match self.strategy {
779 LoadBalanceStrategy::RoundRobin => {
780 self.counter.fetch_add(1, Ordering::Relaxed) % live.len()
781 }
782 LoadBalanceStrategy::Random => {
783 let nanos = SystemTime::now()
784 .duration_since(UNIX_EPOCH)
785 .map(|d| d.subsec_nanos())
786 .unwrap_or(0) as usize;
787 let salt = self.counter.fetch_add(1, Ordering::Relaxed);
788 nanos.wrapping_add(salt) % live.len()
789 }
790 LoadBalanceStrategy::IpHash => {
791 let mut hasher = DefaultHasher::new();
792 client_ip.hash(&mut hasher);
793 (hasher.finish() as usize) % live.len()
794 }
795 LoadBalanceStrategy::LeastConnections => {
796 let connections = self.connections.read().unwrap();
797 live.iter()
798 .enumerate()
799 .min_by_key(|(_, backend)| {
800 connections
801 .get(*backend)
802 .map(|c| c.load(Ordering::Relaxed))
803 .unwrap_or(0)
804 })
805 .map(|(i, _)| i)
806 .unwrap_or(0)
807 }
808 };
809
810 Some(live[idx].clone())
811 }
812
813 fn connection_counter(&self, backend: &str) -> Arc<AtomicUsize> {
816 if let Some(counter) = self.connections.read().unwrap().get(backend) {
817 return Arc::clone(counter);
818 }
819 let mut connections = self.connections.write().unwrap();
820 Arc::clone(
821 connections
822 .entry(backend.to_string())
823 .or_insert_with(|| Arc::new(AtomicUsize::new(0))),
824 )
825 }
826}
827
828struct ConnectionGuard {
831 counter: Arc<AtomicUsize>,
832}
833
834impl Drop for ConnectionGuard {
835 fn drop(&mut self) {
836 self.counter.fetch_sub(1, Ordering::Relaxed);
837 }
838}
839
840impl Application for DynamicProxy {
841 fn execute(&self, request: &Request, conn: &ConnectionInfo) -> Result<Response, String> {
842 let backend = match self.next_backend(&conn.client.ip) {
843 Some(b) => b,
844 None => {
845 return Ok(bad_gateway());
846 }
847 };
848
849 let _connection_guard = if self.strategy == LoadBalanceStrategy::LeastConnections {
850 let counter = self.connection_counter(&backend);
851 counter.fetch_add(1, Ordering::Relaxed);
852 Some(ConnectionGuard { counter })
853 } else {
854 None
855 };
856
857 let (host, port, _) = match crate::proxy_config::health::parse_backend_url(&backend) {
858 Some(t) => t,
859 None => return Ok(bad_gateway()),
860 };
861
862 let mut req_clone;
864 let effective_request = if self.strip_prefix.is_some() || self.add_prefix.is_some() {
865 req_clone = request.clone();
866 if let Some(ref sp) = self.strip_prefix {
867 if let Some(stripped) = req_clone.request_uri.strip_prefix(sp.as_str()) {
868 req_clone.request_uri = if stripped.is_empty() || !stripped.starts_with('/') {
869 format!("/{}", stripped)
870 } else {
871 stripped.to_string()
872 };
873 }
874 }
875 if let Some(ref ap) = self.add_prefix {
876 req_clone.request_uri = format!("{}{}", ap, req_clone.request_uri);
877 }
878 &req_clone
879 } else {
880 request
881 };
882
883 let result = if self.tls {
884 #[cfg(any(feature = "http-client", feature = "http2"))]
885 {
886 crate::proxy::proxy_https1(
887 effective_request,
888 &conn.client.ip,
889 &host,
890 port,
891 self.connect_timeout,
892 self.read_timeout,
893 )
894 }
895 #[cfg(not(any(feature = "http-client", feature = "http2")))]
896 {
897 eprintln!("[proxy] HTTPS upstream requires http-client or http2 feature");
898 Err("TLS upstream not supported in this build".to_string())
899 }
900 } else {
901 crate::proxy::proxy_http1(
902 effective_request,
903 &conn.client.ip,
904 &host,
905 port,
906 self.connect_timeout,
907 self.read_timeout,
908 )
909 };
910
911 match result {
912 Ok(r) => Ok(r),
913 Err(_) => Ok(bad_gateway()),
914 }
915 }
916}
917
918fn bad_gateway() -> Response {
919 use crate::mime_type::MimeType;
920 use crate::range::Range;
921 let cr = Range::get_content_range(
922 b"502 Bad Gateway".to_vec(),
923 MimeType::TEXT_PLAIN.to_string(),
924 );
925 let mut r = Response::new();
926 r.status_code = *STATUS_CODE_REASON_PHRASE.n502_bad_gateway.status_code;
927 r.reason_phrase = STATUS_CODE_REASON_PHRASE.n502_bad_gateway.reason_phrase.to_string();
928 r.content_range_list = vec![cr];
929 r
930}
931
932#[derive(Clone)]
938pub(crate) struct RedirectAdapter {
939 location_template: Arc<String>,
940 status: i16,
941 reason: Arc<String>,
942}
943
944impl RedirectAdapter {
945 pub(crate) fn new(location: String, status: u16) -> Self {
946 let (code, reason) = redirect_status(status);
947 RedirectAdapter {
948 location_template: Arc::new(location),
949 status: code,
950 reason: Arc::new(reason),
951 }
952 }
953}
954
955fn redirect_status(code: u16) -> (i16, String) {
956 let phrase = match code {
957 301 => STATUS_CODE_REASON_PHRASE.n301_moved_permanently.reason_phrase,
958 302 => STATUS_CODE_REASON_PHRASE.n302_found.reason_phrase,
959 307 => STATUS_CODE_REASON_PHRASE.n307_temporary_redirect.reason_phrase,
960 308 => STATUS_CODE_REASON_PHRASE.n308_permanent_redirect.reason_phrase,
961 _ => "Redirect",
962 };
963 (code as i16, phrase.to_string())
964}
965
966impl Application for RedirectAdapter {
967 fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
968 let location = self
969 .location_template
970 .replace("$path", &request.request_uri);
971 use crate::header::Header;
972 let mut r = Response::new();
973 r.status_code = self.status;
974 r.reason_phrase = self.reason.as_ref().clone();
975 r.headers.push(Header { name: "Location".to_string(), value: location });
976 Ok(r)
977 }
978}
979
980#[derive(Clone)]
984pub(crate) struct RespondAdapter {
985 status: i16,
986 reason: Arc<String>,
987 body: Arc<Vec<u8>>,
988 content_type: Arc<String>,
989}
990
991impl RespondAdapter {
992 pub(crate) fn new(status: u16, body: String, content_type: String) -> Self {
993 use crate::response::STATUS_CODE_REASON_PHRASE;
994 let reason = match status {
995 200 => STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string(),
996 201 => STATUS_CODE_REASON_PHRASE.n201_created.reason_phrase.to_string(),
997 204 => STATUS_CODE_REASON_PHRASE.n204_no_content.reason_phrase.to_string(),
998 400 => STATUS_CODE_REASON_PHRASE.n400_bad_request.reason_phrase.to_string(),
999 401 => STATUS_CODE_REASON_PHRASE.n401_unauthorized.reason_phrase.to_string(),
1000 403 => STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string(),
1001 404 => STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string(),
1002 500 => STATUS_CODE_REASON_PHRASE.n500_internal_server_error.reason_phrase.to_string(),
1003 _ => "OK".to_string(),
1004 };
1005 RespondAdapter {
1006 status: status as i16,
1007 reason: Arc::new(reason),
1008 body: Arc::new(body.into_bytes()),
1009 content_type: Arc::new(content_type),
1010 }
1011 }
1012}
1013
1014impl Application for RespondAdapter {
1015 fn execute(&self, _request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1016 use crate::range::Range;
1017 let mut r = Response::new();
1018 r.status_code = self.status;
1019 r.reason_phrase = self.reason.as_ref().clone();
1020 if !self.body.is_empty() {
1021 r.content_range_list = vec![Range::get_content_range(
1022 self.body.as_ref().clone(),
1023 self.content_type.as_ref().clone(),
1024 )];
1025 }
1026 Ok(r)
1027 }
1028}
1029
1030#[derive(Clone)]
1039pub(crate) struct StaticAdapter {
1040 root: Arc<std::path::PathBuf>,
1041 index: Arc<Vec<String>>,
1042}
1043
1044impl StaticAdapter {
1045 pub(crate) fn new(root: String, index: Vec<String>) -> Self {
1046 let index = if index.is_empty() { vec!["index.html".to_string()] } else { index };
1047 StaticAdapter {
1048 root: Arc::new(std::path::PathBuf::from(root)),
1049 index: Arc::new(index),
1050 }
1051 }
1052
1053 fn resolve(&self, request_uri: &str) -> Option<std::path::PathBuf> {
1057 let raw_path = request_uri.split('?').next().unwrap_or(request_uri);
1058 let decoded = crate::url::URL::percent_decode(raw_path);
1059
1060 if decoded.split('/').any(|segment| segment == "..") {
1061 return None;
1062 }
1063
1064 let relative = decoded.trim_start_matches('/');
1065 Some(self.root.join(relative))
1066 }
1067}
1068
1069impl Application for StaticAdapter {
1070 fn execute(&self, request: &Request, _conn: &ConnectionInfo) -> Result<Response, String> {
1071 let mut response = Response::new();
1072
1073 let not_found = |mut response: Response| {
1074 response.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
1075 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
1076 response
1077 };
1078
1079 let candidate = match self.resolve(&request.request_uri) {
1080 Some(p) => p,
1081 None => {
1082 response.status_code = *STATUS_CODE_REASON_PHRASE.n403_forbidden.status_code;
1083 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string();
1084 return Ok(response);
1085 }
1086 };
1087
1088 let mut file_path = candidate;
1089 if file_path.is_dir() {
1090 let indexed = self
1091 .index
1092 .iter()
1093 .map(|name| file_path.join(name))
1094 .find(|p| p.is_file());
1095
1096 file_path = match indexed {
1097 Some(p) => p,
1098 None => return Ok(not_found(response)),
1099 };
1100 }
1101
1102 if !file_path.is_file() {
1103 return Ok(not_found(response));
1104 }
1105
1106 if let (Ok(root_canon), Ok(file_canon)) =
1109 (self.root.canonicalize(), file_path.canonicalize())
1110 {
1111 if !file_canon.starts_with(&root_canon) {
1112 return Ok(not_found(response));
1113 }
1114 }
1115
1116 let path_str = match file_path.to_str() {
1117 Some(s) => s,
1118 None => return Ok(not_found(response)),
1119 };
1120
1121 match crate::range::Range::get_content_range_of_a_file(path_str) {
1122 Ok(content_range) => {
1123 response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
1124 response.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
1125 response.content_range_list = vec![content_range];
1126 Ok(response)
1127 }
1128 Err(_) => Ok(not_found(response)),
1129 }
1130 }
1131}
1132
1133pub(crate) struct PerRouteRateLimit(pub(crate) Arc<crate::rate_limit::RateLimiter>);
1137
1138impl crate::middleware::Middleware for PerRouteRateLimit {
1139 fn handle(
1140 &self,
1141 request: &Request,
1142 conn: &ConnectionInfo,
1143 next: &dyn Application,
1144 ) -> Result<Response, String> {
1145 use crate::error::{AppError, IntoResponse};
1146 if self.0.check(&conn.client.ip) {
1147 next.execute(request, conn)
1148 } else {
1149 Ok(AppError::TooManyRequests.into_response())
1150 }
1151 }
1152}
1153
1154pub(crate) struct BearerAuthMiddleware {
1158 pub(crate) token: Arc<String>,
1159}
1160
1161impl crate::middleware::Middleware for BearerAuthMiddleware {
1162 fn handle(
1163 &self,
1164 request: &Request,
1165 conn: &ConnectionInfo,
1166 next: &dyn Application,
1167 ) -> Result<Response, String> {
1168 use crate::error::{AppError, IntoResponse};
1169 let expected = format!("Bearer {}", self.token);
1170 let authorized = request
1171 .headers
1172 .iter()
1173 .any(|h| h.name.eq_ignore_ascii_case("authorization") && h.value == expected);
1174 if authorized {
1175 next.execute(request, conn)
1176 } else {
1177 Ok(AppError::Unauthorized.into_response())
1178 }
1179 }
1180}
1181
1182pub(crate) fn arc_app<A: Application + Send + Sync + 'static>(
1186 a: A,
1187) -> Arc<dyn Application + Send + Sync> {
1188 Arc::new(a)
1189}
1190
1191pub fn build_from_file() -> (ConfigDrivenApp, Vec<std::thread::JoinHandle<()>>) {
1196 builder::build_from_file()
1197}