foxy/server/
mod.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! HTTP server implementation for Foxy.
6//!
7//! The server is a *thin* wrapper around **hyper-util**.  It owns the
8//! listening socket(s) and translates between Hyper's body types and the
9//! internal [`ProxyRequest`] / [`ProxyResponse`] generics that the core uses.
10//!
11//! **Protocol support**
12//! Uses `hyper_util::server::conn::auto::Builder`, so the same
13//! connection transparently handles both HTTP/1.1 *and* HTTP/2.
14//!
15//! ## Body streaming
16//! Inbound bodies are **streamed** straight into the upstream connection; no
17//! intermediate buffering beyond the configured `server.body_limit` takes
18//! place.  This prevents unbounded memory usage when clients upload large
19//! files but still gives you a safety-valve.
20
21#[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/// Configuration for the HTTP server.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct ServerConfig {
74    /// Host to bind to
75    #[serde(default = "default_host")]
76    pub host: String,
77
78    /// Port to listen on
79    #[serde(default = "default_port")]
80    pub port: u16,
81
82    /// Port to listen on for health/readiness checks
83    #[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/// HTTP server for the proxy.
110#[derive(Debug, Clone)]
111pub struct ProxyServer {
112    /// Server configuration
113    config: ServerConfig,
114    /// Proxy core
115    core: Arc<ProxyCore>,
116    /// Shutdown senders for each connection task
117    shutdown_senders: Arc<RwLock<HashMap<Id, oneshot::Sender<()>>>>,
118    /// Logging middleware for request/response logging
119    logging_middleware: LoggingMiddleware,
120}
121
122impl ProxyServer {
123    /// Create a new proxy server with the given configuration and proxy core.
124    pub fn new(config: ServerConfig, core: Arc<ProxyCore>) -> Self {
125        // Get logging configuration or use defaults
126        let logging_config = match core.config.get::<LoggingConfig>("proxy.logging") {
127            Ok(Some(config)) => config,
128            _ => LoggingConfig::default(),
129        };
130
131        // Create logging middleware
132        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    /// Start the proxy server.
143    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        // prepare signal futures (no errors at creation)
158        let ctrl_c = signal::ctrl_c();
159
160        // On Unix, install the SIGTERM stream once and store it in a variable
161        #[cfg(unix)]
162        let mut term_stream = signal(SignalKind::terminate())
163            .map_err(|e| ProxyError::Other(format!("Cannot install SIGTERM handler: {e}")))?;
164
165        // Build the actual future that we'll await
166        #[cfg(unix)]
167        let sigterm = term_stream.recv();
168        #[cfg(not(unix))]
169        let sigterm = std::future::pending();
170
171        // Pin them on the stack so select! can poll them
172        tokio::pin!(ctrl_c);
173        tokio::pin!(sigterm);
174
175        // Create and use the shared shutdown senders map
176        let shutdown_senders = self.shutdown_senders.clone();
177
178        // Track spawned connection tasks
179        let mut join_set = JoinSet::new();
180        let core = self.core.clone();
181
182        // Flag to indicate shutdown has been initiated
183        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 has been initiated, reject new connections
202                            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                                // Create the connection
225                                let connection = builder.serve_connection(io, service);
226                                
227                                // Pin the connection and enable graceful shutdown
228                                let mut conn = std::pin::pin!(connection);
229        
230                                // Run the connection with graceful shutdown
231                                tokio::select! {
232                                    res = &mut conn => {
233                                        match res {
234                                            Ok(()) => debug_fmt!("Server", "Connection closed normally"),
235                                            Err(e) => {
236                                                // Check if it's a graceful close by examining the error message
237                                                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                                        // Continue running the connection until it completes
250                                        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                                // Clean up the shutdown sender for this task
264                                shutdown_senders_clone.write().await.remove(&task_id);
265                                debug_fmt!("Server", "Connection task {:?} completed", task_id);
266                            });
267                            
268                            // Store the shutdown sender for this task
269                            shutdown_senders.write().await.insert(handle.id(), tx);
270                        }
271                        Err(e) => error_fmt!("Server", "Accept error: {}", e),
272                    }
273                }
274            }
275        }
276
277        // Stop accepting connections and signal existing ones to shut down
278        info_fmt!("Server", "Shutting down; waiting for {} connection(s)", join_set.len());
279
280        // Signal all connections to close gracefully
281        {
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        // Wait for connections to complete gracefully with a timeout
291        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                // Cancel remaining tasks
323                join_set.shutdown().await;
324            }
325        }
326
327        // Ensure health server is also shut down
328        drop(health_server);
329
330        info_fmt!("Server", "Shutdown complete");
331        Ok(())
332    }
333}
334
335/// Convert a hyper request to a proxy request.
336async 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    // Incoming → Stream → reqwest::Body
351    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
370/// Convert a proxy response to a hyper response.
371fn 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
401/// Handle an incoming HTTP request.
402async 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    // Process the request through the logging middleware
409    let remote_addr = req.extensions().get::<SocketAddr>().cloned();
410    let (req, request_info) = logging_middleware.process(req, remote_addr).await;
411
412    // Start Swagger UI Handling
413    #[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    // End Swagger UI Handling
427
428    // Start timing for upstream request
429    let upstream_start = Instant::now();
430    // ---------- OpenTelemetry SERVER span ----------
431    #[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    /* ---- convert Hyper → ProxyRequest ---- */
466    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    // ---------- core processing ----------
483    #[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    // ---------- finalise span ----------
496    #[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    /* ---------- map response ---------- */
510    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                    // Calculate upstream duration
521                    let upstream_duration = upstream_start.elapsed();
522
523                    // Log the response with timing information
524                    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    // Log error responses too
574    #[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// Utility function to extract the context from the incoming request headers
596#[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}