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