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 serde::{Deserialize, Serialize};
46#[cfg(feature = "opentelemetry")]
47use std::borrow::Cow;
48use std::collections::HashMap;
49use std::convert::Infallible;
50use std::net::SocketAddr;
51use std::sync::Arc;
52use std::time::Instant;
53use tokio::signal;
54use tokio::sync::RwLock;
55use tokio::sync::oneshot;
56use tokio::task::{Id, JoinSet};
57
58#[cfg(unix)]
59use tokio::signal::unix::{SignalKind, signal};
60
61#[cfg(feature = "opentelemetry")]
62use opentelemetry::{
63 Context, KeyValue, global,
64 trace::{Span, SpanBuilder, SpanKind, Status},
65 trace::{TraceContextExt, Tracer},
66};
67#[cfg(feature = "opentelemetry")]
68use opentelemetry_http::HeaderExtractor;
69#[cfg(feature = "opentelemetry")]
70use opentelemetry_semantic_conventions::attribute::{
71 HTTP_FLAVOR, HTTP_HOST, HTTP_METHOD, HTTP_REQUEST_CONTENT_LENGTH, HTTP_RESPONSE_STATUS_CODE,
72 HTTP_SCHEME, HTTP_URL, HTTP_USER_AGENT, NET_PEER_IP,
73};
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct ServerConfig {
78 #[serde(default = "default_host")]
80 pub(crate) host: String,
81
82 #[serde(default = "default_port")]
84 pub(crate) port: u16,
85
86 #[serde(default = "default_health_port")]
88 pub(crate) health_port: u16,
89}
90
91pub(crate) fn default_host() -> String {
92 "127.0.0.1".to_string()
93}
94
95pub(crate) fn default_port() -> u16 {
96 8080
97}
98
99pub(crate) fn default_health_port() -> u16 {
100 8081
101}
102
103impl Default for ServerConfig {
104 fn default() -> Self {
105 Self {
106 host: default_host(),
107 port: default_port(),
108 health_port: default_health_port(),
109 }
110 }
111}
112
113#[derive(Debug, Clone)]
115pub struct ProxyServer {
116 pub(crate) config: ServerConfig,
118 core: Arc<ProxyCore>,
120 shutdown_senders: Arc<RwLock<HashMap<Id, oneshot::Sender<()>>>>,
122 logging_middleware: LoggingMiddleware,
124}
125
126impl ProxyServer {
127 pub fn new(config: ServerConfig, core: Arc<ProxyCore>) -> Self {
129 let logging_config = match core.config.get::<LoggingConfig>("proxy.logging") {
131 Ok(Some(config)) => config,
132 _ => LoggingConfig::default(),
133 };
134
135 let logging_middleware = LoggingMiddleware::new(logging_config);
137
138 Self {
139 config,
140 core,
141 shutdown_senders: Arc::new(RwLock::new(HashMap::new())),
142 logging_middleware,
143 }
144 }
145
146 pub fn core(&self) -> &Arc<ProxyCore> {
148 &self.core
149 }
150
151 pub async fn start(&self) -> Result<(), ProxyError> {
153 let addr = format!("{}:{}", self.config.host, self.config.port)
154 .parse::<SocketAddr>()
155 .map_err(|e| ProxyError::Other(format!("Invalid server address: {e}")))?;
156
157 let listener = tokio::net::TcpListener::bind(addr)
158 .await
159 .map_err(|e| ProxyError::Other(format!("Failed to bind: {e}")))?;
160
161 info_fmt!("Server", "Foxy proxy listening on http://{}", addr);
162
163 let health_server = HealthServer::new(self.config.health_port);
164 health_server.set_ready();
165
166 let ctrl_c = signal::ctrl_c();
168
169 #[cfg(unix)]
171 let mut term_stream = signal(SignalKind::terminate())
172 .map_err(|e| ProxyError::Other(format!("Cannot install SIGTERM handler: {e}")))?;
173
174 #[cfg(unix)]
176 let sigterm = term_stream.recv();
177 #[cfg(not(unix))]
178 let sigterm = std::future::pending();
179
180 tokio::pin!(ctrl_c);
182 tokio::pin!(sigterm);
183
184 let shutdown_senders = self.shutdown_senders.clone();
186
187 let mut join_set = JoinSet::new();
189 let core = self.core.clone();
190
191 let shutdown_initiated = Arc::new(std::sync::atomic::AtomicBool::new(false));
193 let shutdown_initiated_clone = shutdown_initiated.clone();
194
195 loop {
196 tokio::select! {
197 _ = &mut ctrl_c => {
198 info_fmt!("Server", "Received Ctrl-C; initiating graceful shutdown");
199 shutdown_initiated_clone.store(true, std::sync::atomic::Ordering::SeqCst);
200 break;
201 }
202 _ = &mut sigterm => {
203 info_fmt!("Server", "Received SIGTERM; initiating graceful shutdown");
204 shutdown_initiated_clone.store(true, std::sync::atomic::Ordering::SeqCst);
205 break;
206 }
207 accept = listener.accept() => {
208 match accept {
209 Ok((stream, remote_addr)) => {
210 if shutdown_initiated.load(std::sync::atomic::Ordering::SeqCst) {
212 info_fmt!("Server", "Rejecting new connection during shutdown");
213 continue;
214 }
215
216 let core = core.clone();
217 let client_ip = remote_addr.ip().to_string();
218 let logging_middleware = self.logging_middleware.clone();
219 let (tx, rx) = oneshot::channel();
220 let shutdown_senders_clone = shutdown_senders.clone();
221
222 let handle = join_set.spawn(async move {
223 let task_id = tokio::task::id();
224
225 let service = service_fn(move |req: Request<Incoming>| {
226 debug_fmt!("Server", "Incoming over {:?}", &req.version());
227 handle_request(req, core.clone(), client_ip.clone(), logging_middleware.clone())
228 });
229 let io = TokioIo::new(stream);
230
231 let builder = AutoBuilder::new(TokioExecutor::new());
232
233 let connection = builder.serve_connection(io, service);
235
236 let mut conn = std::pin::pin!(connection);
238
239 tokio::select! {
241 res = &mut conn => {
242 match res {
243 Ok(()) => debug_fmt!("Server", "Connection closed normally"),
244 Err(e) => {
245 let err_str = e.to_string();
247 if !err_str.contains("connection closed") &&
248 !err_str.contains("connection reset") {
249 error_fmt!("Server", "Connection error: {}", e);
250 }
251 }
252 }
253 }
254 _ = rx => {
255 debug_fmt!("Server", "Connection received shutdown signal, waiting for graceful close");
256 conn.as_mut().graceful_shutdown();
257
258 match conn.await {
260 Ok(()) => debug_fmt!("Server", "Connection closed gracefully after shutdown signal"),
261 Err(e) => {
262 let err_str = e.to_string();
263 if !err_str.contains("connection closed") &&
264 !err_str.contains("connection reset") {
265 error_fmt!("Server", "Connection error during graceful shutdown: {}", e);
266 }
267 }
268 }
269 }
270 }
271
272 shutdown_senders_clone.write().await.remove(&task_id);
274 debug_fmt!("Server", "Connection task {:?} completed", task_id);
275 });
276
277 shutdown_senders.write().await.insert(handle.id(), tx);
279 }
280 Err(e) => error_fmt!("Server", "Accept error: {}", e),
281 }
282 }
283 }
284 }
285
286 info_fmt!(
288 "Server",
289 "Shutting down; waiting for {} connection(s)",
290 join_set.len()
291 );
292
293 {
295 let mut senders = shutdown_senders.write().await;
296 info_fmt!(
297 "Server",
298 "Signaling {} connections to shut down",
299 senders.len()
300 );
301 for (task_id, sender) in senders.drain() {
302 debug_fmt!("Server", "Sending shutdown signal to task {:?}", task_id);
303 let _ = sender.send(());
304 }
305 }
306
307 let shutdown_timeout = tokio::time::Duration::from_secs(30);
309 let start_time = tokio::time::Instant::now();
310
311 let shutdown_future = async {
312 let mut completed = 0;
313 let total = join_set.len();
314
315 while let Some(res) = join_set.join_next().await {
316 completed += 1;
317 match res {
318 Ok(_) => debug_fmt!(
319 "Server",
320 "Connection task completed ({}/{})",
321 completed,
322 total
323 ),
324 Err(e) if e.is_cancelled() => debug_fmt!(
325 "Server",
326 "Connection task cancelled ({}/{})",
327 completed,
328 total
329 ),
330 Err(e) => error_fmt!(
331 "Server",
332 "Connection task failed ({}/{}): {}",
333 completed,
334 total,
335 e
336 ),
337 }
338
339 let elapsed = start_time.elapsed();
340 if completed % 10 == 0 || total - completed < 10 {
341 info_fmt!(
342 "Server",
343 "Shutdown progress: {}/{} connections closed (elapsed: {:.1}s)",
344 completed,
345 total,
346 elapsed.as_secs_f32()
347 );
348 }
349 }
350 };
351
352 match tokio::time::timeout(shutdown_timeout, shutdown_future).await {
353 Ok(_) => {
354 let elapsed = start_time.elapsed();
355 info_fmt!(
356 "Server",
357 "All connections drained gracefully in {:.1}s",
358 elapsed.as_secs_f32()
359 );
360 }
361 Err(_) => {
362 warn_fmt!(
363 "Server",
364 "Shutdown timed out after {} seconds, some connections may be forcefully closed",
365 shutdown_timeout.as_secs()
366 );
367 join_set.shutdown().await;
369 }
370 }
371
372 drop(health_server);
374
375 info_fmt!("Server", "Shutdown complete");
376 Ok(())
377 }
378}
379
380async fn convert_hyper_request(
382 req: Request<Incoming>,
383 client_ip: String,
384) -> Result<ProxyRequest, ProxyError> {
385 let method = HttpMethod::from(req.method());
386 let uri = req.uri().clone();
387 let path = uri.path().to_owned();
388 let query = uri.query().map(|q| q.to_owned());
389 let headers = req.headers().clone();
390
391 trace_fmt!(
392 "Server",
393 "Converting request: {} {} with {} headers",
394 method,
395 path,
396 headers.len()
397 );
398
399 let hyper_stream = req.into_body().into_data_stream();
401 let byte_stream = hyper_stream.map_ok(Bytes::from);
402 let body = reqwest::Body::wrap_stream(byte_stream);
403
404 Ok(ProxyRequest {
405 method,
406 path,
407 query,
408 headers,
409 body,
410 context: Arc::new(RwLock::new(RequestContext {
411 client_ip: Some(client_ip),
412 start_time: Some(std::time::Instant::now()),
413 attributes: std::collections::HashMap::new(),
414 })),
415 custom_target: None,
416 })
417}
418
419fn convert_proxy_response(resp: ProxyResponse) -> Result<Response<Body>, ProxyError> {
421 trace_fmt!(
422 "Server",
423 "Converting response with status {} and {} headers",
424 resp.status,
425 resp.headers.len()
426 );
427
428 let stream = resp.body.into_data_stream().map_err(|e| {
429 error_fmt!("Server", "Error streaming response body: {}", e);
430 std::io::Error::other(e)
431 });
432
433 let body = Body::wrap_stream(stream);
434
435 let mut builder = Response::builder().status(resp.status);
436 let mut_headers = builder.headers_mut().ok_or_else(|| {
437 error_fmt!(
438 "Server",
439 "Failed to get mutable headers from response builder"
440 );
441 ProxyError::Other("Failed to build response: unable to get mutable headers".into())
442 })?;
443 *mut_headers = resp.headers;
444
445 builder.body(body).map_err(|e| {
446 let err = ProxyError::Other(e.to_string());
447 error_fmt!("Server", "Failed to build response: {}", err);
448 err
449 })
450}
451
452async fn handle_request(
454 req: Request<Incoming>,
455 core: Arc<ProxyCore>,
456 client_ip: String,
457 logging_middleware: LoggingMiddleware,
458) -> Result<Response<Body>, Infallible> {
459 let remote_addr = req.extensions().get::<SocketAddr>().cloned();
461 let (req, request_info) = logging_middleware.process(req, remote_addr);
462
463 #[cfg(feature = "swagger-ui")]
465 {
466 if let Ok(Some(swagger_config)) = core.config.get::<SwaggerUIConfig>("proxy.swagger_ui") {
467 if swagger_config.enabled
468 && (req.uri().path().eq(&swagger_config.path)
469 || req.uri().path().starts_with(&swagger_config.path))
470 {
471 let swagger_response = swagger::handle_swagger_request(&req, &swagger_config)
472 .await
473 .unwrap();
474 return Ok(swagger_response);
475 }
476 }
477 }
478 let upstream_start = Instant::now();
482 #[cfg(feature = "opentelemetry")]
484 let span_context = {
485 let method = req.method().as_str().to_owned();
486 let path = req.uri().path().to_owned();
487 let full_url = req.uri().clone().to_string();
488 let scheme = req.uri().scheme_str().unwrap_or("http").to_owned();
489 let host = req
490 .headers()
491 .get("host")
492 .and_then(|v| v.to_str().ok())
493 .unwrap_or("-")
494 .to_owned();
495 let http_version = match req.version() {
496 hyper::Version::HTTP_10 => "1.0",
497 hyper::Version::HTTP_11 => "1.1",
498 hyper::Version::HTTP_2 => "2",
499 hyper::Version::HTTP_3 => "3",
500 _ => "unknown",
501 };
502 let req_content_len = req
503 .headers()
504 .get("content-length")
505 .and_then(|v| v.to_str().ok())
506 .and_then(|s| s.parse::<i64>().ok())
507 .unwrap_or(0);
508 let user_agent = req
509 .headers()
510 .get("user-agent")
511 .and_then(|v| v.to_str().ok())
512 .unwrap_or("-")
513 .to_owned();
514 let peer_ip = client_ip.as_str().to_owned();
515
516 let context = extract_context_from_request(&req);
517 let mut span = global::tracer("foxy::proxy").build_with_context(
518 SpanBuilder {
519 name: Cow::from(format!("{method} {path}")),
520 span_kind: Some(SpanKind::Server),
521 ..Default::default()
522 },
523 &context,
524 );
525
526 span.set_attributes([
527 KeyValue::new(HTTP_METHOD, method),
528 KeyValue::new(HTTP_URL, full_url.clone()),
529 KeyValue::new(HTTP_SCHEME, scheme),
530 KeyValue::new(HTTP_HOST, host),
531 KeyValue::new(HTTP_FLAVOR, http_version),
532 KeyValue::new(HTTP_REQUEST_CONTENT_LENGTH, req_content_len),
533 KeyValue::new(HTTP_USER_AGENT, user_agent),
534 KeyValue::new(NET_PEER_IP, peer_ip),
535 ]);
536
537 context.with_span(span)
538 };
539
540 let method = req.method().clone();
542 let path = req.uri().path().to_owned();
543
544 debug_fmt!("Server", "Received request: {} {}", method, path);
545
546 let proxy_req = match convert_hyper_request(req, client_ip.clone()).await {
547 Ok(r) => r,
548 Err(e) => {
549 error_fmt!(
550 "Server",
551 "Failed to convert request {} {}: {}",
552 method,
553 path,
554 e
555 );
556 return Ok(Response::builder()
557 .status(500)
558 .body(Body::from("Internal Server Error"))
559 .unwrap());
560 }
561 };
562
563 #[cfg(feature = "opentelemetry")]
565 let span_clone = span_context.clone();
566
567 #[cfg(feature = "opentelemetry")]
568 let span_ref = span_context.span();
569
570 #[cfg(feature = "opentelemetry")]
571 let result = core.process_request(proxy_req, Some(span_clone)).await;
572
573 #[cfg(not(feature = "opentelemetry"))]
574 let result = core.process_request(proxy_req).await;
575
576 #[cfg(feature = "opentelemetry")]
578 {
579 match result.as_ref() {
580 Ok(_r) => span_ref.set_status(Status::Ok),
581 Err(e) => {
582 span_ref.record_error(e);
583 span_ref.set_status(Status::Error {
584 description: Cow::from(e.to_string()),
585 })
586 }
587 }
588 }
589
590 let response: Result<Response<Body>, Infallible> = match result {
592 Ok(proxy_resp) => {
593 debug_fmt!(
594 "Server",
595 "Successfully processed request {} {} -> {}",
596 method,
597 path,
598 proxy_resp.status
599 );
600 match convert_proxy_response(proxy_resp) {
601 Ok(resp) => {
602 let upstream_duration = upstream_start.elapsed();
604
605 logging_middleware.log_response(&resp, &request_info, Some(upstream_duration));
607
608 Ok(resp)
609 }
610 Err(e) => {
611 error_fmt!(
612 "Server",
613 "Failed to convert response for {} {}: {}",
614 method,
615 path,
616 e
617 );
618 Ok(Response::builder()
619 .status(500)
620 .body(Body::from("Internal Server Error"))
621 .unwrap())
622 }
623 }
624 }
625 Err(e) => {
626 let (status, msg) = match &e {
627 ProxyError::Timeout(d) => {
628 warn_fmt!(
629 "Server",
630 "Request {} {} timed out after {:?}",
631 method,
632 path,
633 d
634 );
635 (504, format!("Gateway Timeout after {d:?}"))
636 }
637 ProxyError::RoutingError(msg) => {
638 warn_fmt!("Server", "Routing error for {} {}: {}", method, path, msg);
639 (404, "Route not found".into())
640 }
641 ProxyError::SecurityError(msg) => {
642 warn_fmt!("Server", "Security error for {} {}: {}", method, path, msg);
643 (403, "Forbidden".into())
644 }
645 ProxyError::ClientError(err) => {
646 error_fmt!("Server", "Client error for {} {}: {}", method, path, err);
647 (502, "Bad Gateway".into())
648 }
649 _ => {
650 error_fmt!(
651 "Server",
652 "Internal error processing {} {}: {}",
653 method,
654 path,
655 e
656 );
657 (500, "Internal Server Error".into())
658 }
659 };
660
661 Ok(Response::builder()
662 .status(status)
663 .body(Body::from(msg))
664 .unwrap())
665 }
666 };
667
668 #[allow(clippy::collapsible_if)]
670 if let Ok(resp) = &response {
671 if resp.status().is_client_error() || resp.status().is_server_error() {
672 logging_middleware.log_response(resp, &request_info, None);
673 }
674 }
675
676 #[cfg(feature = "opentelemetry")]
677 {
678 let status_code = response.as_ref().unwrap().status().as_u16();
679
680 span_ref.set_attribute(KeyValue::new(HTTP_RESPONSE_STATUS_CODE, status_code as i64));
681 span_ref.end();
682 }
683
684 response
685}
686
687#[cfg(feature = "opentelemetry")]
689fn extract_context_from_request(req: &Request<Incoming>) -> Context {
690 global::get_text_map_propagator(|propagator| {
691 propagator.extract(&HeaderExtractor(req.headers()))
692 })
693}