foxy/filters/
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//! Built-in filters
6//!
7//! Filters are **opt-in** – you must reference them in the `filters` array of
8//! a `route` for them to execute.  Each filter is documented below together
9//! with its configuration schema.
10
11#[cfg(test)]
12mod tests;
13
14use std::cmp;
15use std::sync::Arc;
16use std::time::Instant;
17use async_trait::async_trait;
18use bytes::Bytes;
19use futures_util::{stream, StreamExt, TryStreamExt};
20use http_body_util::BodyExt;
21use log::{trace, debug, info, warn, error, Level};
22use regex::Regex;
23use serde::{Serialize, Deserialize};
24use once_cell::sync::Lazy;
25use std::collections::HashMap;
26use std::sync::RwLock;
27
28use crate::core::{
29    Filter, FilterType, ProxyRequest, ProxyResponse, ProxyError
30};
31
32/// Constructor signature every dynamic filter must implement
33pub type FilterConstructor =
34fn(serde_json::Value) -> Result<Arc<dyn Filter>, ProxyError>;
35
36
37/// Global registry – `register_filter()` writes to it,
38/// `FilterFactory::create_filter()` reads from it.
39static FILTER_REGISTRY: Lazy<RwLock<HashMap<String, FilterConstructor>>> =
40    Lazy::new(|| RwLock::new(HashMap::new()));
41
42/// Register a filter under a unique name.
43/// Call this **before** you build Foxy:
44///
45/// ```rust
46/// use log::Level::Debug;
47/// use foxy::{filters::register_filter, Filter};
48///
49/// #[derive(Debug)]
50/// struct MyFilter;
51/// impl MyFilter {
52///     fn new(_cfg: serde_json::Value) -> Self { Self }
53/// }
54///
55/// #[async_trait::async_trait]
56/// impl foxy::Filter for MyFilter {
57///     fn filter_type(&self) -> foxy::FilterType { foxy::FilterType::Pre }
58///     fn name(&self) -> &str { "my_filter" }
59/// }
60///
61/// register_filter("my_filter", |cfg| {
62///     // turn `cfg` → your filter instance
63///     Ok(std::sync::Arc::new(MyFilter::new(cfg)))
64/// });
65/// ```
66pub fn register_filter(name: &str, ctor: FilterConstructor) {
67    FILTER_REGISTRY
68        .write()
69        .expect("FILTER_REGISTRY poisoned")
70        .insert(name.to_string(), ctor);
71}
72
73/// Internal helper – fetch a constructor if somebody registered one.
74fn get_registered_filter(name: &str) -> Option<FilterConstructor> {
75    FILTER_REGISTRY
76        .read()
77        .expect("FILTER_REGISTRY poisoned")
78        .get(name)
79        .copied()
80}
81
82/// Configuration for a logging filter.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct LoggingFilterConfig {
85    /// Whether to log request headers
86    #[serde(default = "default_true")]
87    pub log_request_headers: bool,
88
89    /// Whether to log request body
90    #[serde(default = "default_false")]
91    pub log_request_body: bool,
92
93    /// Whether to log response headers
94    #[serde(default = "default_true")]
95    pub log_response_headers: bool,
96
97    /// Whether to log response body
98    #[serde(default = "default_false")]
99    pub log_response_body: bool,
100
101    /// Log level to use
102    #[serde(default = "default_log_level")]
103    pub log_level: String,
104
105    /// Maximum body size to log (in bytes)
106    #[serde(default = "default_max_body_size")]
107    pub max_body_size: usize,
108}
109
110fn default_true() -> bool {
111    true
112}
113
114fn default_false() -> bool {
115    false
116}
117
118fn default_log_level() -> String {
119    "trace".to_string()
120}
121
122fn default_max_body_size() -> usize {
123    1024 // Default to 1KB
124}
125
126impl Default for LoggingFilterConfig {
127    fn default() -> Self {
128        Self {
129            log_request_headers: true,
130            log_request_body: false,
131            log_response_headers: true,
132            log_response_body: false,
133            log_level: "trace".to_string(),
134            max_body_size: 1024,
135        }
136    }
137}
138
139/// A filter that logs HTTP requests and responses.
140#[derive(Debug)]
141pub struct LoggingFilter {
142    config: LoggingFilterConfig,
143}
144
145impl LoggingFilter {
146    /// Create a new logging filter with the given configuration.
147    pub fn new(config: LoggingFilterConfig) -> Self {
148        Self { config }
149    }
150
151    /// Create a new logging filter with default configuration.
152    pub fn default() -> Self {
153        Self::new(LoggingFilterConfig::default())
154    }
155
156    /// Get the log level from the configuration.
157    fn get_log_level(&self) -> Level {
158        match self.config.log_level.to_lowercase().as_str() {
159            "error" => Level::Error,
160            "warn" => Level::Warn,
161            "info" => Level::Info,
162            "debug" => Level::Debug,
163            "trace" => Level::Trace,
164            _ => Level::Trace,
165        }
166    }
167
168    /// Log a message at the configured log level.
169    fn log(&self, message: &str) {
170        match self.get_log_level() {
171            Level::Error => error!("{}", message),
172            Level::Warn => warn!("{}", message),
173            Level::Info => info!("{}", message),
174            Level::Debug => debug!("{}", message),
175            Level::Trace => trace!("{}", message),
176        }
177    }
178
179    /// Format headers for logging.
180    fn format_headers(&self, headers: &reqwest::header::HeaderMap) -> String {
181        let mut header_lines = Vec::new();
182        for (name, value) in headers.iter() {
183            if let Ok(value_str) = value.to_str() {
184                header_lines.push(format!("{}: {}", name, value_str));
185            }
186        }
187        header_lines.join("\n")
188    }
189
190    /// Format body for logging (with size limits).
191    fn format_body(&self, body: &[u8]) -> String {
192        if body.is_empty() {
193            return "[Empty body]".to_string();
194        }
195
196        let body_size = body.len();
197
198        if body_size > self.config.max_body_size {
199            return format!(
200                "[Body truncated, showing {}/{} bytes]\n{}",
201                self.config.max_body_size,
202                body_size,
203                String::from_utf8_lossy(&body[0..self.config.max_body_size])
204            );
205        }
206
207        String::from_utf8_lossy(body).to_string()
208    }
209}
210
211#[async_trait]
212impl Filter for LoggingFilter {
213    fn filter_type(&self) -> FilterType {
214        FilterType::Both
215    }
216
217    fn name(&self) -> &str {
218        "logging"
219    }
220
221    async fn pre_filter(&self, mut request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
222        if self.config.log_request_headers {
223            self.log(&format!(">> {} {}", request.method, request.path));
224            for (k, v) in request.headers.iter() {
225                self.log(&format!(">> {}: {:?}", k, v));
226            }
227        }
228        if self.config.log_request_body {
229            let (new_body, snippet) = tee_body(request.body, 1_000).await?;
230            let truncated = if snippet.len() == 1000 {"(truncated)"} else {""};
231            
232            self.log(&format!(">> Request Body:\n{}{}", snippet, truncated));
233            request.body = new_body;
234        }
235        Ok(request)
236    }
237
238    async fn post_filter(
239        &self,
240        _req: ProxyRequest,
241        mut response: ProxyResponse,
242    ) -> Result<ProxyResponse, ProxyError> {
243        if self.config.log_response_headers {
244            self.log(&format!("<< {}", response.status));
245            for (k, v) in response.headers.iter() {
246                self.log(&format!("<< {}: {:?}", k, v));
247            }
248        }
249        if self.config.log_response_body {
250            let (new_body, snippet) = tee_body(response.body, 1_000).await?;
251            let truncated = if snippet.len() == 1000 {"(truncated)"} else {""};
252
253            self.log(&format!(">> Response Body:\n{}{}", snippet, truncated));
254            response.body = new_body;
255        }
256        Ok(response)
257    }
258}
259
260async fn tee_body(
261    body: reqwest::Body,
262    limit: usize,
263) -> Result<(reqwest::Body, String), ProxyError> {
264    // Turn the body into a stream of Bytes
265    let mut stream_in = body.into_data_stream();
266    
267    // Create a buffer to capture the first `limit` bytes
268    let mut captured = Vec::<u8>::with_capacity(limit);
269    
270    // Create a vector to collect chunks for replay
271    let mut chunks = Vec::new();
272    
273    // Read chunks until we have enough bytes or reach EOF
274    while captured.len() < limit {
275        match stream_in.next().await {
276            Some(Ok(chunk)) => {
277                // Store the chunk for replay
278                let chunk_clone = chunk.clone();
279                chunks.push(Ok(chunk));
280                
281                // Capture bytes up to the limit
282                if captured.len() < limit {
283                    let remaining = limit - captured.len();
284                    let take = cmp::min(remaining, chunk_clone.len());
285                    captured.extend_from_slice(&chunk_clone[..take]);
286                }
287            }
288            Some(Err(e)) => return Err(ProxyError::Other(e.to_string())),
289            None => break, // EOF
290        }
291    }
292    
293    // Create a stream that yields our buffered chunks followed by any remaining chunks
294    let combined_stream = futures_util::stream::iter(chunks)
295        .chain(stream_in.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)));
296    
297    // Wrap the stream back into a reqwest::Body
298    let new_body = reqwest::Body::wrap_stream(combined_stream);
299    
300    // Convert captured bytes to string
301    let snippet = String::from_utf8_lossy(&captured).to_string();
302    
303    Ok((new_body, snippet))
304}
305
306/// Configuration for a header modification filter.
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct HeaderFilterConfig {
309    /// Headers to add or replace in the request
310    #[serde(default)]
311    pub add_request_headers: std::collections::HashMap<String, String>,
312
313    /// Headers to remove from the request
314    #[serde(default)]
315    pub remove_request_headers: Vec<String>,
316
317    /// Headers to add or replace in the response
318    #[serde(default)]
319    pub add_response_headers: std::collections::HashMap<String, String>,
320
321    /// Headers to remove from the response
322    #[serde(default)]
323    pub remove_response_headers: Vec<String>,
324}
325
326impl Default for HeaderFilterConfig {
327    fn default() -> Self {
328        Self {
329            add_request_headers: std::collections::HashMap::new(),
330            remove_request_headers: Vec::new(),
331            add_response_headers: std::collections::HashMap::new(),
332            remove_response_headers: Vec::new(),
333        }
334    }
335}
336
337/// A filter that modifies HTTP headers.
338#[derive(Debug)]
339pub struct HeaderFilter {
340    config: HeaderFilterConfig,
341}
342
343impl HeaderFilter {
344    /// Create a new header filter with the given configuration.
345    pub fn new(config: HeaderFilterConfig) -> Self {
346        Self { config }
347    }
348
349    /// Create a new header filter with default configuration.
350    pub fn default() -> Self {
351        Self::new(HeaderFilterConfig::default())
352    }
353
354    /// Apply header modifications to the given header map.
355    fn apply_headers(&self, headers: &mut reqwest::header::HeaderMap,
356                     add_headers: &std::collections::HashMap<String, String>,
357                     remove_headers: &[String]) {
358        // Remove headers
359        for header_name in remove_headers {
360            if let Ok(name) = reqwest::header::HeaderName::from_bytes(header_name.as_bytes()) {
361                headers.remove(&name);
362            }
363        }
364
365        // Add or replace headers
366        for (name, value) in add_headers {
367            if let (Ok(header_name), Ok(header_value)) = (
368                reqwest::header::HeaderName::from_bytes(name.as_bytes()),
369                reqwest::header::HeaderValue::from_str(value)
370            ) {
371                headers.insert(header_name, header_value);
372            }
373        }
374    }
375}
376
377#[async_trait]
378impl Filter for HeaderFilter {
379    fn filter_type(&self) -> FilterType {
380        FilterType::Both
381    }
382
383    fn name(&self) -> &str {
384        "header"
385    }
386
387    async fn pre_filter(&self, mut request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
388        self.apply_headers(
389            &mut request.headers,
390            &self.config.add_request_headers,
391            &self.config.remove_request_headers
392        );
393
394        Ok(request)
395    }
396
397    async fn post_filter(&self, _request: ProxyRequest, mut response: ProxyResponse) -> Result<ProxyResponse, ProxyError> {
398        self.apply_headers(
399            &mut response.headers,
400            &self.config.add_response_headers,
401            &self.config.remove_response_headers
402        );
403
404        Ok(response)
405    }
406}
407
408/// Configuration for a timeout filter.
409#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct TimeoutFilterConfig {
411    /// Timeout in milliseconds
412    pub timeout_ms: u64,
413}
414
415impl Default for TimeoutFilterConfig {
416    fn default() -> Self {
417        Self {
418            timeout_ms: 30000, // 30 seconds
419        }
420    }
421}
422
423/// A filter that enforces request timeouts.
424#[derive(Debug)]
425pub struct TimeoutFilter {
426    config: TimeoutFilterConfig,
427}
428
429impl TimeoutFilter {
430    /// Create a new timeout filter with the given configuration.
431    pub fn new(config: TimeoutFilterConfig) -> Self {
432        Self { config }
433    }
434
435    /// Create a new timeout filter with default configuration.
436    pub fn default() -> Self {
437        Self::new(TimeoutFilterConfig::default())
438    }
439}
440
441#[async_trait]
442impl Filter for TimeoutFilter {
443    fn filter_type(&self) -> FilterType {
444        FilterType::Pre
445    }
446
447    fn name(&self) -> &str {
448        "timeout"
449    }
450
451    async fn pre_filter(&self, request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
452        // Store the timeout in the request context
453        match request.context.write().await {
454            mut context => {
455                context.attributes.insert(
456                    "timeout_ms".to_string(),
457                    serde_json::to_value(self.config.timeout_ms).unwrap()
458                );
459            }
460        }
461
462        Ok(request)
463    }
464}
465
466/// Configuration for a path rewrite filter.
467#[derive(Debug, Clone, Serialize, Deserialize)]
468pub struct PathRewriteFilterConfig {
469    /// The pattern to match (regex)
470    pub pattern: String,
471    /// The replacement pattern
472    pub replacement: String,
473    /// Whether to apply on the request path
474    #[serde(default = "default_true")]
475    pub rewrite_request: bool,
476    /// Whether to apply on the response path (if found in headers or body)
477    #[serde(default = "default_false")]
478    pub rewrite_response: bool,
479}
480
481/// A filter that rewrites request and response paths based on regex patterns.
482#[derive(Debug)]
483pub struct PathRewriteFilter {
484    /// The configuration for this filter
485    config: PathRewriteFilterConfig,
486    /// Compiled regex for path matching
487    regex: Regex,
488}
489
490impl PathRewriteFilter {
491    /// Create a new path rewrite filter with the given configuration.
492    pub fn new(config: PathRewriteFilterConfig) -> Result<Self, ProxyError> {
493        // Compile the regex
494        let regex = Regex::new(&config.pattern)
495            .map_err(|e| {
496                let err = ProxyError::FilterError(format!("Invalid regex pattern '{}': {}", config.pattern, e));
497                log::error!("{}", err);
498                err
499            })?;
500
501        Ok(Self { config, regex })
502    }
503
504    /// Create a new path rewrite filter with default configuration.
505    pub fn default() -> Result<Self, ProxyError> {
506        Self::new(PathRewriteFilterConfig {
507            pattern: "(.*)".to_string(),
508            replacement: "$1".to_string(),
509            rewrite_request: true,
510            rewrite_response: false,
511        })
512    }
513}
514
515#[async_trait]
516impl Filter for PathRewriteFilter {
517    fn filter_type(&self) -> FilterType {
518        FilterType::Both
519    }
520
521    fn name(&self) -> &str {
522        "path_rewrite"
523    }
524
525    async fn pre_filter(&self, mut request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
526        if self.config.rewrite_request {
527            // Apply path rewriting on the request path
528            let original_path = request.path.clone();
529            let rewritten_path = self.regex.replace_all(&request.path, &self.config.replacement).to_string();
530
531            if rewritten_path != original_path {
532                log::debug!("Rewriting path from {} to {}", original_path, rewritten_path);
533                request.path = rewritten_path;
534            } else {
535                log::trace!("Path rewrite pattern matched but did not change path: {}", original_path);
536            }
537        }
538
539        Ok(request)
540    }
541
542    async fn post_filter(&self, _request: ProxyRequest, response: ProxyResponse) -> Result<ProxyResponse, ProxyError> {
543        if self.config.rewrite_response {
544            log::debug!("Response path rewriting is configured but not implemented yet");
545            // TODO: Implement response path rewriting when needed
546            // This would require parsing and modifying the response body
547            // which is complex and content-type dependent
548        }
549
550        Ok(response)
551    }
552}
553
554/// Factory for creating filters based on configuration.
555#[derive(Debug)]
556pub struct FilterFactory;
557
558impl FilterFactory {
559    /// Create a filter based on the filter type and configuration.
560    pub fn create_filter(filter_type: &str, config: serde_json::Value) -> Result<Arc<dyn Filter>, ProxyError> {
561        log::debug!("Creating filter of type '{}' with config: {}", filter_type, config);
562
563        // See if we've got an external filter registered of that name
564        if let Some(ctor) = get_registered_filter(filter_type) {
565            return ctor(config);
566        }
567        
568        match filter_type {
569            "logging" => {
570                let config: LoggingFilterConfig = serde_json::from_value(config)
571                    .map_err(|e| {
572                        let err = ProxyError::FilterError(format!("Invalid logging filter config: {}", e));
573                        log::error!("{}", err);
574                        err
575                    })?;
576                Ok(Arc::new(LoggingFilter::new(config)))
577            },
578            "header" => {
579                let config: HeaderFilterConfig = serde_json::from_value(config)
580                    .map_err(|e| {
581                        let err = ProxyError::FilterError(format!("Invalid header filter config: {}", e));
582                        log::error!("{}", err);
583                        err
584                    })?;
585                Ok(Arc::new(HeaderFilter::new(config)))
586            },
587            "timeout" => {
588                let config: TimeoutFilterConfig = serde_json::from_value(config)
589                    .map_err(|e| {
590                        let err = ProxyError::FilterError(format!("Invalid timeout filter config: {}", e));
591                        log::error!("{}", err);
592                        err
593                    })?;
594                Ok(Arc::new(TimeoutFilter::new(config)))
595            },
596            "path_rewrite" => {
597                let config: PathRewriteFilterConfig = serde_json::from_value(config)
598                    .map_err(|e| {
599                        let err = ProxyError::FilterError(format!("Invalid path rewrite filter config: {}", e));
600                        log::error!("{}", err);
601                        err
602                    })?;
603                
604                match PathRewriteFilter::new(config) {
605                    Ok(filter) => Ok(Arc::new(filter)),
606                    Err(e) => {
607                        log::error!("Failed to create path rewrite filter: {}", e);
608                        Err(e)
609                    }
610                }
611            },
612            _ => {
613                let err = ProxyError::FilterError(format!("Unknown filter type: {}", filter_type));
614                log::error!("{}", err);
615                Err(err)
616            },
617        }
618    }
619}