foxy/router/
predicates.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//! Predicate implementations for router matching.
6//!
7//! This module provides various predicates that can be used for route matching.
8
9use std::collections::HashMap;
10use async_trait::async_trait;
11use regex::Regex;
12use serde::{Serialize, Deserialize};
13
14use crate::core::{ProxyRequest, HttpMethod, ProxyError};
15use super::Predicate;
16
17/// Configuration for a path predicate.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct PathPredicateConfig {
20    /// The path pattern to match
21    pub pattern: String,
22}
23
24/// A predicate that matches on request path.
25#[derive(Debug)]
26pub struct PathPredicate {
27    /// The configuration for this predicate
28    #[allow(dead_code)]
29    config: PathPredicateConfig,
30    /// Compiled regex for path matching
31    regex: Regex,
32}
33
34impl PathPredicate {
35    /// Create a new path predicate with the given configuration.
36    pub fn new(config: PathPredicateConfig) -> Result<Self, ProxyError> {
37        // Convert the path pattern to a regex
38        let regex_pattern = Self::pattern_to_regex(&config.pattern);
39
40        // Compile the regex
41        let regex = Regex::new(&regex_pattern)
42            .map_err(|e| ProxyError::RoutingError(format!("Invalid path predicate regex pattern '{}': {}", config.pattern, e)))?;
43
44        Ok(Self { config, regex })
45    }
46
47    /// Convert a path pattern to a regex pattern.
48    fn pattern_to_regex(pattern: &str) -> String {
49        let mut regex_pattern = "^".to_string();
50
51        let mut chars = pattern.chars().peekable();
52        while let Some(c) = chars.next() {
53            match c {
54                // Handle path parameters like :id
55                ':' => {
56                    let mut param_name = String::new();
57                    while let Some(&next_char) = chars.peek() {
58                        if next_char.is_alphanumeric() || next_char == '_' {
59                            param_name.push(chars.next().unwrap());
60                        } else {
61                            break;
62                        }
63                    }
64
65                    // Add a capturing group for the parameter
66                    regex_pattern.push_str("([^/]+)");
67                },
68                // Handle wildcards like *
69                '*' => {
70                    regex_pattern.push_str("(.*)");
71                },
72                // Escape special regex characters
73                '.' | '^' | '$' | '|' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '\\' => {
74                    regex_pattern.push('\\');
75                    regex_pattern.push(c);
76                },
77                // Regular characters
78                _ => {
79                    regex_pattern.push(c);
80                }
81            }
82        }
83
84        regex_pattern.push('$');
85        regex_pattern
86    }
87}
88
89#[async_trait]
90impl Predicate for PathPredicate {
91    async fn matches(&self, request: &ProxyRequest) -> bool {
92        self.regex.is_match(&request.path)
93    }
94
95    fn predicate_type(&self) -> &str {
96        "path"
97    }
98}
99
100/// Configuration for a method predicate.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct MethodPredicateConfig {
103    /// The HTTP methods to match
104    pub methods: Vec<HttpMethod>,
105}
106
107/// A predicate that matches on HTTP method.
108#[derive(Debug)]
109pub struct MethodPredicate {
110    /// The configuration for this predicate
111    config: MethodPredicateConfig,
112}
113
114impl MethodPredicate {
115    /// Create a new method predicate with the given configuration.
116    pub fn new(config: MethodPredicateConfig) -> Self {
117        Self { config }
118    }
119}
120
121#[async_trait]
122impl Predicate for MethodPredicate {
123    async fn matches(&self, request: &ProxyRequest) -> bool {
124        self.config.methods.contains(&request.method)
125    }
126
127    fn predicate_type(&self) -> &str {
128        "method"
129    }
130}
131
132/// Configuration for a header predicate.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct HeaderPredicateConfig {
135    /// The headers to match (name and value)
136    pub headers: HashMap<String, String>,
137    /// Whether to require exact match for header values
138    #[serde(default)]
139    pub exact_match: bool,
140}
141
142/// A predicate that matches on request headers.
143#[derive(Debug)]
144pub struct HeaderPredicate {
145    /// The configuration for this predicate
146    config: HeaderPredicateConfig,
147}
148
149impl HeaderPredicate {
150    /// Create a new header predicate with the given configuration.
151    pub fn new(config: HeaderPredicateConfig) -> Self {
152        Self { config }
153    }
154}
155
156#[async_trait]
157impl Predicate for HeaderPredicate {
158    async fn matches(&self, request: &ProxyRequest) -> bool {
159        for (name, expected_value) in &self.config.headers {
160            // Try to get the header
161            if let Some(header_value) = request.headers.get(name) {
162                // Convert to string for comparison
163                if let Ok(actual_value) = header_value.to_str() {
164                    if self.config.exact_match {
165                        // Exact match
166                        if actual_value != expected_value {
167                            return false;
168                        }
169                    } else {
170                        // Contains match
171                        if !actual_value.contains(expected_value) {
172                            return false;
173                        }
174                    }
175                } else {
176                    // Not a valid UTF-8 string
177                    return false;
178                }
179            } else {
180                // Header not found
181                return false;
182            }
183        }
184
185        // All headers matched
186        true
187    }
188
189    fn predicate_type(&self) -> &str {
190        "header"
191    }
192}
193
194/// Configuration for a query parameter predicate.
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct QueryPredicateConfig {
197    /// The query parameters to match (name and value)
198    pub params: HashMap<String, String>,
199    /// Whether to require exact match for parameter values
200    #[serde(default)]
201    pub exact_match: bool,
202}
203
204/// A predicate that matches on query parameters.
205#[derive(Debug)]
206pub struct QueryPredicate {
207    /// The configuration for this predicate
208    config: QueryPredicateConfig,
209}
210
211impl QueryPredicate {
212    /// Create a new query predicate with the given configuration.
213    pub fn new(config: QueryPredicateConfig) -> Self {
214        Self { config }
215    }
216
217    /// Parse query parameters from a query string.
218    fn parse_query_params(query: &str) -> HashMap<String, String> {
219        let mut params = HashMap::new();
220
221        for pair in query.split('&') {
222            let mut iter = pair.split('=');
223            if let (Some(key), Some(value)) = (iter.next(), iter.next()) {
224                params.insert(key.to_string(), value.to_string());
225            }
226        }
227
228        params
229    }
230}
231
232#[async_trait]
233impl Predicate for QueryPredicate {
234    async fn matches(&self, request: &ProxyRequest) -> bool {
235        // If no query parameters to match, then it's a match
236        if self.config.params.is_empty() {
237            return true;
238        }
239
240        // If the request has no query string, it's not a match
241        if let Some(query) = &request.query {
242            let params = Self::parse_query_params(query);
243
244            for (name, expected_value) in &self.config.params {
245                // Try to get the parameter
246                if let Some(actual_value) = params.get(name) {
247                    if self.config.exact_match {
248                        // Exact match
249                        if actual_value != expected_value {
250                            return false;
251                        }
252                    } else {
253                        // Contains match
254                        if !actual_value.contains(expected_value) {
255                            return false;
256                        }
257                    }
258                } else {
259                    // Parameter not found
260                    return false;
261                }
262            }
263
264            // All parameters matched
265            true
266        } else {
267            false
268        }
269    }
270
271    fn predicate_type(&self) -> &str {
272        "query"
273    }
274}