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