feat(rustproxy): introduce a Rust-powered proxy engine and workspace with core crates for proxy functionality, ACME/TLS support, passthrough and HTTP proxies, metrics, nftables integration, routing/security, management IPC, tests, and README updates
This commit is contained in:
158
rust/crates/rustproxy-config/src/validation.rs
Normal file
158
rust/crates/rustproxy-config/src/validation.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::route_types::{RouteConfig, RouteActionType};
|
||||
|
||||
/// Validation errors for route configurations.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ValidationError {
|
||||
#[error("Route '{name}' has no targets but action type is 'forward'")]
|
||||
MissingTargets { name: String },
|
||||
|
||||
#[error("Route '{name}' has empty targets list")]
|
||||
EmptyTargets { name: String },
|
||||
|
||||
#[error("Route '{name}' has no ports specified")]
|
||||
NoPorts { name: String },
|
||||
|
||||
#[error("Route '{name}' port {port} is invalid (must be 1-65535)")]
|
||||
InvalidPort { name: String, port: u16 },
|
||||
|
||||
#[error("Route '{name}': socket-handler action type is not supported in JSON config")]
|
||||
SocketHandlerInJson { name: String },
|
||||
|
||||
#[error("Route '{name}': duplicate route ID '{id}'")]
|
||||
DuplicateId { name: String, id: String },
|
||||
|
||||
#[error("Route '{name}': {message}")]
|
||||
Custom { name: String, message: String },
|
||||
}
|
||||
|
||||
/// Validate a single route configuration.
|
||||
pub fn validate_route(route: &RouteConfig) -> Result<(), Vec<ValidationError>> {
|
||||
let mut errors = Vec::new();
|
||||
let name = route.name.clone().unwrap_or_else(|| {
|
||||
route.id.clone().unwrap_or_else(|| "unnamed".to_string())
|
||||
});
|
||||
|
||||
// Check ports
|
||||
let ports = route.listening_ports();
|
||||
if ports.is_empty() {
|
||||
errors.push(ValidationError::NoPorts { name: name.clone() });
|
||||
}
|
||||
for &port in &ports {
|
||||
if port == 0 {
|
||||
errors.push(ValidationError::InvalidPort {
|
||||
name: name.clone(),
|
||||
port,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check forward action has targets
|
||||
if route.action.action_type == RouteActionType::Forward {
|
||||
match &route.action.targets {
|
||||
None => {
|
||||
errors.push(ValidationError::MissingTargets { name: name.clone() });
|
||||
}
|
||||
Some(targets) if targets.is_empty() => {
|
||||
errors.push(ValidationError::EmptyTargets { name: name.clone() });
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate an entire list of routes.
|
||||
pub fn validate_routes(routes: &[RouteConfig]) -> Result<(), Vec<ValidationError>> {
|
||||
let mut all_errors = Vec::new();
|
||||
let mut seen_ids = std::collections::HashSet::new();
|
||||
|
||||
for route in routes {
|
||||
// Check for duplicate IDs
|
||||
if let Some(id) = &route.id {
|
||||
if !seen_ids.insert(id.clone()) {
|
||||
let name = route.name.clone().unwrap_or_else(|| id.clone());
|
||||
all_errors.push(ValidationError::DuplicateId {
|
||||
name,
|
||||
id: id.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Validate individual route
|
||||
if let Err(errors) = validate_route(route) {
|
||||
all_errors.extend(errors);
|
||||
}
|
||||
}
|
||||
|
||||
if all_errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(all_errors)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::route_types::*;
|
||||
|
||||
fn make_valid_route() -> RouteConfig {
|
||||
crate::helpers::create_http_route("example.com", "localhost", 8080)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_route_passes() {
|
||||
let route = make_valid_route();
|
||||
assert!(validate_route(&route).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_targets() {
|
||||
let mut route = make_valid_route();
|
||||
route.action.targets = None;
|
||||
let errors = validate_route(&route).unwrap_err();
|
||||
assert!(errors.iter().any(|e| matches!(e, ValidationError::MissingTargets { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_targets() {
|
||||
let mut route = make_valid_route();
|
||||
route.action.targets = Some(vec![]);
|
||||
let errors = validate_route(&route).unwrap_err();
|
||||
assert!(errors.iter().any(|e| matches!(e, ValidationError::EmptyTargets { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_port_zero() {
|
||||
let mut route = make_valid_route();
|
||||
route.route_match.ports = PortRange::Single(0);
|
||||
let errors = validate_route(&route).unwrap_err();
|
||||
assert!(errors.iter().any(|e| matches!(e, ValidationError::InvalidPort { port: 0, .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_ids() {
|
||||
let mut r1 = make_valid_route();
|
||||
r1.id = Some("route-1".to_string());
|
||||
let mut r2 = make_valid_route();
|
||||
r2.id = Some("route-1".to_string());
|
||||
let errors = validate_routes(&[r1, r2]).unwrap_err();
|
||||
assert!(errors.iter().any(|e| matches!(e, ValidationError::DuplicateId { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_errors_collected() {
|
||||
let mut r1 = make_valid_route();
|
||||
r1.action.targets = None; // MissingTargets
|
||||
r1.route_match.ports = PortRange::Single(0); // InvalidPort
|
||||
let errors = validate_route(&r1).unwrap_err();
|
||||
assert!(errors.len() >= 2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user