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