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