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