foxy/router/
predicates.rs1use 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};
16use crate::warn_fmt;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct PathPredicateConfig {
21 pub pattern: String,
23}
24
25#[derive(Debug)]
27pub struct PathPredicate {
28 #[allow(dead_code)]
30 config: PathPredicateConfig,
31 regex: Regex,
33}
34
35impl PathPredicate {
36 pub fn new(config: PathPredicateConfig) -> Result<Self, ProxyError> {
38 let regex_pattern = Self::pattern_to_regex(&config.pattern);
40
41 let regex = Regex::new(®ex_pattern).map_err(|e| {
43 ProxyError::RoutingError(format!(
44 "Invalid path predicate regex pattern '{}': {}",
45 config.pattern, e
46 ))
47 })?;
48
49 Ok(Self { config, regex })
50 }
51
52 fn pattern_to_regex(pattern: &str) -> String {
54 let mut regex_pattern = "^".to_string();
55
56 let mut chars = pattern.chars().peekable();
57 while let Some(c) = chars.next() {
58 match c {
59 ':' => {
61 let mut param_name = String::new();
62 while let Some(&next_char) = chars.peek() {
63 if next_char.is_alphanumeric() || next_char == '_' {
64 param_name.push(chars.next().unwrap());
65 } else {
66 break;
67 }
68 }
69
70 regex_pattern.push_str("([^/]+)");
72 }
73 '*' => {
75 regex_pattern.push_str("(.*)");
76 }
77 '.' | '^' | '$' | '|' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '\\' => {
79 regex_pattern.push('\\');
80 regex_pattern.push(c);
81 }
82 _ => {
84 regex_pattern.push(c);
85 }
86 }
87 }
88
89 regex_pattern.push('$');
90 regex_pattern
91 }
92}
93
94#[async_trait]
95impl Predicate for PathPredicate {
96 async fn matches(&self, request: &ProxyRequest) -> bool {
97 self.regex.is_match(&request.path)
98 }
99
100 fn predicate_type(&self) -> &str {
101 "path"
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct MethodPredicateConfig {
108 pub methods: Vec<HttpMethod>,
110}
111
112#[derive(Debug)]
114pub struct MethodPredicate {
115 config: MethodPredicateConfig,
117}
118
119impl MethodPredicate {
120 pub fn new(config: MethodPredicateConfig) -> Self {
122 Self { config }
123 }
124}
125
126#[async_trait]
127impl Predicate for MethodPredicate {
128 async fn matches(&self, request: &ProxyRequest) -> bool {
129 self.config.methods.contains(&request.method)
130 }
131
132 fn predicate_type(&self) -> &str {
133 "method"
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct HeaderPredicateConfig {
140 pub headers: HashMap<String, String>,
142 #[serde(default)]
144 pub exact_match: bool,
145}
146
147#[derive(Debug)]
149pub struct HeaderPredicate {
150 config: HeaderPredicateConfig,
152}
153
154impl HeaderPredicate {
155 pub fn new(config: HeaderPredicateConfig) -> Self {
157 Self { config }
158 }
159}
160
161#[async_trait]
162impl Predicate for HeaderPredicate {
163 async fn matches(&self, request: &ProxyRequest) -> bool {
164 for (name, expected_value) in &self.config.headers {
165 if let Some(header_value) = request.headers.get(name) {
167 if let Ok(actual_value) = header_value.to_str() {
169 if self.config.exact_match {
170 if actual_value != expected_value {
172 return false;
173 }
174 } else {
175 if !actual_value.contains(expected_value) {
177 return false;
178 }
179 }
180 } else {
181 return false;
183 }
184 } else {
185 return false;
187 }
188 }
189
190 true
192 }
193
194 fn predicate_type(&self) -> &str {
195 "header"
196 }
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct QueryPredicateConfig {
202 pub params: HashMap<String, String>,
204 #[serde(default)]
206 pub exact_match: bool,
207}
208
209#[derive(Debug)]
211pub struct QueryPredicate {
212 config: QueryPredicateConfig,
214}
215
216impl QueryPredicate {
217 pub fn new(config: QueryPredicateConfig) -> Self {
219 Self { config }
220 }
221
222 fn validate_query_value(key: &str, value: &str) -> String {
227 let mut sanitized = value.to_string();
228 let mut warnings = Vec::new();
229
230 if value.contains('\r') || value.contains('\n') {
232 warnings.push("CRLF injection");
233 sanitized = sanitized.replace(['\r', '\n'], "");
234 }
235
236 if value.contains("../") || value.contains("..\\") {
238 warnings.push("path traversal");
239 }
240
241 let xss_patterns = ["<script", "javascript:", "onload=", "onerror=", "onclick="];
243 for pattern in &xss_patterns {
244 if value.to_lowercase().contains(pattern) {
245 warnings.push("potential XSS");
246 break;
247 }
248 }
249
250 let sql_patterns = [
252 "union select",
253 "drop table",
254 "insert into",
255 "delete from",
256 "'or'1'='1",
257 ];
258 let lower = value.to_lowercase();
259 for pattern in &sql_patterns {
260 if lower.contains(pattern) {
261 warnings.push("potential SQL injection");
262 break;
263 }
264 }
265
266 let cmd_patterns = [";", "|", "&", "`", "$", "$("];
268 for pattern in &cmd_patterns {
269 if value.contains(pattern) {
270 warnings.push("potential command injection");
271 break;
272 }
273 }
274
275 if value.contains('\0') {
277 warnings.push("null byte injection");
278 sanitized = sanitized.replace('\0', "");
279 }
280
281 if !warnings.is_empty() {
283 warn_fmt!(
284 "QueryPredicate",
285 "Suspicious query parameter detected - key: '{}', value: '{}', patterns: [{}]",
286 key,
287 value,
288 warnings.join(", ")
289 );
290 }
291
292 sanitized
293 }
294
295 fn parse_query_params(query: &str) -> HashMap<String, String> {
297 let mut params = HashMap::new();
298
299 const MAX_QUERY_LENGTH: usize = 8192;
301 if query.len() > MAX_QUERY_LENGTH {
302 warn_fmt!(
303 "QueryPredicate",
304 "Query string too long: {} bytes (max: {})",
305 query.len(),
306 MAX_QUERY_LENGTH
307 );
308 return params;
309 }
310
311 for pair in query.split('&') {
312 let mut iter = pair.split('=');
313 if let (Some(key), Some(value)) = (iter.next(), iter.next()) {
314 let decoded_key = urlencoding::decode(key).unwrap_or_else(|_| {
316 warn_fmt!("QueryPredicate", "Failed to URL decode key: {}", key);
317 key.into()
318 });
319 let decoded_value = urlencoding::decode(value).unwrap_or_else(|_| {
320 warn_fmt!("QueryPredicate", "Failed to URL decode value: {}", value);
321 value.into()
322 });
323
324 let sanitized_value = Self::validate_query_value(&decoded_key, &decoded_value);
326
327 params.insert(decoded_key.to_string(), sanitized_value);
328 }
329 }
330
331 params
332 }
333}
334
335#[async_trait]
336impl Predicate for QueryPredicate {
337 async fn matches(&self, request: &ProxyRequest) -> bool {
338 if self.config.params.is_empty() {
340 return true;
341 }
342
343 if let Some(query) = &request.query {
345 let params = Self::parse_query_params(query);
346
347 for (name, expected_value) in &self.config.params {
348 if let Some(actual_value) = params.get(name) {
350 if self.config.exact_match {
351 if actual_value != expected_value {
353 return false;
354 }
355 } else {
356 if !actual_value.contains(expected_value) {
358 return false;
359 }
360 }
361 } else {
362 return false;
364 }
365 }
366
367 true
369 } else {
370 false
371 }
372 }
373
374 fn predicate_type(&self) -> &str {
375 "query"
376 }
377}