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