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