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::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/// 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 = {
223                                    let mut b = AutoBuilder::new(TokioExecutor::new());
224                                    b.http1();
225                                    b.http2();
226                                    b
227                                };
228        
229                                // Create the connection
230                                let connection = builder.serve_connection(io, service);
231                                
232                                // Pin the connection and enable graceful shutdown
233                                let mut conn = std::pin::pin!(connection);
234        
235                                // Run the connection with graceful shutdown
236                                tokio::select! {
237                                    res = &mut conn => {
238                                        match res {
239                                            Ok(()) => debug_fmt!("Server", "Connection closed normally"),
240                                            Err(e) => {
241                                                // Check if it's a graceful close by examining the error message
242                                                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                                        // Continue running the connection until it completes
255                                        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                                // Clean up the shutdown sender for this task
269                                shutdown_senders_clone.write().await.remove(&task_id);
270                                debug_fmt!("Server", "Connection task {:?} completed", task_id);
271                            });
272                            
273                            // Store the shutdown sender for this task
274                            shutdown_senders.write().await.insert(handle.id(), tx);
275                        }
276                        Err(e) => error_fmt!("Server", "Accept error: {}", e),
277                    }
278                }
279            }
280        }
281
282        // Stop accepting connections and signal existing ones to shut down
283        info_fmt!("Server", "Shutting down; waiting for {} connection(s)", join_set.len());
284
285        // Signal all connections to close gracefully
286        {
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        // Wait for connections to complete gracefully with a timeout
296        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                // Cancel remaining tasks
328                join_set.shutdown().await;
329            }
330        }
331
332        // Ensure health server is also shut down
333        drop(health_server);
334
335        info_fmt!("Server", "Shutdown complete");
336        Ok(())
337    }
338}
339
340/// Convert a hyper request to a proxy request.
341async 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    // Incoming → Stream → reqwest::Body
356    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
375/// Convert a proxy response to a hyper response.
376fn 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
406/// Handle an incoming HTTP request.
407async 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    // Process the request through the logging middleware
414    let remote_addr = req.extensions().get::<SocketAddr>().cloned();
415    let (req, request_info) = logging_middleware.process(req, remote_addr).await;
416
417    // Start Swagger UI Handling
418    #[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    // End Swagger UI Handling
432
433    // Start timing for upstream request
434    let upstream_start = Instant::now();
435    // ---------- OpenTelemetry SERVER span ----------
436    #[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    /* ---- convert Hyper → ProxyRequest ---- */
471    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    // ---------- core processing ----------
488    #[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    // ---------- finalise span ----------
501    #[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    /* ---------- map response ---------- */
515    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                    // Calculate upstream duration
526                    let upstream_duration = upstream_start.elapsed();
527
528                    // Log the response with timing information
529                    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    // Log error responses too
579    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// Utility function to extract the context from the incoming request headers
600#[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}