1mod health;
22#[cfg(feature = "swagger-ui")]
23pub mod swagger;
24#[cfg(test)]
25#[path = "../../tests/unit/server/tests.rs"]
26mod tests;
27
28use crate::core::{HttpMethod, ProxyCore, ProxyError, ProxyRequest, ProxyResponse, RequestContext};
29use crate::logging::config::LoggingConfig;
30use crate::logging::middleware::LoggingMiddleware;
31#[cfg(feature = "swagger-ui")]
32use crate::server::swagger::SwaggerUIConfig;
33use crate::{debug_fmt, error_fmt, info_fmt, trace_fmt, warn_fmt};
34use bytes::Bytes;
35use futures_util::TryStreamExt;
36use health::HealthServer;
37use http_body_util::BodyExt;
38use hyper::body::Incoming;
39use hyper::service::service_fn;
40use hyper::{Request, Response};
41use hyper_util::rt::TokioExecutor;
42use hyper_util::rt::TokioIo;
43use hyper_util::server::conn::auto::Builder as AutoBuilder;
44use reqwest::Body;
45use reqwest::header::{HeaderMap, HeaderValue};
46use serde::{Deserialize, Serialize};
47#[cfg(feature = "opentelemetry")]
48use std::borrow::Cow;
49use std::collections::HashMap;
50use std::convert::Infallible;
51use std::net::SocketAddr;
52use std::sync::Arc;
53use std::time::Instant;
54use tokio::signal;
55use tokio::sync::RwLock;
56use tokio::sync::oneshot;
57use tokio::task::{Id, JoinSet};
58
59#[cfg(unix)]
60use tokio::signal::unix::{SignalKind, signal};
61
62#[cfg(feature = "opentelemetry")]
63use opentelemetry::{
64 Context, KeyValue, global,
65 trace::{Span, SpanBuilder, SpanKind, Status},
66 trace::{TraceContextExt, Tracer},
67};
68#[cfg(feature = "opentelemetry")]
69use opentelemetry_http::HeaderExtractor;
70#[cfg(feature = "opentelemetry")]
71use opentelemetry_semantic_conventions::attribute::{
72 HTTP_FLAVOR, HTTP_HOST, HTTP_METHOD, HTTP_REQUEST_CONTENT_LENGTH, HTTP_RESPONSE_STATUS_CODE,
73 HTTP_SCHEME, HTTP_URL, HTTP_USER_AGENT, NET_PEER_IP,
74};
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct ServerConfig {
79 #[serde(default = "default_host")]
81 pub(crate) host: String,
82
83 #[serde(default = "default_port")]
85 pub(crate) port: u16,
86
87 #[serde(default = "default_health_port")]
89 pub(crate) health_port: u16,
90}
91
92pub(crate) fn default_host() -> String {
93 "127.0.0.1".to_string()
94}
95
96pub(crate) fn default_port() -> u16 {
97 8080
98}
99
100pub(crate) fn default_health_port() -> u16 {
101 8081
102}
103
104impl Default for ServerConfig {
105 fn default() -> Self {
106 Self {
107 host: default_host(),
108 port: default_port(),
109 health_port: default_health_port(),
110 }
111 }
112}
113
114#[derive(Debug, Clone)]
116pub struct ProxyServer {
117 pub(crate) config: ServerConfig,
119 core: Arc<ProxyCore>,
121 shutdown_senders: Arc<RwLock<HashMap<Id, oneshot::Sender<()>>>>,
123 logging_middleware: LoggingMiddleware,
125}
126
127impl ProxyServer {
128 pub fn new(config: ServerConfig, core: Arc<ProxyCore>) -> Self {
130 let logging_config = match core.config.get::<LoggingConfig>("proxy.logging") {
132 Ok(Some(config)) => config,
133 _ => LoggingConfig::default(),
134 };
135
136 let logging_middleware = LoggingMiddleware::new(logging_config);
138
139 Self {
140 config,
141 core,
142 shutdown_senders: Arc::new(RwLock::new(HashMap::new())),
143 logging_middleware,
144 }
145 }
146
147 pub fn core(&self) -> &Arc<ProxyCore> {
149 &self.core
150 }
151
152 #[cfg(unix)]
154 fn setup_signal_handlers() -> Result<
155 (
156 impl std::future::Future<Output = Result<(), std::io::Error>>,
157 tokio::signal::unix::Signal,
158 ),
159 ProxyError,
160 > {
161 let ctrl_c = signal::ctrl_c();
162 let term_stream = signal(SignalKind::terminate())
163 .map_err(|e| ProxyError::Other(format!("Cannot install SIGTERM handler: {e}")))?;
164 Ok((ctrl_c, term_stream))
165 }
166
167 #[cfg(not(unix))]
169 fn setup_signal_handlers()
170 -> Result<impl std::future::Future<Output = Result<(), std::io::Error>>, ProxyError> {
171 Ok(signal::ctrl_c())
172 }
173
174 async fn setup_listener(&self) -> Result<tokio::net::TcpListener, ProxyError> {
176 let addr = format!("{}:{}", self.config.host, self.config.port)
177 .parse::<SocketAddr>()
178 .map_err(|e| ProxyError::Other(format!("Invalid server address: {e}")))?;
179
180 let listener = tokio::net::TcpListener::bind(addr)
181 .await
182 .map_err(|e| ProxyError::Other(format!("Failed to bind: {e}")))?;
183
184 info_fmt!("Server", "Foxy proxy listening on http://{}", addr);
185 Ok(listener)
186 }
187
188 async fn handle_new_connection(
190 &self,
191 stream: tokio::net::TcpStream,
192 remote_addr: SocketAddr,
193 core: Arc<ProxyCore>,
194 shutdown_senders: Arc<RwLock<HashMap<Id, oneshot::Sender<()>>>>,
195 join_set: &mut JoinSet<()>,
196 ) {
197 let client_ip = remote_addr.ip().to_string();
198 let logging_middleware = self.logging_middleware.clone();
199 let (tx, rx) = oneshot::channel();
200 let shutdown_senders_clone = shutdown_senders.clone();
201
202 let handle = join_set.spawn(async move {
203 let task_id = tokio::task::id();
204
205 let service = service_fn(move |req: Request<Incoming>| {
206 debug_fmt!("Server", "Incoming over {:?}", &req.version());
207 handle_request(req, core.clone(), client_ip.clone(), logging_middleware.clone())
208 });
209 let io = TokioIo::new(stream);
210
211 let builder = AutoBuilder::new(TokioExecutor::new());
212
213 let connection = builder.serve_connection(io, service);
215
216 let mut conn = std::pin::pin!(connection);
218
219 tokio::select! {
221 res = &mut conn => {
222 Self::handle_connection_result(res);
223 }
224 _ = rx => {
225 debug_fmt!("Server", "Connection received shutdown signal, waiting for graceful close");
226 conn.as_mut().graceful_shutdown();
227
228 Self::handle_connection_result(conn.await);
230 }
231 }
232
233 shutdown_senders_clone.write().await.remove(&task_id);
235 debug_fmt!("Server", "Connection task {:?} completed", task_id);
236 });
237
238 shutdown_senders.write().await.insert(handle.id(), tx);
240 }
241
242 fn handle_connection_result(res: Result<(), Box<dyn std::error::Error + Send + Sync>>) {
244 match res {
245 Ok(()) => debug_fmt!("Server", "Connection closed normally"),
246 Err(e) => {
247 let err_str = e.to_string();
249 if !err_str.contains("connection closed") && !err_str.contains("connection reset") {
250 error_fmt!("Server", "Connection error: {}", e);
251 }
252 }
253 }
254 }
255
256 async fn graceful_shutdown(
258 &self,
259 mut join_set: JoinSet<()>,
260 shutdown_senders: Arc<RwLock<HashMap<Id, oneshot::Sender<()>>>>,
261 ) -> Result<(), ProxyError> {
262 info_fmt!(
264 "Server",
265 "Shutting down; waiting for {} connection(s)",
266 join_set.len()
267 );
268
269 {
271 let mut senders = shutdown_senders.write().await;
272 info_fmt!(
273 "Server",
274 "Signaling {} connections to shut down",
275 senders.len()
276 );
277 for (task_id, sender) in senders.drain() {
278 debug_fmt!("Server", "Sending shutdown signal to task {:?}", task_id);
279 let _ = sender.send(());
280 }
281 }
282
283 let shutdown_timeout = tokio::time::Duration::from_secs(30);
285 let start_time = tokio::time::Instant::now();
286
287 let shutdown_future = async {
288 let mut completed = 0;
289 let total = join_set.len();
290
291 while let Some(res) = join_set.join_next().await {
292 completed += 1;
293 match res {
294 Ok(_) => debug_fmt!(
295 "Server",
296 "Connection task completed ({}/{})",
297 completed,
298 total
299 ),
300 Err(e) if e.is_cancelled() => debug_fmt!(
301 "Server",
302 "Connection task cancelled ({}/{})",
303 completed,
304 total
305 ),
306 Err(e) => error_fmt!(
307 "Server",
308 "Connection task failed ({}/{}): {}",
309 completed,
310 total,
311 e
312 ),
313 }
314
315 let elapsed = start_time.elapsed();
316 if completed % 10 == 0 || total - completed < 10 {
317 info_fmt!(
318 "Server",
319 "Shutdown progress: {}/{} connections closed (elapsed: {:.1}s)",
320 completed,
321 total,
322 elapsed.as_secs_f32()
323 );
324 }
325 }
326 };
327
328 match tokio::time::timeout(shutdown_timeout, shutdown_future).await {
329 Ok(_) => {
330 let elapsed = start_time.elapsed();
331 info_fmt!(
332 "Server",
333 "All connections drained gracefully in {:.1}s",
334 elapsed.as_secs_f32()
335 );
336 }
337 Err(_) => {
338 warn_fmt!(
339 "Server",
340 "Shutdown timed out after {} seconds, some connections may be forcefully closed",
341 shutdown_timeout.as_secs()
342 );
343 join_set.shutdown().await;
345 }
346 }
347
348 info_fmt!("Server", "Shutdown complete");
349 Ok(())
350 }
351
352 pub async fn start(&self) -> Result<(), ProxyError> {
354 let listener = self.setup_listener().await?;
356
357 let health_server = HealthServer::new(self.config.health_port);
359 health_server.set_ready();
360
361 #[cfg(unix)]
363 let (ctrl_c, mut term_stream) = Self::setup_signal_handlers()?;
364 #[cfg(not(unix))]
365 let ctrl_c = Self::setup_signal_handlers()?;
366
367 #[cfg(unix)]
369 let sigterm = term_stream.recv();
370 #[cfg(not(unix))]
371 let sigterm = std::future::pending();
372
373 tokio::pin!(ctrl_c);
375 tokio::pin!(sigterm);
376
377 let shutdown_senders = self.shutdown_senders.clone();
379
380 let mut join_set = JoinSet::new();
382 let core = self.core.clone();
383
384 let shutdown_initiated = Arc::new(std::sync::atomic::AtomicBool::new(false));
386 let shutdown_initiated_clone = shutdown_initiated.clone();
387
388 loop {
390 tokio::select! {
391 _ = &mut ctrl_c => {
392 info_fmt!("Server", "Received Ctrl-C; initiating graceful shutdown");
393 shutdown_initiated_clone.store(true, std::sync::atomic::Ordering::SeqCst);
394 break;
395 }
396 _ = &mut sigterm => {
397 info_fmt!("Server", "Received SIGTERM; initiating graceful shutdown");
398 shutdown_initiated_clone.store(true, std::sync::atomic::Ordering::SeqCst);
399 break;
400 }
401 accept = listener.accept() => {
402 match accept {
403 Ok((stream, remote_addr)) => {
404 if shutdown_initiated.load(std::sync::atomic::Ordering::SeqCst) {
406 info_fmt!("Server", "Rejecting new connection during shutdown");
407 continue;
408 }
409
410 self.handle_new_connection(
411 stream,
412 remote_addr,
413 core.clone(),
414 shutdown_senders.clone(),
415 &mut join_set,
416 ).await;
417 }
418 Err(e) => error_fmt!("Server", "Accept error: {}", e),
419 }
420 }
421 }
422 }
423
424 self.graceful_shutdown(join_set, shutdown_senders).await?;
426
427 drop(health_server);
429
430 Ok(())
431 }
432}
433
434pub fn validate_headers(headers: &mut HeaderMap) -> Result<(), ProxyError> {
439 let has_content_length = headers.contains_key("content-length");
441 let has_transfer_encoding = headers.contains_key("transfer-encoding");
442
443 if has_content_length && has_transfer_encoding {
444 warn_fmt!(
448 "Server",
449 "Request contains both Content-Length and Transfer-Encoding headers - removing Content-Length to prevent request smuggling"
450 );
451 headers.remove("content-length");
452 }
453
454 if let Some(te_header) = headers.get("transfer-encoding") {
456 if let Ok(te_value) = te_header.to_str() {
457 let te_lower = te_value.to_lowercase();
459 if te_lower.contains("chunked") && te_lower != "chunked" {
460 warn_fmt!(
462 "Server",
463 "Suspicious Transfer-Encoding header value: {} - normalizing to 'chunked'",
464 te_value
465 );
466 headers.insert("transfer-encoding", HeaderValue::from_static("chunked"));
467 }
468 }
469 }
470
471 for (name, value) in headers.iter() {
473 if let Ok(value_str) = value.to_str() {
474 if value_str.contains('\r') || value_str.contains('\n') {
475 let err = ProxyError::SecurityError(format!(
476 "CRLF injection detected in header '{name}': contains newline characters"
477 ));
478 warn_fmt!("Server", "{}", err);
479 return Err(err);
480 }
481 }
482 }
483
484 let host_headers: Vec<_> = headers.get_all("host").iter().collect();
486 if host_headers.len() > 1 {
487 let err = ProxyError::SecurityError(
488 "Multiple Host headers detected - potential request smuggling attack".to_string(),
489 );
490 warn_fmt!("Server", "{}", err);
491 return Err(err);
492 }
493
494 if let Some(cl_header) = headers.get("content-length") {
496 if let Ok(cl_value) = cl_header.to_str() {
497 if cl_value.contains(',') {
499 let err = ProxyError::SecurityError(
500 "Multiple Content-Length values detected - potential request smuggling attack"
501 .to_string(),
502 );
503 warn_fmt!("Server", "{}", err);
504 return Err(err);
505 }
506
507 if cl_value.parse::<u64>().is_err() {
509 let err = ProxyError::SecurityError(format!(
510 "Invalid Content-Length header value: {cl_value}"
511 ));
512 warn_fmt!("Server", "{}", err);
513 return Err(err);
514 }
515 }
516 }
517
518 Ok(())
519}
520
521async fn convert_hyper_request(
523 req: Request<Incoming>,
524 client_ip: String,
525) -> Result<ProxyRequest, ProxyError> {
526 let method = HttpMethod::from(req.method());
527 let uri = req.uri().clone();
528 let path = uri.path().to_owned();
529 let query = uri.query().map(|q| q.to_owned());
530 let mut headers = req.headers().clone();
531
532 trace_fmt!(
533 "Server",
534 "Converting request: {} {} with {} headers",
535 method,
536 path,
537 headers.len()
538 );
539
540 validate_headers(&mut headers)?;
542
543 let hyper_stream = req.into_body().into_data_stream();
545 let byte_stream = hyper_stream.map_ok(Bytes::from);
546 let body = reqwest::Body::wrap_stream(byte_stream);
547
548 Ok(ProxyRequest {
549 method,
550 path,
551 query,
552 headers,
553 body,
554 context: Arc::new(RwLock::new(RequestContext {
555 client_ip: Some(client_ip),
556 start_time: Some(std::time::Instant::now()),
557 attributes: std::collections::HashMap::new(),
558 })),
559 custom_target: None,
560 })
561}
562
563fn convert_proxy_response(resp: ProxyResponse) -> Result<Response<Body>, ProxyError> {
565 trace_fmt!(
566 "Server",
567 "Converting response with status {} and {} headers",
568 resp.status,
569 resp.headers.len()
570 );
571
572 let stream = resp.body.into_data_stream().map_err(|e| {
573 error_fmt!("Server", "Error streaming response body: {}", e);
574 std::io::Error::other(e)
575 });
576
577 let body = Body::wrap_stream(stream);
578
579 let mut builder = Response::builder().status(resp.status);
580 let mut_headers = builder.headers_mut().ok_or_else(|| {
581 error_fmt!(
582 "Server",
583 "Failed to get mutable headers from response builder"
584 );
585 ProxyError::Other("Failed to build response: unable to get mutable headers".into())
586 })?;
587 *mut_headers = resp.headers;
588
589 builder.body(body).map_err(|e| {
590 let err = ProxyError::Other(e.to_string());
591 error_fmt!("Server", "Failed to build response: {}", err);
592 err
593 })
594}
595
596async fn handle_request(
598 req: Request<Incoming>,
599 core: Arc<ProxyCore>,
600 client_ip: String,
601 logging_middleware: LoggingMiddleware,
602) -> Result<Response<Body>, Infallible> {
603 let remote_addr = req.extensions().get::<SocketAddr>().cloned();
605 let (req, request_info) = logging_middleware.process(req, remote_addr);
606
607 #[cfg(feature = "swagger-ui")]
609 {
610 if let Ok(Some(swagger_config)) = core.config.get::<SwaggerUIConfig>("proxy.swagger_ui") {
611 if swagger_config.enabled
612 && (req.uri().path().eq(&swagger_config.path)
613 || req.uri().path().starts_with(&swagger_config.path))
614 {
615 let swagger_response = swagger::handle_swagger_request(&req, &swagger_config)
616 .await
617 .unwrap();
618 return Ok(swagger_response);
619 }
620 }
621 }
622 let upstream_start = Instant::now();
626 #[cfg(feature = "opentelemetry")]
628 let span_context = {
629 let method = req.method().as_str().to_owned();
630 let path = req.uri().path().to_owned();
631 let full_url = req.uri().clone().to_string();
632 let scheme = req.uri().scheme_str().unwrap_or("http").to_owned();
633 let host = req
634 .headers()
635 .get("host")
636 .and_then(|v| v.to_str().ok())
637 .unwrap_or("-")
638 .to_owned();
639 let http_version = match req.version() {
640 hyper::Version::HTTP_10 => "1.0",
641 hyper::Version::HTTP_11 => "1.1",
642 hyper::Version::HTTP_2 => "2",
643 hyper::Version::HTTP_3 => "3",
644 _ => "unknown",
645 };
646 let req_content_len = req
647 .headers()
648 .get("content-length")
649 .and_then(|v| v.to_str().ok())
650 .and_then(|s| s.parse::<i64>().ok())
651 .unwrap_or(0);
652 let user_agent = req
653 .headers()
654 .get("user-agent")
655 .and_then(|v| v.to_str().ok())
656 .unwrap_or("-")
657 .to_owned();
658 let peer_ip = client_ip.as_str().to_owned();
659
660 let context = extract_context_from_request(&req);
661 let mut span = global::tracer("foxy::proxy").build_with_context(
662 SpanBuilder {
663 name: Cow::from(format!("{method} {path}")),
664 span_kind: Some(SpanKind::Server),
665 ..Default::default()
666 },
667 &context,
668 );
669
670 span.set_attributes([
671 KeyValue::new(HTTP_METHOD, method),
672 KeyValue::new(HTTP_URL, full_url.clone()),
673 KeyValue::new(HTTP_SCHEME, scheme),
674 KeyValue::new(HTTP_HOST, host),
675 KeyValue::new(HTTP_FLAVOR, http_version),
676 KeyValue::new(HTTP_REQUEST_CONTENT_LENGTH, req_content_len),
677 KeyValue::new(HTTP_USER_AGENT, user_agent),
678 KeyValue::new(NET_PEER_IP, peer_ip),
679 ]);
680
681 context.with_span(span)
682 };
683
684 let method = req.method().clone();
686 let path = req.uri().path().to_owned();
687
688 debug_fmt!("Server", "Received request: {} {}", method, path);
689
690 let proxy_req = match convert_hyper_request(req, client_ip.clone()).await {
691 Ok(r) => r,
692 Err(e) => {
693 error_fmt!(
694 "Server",
695 "Failed to convert request {} {}: {}",
696 method,
697 path,
698 e
699 );
700 return Ok(Response::builder()
701 .status(500)
702 .body(Body::from("Internal Server Error"))
703 .unwrap());
704 }
705 };
706
707 #[cfg(feature = "opentelemetry")]
709 let span_clone = span_context.clone();
710
711 #[cfg(feature = "opentelemetry")]
712 let span_ref = span_context.span();
713
714 #[cfg(feature = "opentelemetry")]
715 let result = core.process_request(proxy_req, Some(span_clone)).await;
716
717 #[cfg(not(feature = "opentelemetry"))]
718 let result = core.process_request(proxy_req).await;
719
720 #[cfg(feature = "opentelemetry")]
722 {
723 match result.as_ref() {
724 Ok(_r) => span_ref.set_status(Status::Ok),
725 Err(e) => {
726 span_ref.record_error(e);
727 span_ref.set_status(Status::Error {
728 description: Cow::from(e.to_string()),
729 })
730 }
731 }
732 }
733
734 let response: Result<Response<Body>, Infallible> = match result {
736 Ok(proxy_resp) => {
737 debug_fmt!(
738 "Server",
739 "Successfully processed request {} {} -> {}",
740 method,
741 path,
742 proxy_resp.status
743 );
744 match convert_proxy_response(proxy_resp) {
745 Ok(resp) => {
746 let upstream_duration = upstream_start.elapsed();
748
749 logging_middleware.log_response(&resp, &request_info, Some(upstream_duration));
751
752 Ok(resp)
753 }
754 Err(e) => {
755 error_fmt!(
756 "Server",
757 "Failed to convert response for {} {}: {}",
758 method,
759 path,
760 e
761 );
762 Ok(Response::builder()
763 .status(500)
764 .body(Body::from("Internal Server Error"))
765 .unwrap())
766 }
767 }
768 }
769 Err(e) => {
770 let (status, msg) = match &e {
771 ProxyError::Timeout(d) => {
772 warn_fmt!(
773 "Server",
774 "Request {} {} timed out after {:?}",
775 method,
776 path,
777 d
778 );
779 (504, format!("Gateway Timeout after {d:?}"))
780 }
781 ProxyError::RoutingError(msg) => {
782 warn_fmt!("Server", "Routing error for {} {}: {}", method, path, msg);
783 (404, "Route not found".into())
784 }
785 ProxyError::SecurityError(msg) => {
786 warn_fmt!("Server", "Security error for {} {}: {}", method, path, msg);
787 (403, "Forbidden".into())
788 }
789 ProxyError::ClientError(err) => {
790 error_fmt!("Server", "Client error for {} {}: {}", method, path, err);
791 (502, "Bad Gateway".into())
792 }
793 _ => {
794 error_fmt!(
795 "Server",
796 "Internal error processing {} {}: {}",
797 method,
798 path,
799 e
800 );
801 (500, "Internal Server Error".into())
802 }
803 };
804
805 Ok(Response::builder()
806 .status(status)
807 .body(Body::from(msg))
808 .unwrap())
809 }
810 };
811
812 #[allow(clippy::collapsible_if)]
814 if let Ok(resp) = &response {
815 if resp.status().is_client_error() || resp.status().is_server_error() {
816 logging_middleware.log_response(resp, &request_info, None);
817 }
818 }
819
820 #[cfg(feature = "opentelemetry")]
821 {
822 let status_code = response.as_ref().unwrap().status().as_u16();
823
824 span_ref.set_attribute(KeyValue::new(HTTP_RESPONSE_STATUS_CODE, status_code as i64));
825 span_ref.end();
826 }
827
828 response
829}
830
831#[cfg(feature = "opentelemetry")]
833fn extract_context_from_request(req: &Request<Incoming>) -> Context {
834 global::get_text_map_propagator(|propagator| {
835 propagator.extract(&HeaderExtractor(req.headers()))
836 })
837}