1#[cfg(test)]
22mod tests;
23mod health;
24#[cfg(feature = "swagger-ui")]
25pub mod swagger;
26
27use std::borrow::Cow;
28use std::sync::Arc;
29use std::net::SocketAddr;
30use std::convert::Infallible;
31use tokio::sync::RwLock;
32use hyper::body::Incoming;
33use hyper::{Request, Response};
34use hyper_util::server::conn::auto::Builder as AutoBuilder;
35use hyper_util::rt::TokioExecutor;
36use hyper::service::service_fn;
37use hyper_util::rt::TokioIo;
38use bytes::Bytes;
39use futures_util::TryStreamExt;
40use http_body_util::{BodyExt, Full};
41use reqwest::Body;
42use serde::{Serialize, Deserialize};
43use crate::{error_fmt, warn_fmt, info_fmt, debug_fmt, trace_fmt};
44use tokio::signal;
45use crate::logging::middleware::LoggingMiddleware;
46use crate::logging::config::LoggingConfig;
47use std::time::Instant;use tokio::task::{Id, JoinSet};
48use crate::core::{ProxyCore, ProxyRequest, ProxyResponse, ProxyError, HttpMethod, RequestContext};
49use std::collections::HashMap;
50use tokio::sync::oneshot;
51use health::HealthServer;
52#[cfg(feature = "swagger-ui")]
53use crate::server::swagger::SwaggerUIConfig;
54
55#[cfg(unix)]
56use tokio::signal::unix::{signal, SignalKind};
57
58#[cfg(feature = "opentelemetry")]
59use opentelemetry::{
60 global,
61 trace::{TraceContextExt, Tracer},
62 KeyValue,
63 Context,
64 trace::{Span, SpanBuilder, SpanKind, Status, SpanRef}
65};
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_STATUS_CODE, 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 = {
223 let mut b = AutoBuilder::new(TokioExecutor::new());
224 b.http1();
225 b.http2();
226 b
227 };
228
229 let connection = builder.serve_connection(io, service);
231
232 let mut conn = std::pin::pin!(connection);
234
235 tokio::select! {
237 res = &mut conn => {
238 match res {
239 Ok(()) => debug_fmt!("Server", "Connection closed normally"),
240 Err(e) => {
241 let err_str = e.to_string();
243 if !err_str.contains("connection closed") &&
244 !err_str.contains("connection reset") {
245 error_fmt!("Server", "Connection error: {}", e);
246 }
247 }
248 }
249 }
250 _ = rx => {
251 debug_fmt!("Server", "Connection received shutdown signal, waiting for graceful close");
252 conn.as_mut().graceful_shutdown();
253
254 match conn.await {
256 Ok(()) => debug_fmt!("Server", "Connection closed gracefully after shutdown signal"),
257 Err(e) => {
258 let err_str = e.to_string();
259 if !err_str.contains("connection closed") &&
260 !err_str.contains("connection reset") {
261 error_fmt!("Server", "Connection error during graceful shutdown: {}", e);
262 }
263 }
264 }
265 }
266 }
267
268 shutdown_senders_clone.write().await.remove(&task_id);
270 debug_fmt!("Server", "Connection task {:?} completed", task_id);
271 });
272
273 shutdown_senders.write().await.insert(handle.id(), tx);
275 }
276 Err(e) => error_fmt!("Server", "Accept error: {}", e),
277 }
278 }
279 }
280 }
281
282 info_fmt!("Server", "Shutting down; waiting for {} connection(s)", join_set.len());
284
285 {
287 let mut senders = shutdown_senders.write().await;
288 info_fmt!("Server", "Signaling {} connections to shut down", senders.len());
289 for (task_id, sender) in senders.drain() {
290 debug_fmt!("Server", "Sending shutdown signal to task {:?}", task_id);
291 let _ = sender.send(());
292 }
293 }
294
295 let shutdown_timeout = tokio::time::Duration::from_secs(30);
297 let start_time = tokio::time::Instant::now();
298
299 let shutdown_future = async {
300 let mut completed = 0;
301 let total = join_set.len();
302
303 while let Some(res) = join_set.join_next().await {
304 completed += 1;
305 match res {
306 Ok(_) => debug_fmt!("Server", "Connection task completed ({}/{})", completed, total),
307 Err(e) if e.is_cancelled() => debug_fmt!("Server", "Connection task cancelled ({}/{})", completed, total),
308 Err(e) => error_fmt!("Server", "Connection task failed ({}/{}): {}", completed, total, e),
309 }
310
311 let elapsed = start_time.elapsed();
312 if completed % 10 == 0 || total - completed < 10 {
313 info_fmt!("Server", "Shutdown progress: {}/{} connections closed (elapsed: {:.1}s)",
314 completed, total, elapsed.as_secs_f32());
315 }
316 }
317 };
318
319 match tokio::time::timeout(shutdown_timeout, shutdown_future).await {
320 Ok(_) => {
321 let elapsed = start_time.elapsed();
322 info_fmt!("Server", "All connections drained gracefully in {:.1}s", elapsed.as_secs_f32());
323 }
324 Err(_) => {
325 warn_fmt!("Server", "Shutdown timed out after {} seconds, some connections may be forcefully closed",
326 shutdown_timeout.as_secs());
327 join_set.shutdown().await;
329 }
330 }
331
332 drop(health_server);
334
335 info_fmt!("Server", "Shutdown complete");
336 Ok(())
337 }
338}
339
340async fn convert_hyper_request(
342 req: Request<Incoming>,
343 client_ip: String,
344) -> Result<ProxyRequest, ProxyError> {
345
346 let method = HttpMethod::from(req.method());
347 let uri = req.uri().clone();
348 let path = uri.path().to_owned();
349 let query = uri.query().map(|q| q.to_owned());
350 let headers = req.headers().clone();
351
352 trace_fmt!("Server", "Converting request: {} {} with {} headers",
353 method, path, headers.len());
354
355 let hyper_stream = req.into_body().into_data_stream();
357 let byte_stream = hyper_stream.map_ok(Bytes::from);
358 let body = reqwest::Body::wrap_stream(byte_stream);
359
360 Ok(ProxyRequest {
361 method,
362 path,
363 query,
364 headers,
365 body,
366 context: Arc::new(RwLock::new(RequestContext {
367 client_ip: Some(client_ip),
368 start_time: Some(std::time::Instant::now()),
369 attributes: std::collections::HashMap::new(),
370 })),
371 custom_target: None,
372 })
373}
374
375fn convert_proxy_response(resp: ProxyResponse) -> Result<Response<Body>, ProxyError> {
377 trace_fmt!("Server", "Converting response with status {} and {} headers",
378 resp.status, resp.headers.len());
379
380 let stream = resp
381 .body
382 .into_data_stream()
383 .map_err(|e| {
384 error_fmt!("Server", "Error streaming response body: {}", e);
385 std::io::Error::new(std::io::ErrorKind::Other, e)
386 });
387
388 let body = Body::wrap_stream(stream);
389
390 let mut builder = Response::builder().status(resp.status);
391 let mut_headers = builder.headers_mut().ok_or_else(|| {
392 error_fmt!("Server", "Failed to get mutable headers from response builder");
393 ProxyError::Other("Failed to build response: unable to get mutable headers".into())
394 })?;
395 *mut_headers = resp.headers;
396
397 builder
398 .body(body)
399 .map_err(|e| {
400 let err = ProxyError::Other(e.to_string());
401 error_fmt!("Server", "Failed to build response: {}", err);
402 err
403 })
404}
405
406async fn handle_request(
408 req: Request<Incoming>,
409 core: Arc<ProxyCore>,
410 client_ip: String,
411 logging_middleware: LoggingMiddleware,
412) -> Result<Response<Body>, Infallible> {
413 let remote_addr = req.extensions().get::<SocketAddr>().cloned();
415 let (req, request_info) = logging_middleware.process(req, remote_addr).await;
416
417 #[cfg(feature = "swagger-ui")]
419 {
420 if let Ok(Some(swagger_config)) = core.config.get::<SwaggerUIConfig>("proxy.swagger_ui") {
421 if swagger_config.enabled
422 && (req.uri().path().eq(&swagger_config.path)
423 || req.uri().path().starts_with(&swagger_config.path)) {
424 let swagger_response = swagger::handle_swagger_request(&req, &swagger_config)
425 .await
426 .unwrap();
427 return Ok(swagger_response);
428 }
429 }
430 }
431 let upstream_start = Instant::now();
435 #[cfg(feature = "opentelemetry")]
437 let span_context = {
438 let method = req.method().as_str().to_owned();
439 let path = req.uri().path().to_owned();
440 let full_url = req.uri().clone().to_string();
441 let scheme = req.uri().scheme_str().unwrap_or("http").to_owned();
442 let host = req.headers().get("host").and_then(|v| v.to_str().ok()).unwrap_or("-").to_owned();
443 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" };
444 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);
445 let user_agent = req.headers().get("user-agent").and_then(|v| v.to_str().ok()).unwrap_or("-").to_owned();
446 let peer_ip = client_ip.as_str().to_owned();
447
448 let context = extract_context_from_request(&req);
449 let mut span = global::tracer("foxy::proxy")
450 .build_with_context(SpanBuilder {
451 name: Cow::from(format!("{method} {path}")),
452 span_kind: Some(SpanKind::Server),
453 ..Default::default()
454 }, &context);
455
456 span.set_attributes([
457 KeyValue::new(HTTP_METHOD, method),
458 KeyValue::new(HTTP_URL, full_url.clone()),
459 KeyValue::new(HTTP_SCHEME, scheme),
460 KeyValue::new(HTTP_HOST, host),
461 KeyValue::new(HTTP_FLAVOR, http_version),
462 KeyValue::new(HTTP_REQUEST_CONTENT_LENGTH, req_content_len),
463 KeyValue::new(HTTP_USER_AGENT, user_agent),
464 KeyValue::new(NET_PEER_IP, peer_ip),
465 ]);
466
467 context.with_span(span)
468 };
469
470 let method = req.method().clone();
472 let path = req.uri().path().to_owned();
473
474 debug_fmt!("Server", "Received request: {} {}", method, path);
475
476 let proxy_req = match convert_hyper_request(req, client_ip.clone()).await {
477 Ok(r) => r,
478 Err(e) => {
479 error_fmt!("Server", "Failed to convert request {} {}: {}", method, path, e);
480 return Ok(Response::builder()
481 .status(500)
482 .body(Body::from("Internal Server Error"))
483 .unwrap());
484 }
485 };
486
487 #[cfg(feature = "opentelemetry")]
489 let span_clone = span_context.clone();
490
491 #[cfg(feature = "opentelemetry")]
492 let span_ref = span_context.span();
493
494 #[cfg(feature = "opentelemetry")]
495 let result = core.process_request(proxy_req, Some(span_clone)).await;
496
497 #[cfg(not(feature = "opentelemetry"))]
498 let result = core.process_request(proxy_req).await;
499
500 #[cfg(feature = "opentelemetry")]
502 {
503 match result.as_ref() {
504 Ok(r) => {
505 span_ref.set_status(Status::Ok)
506 },
507 Err(e) => {
508 span_ref.record_error(e);
509 span_ref.set_status(Status::Error { description: Cow::from(e.to_string()) })
510 }
511 }
512 }
513
514 let response: Result<Response<Body>, Infallible> = match result {
516 Ok(proxy_resp) => {
517 debug_fmt!("Server",
518 "Successfully processed request {} {} -> {}",
519 method,
520 path,
521 proxy_resp.status
522 );
523 match convert_proxy_response(proxy_resp) {
524 Ok(resp) => {
525 let upstream_duration = upstream_start.elapsed();
527
528 logging_middleware.log_response(&resp, &request_info, Some(upstream_duration));
530
531 Ok(resp)
532 },
533 Err(e) => {
534 error_fmt!("Server",
535 "Failed to convert response for {} {}: {}",
536 method,
537 path,
538 e
539 );
540 Ok(Response::builder()
541 .status(500)
542 .body(Body::from("Internal Server Error"))
543 .unwrap())
544 }
545 }
546 }
547 Err(e) => {
548 let (status, msg) = match &e {
549 ProxyError::Timeout(d) => {
550 warn_fmt!("Server", "Request {} {} timed out after {:?}", method, path, d);
551 (504, format!("Gateway Timeout after {d:?}"))
552 }
553 ProxyError::RoutingError(msg) => {
554 warn_fmt!("Server", "Routing error for {} {}: {}", method, path, msg);
555 (404, "Route not found".into())
556 }
557 ProxyError::SecurityError(msg) => {
558 warn_fmt!("Server", "Security error for {} {}: {}", method, path, msg);
559 (403, "Forbidden".into())
560 }
561 ProxyError::ClientError(err) => {
562 error_fmt!("Server", "Client error for {} {}: {}", method, path, err);
563 (502, "Bad Gateway".into())
564 }
565 _ => {
566 error_fmt!("Server", "Internal error processing {} {}: {}", method, path, e);
567 (500, "Internal Server Error".into())
568 }
569 };
570
571 Ok(Response::builder()
572 .status(status)
573 .body(Body::from(msg))
574 .unwrap())
575 }
576 };
577
578 if let Ok(resp) = &response {
580 if resp.status().is_client_error() || resp.status().is_server_error() {
581 logging_middleware.log_response(resp, &request_info, None);
582 }
583 }
584
585 #[cfg(feature = "opentelemetry")]
586 {
587 let status_code = response.as_ref().unwrap().status().as_u16();
588
589 span_ref.set_attribute(KeyValue::new(
590 HTTP_RESPONSE_STATUS_CODE,
591 status_code as i64,
592 ));
593 span_ref.end();
594 }
595
596 response
597}
598
599#[cfg(feature = "opentelemetry")]
601fn extract_context_from_request(req: &Request<Incoming>) -> Context {
602 global::get_text_map_propagator(|propagator| {
603 propagator.extract(&HeaderExtractor(req.headers()))
604 })
605}