foxy/core/
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//! Core primitives – requests, responses, filters & routing.
6//!
7//! Everything that physically moves through the proxy pipeline is defined
8//! in this module.  No protocol-level logic lives here; that sits in
9//! `server.rs` (IO) and `filters.rs` (behaviour).
10
11#[cfg(test)]
12mod tests;
13
14use std::collections::HashMap;
15use std::sync::Arc;
16use std::time::{Duration, Instant};
17use std::{fmt, mem};
18use thiserror::Error;
19use crate::security::{ProviderConfig, SecurityChain, SecurityProvider, SecurityStage};
20use tokio::sync::RwLock;
21use tokio::time::timeout;
22use serde::{Serialize, Deserialize};
23
24use crate::config::Config;
25
26/// Errors that can occur during proxy operations.
27#[derive(Error, Debug)]
28pub enum ProxyError {
29    /// HTTP client error
30    #[error("HTTP client error: {0}")]
31    ClientError(#[from] reqwest::Error),
32
33    /// IO error
34    #[error("IO error: {0}")]
35    IoError(#[from] std::io::Error),
36
37    /// Timeout error
38    #[error("request timed out after {0:?}")]
39    Timeout(Duration),
40
41    /// Router error
42    #[error("routing error: {0}")]
43    RoutingError(String),
44
45    /// Filter error
46    #[error("filter error: {0}")]
47    FilterError(String),
48
49    /// Configuration error
50    #[error("configuration error: {0}")]
51    ConfigError(String),
52
53    /// Security provider error
54    #[error("security error: {0}")]
55    SecurityError(String),
56
57    /// Generic error
58    #[error("{0}")]
59    Other(String),
60}
61
62impl From<crate::config::error::ConfigError> for ProxyError {
63    fn from(err: crate::config::error::ConfigError) -> Self {
64        ProxyError::ConfigError(err.to_string())
65    }
66}
67
68impl From<globset::Error> for ProxyError {
69    fn from(e: globset::Error) -> Self {
70        ProxyError::SecurityError(e.to_string())
71    }
72}
73
74impl From<jsonwebtoken::errors::Error> for ProxyError {
75    fn from(e: jsonwebtoken::errors::Error) -> Self {
76        ProxyError::SecurityError(e.to_string())
77    }
78}
79
80/// HTTP methods supported by the proxy.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "UPPERCASE")]
83pub enum HttpMethod {
84    Get,
85    Post,
86    Put,
87    Delete,
88    Head,
89    Options,
90    Patch,
91    Trace,
92    Connect,
93}
94
95impl fmt::Display for HttpMethod {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        match self {
98            HttpMethod::Get => write!(f, "GET"),
99            HttpMethod::Post => write!(f, "POST"),
100            HttpMethod::Put => write!(f, "PUT"),
101            HttpMethod::Delete => write!(f, "DELETE"),
102            HttpMethod::Head => write!(f, "HEAD"),
103            HttpMethod::Options => write!(f, "OPTIONS"),
104            HttpMethod::Patch => write!(f, "PATCH"),
105            HttpMethod::Trace => write!(f, "TRACE"),
106            HttpMethod::Connect => write!(f, "CONNECT"),
107        }
108    }
109}
110
111impl From<&reqwest::Method> for HttpMethod {
112    fn from(method: &reqwest::Method) -> Self {
113        match *method {
114            reqwest::Method::GET => HttpMethod::Get,
115            reqwest::Method::POST => HttpMethod::Post,
116            reqwest::Method::PUT => HttpMethod::Put,
117            reqwest::Method::DELETE => HttpMethod::Delete,
118            reqwest::Method::HEAD => HttpMethod::Head,
119            reqwest::Method::OPTIONS => HttpMethod::Options,
120            reqwest::Method::PATCH => HttpMethod::Patch,
121            reqwest::Method::TRACE => HttpMethod::Trace,
122            reqwest::Method::CONNECT => HttpMethod::Connect,
123            _ => HttpMethod::Get, // Default to GET for unsupported methods
124        }
125    }
126}
127
128impl From<HttpMethod> for reqwest::Method {
129    fn from(method: HttpMethod) -> Self {
130        match method {
131            HttpMethod::Get => reqwest::Method::GET,
132            HttpMethod::Post => reqwest::Method::POST,
133            HttpMethod::Put => reqwest::Method::PUT,
134            HttpMethod::Delete => reqwest::Method::DELETE,
135            HttpMethod::Head => reqwest::Method::HEAD,
136            HttpMethod::Options => reqwest::Method::OPTIONS,
137            HttpMethod::Patch => reqwest::Method::PATCH,
138            HttpMethod::Trace => reqwest::Method::TRACE,
139            HttpMethod::Connect => reqwest::Method::CONNECT,
140        }
141    }
142}
143
144/// Represents an HTTP request that can be processed by the proxy.
145#[derive(Debug)]
146pub struct ProxyRequest {
147    pub method: HttpMethod,
148    pub path: String,
149    pub query: Option<String>,
150    pub headers: reqwest::header::HeaderMap,
151    pub body: reqwest::Body,
152    pub context: Arc<RwLock<RequestContext>>,
153}
154
155impl Clone for ProxyRequest {
156    fn clone(&self) -> Self {
157        // A streaming body can't be duplicated.  Give filters an empty one.
158        Self {
159            method:   self.method,
160            path:     self.path.clone(),
161            query:    self.query.clone(),
162            headers:  self.headers.clone(),
163            body:     reqwest::Body::from(""),
164            context:  self.context.clone(),
165        }
166    }
167}
168
169/// Represents an HTTP response returned by the proxy.
170#[derive(Debug)]
171pub struct ProxyResponse {
172    pub status: u16,
173    pub headers: reqwest::header::HeaderMap,
174    pub body: reqwest::Body,
175    pub context: Arc<RwLock<ResponseContext>>,
176}
177
178/// Context data that can be attached to a request and accessed by filters.
179#[derive(Debug, Default, Clone)]
180pub struct RequestContext {
181    /// The original client's IP address
182    pub client_ip: Option<String>,
183    /// The start time of the request
184    pub start_time: Option<std::time::Instant>,
185    /// Custom attributes that can be set by filters
186    pub attributes: std::collections::HashMap<String, serde_json::Value>,
187}
188
189/// Context data that can be attached to a response and accessed by filters.
190#[derive(Debug, Default, Clone)]
191pub struct ResponseContext {
192    /// The time when the response was received from the target
193    pub receive_time: Option<std::time::Instant>,
194    /// Custom attributes that can be set by filters
195    pub attributes: std::collections::HashMap<String, serde_json::Value>,
196}
197
198/// Core proxy server implementation.
199#[derive(Debug)]
200pub struct ProxyCore {
201    /// Configuration for the proxy
202    pub config: Arc<Config>,
203    /// HTTP client for making outbound requests
204    pub client: reqwest::Client,
205    /// Router for matching requests to routes
206    pub router: Arc<dyn Router>,
207    /// Global filters that apply to all routes
208    pub global_filters: Arc<RwLock<Vec<Arc<dyn Filter>>>>,
209    /// Security chain that applies to all routes
210    pub security_chain: Arc<RwLock<SecurityChain>>,
211}
212
213impl ProxyCore {
214    /// Create a new proxy core with the given configuration and router.
215    pub async fn new(config: Arc<Config>, router: Arc<dyn Router>) -> Result<Self, ProxyError> {
216        // Configure the HTTP client based on the configuration
217        let timeout_secs: u64 = config.get_or_default("proxy.timeout", 30)?;
218
219        let client = reqwest::Client::builder()
220            .timeout(Duration::from_secs(timeout_secs))
221            .build()
222            .map_err(ProxyError::ClientError)?;
223
224        let security_config = config
225            .get::<Vec<ProviderConfig>>("proxy.security_chain")
226            .unwrap_or_default();
227
228        let security_chain = SecurityChain::from_configs(
229            security_config.unwrap_or_default()
230        ).await?;
231
232        Ok(Self {
233            config,
234            client,
235            router,
236            global_filters: Arc::new(RwLock::new(Vec::new())),
237            security_chain: Arc::new(RwLock::new(security_chain)),
238        })
239    }
240
241    /// Add a global filter.
242    pub async fn add_global_filter(&self, filter: Arc<dyn Filter>) {
243        let mut filters = self.global_filters.write().await;
244        filters.push(filter);
245    }
246
247    /// Add a security filter to the chain.
248    pub async fn add_security_provider(&self, p: Arc<dyn SecurityProvider>) {
249        self.security_chain.write().await.add(p);
250    }
251
252    /// Process a request through the proxy.
253    pub async fn process_request(
254        &self,
255        request: ProxyRequest,
256    ) -> Result<ProxyResponse, ProxyError> {
257        let overall_start = Instant::now();
258        let method = request.method.to_string();
259        let path = request.path.clone();
260
261        log::trace!("Processing request: {} {}", method, path);
262
263        /* ---------- Security chain pre auth ---------- */
264        let mut request = match self.security_chain.read().await.apply_pre(request).await {
265            Ok(req) => {
266                log::trace!("Security pre-auth passed for {} {}", method, path);
267                req
268            },
269            Err(e) => {
270                log::warn!("Security pre-auth failed for {} {}: {}", method, path, e);
271                return Err(e);
272            }
273        };
274
275        /* ---------- PRE-filters ---------- */
276        for f in self.global_filters.read().await.iter() {
277            if f.filter_type().is_pre() || f.filter_type().is_both() {
278                log::trace!("Applying global pre-filter: {}", f.name());
279                match f.pre_filter(request).await {
280                    Ok(req) => request = req,
281                    Err(e) => {
282                        log::error!("Global pre-filter '{}' failed: {}", f.name(), e);
283                        return Err(e);
284                    }
285                }
286            }
287        }
288        
289        let route = match self.router.route(&request).await {
290            Ok(r) => {
291                log::debug!("Request {} {} matched route: {}", method, path, r.id);
292                r
293            },
294            Err(e) => {
295                log::warn!("No route found for {} {}: {}", method, path, e);
296                return Err(e);
297            }
298        };
299        
300        let route_filters = route.filters.clone().unwrap_or_default();
301        for f in &route_filters {
302            if f.filter_type().is_pre() || f.filter_type().is_both() {
303                log::trace!("Applying route pre-filter: {}", f.name());
304                match f.pre_filter(request).await {
305                    Ok(req) => request = req,
306                    Err(e) => {
307                        log::error!("Route pre-filter '{}' failed: {}", f.name(), e);
308                        return Err(e);
309                    }
310                }
311            }
312        }
313
314        /* ---------- build outbound req ---------- */
315        let url = format!("{}{}", route.target_base_url, request.path);
316        log::debug!("Forwarding to target: {}", url);
317        let outbound_body = mem::replace(&mut request.body, reqwest::Body::from(""));
318
319        let mut builder = self
320            .client
321            .request(request.method.into(), &url)
322            .headers(request.headers.clone())
323            .body(outbound_body);
324
325        if let Some(q) = &request.query {
326            builder = builder.query(&[(q, "")]);
327        }
328
329        /* ---------- send with timeout ---------- */
330        let timeout_dur =
331            Duration::from_secs(self.config.get_or_default("proxy.timeout", 30).unwrap_or_else(|e| {
332                log::error!("Failed to get timeout config: {}", e);
333                30 // Default to 30 seconds on error
334            }));
335
336        let upstream_start = Instant::now();
337        log::trace!("Sending request to upstream with timeout: {:?}", timeout_dur);
338        
339        let resp = match timeout(timeout_dur, builder.send()).await {
340            Ok(result) => match result {
341                Ok(response) => response,
342                Err(e) => {
343                    log::error!("Upstream request failed: {}", e);
344                    return Err(ProxyError::ClientError(e));
345                }
346            },
347            Err(_) => {
348                log::warn!("Request to {} timed out after {:?}", url, timeout_dur);
349                return Err(ProxyError::Timeout(timeout_dur));
350            }
351        };
352        
353        let upstream_elapsed = upstream_start.elapsed();
354        log::trace!("Received response from upstream in {:?}", upstream_elapsed);
355
356        /* ---------- wrap streaming response ---------- */
357        let status = resp.status().as_u16();
358        let headers = resp.headers().clone();
359        let body = reqwest::Body::wrap_stream(resp.bytes_stream());
360
361        let mut proxy_resp = ProxyResponse {
362            status,
363            headers,
364            body,
365            context: Arc::new(RwLock::new(ResponseContext::default())),
366        };
367        proxy_resp.context.write().await.receive_time = Some(Instant::now());
368
369        log::debug!("Upstream responded with status: {}", status);
370
371        /* ---------- POST-filters ---------- */
372        for f in &route_filters {
373            if f.filter_type().is_post() || f.filter_type().is_both() {
374                log::trace!("Applying route post-filter: {}", f.name());
375                match f.post_filter(request.clone(), proxy_resp).await {
376                    Ok(resp) => proxy_resp = resp,
377                    Err(e) => {
378                        log::error!("Route post-filter '{}' failed: {}", f.name(), e);
379                        return Err(e);
380                    }
381                }
382            }
383        }
384        
385        for f in self.global_filters.read().await.iter() {
386            if f.filter_type().is_post() || f.filter_type().is_both() {
387                log::trace!("Applying global post-filter: {}", f.name());
388                match f.post_filter(request.clone(), proxy_resp).await {
389                    Ok(resp) => proxy_resp = resp,
390                    Err(e) => {
391                        log::error!("Global post-filter '{}' failed: {}", f.name(), e);
392                        return Err(e);
393                    }
394                }
395            }
396        }
397
398        /* ---------- Security chain post auth ---------- */
399        proxy_resp = match self.security_chain.read().await.apply_post(request.clone(), proxy_resp).await {
400            Ok(resp) => {
401                log::trace!("Security post-auth passed for {} {}", method, path);
402                resp
403            },
404            Err(e) => {
405                log::warn!("Security post-auth failed for {} {}: {}", method, path, e);
406                return Err(e);
407            }
408        };
409        
410        /* ---------- timing log ---------- */
411        let overall_elapsed = overall_start.elapsed();
412        let internal_elapsed = overall_elapsed.saturating_sub(upstream_elapsed);
413
414        log::debug!(
415            "[timing] {} {} -> {} | total={:?} upstream={:?} internal={:?}",
416            request.method,
417            request.path,
418            proxy_resp.status,
419            overall_elapsed,
420            upstream_elapsed,
421            internal_elapsed
422        );
423
424        Ok(proxy_resp)
425    }
426}
427
428/// Describes when a filter should be applied.
429#[derive(Debug, Clone, Copy, PartialEq, Eq)]
430pub enum FilterType {
431    /// Filter applied before the request is sent to the target
432    Pre,
433    /// Filter applied after the response is received from the target
434    Post,
435    /// Filter applied both before and after
436    Both,
437}
438
439impl FilterType {
440    /// Returns true if this is a pre-filter or both.
441    pub fn is_pre(&self) -> bool {
442        matches!(self, FilterType::Pre | FilterType::Both)
443    }
444
445    /// Returns true if this is a post-filter or both.
446    pub fn is_post(&self) -> bool {
447        matches!(self, FilterType::Post | FilterType::Both)
448    }
449
450    /// Returns true if this is both a pre and post filter.
451    pub fn is_both(&self) -> bool {
452        matches!(self, FilterType::Both)
453    }
454}
455
456/// A filter that processes requests and responses.
457#[async_trait::async_trait]
458pub trait Filter: fmt::Debug + Send + Sync {
459    /// Get the filter type.
460    fn filter_type(&self) -> FilterType;
461
462    /// Get the filter name.
463    fn name(&self) -> &str;
464
465    /// Process a request before it is sent to the target.
466    async fn pre_filter(&self, request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
467        // Default implementation: pass through the request unchanged
468        Ok(request)
469    }
470
471    /// Process a response after it is received from the target.
472    async fn post_filter(&self, _request: ProxyRequest, response: ProxyResponse) -> Result<ProxyResponse, ProxyError> {
473        // Default implementation: pass through the response unchanged
474        Ok(response)
475    }
476}
477
478/// A route that the proxy can forward requests to.
479#[derive(Debug, Clone)]
480pub struct Route {
481    /// The ID of the route (for logging and reference)
482    pub id: String,
483    /// The base URL of the target
484    pub target_base_url: String,
485    /// The path pattern that this route matches
486    pub path_pattern: String,
487    /// The filters that should be applied to this route
488    pub filters: Option<Vec<Arc<dyn Filter>>>,
489}
490
491/// A router that matches requests to routes.
492#[async_trait::async_trait]
493pub trait Router: fmt::Debug + Send + Sync {
494    /// Find a route for the given request.
495    async fn route(&self, request: &ProxyRequest) -> Result<Route, ProxyError>;
496
497    /// Get all routes managed by this router.
498    async fn get_routes(&self) -> Vec<Route>;
499
500    /// Add a new route to the router.
501    async fn add_route(&self, route: Route) -> Result<(), ProxyError>;
502
503    /// Remove a route from the router.
504    async fn remove_route(&self, route_id: &str) -> Result<(), ProxyError>;
505}