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    // Collect prefix while buffering chunks for replay
215    let mut captured = Vec::<u8>::new();
216    let mut buffered  = Vec::<Result<Bytes, std::io::Error>>::new();
217
218    while captured.len() < limit {
219        match stream_in.try_next().await {
220            Ok(Some(chunk)) => {
221                let take = cmp::min(limit - captured.len(), chunk.len());
222                captured.extend_from_slice(&chunk[..take]);
223                buffered.push(Ok(chunk));
224            }
225            Ok(None) => break,            // EOF
226            Err(e) => return Err(ProxyError::Other(e.to_string())),
227        }
228    }
229
230    // `stream_in` now yields the remainder of the body.
231    // Concatenate the buffered prefix with the remaining stream.
232    let combined_stream = stream::iter(buffered).chain(
233        stream_in.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)),
234    );
235
236    let new_body = reqwest::Body::wrap_stream(combined_stream);
237    let snippet  = String::from_utf8_lossy(&captured).to_string();
238
239    Ok((new_body, snippet))
240}
241
242/// Configuration for a header modification filter.
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct HeaderFilterConfig {
245    /// Headers to add or replace in the request
246    #[serde(default)]
247    pub add_request_headers: std::collections::HashMap<String, String>,
248
249    /// Headers to remove from the request
250    #[serde(default)]
251    pub remove_request_headers: Vec<String>,
252
253    /// Headers to add or replace in the response
254    #[serde(default)]
255    pub add_response_headers: std::collections::HashMap<String, String>,
256
257    /// Headers to remove from the response
258    #[serde(default)]
259    pub remove_response_headers: Vec<String>,
260}
261
262impl Default for HeaderFilterConfig {
263    fn default() -> Self {
264        Self {
265            add_request_headers: std::collections::HashMap::new(),
266            remove_request_headers: Vec::new(),
267            add_response_headers: std::collections::HashMap::new(),
268            remove_response_headers: Vec::new(),
269        }
270    }
271}
272
273/// A filter that modifies HTTP headers.
274#[derive(Debug)]
275pub struct HeaderFilter {
276    config: HeaderFilterConfig,
277}
278
279impl HeaderFilter {
280    /// Create a new header filter with the given configuration.
281    pub fn new(config: HeaderFilterConfig) -> Self {
282        Self { config }
283    }
284
285    /// Create a new header filter with default configuration.
286    pub fn default() -> Self {
287        Self::new(HeaderFilterConfig::default())
288    }
289
290    /// Apply header modifications to the given header map.
291    fn apply_headers(&self, headers: &mut reqwest::header::HeaderMap,
292                     add_headers: &std::collections::HashMap<String, String>,
293                     remove_headers: &[String]) {
294        // Remove headers
295        for header_name in remove_headers {
296            if let Ok(name) = reqwest::header::HeaderName::from_bytes(header_name.as_bytes()) {
297                headers.remove(&name);
298            }
299        }
300
301        // Add or replace headers
302        for (name, value) in add_headers {
303            if let (Ok(header_name), Ok(header_value)) = (
304                reqwest::header::HeaderName::from_bytes(name.as_bytes()),
305                reqwest::header::HeaderValue::from_str(value)
306            ) {
307                headers.insert(header_name, header_value);
308            }
309        }
310    }
311}
312
313#[async_trait]
314impl Filter for HeaderFilter {
315    fn filter_type(&self) -> FilterType {
316        FilterType::Both
317    }
318
319    fn name(&self) -> &str {
320        "header"
321    }
322
323    async fn pre_filter(&self, mut request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
324        self.apply_headers(
325            &mut request.headers,
326            &self.config.add_request_headers,
327            &self.config.remove_request_headers
328        );
329
330        Ok(request)
331    }
332
333    async fn post_filter(&self, _request: ProxyRequest, mut response: ProxyResponse) -> Result<ProxyResponse, ProxyError> {
334        self.apply_headers(
335            &mut response.headers,
336            &self.config.add_response_headers,
337            &self.config.remove_response_headers
338        );
339
340        Ok(response)
341    }
342}
343
344/// Configuration for a timeout filter.
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct TimeoutFilterConfig {
347    /// Timeout in milliseconds
348    pub timeout_ms: u64,
349}
350
351impl Default for TimeoutFilterConfig {
352    fn default() -> Self {
353        Self {
354            timeout_ms: 30000, // 30 seconds
355        }
356    }
357}
358
359/// A filter that enforces request timeouts.
360#[derive(Debug)]
361pub struct TimeoutFilter {
362    config: TimeoutFilterConfig,
363}
364
365impl TimeoutFilter {
366    /// Create a new timeout filter with the given configuration.
367    pub fn new(config: TimeoutFilterConfig) -> Self {
368        Self { config }
369    }
370
371    /// Create a new timeout filter with default configuration.
372    pub fn default() -> Self {
373        Self::new(TimeoutFilterConfig::default())
374    }
375}
376
377#[async_trait]
378impl Filter for TimeoutFilter {
379    fn filter_type(&self) -> FilterType {
380        FilterType::Pre
381    }
382
383    fn name(&self) -> &str {
384        "timeout"
385    }
386
387    async fn pre_filter(&self, request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
388        // Store the timeout in the request context
389        match request.context.write().await {
390            mut context => {
391                context.attributes.insert(
392                    "timeout_ms".to_string(),
393                    serde_json::to_value(self.config.timeout_ms).unwrap()
394                );
395            }
396        }
397
398        Ok(request)
399    }
400}
401
402/// Configuration for a path rewrite filter.
403#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct PathRewriteFilterConfig {
405    /// The pattern to match (regex)
406    pub pattern: String,
407    /// The replacement pattern
408    pub replacement: String,
409    /// Whether to apply on the request path
410    #[serde(default = "default_true")]
411    pub rewrite_request: bool,
412    /// Whether to apply on the response path (if found in headers or body)
413    #[serde(default = "default_false")]
414    pub rewrite_response: bool,
415}
416
417/// A filter that rewrites request and response paths based on regex patterns.
418#[derive(Debug)]
419pub struct PathRewriteFilter {
420    /// The configuration for this filter
421    config: PathRewriteFilterConfig,
422    /// Compiled regex for path matching
423    regex: Regex,
424}
425
426impl PathRewriteFilter {
427    /// Create a new path rewrite filter with the given configuration.
428    pub fn new(config: PathRewriteFilterConfig) -> Self {
429        // Compile the regex
430        let regex = Regex::new(&config.pattern)
431            .expect("Failed to compile path rewrite pattern");
432
433        Self { config, regex }
434    }
435
436    /// Create a new path rewrite filter with default configuration.
437    pub fn default() -> Self {
438        Self::new(PathRewriteFilterConfig {
439            pattern: "(.*)".to_string(),
440            replacement: "$1".to_string(),
441            rewrite_request: true,
442            rewrite_response: false,
443        })
444    }
445}
446
447#[async_trait]
448impl Filter for PathRewriteFilter {
449    fn filter_type(&self) -> FilterType {
450        FilterType::Both
451    }
452
453    fn name(&self) -> &str {
454        "path_rewrite"
455    }
456
457    async fn pre_filter(&self, mut request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
458        if self.config.rewrite_request {
459            // Apply path rewriting on the request path
460            let rewritten_path = self.regex.replace_all(&request.path, &self.config.replacement).to_string();
461
462            if rewritten_path != request.path {
463                debug!("Rewriting path from {} to {}", request.path, rewritten_path);
464                request.path = rewritten_path;
465            }
466        }
467
468        Ok(request)
469    }
470
471    async fn post_filter(&self, _request: ProxyRequest, response: ProxyResponse) -> Result<ProxyResponse, ProxyError> {
472        // For now, we don't implement response path rewriting 
473        // as it would require parsing and modifying the response body
474        // which is complex and content-type dependent
475
476        Ok(response)
477    }
478}
479
480/// Factory for creating filters based on configuration.
481#[derive(Debug)]
482pub struct FilterFactory;
483
484impl FilterFactory {
485    /// Create a filter based on the filter type and configuration.
486    pub fn create_filter(filter_type: &str, config: serde_json::Value) -> Result<Arc<dyn Filter>, ProxyError> {
487        match filter_type {
488            "logging" => {
489                let config: LoggingFilterConfig = serde_json::from_value(config)
490                    .map_err(|e| ProxyError::FilterError(format!("Invalid logging filter config: {}", e)))?;
491                Ok(Arc::new(LoggingFilter::new(config)))
492            },
493            "header" => {
494                let config: HeaderFilterConfig = serde_json::from_value(config)
495                    .map_err(|e| ProxyError::FilterError(format!("Invalid header filter config: {}", e)))?;
496                Ok(Arc::new(HeaderFilter::new(config)))
497            },
498            "timeout" => {
499                let config: TimeoutFilterConfig = serde_json::from_value(config)
500                    .map_err(|e| ProxyError::FilterError(format!("Invalid timeout filter config: {}", e)))?;
501                Ok(Arc::new(TimeoutFilter::new(config)))
502            },
503            "path_rewrite" => {
504                let config: PathRewriteFilterConfig = serde_json::from_value(config)
505                    .map_err(|e| ProxyError::FilterError(format!("Invalid path rewrite filter config: {}", e)))?;
506                Ok(Arc::new(PathRewriteFilter::new(config)))
507            },
508            _ => Err(ProxyError::FilterError(format!("Unknown filter type: {}", filter_type))),
509        }
510    }
511}