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