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