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::sync::Arc;
26use std::net::SocketAddr;
27use std::convert::Infallible;
28use tokio::sync::RwLock;
29use hyper::body::Incoming;
30use hyper::{Request, Response};
31use hyper_util::server::conn::auto::Builder as AutoBuilder;
32use hyper_util::rt::TokioExecutor;
33use hyper::service::service_fn;
34use hyper_util::rt::TokioIo;
35use bytes::Bytes;
36use futures_util::TryStreamExt;
37use http_body_util::{BodyExt, Full};
38use reqwest::Body;
39use serde::{Serialize, Deserialize};
40use log::{debug, info, warn, error, trace};
41use tokio::signal;
42use tokio::task::{Id, JoinSet};
43use crate::core::{ProxyCore, ProxyRequest, ProxyResponse, ProxyError, HttpMethod, RequestContext};
44use std::collections::HashMap;
45use tokio::sync::oneshot;
46use health::HealthServer;
47
48#[cfg(unix)]
49use tokio::signal::unix::{signal, SignalKind};
50
51/// Configuration for the HTTP server.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct ServerConfig {
54    /// Host to bind to
55    #[serde(default = "default_host")]
56    pub host: String,
57
58    /// Port to listen on
59    #[serde(default = "default_port")]
60    pub port: u16,
61
62    /// Port to listen on for health/readiness checks
63    #[serde(default = "default_health_port")]
64    pub health_port: u16,
65}
66
67fn default_host() -> String {
68    "127.0.0.1".to_string()
69}
70
71fn default_port() -> u16 {
72    8080
73}
74
75fn default_health_port() -> u16 {
76    8081
77}
78
79impl Default for ServerConfig {
80    fn default() -> Self {
81        Self {
82            host: default_host(),
83            port: default_port(),
84            health_port: default_health_port(),
85        }
86    }
87}
88
89/// HTTP server for the proxy.
90#[derive(Debug, Clone)]
91pub struct ProxyServer {
92    /// Server configuration
93    config: ServerConfig,
94    /// Proxy core
95    core: Arc<ProxyCore>,
96    /// Shutdown senders for each connection task
97    shutdown_senders: Arc<RwLock<HashMap<Id, oneshot::Sender<()>>>>,
98}
99
100impl ProxyServer {
101    /// Create a new proxy server with the given configuration and proxy core.
102    pub fn new(config: ServerConfig, core: Arc<ProxyCore>) -> Self {
103        Self { 
104            config, 
105            core,
106            shutdown_senders: Arc::new(RwLock::new(HashMap::new())),
107        }
108    }
109
110    /// Start the proxy server.
111    pub async fn start(&self) -> Result<(), ProxyError> {
112        let addr = format!("{}:{}", self.config.host, self.config.port)
113            .parse::<SocketAddr>()
114            .map_err(|e| ProxyError::Other(format!("Invalid server address: {}", e)))?;
115        
116        let listener = tokio::net::TcpListener::bind(addr)
117            .await
118            .map_err(|e| ProxyError::Other(format!("Failed to bind: {}", e)))?;
119        
120        info!("Foxy proxy listening on http://{}", addr);
121
122        let health_server = HealthServer::new(self.config.health_port);
123        health_server.set_ready();
124
125        // prepare signal futures (no errors at creation)
126        let ctrl_c = signal::ctrl_c();
127
128        // On Unix, install the SIGTERM stream once and store it in a variable
129        #[cfg(unix)]
130        let mut term_stream = signal(SignalKind::terminate())
131            .map_err(|e| ProxyError::Other(format!("Cannot install SIGTERM handler: {}", e)))?;
132
133        // Build the actual future that we'll await
134        #[cfg(unix)]
135        let sigterm = term_stream.recv();
136        #[cfg(not(unix))]
137        let sigterm = std::future::pending();
138
139        // Pin them on the stack so select! can poll them
140        tokio::pin!(ctrl_c);
141        tokio::pin!(sigterm);
142
143        // Create and use the shared shutdown senders map
144        let shutdown_senders = self.shutdown_senders.clone();
145        
146        // Track spawned connection tasks
147        let mut join_set = JoinSet::new();
148        let core = self.core.clone();
149
150        // Flag to indicate shutdown has been initiated
151        let shutdown_initiated = Arc::new(std::sync::atomic::AtomicBool::new(false));
152        let shutdown_initiated_clone = shutdown_initiated.clone();
153
154        loop {
155            tokio::select! {
156                _ = &mut ctrl_c => {
157                    info!("Received Ctrl-C; initiating graceful shutdown");
158                    shutdown_initiated_clone.store(true, std::sync::atomic::Ordering::SeqCst);
159                    break;
160                }
161                _ = &mut sigterm => {
162                    info!("Received SIGTERM; initiating graceful shutdown");
163                    shutdown_initiated_clone.store(true, std::sync::atomic::Ordering::SeqCst);
164                    break;
165                }
166                accept = listener.accept() => {
167                    match accept {
168                        Ok((stream, remote_addr)) => {
169                            // If shutdown has been initiated, reject new connections
170                            if shutdown_initiated.load(std::sync::atomic::Ordering::SeqCst) {
171                                info!("Rejecting new connection during shutdown");
172                                continue;
173                            }
174
175                            let core = core.clone();
176                            let client_ip = remote_addr.ip().to_string();
177                            let (tx, rx) = oneshot::channel();
178                            
179                            let handle = join_set.spawn(async move {
180                                let service = service_fn(move |req: Request<Incoming>| {
181                                    debug!("Incoming over {:?}", req.version());
182                                    handle_request(req, core.clone(), client_ip.clone())
183                                });
184                                let io = TokioIo::new(stream);
185
186                                // Use the shutdown signal to properly close connections
187                                let builder = {
188                                    let mut b = AutoBuilder::new(TokioExecutor::new());
189                                    b.http1();
190                                    b.http2();
191                                    b
192                                };
193
194                                // Create a graceful shutdown future
195                                let graceful_shutdown = async {
196                                    // Wait for the shutdown signal
197                                    let _ = rx.await;
198                                    debug!("Connection received shutdown signal");
199                                };
200
201                                // Create the connection future
202                                let connection = builder.serve_connection(io, service);
203
204                                // Run both futures concurrently
205                                tokio::select! {
206                                    res = connection => {
207                                        if let Err(e) = res {
208                                            error!("Connection error: {}", e);
209                                        }
210                                    }
211                                    _ = graceful_shutdown => {
212                                        debug!("Connection shutting down gracefully");
213                                    }
214                                }
215                            });
216                            
217                            // Store the shutdown sender for this task
218                            shutdown_senders.write().await.insert(handle.id(), tx);
219                        }
220                        Err(e) => error!("Accept error: {}", e),
221                    }
222                }
223            }
224        }
225
226        // Stop accepting connections and signal existing ones to shut down
227        info!("Shutting down; waiting for {} connection(s)", join_set.len());
228        
229        // Signal all connections to close gracefully
230        {
231            let mut senders = shutdown_senders.write().await;
232            for (_, sender) in senders.drain() {
233                let _ = sender.send(());
234            }
235        }
236        
237        // Wait for connections to complete gracefully with a timeout
238        let shutdown_timeout = tokio::time::Duration::from_secs(30);
239        let shutdown_future = async {
240            while let Some(res) = join_set.join_next().await {
241                if let Err(e) = res {
242                    error!("Connection task failed: {}", e);
243                }
244            }
245        };
246
247        match tokio::time::timeout(shutdown_timeout, shutdown_future).await {
248            Ok(_) => info!("All connections drained gracefully"),
249            Err(_) => warn!("Shutdown timed out after {} seconds", shutdown_timeout.as_secs()),
250        }
251        
252        info!("Shutdown complete");
253        Ok(())
254    }
255}
256
257/// Convert a hyper request to a proxy request.
258async fn convert_hyper_request(
259    req: Request<Incoming>,
260    client_ip: String,
261) -> Result<ProxyRequest, ProxyError> {
262    let method = HttpMethod::from(req.method());
263    let uri = req.uri().clone();
264    let path = uri.path().to_owned();
265    let query = uri.query().map(|q| q.to_owned());
266    let headers = req.headers().clone();
267
268    log::trace!("Converting request: {} {} with {} headers", 
269        method, path, headers.len());
270
271    // Incoming → Stream → reqwest::Body
272    let hyper_stream = req.into_body().into_data_stream();
273    let byte_stream = hyper_stream.map_ok(Bytes::from);
274    let body = reqwest::Body::wrap_stream(byte_stream);
275
276    Ok(ProxyRequest {
277        method,
278        path,
279        query,
280        headers,
281        body,
282        context: Arc::new(RwLock::new(RequestContext {
283            client_ip: Some(client_ip),
284            start_time: Some(std::time::Instant::now()),
285            attributes: std::collections::HashMap::new(),
286        })),
287    })
288}
289
290/// Convert a proxy response to a hyper response.
291fn convert_proxy_response(resp: ProxyResponse) -> Result<Response<Body>, ProxyError> {
292    log::trace!("Converting response with status {} and {} headers", 
293        resp.status, resp.headers.len());
294        
295    let stream = resp
296        .body
297        .into_data_stream()
298        .map_err(|e| {
299            log::error!("Error streaming response body: {}", e);
300            std::io::Error::new(std::io::ErrorKind::Other, e)
301        });
302
303    let body = Body::wrap_stream(stream);
304
305    let mut builder = Response::builder().status(resp.status);
306    match builder.headers_mut() {
307        Some(headers) => {
308            *headers = resp.headers;
309            Ok(())
310        },
311        None => {
312            log::error!("Failed to get mutable headers from response builder");
313            Err(ProxyError::Other("unable to set headers".into()))
314        }
315    }?;
316
317    builder
318        .body(body)
319        .map_err(|e| {
320            let err = ProxyError::Other(e.to_string());
321            log::error!("Failed to build response: {}", err);
322            err
323        })
324}
325
326/// Handle an incoming HTTP request.
327async fn handle_request(
328    req: Request<Incoming>,
329    core: Arc<ProxyCore>,
330    client_ip: String,
331) -> Result<Response<Body>, Infallible> {
332    /* ---- convert Hyper → ProxyRequest ---- */
333    let method = req.method().clone();
334    let path = req.uri().path().to_owned();
335    
336    log::debug!("Received request: {} {}", method, path);
337    
338    let proxy_req = match convert_hyper_request(req, client_ip.clone()).await {
339        Ok(r) => r,
340        Err(e) => {
341            log::error!("Failed to convert request {} {}: {}", method, path, e);
342            return Ok(Response::builder()
343                .status(500)
344                .body(Body::from("Internal Server Error"))
345                .unwrap());
346        }
347    };
348
349    /* ---- core processing ---- */
350    match core.process_request(proxy_req).await {
351        Ok(proxy_resp) => {
352            log::debug!("Successfully processed request {} {} -> {}", method, path, proxy_resp.status);
353            match convert_proxy_response(proxy_resp) {
354                Ok(resp) => Ok(resp),
355                Err(e) => {
356                    log::error!("Failed to convert response for {} {}: {}", method, path, e);
357                    Ok(Response::builder()
358                        .status(500)
359                        .body(Body::from("Internal Server Error"))
360                        .unwrap())
361                }
362            }
363        },
364        Err(e) => {
365            let (status, msg) = match &e {
366                ProxyError::Timeout(d) => {
367                    log::warn!("Request {} {} timed out after {:?}", method, path, d);
368                    (504, format!("Gateway Timeout after {d:?}"))
369                },
370                ProxyError::RoutingError(msg) => {
371                    log::warn!("Routing error for {} {}: {}", method, path, msg);
372                    (404, "Route not found".into())
373                },
374                ProxyError::SecurityError(msg) => {
375                    log::warn!("Security error for {} {}: {}", method, path, msg);
376                    (403, "Forbidden".into())
377                },
378                ProxyError::ClientError(err) => {
379                    log::error!("Client error for {} {}: {}", method, path, err);
380                    (502, "Bad Gateway".into())
381                },
382                _ => {
383                    log::error!("Internal error processing {} {}: {}", method, path, e);
384                    (500, "Internal Server Error".into())
385                },
386            };
387            
388            Ok(Response::builder()
389                .status(status)
390                .body(Body::from(msg))
391                .unwrap())
392        }
393    }
394}