use async_trait::async_trait;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::Predicate;
use crate::core::{HttpMethod, ProxyError, ProxyRequest};
use crate::warn_fmt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathPredicateConfig {
pub pattern: String,
}
#[derive(Debug)]
pub struct PathPredicate {
#[allow(dead_code)]
config: PathPredicateConfig,
regex: Regex,
}
impl PathPredicate {
pub fn new(config: PathPredicateConfig) -> Result<Self, ProxyError> {
let regex_pattern = Self::pattern_to_regex(&config.pattern);
let regex = Regex::new(®ex_pattern).map_err(|e| {
ProxyError::RoutingError(format!(
"Invalid path predicate regex pattern '{}': {}",
config.pattern, e
))
})?;
Ok(Self { config, regex })
}
fn pattern_to_regex(pattern: &str) -> String {
let mut regex_pattern = "^".to_string();
let mut chars = pattern.chars().peekable();
while let Some(c) = chars.next() {
match c {
':' => {
let mut param_name = String::new();
while let Some(&next_char) = chars.peek() {
if next_char.is_alphanumeric() || next_char == '_' {
param_name.push(chars.next().unwrap());
} else {
break;
}
}
regex_pattern.push_str("([^/]+)");
}
'*' => {
regex_pattern.push_str("(.*)");
}
'.' | '^' | '$' | '|' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '\\' => {
regex_pattern.push('\\');
regex_pattern.push(c);
}
_ => {
regex_pattern.push(c);
}
}
}
regex_pattern.push('$');
regex_pattern
}
}
#[async_trait]
impl Predicate for PathPredicate {
async fn matches(&self, request: &ProxyRequest) -> bool {
self.regex.is_match(&request.path)
}
fn predicate_type(&self) -> &str {
"path"
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MethodPredicateConfig {
pub methods: Vec<HttpMethod>,
}
#[derive(Debug)]
pub struct MethodPredicate {
config: MethodPredicateConfig,
}
impl MethodPredicate {
pub fn new(config: MethodPredicateConfig) -> Self {
Self { config }
}
}
#[async_trait]
impl Predicate for MethodPredicate {
async fn matches(&self, request: &ProxyRequest) -> bool {
self.config.methods.contains(&request.method)
}
fn predicate_type(&self) -> &str {
"method"
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeaderPredicateConfig {
pub headers: HashMap<String, String>,
#[serde(default)]
pub exact_match: bool,
}
#[derive(Debug)]
pub struct HeaderPredicate {
config: HeaderPredicateConfig,
}
impl HeaderPredicate {
pub fn new(config: HeaderPredicateConfig) -> Self {
Self { config }
}
}
#[async_trait]
impl Predicate for HeaderPredicate {
async fn matches(&self, request: &ProxyRequest) -> bool {
for (name, expected_value) in &self.config.headers {
if let Some(header_value) = request.headers.get(name) {
if let Ok(actual_value) = header_value.to_str() {
if self.config.exact_match {
if actual_value != expected_value {
return false;
}
} else {
if !actual_value.contains(expected_value) {
return false;
}
}
} else {
return false;
}
} else {
return false;
}
}
true
}
fn predicate_type(&self) -> &str {
"header"
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryPredicateConfig {
pub params: HashMap<String, String>,
#[serde(default)]
pub exact_match: bool,
}
#[derive(Debug)]
pub struct QueryPredicate {
config: QueryPredicateConfig,
}
impl QueryPredicate {
pub fn new(config: QueryPredicateConfig) -> Self {
Self { config }
}
fn validate_query_value(key: &str, value: &str) -> String {
let mut sanitized = value.to_string();
let mut warnings = Vec::new();
if value.contains('\r') || value.contains('\n') {
warnings.push("CRLF injection");
sanitized = sanitized.replace(['\r', '\n'], "");
}
if value.contains("../") || value.contains("..\\") {
warnings.push("path traversal");
}
let xss_patterns = ["<script", "javascript:", "onload=", "onerror=", "onclick="];
for pattern in &xss_patterns {
if value.to_lowercase().contains(pattern) {
warnings.push("potential XSS");
break;
}
}
let sql_patterns = [
"union select",
"drop table",
"insert into",
"delete from",
"'or'1'='1",
];
let lower = value.to_lowercase();
for pattern in &sql_patterns {
if lower.contains(pattern) {
warnings.push("potential SQL injection");
break;
}
}
let cmd_patterns = [";", "|", "&", "`", "$", "$("];
for pattern in &cmd_patterns {
if value.contains(pattern) {
warnings.push("potential command injection");
break;
}
}
if value.contains('\0') {
warnings.push("null byte injection");
sanitized = sanitized.replace('\0', "");
}
if !warnings.is_empty() {
warn_fmt!(
"QueryPredicate",
"Suspicious query parameter detected - key: '{}', value: '{}', patterns: [{}]",
key,
value,
warnings.join(", ")
);
}
sanitized
}
fn parse_query_params(query: &str) -> HashMap<String, String> {
let mut params = HashMap::new();
const MAX_QUERY_LENGTH: usize = 8192;
if query.len() > MAX_QUERY_LENGTH {
warn_fmt!(
"QueryPredicate",
"Query string too long: {} bytes (max: {})",
query.len(),
MAX_QUERY_LENGTH
);
return params;
}
for pair in query.split('&') {
let mut iter = pair.split('=');
if let (Some(key), Some(value)) = (iter.next(), iter.next()) {
let decoded_key = urlencoding::decode(key).unwrap_or_else(|_| {
warn_fmt!("QueryPredicate", "Failed to URL decode key: {}", key);
key.into()
});
let decoded_value = urlencoding::decode(value).unwrap_or_else(|_| {
warn_fmt!("QueryPredicate", "Failed to URL decode value: {}", value);
value.into()
});
let sanitized_value = Self::validate_query_value(&decoded_key, &decoded_value);
params.insert(decoded_key.to_string(), sanitized_value);
}
}
params
}
}
#[async_trait]
impl Predicate for QueryPredicate {
async fn matches(&self, request: &ProxyRequest) -> bool {
if self.config.params.is_empty() {
return true;
}
if let Some(query) = &request.query {
let params = Self::parse_query_params(query);
for (name, expected_value) in &self.config.params {
if let Some(actual_value) = params.get(name) {
if self.config.exact_match {
if actual_value != expected_value {
return false;
}
} else {
if !actual_value.contains(expected_value) {
return false;
}
}
} else {
return false;
}
}
true
} else {
false
}
}
fn predicate_type(&self) -> &str {
"query"
}
}