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