1use crate::error::{P2PError, P2pResult};
60
61use std::collections::HashMap;
62use std::net::{IpAddr, SocketAddr};
63use std::path::Path;
64use std::sync::Arc;
65use std::time::Duration;
66use thiserror::Error;
67
68const MAX_PEER_ID_LENGTH: usize = 64;
70const MIN_PEER_ID_LENGTH: usize = 16;
71const MAX_MESSAGE_SIZE: usize = 16 * 1024 * 1024; const MAX_PATH_LENGTH: usize = 4096;
73const MAX_KEY_SIZE: usize = 1024 * 1024; const MAX_VALUE_SIZE: usize = 10 * 1024 * 1024; const MAX_FILE_NAME_LENGTH: usize = 255;
76
77const DEFAULT_RATE_LIMIT_WINDOW: Duration = Duration::from_secs(60);
79const DEFAULT_MAX_REQUESTS_PER_WINDOW: u32 = 1000;
80const DEFAULT_BURST_SIZE: u32 = 100;
81
82#[derive(Debug, Error)]
86pub enum ValidationError {
87 #[error("Invalid peer ID format: {0}")]
88 InvalidPeerId(String),
89
90 #[error("Invalid network address: {0}")]
91 InvalidAddress(String),
92
93 #[error("Message size exceeds limit: {size} > {limit}")]
94 MessageTooLarge { size: usize, limit: usize },
95
96 #[error("Invalid file path: {0}")]
97 InvalidPath(String),
98
99 #[error("Path traversal attempt detected: {0}")]
100 PathTraversal(String),
101
102 #[error("Invalid key size: {size} bytes (max: {max})")]
103 InvalidKeySize { size: usize, max: usize },
104
105 #[error("Invalid value size: {size} bytes (max: {max})")]
106 InvalidValueSize { size: usize, max: usize },
107
108 #[error("Invalid cryptographic parameter: {0}")]
109 InvalidCryptoParam(String),
110
111 #[error("Rate limit exceeded for {identifier}")]
112 RateLimitExceeded { identifier: String },
113
114 #[error("Invalid format: {0}")]
115 InvalidFormat(String),
116
117 #[error("Value out of range: {value} (min: {min}, max: {max})")]
118 OutOfRange { value: i64, min: i64, max: i64 },
119}
120
121impl From<ValidationError> for P2PError {
122 fn from(err: ValidationError) -> Self {
123 P2PError::Validation(err.to_string().into())
124 }
125}
126
127#[derive(Debug, Clone)]
129pub struct ValidationContext {
130 pub max_message_size: usize,
131 pub max_key_size: usize,
132 pub max_value_size: usize,
133 pub max_path_length: usize,
134 pub allow_localhost: bool,
135 pub allow_private_ips: bool,
136 pub rate_limiter: Option<Arc<RateLimiter>>,
137}
138
139impl Default for ValidationContext {
140 fn default() -> Self {
141 Self {
142 max_message_size: MAX_MESSAGE_SIZE,
143 max_key_size: MAX_KEY_SIZE,
144 max_value_size: MAX_VALUE_SIZE,
145 max_path_length: MAX_PATH_LENGTH,
146 allow_localhost: false,
147 allow_private_ips: false,
148 rate_limiter: None,
149 }
150 }
151}
152
153impl ValidationContext {
154 pub fn new() -> Self {
156 Self::default()
157 }
158
159 pub fn with_rate_limiting(mut self, limiter: Arc<RateLimiter>) -> Self {
161 self.rate_limiter = Some(limiter);
162 self
163 }
164
165 pub fn allow_localhost(mut self) -> Self {
167 self.allow_localhost = true;
168 self
169 }
170
171 pub fn allow_private_ips(mut self) -> Self {
173 self.allow_private_ips = true;
174 self
175 }
176}
177
178pub trait Validate {
180 fn validate(&self, ctx: &ValidationContext) -> P2pResult<()>;
182}
183
184pub trait Sanitize {
186 fn sanitize(&self) -> Self;
188}
189
190pub fn validate_network_address(addr: &SocketAddr, ctx: &ValidationContext) -> P2pResult<()> {
194 let ip = addr.ip();
195
196 if ip.is_loopback() && !ctx.allow_localhost {
198 return Err(
199 ValidationError::InvalidAddress("Localhost addresses not allowed".to_string()).into(),
200 );
201 }
202
203 if is_private_ip(&ip) && !ctx.allow_private_ips {
205 return Err(ValidationError::InvalidAddress(
206 "Private IP addresses not allowed".to_string(),
207 )
208 .into());
209 }
210
211 if addr.port() == 0 {
213 return Err(ValidationError::InvalidAddress("Port 0 is not allowed".to_string()).into());
214 }
215
216 Ok(())
217}
218
219fn is_private_ip(ip: &IpAddr) -> bool {
221 match ip {
222 IpAddr::V4(ipv4) => ipv4.is_private(),
223 IpAddr::V6(ipv6) => ipv6.is_unique_local() || ipv6.is_unicast_link_local(),
224 }
225}
226
227pub fn validate_peer_id(peer_id: &str) -> P2pResult<()> {
231 if peer_id.len() < MIN_PEER_ID_LENGTH || peer_id.len() > MAX_PEER_ID_LENGTH {
233 return Err(ValidationError::InvalidPeerId(format!(
234 "Length must be between {} and {} characters",
235 MIN_PEER_ID_LENGTH, MAX_PEER_ID_LENGTH
236 ))
237 .into());
238 }
239
240 if !peer_id
241 .chars()
242 .all(|ch| ch.is_alphanumeric() || ch == '_' || ch == '-')
243 {
244 return Err(ValidationError::InvalidPeerId(
245 "Must contain only alphanumeric characters, hyphens, and underscores".to_string(),
246 )
247 .into());
248 }
249
250 Ok(())
251}
252
253pub fn validate_message_size(size: usize, max_size: usize) -> P2pResult<()> {
257 if size > max_size {
258 return Err(ValidationError::MessageTooLarge {
259 size,
260 limit: max_size,
261 }
262 .into());
263 }
264 Ok(())
265}
266
267pub fn validate_file_path(path: &Path) -> P2pResult<()> {
271 let path_str = path.to_string_lossy();
272
273 if path_str.len() > MAX_PATH_LENGTH {
275 return Err(ValidationError::InvalidPath(format!(
276 "Path too long: {} > {}",
277 path_str.len(),
278 MAX_PATH_LENGTH
279 ))
280 .into());
281 }
282
283 let decoded = path_str
285 .replace("%2e", ".")
286 .replace("%2f", "/")
287 .replace("%5c", "\\");
288
289 let traversal_patterns = ["../", "..\\", "..", "..;", "....//", "%2e%2e", "%252e%252e"];
291 for pattern in &traversal_patterns {
292 if path_str.contains(pattern) || decoded.contains(pattern) {
293 return Err(ValidationError::PathTraversal(path_str.to_string()).into());
294 }
295 }
296
297 if path_str.contains('\0') {
299 return Err(ValidationError::InvalidPath("Path contains null bytes".to_string()).into());
300 }
301
302 let dangerous_chars = ['|', '&', ';', '$', '`', '\n'];
304 if path_str.chars().any(|c| dangerous_chars.contains(&c)) {
305 return Err(
306 ValidationError::InvalidPath("Path contains dangerous characters".to_string()).into(),
307 );
308 }
309
310 for component in path.components() {
312 if let Some(name) = component.as_os_str().to_str() {
313 if name.len() > MAX_FILE_NAME_LENGTH {
314 return Err(ValidationError::InvalidPath(format!(
315 "Component '{}' exceeds maximum length",
316 name
317 ))
318 .into());
319 }
320
321 if name.contains('\0') {
323 return Err(ValidationError::InvalidPath(format!(
324 "Component '{}' contains invalid characters",
325 name
326 ))
327 .into());
328 }
329 }
330 }
331
332 Ok(())
333}
334
335pub fn validate_key_size(size: usize, expected: usize) -> P2pResult<()> {
339 if size != expected {
340 return Err(ValidationError::InvalidCryptoParam(format!(
341 "Invalid key size: expected {} bytes, got {}",
342 expected, size
343 ))
344 .into());
345 }
346 Ok(())
347}
348
349pub fn validate_nonce_size(size: usize, expected: usize) -> P2pResult<()> {
351 if size != expected {
352 return Err(ValidationError::InvalidCryptoParam(format!(
353 "Invalid nonce size: expected {} bytes, got {}",
354 expected, size
355 ))
356 .into());
357 }
358 Ok(())
359}
360
361pub fn validate_dht_key(key: &[u8], ctx: &ValidationContext) -> P2pResult<()> {
365 if key.is_empty() {
366 return Err(ValidationError::InvalidFormat("DHT key cannot be empty".to_string()).into());
367 }
368
369 if key.len() > ctx.max_key_size {
370 return Err(ValidationError::InvalidKeySize {
371 size: key.len(),
372 max: ctx.max_key_size,
373 }
374 .into());
375 }
376
377 Ok(())
378}
379
380pub fn validate_dht_value(value: &[u8], ctx: &ValidationContext) -> P2pResult<()> {
382 if value.len() > ctx.max_value_size {
383 return Err(ValidationError::InvalidValueSize {
384 size: value.len(),
385 max: ctx.max_value_size,
386 }
387 .into());
388 }
389
390 Ok(())
391}
392
393#[derive(Debug)]
397pub struct RateLimiter {
398 engine: crate::rate_limit::SharedEngine<IpAddr>,
400 #[allow(dead_code)]
402 config: RateLimitConfig,
403}
404
405#[derive(Debug, Clone)]
407pub struct RateLimitConfig {
408 pub window: Duration,
410 pub max_requests: u32,
412 pub burst_size: u32,
414 pub adaptive: bool,
416 pub cleanup_interval: Duration,
418}
419
420impl Default for RateLimitConfig {
421 fn default() -> Self {
422 Self {
423 window: DEFAULT_RATE_LIMIT_WINDOW,
424 max_requests: DEFAULT_MAX_REQUESTS_PER_WINDOW,
425 burst_size: DEFAULT_BURST_SIZE,
426 adaptive: true,
427 cleanup_interval: Duration::from_secs(300), }
429 }
430}
431
432impl RateLimiter {
435 pub fn new(config: RateLimitConfig) -> Self {
437 let engine_cfg = crate::rate_limit::EngineConfig {
438 window: config.window,
439 max_requests: config.max_requests,
440 burst_size: config.burst_size,
441 };
442 Self {
443 engine: std::sync::Arc::new(crate::rate_limit::Engine::new(engine_cfg)),
444 config,
445 }
446 }
447
448 pub fn check_ip(&self, ip: &IpAddr) -> P2pResult<()> {
450 if !self.engine.try_consume_global() {
452 return Err(ValidationError::RateLimitExceeded {
453 identifier: "global".to_string(),
454 }
455 .into());
456 }
457
458 if !self.engine.try_consume_key(ip) {
460 return Err(ValidationError::RateLimitExceeded {
461 identifier: ip.to_string(),
462 }
463 .into());
464 }
465
466 Ok(())
467 }
468
469 pub fn cleanup(&self) {
471 }
473}
474
475#[derive(Debug)]
479pub struct NetworkMessage {
480 pub peer_id: String,
481 pub payload: Vec<u8>,
482 pub timestamp: u64,
483}
484
485impl Validate for NetworkMessage {
486 fn validate(&self, ctx: &ValidationContext) -> P2pResult<()> {
487 validate_peer_id(&self.peer_id)?;
489
490 validate_message_size(self.payload.len(), ctx.max_message_size)?;
492
493 let now = std::time::SystemTime::now()
495 .duration_since(std::time::UNIX_EPOCH)
496 .map_err(|e| P2PError::Internal(format!("System time error: {}", e).into()))?
497 .as_secs();
498
499 if self.timestamp > now + 300 {
500 return Err(
502 ValidationError::InvalidFormat("Timestamp too far in future".to_string()).into(),
503 );
504 }
505
506 Ok(())
507 }
508}
509
510#[derive(Debug)]
512pub struct ApiRequest {
513 pub method: String,
514 pub path: String,
515 pub params: HashMap<String, String>,
516}
517
518impl Validate for ApiRequest {
519 fn validate(&self, _ctx: &ValidationContext) -> P2pResult<()> {
520 match self.method.as_str() {
522 "GET" | "POST" | "PUT" | "DELETE" => {}
523 _ => {
524 return Err(ValidationError::InvalidFormat(format!(
525 "Invalid HTTP method: {}",
526 self.method
527 ))
528 .into());
529 }
530 }
531
532 if !self.path.starts_with('/') {
534 return Err(
535 ValidationError::InvalidFormat("Path must start with /".to_string()).into(),
536 );
537 }
538
539 if self.path.contains("..") {
540 return Err(ValidationError::PathTraversal(self.path.clone()).into());
541 }
542
543 for (key, value) in &self.params {
545 if key.is_empty() {
546 return Err(
547 ValidationError::InvalidFormat("Empty parameter key".to_string()).into(),
548 );
549 }
550
551 let lower_value = value.to_lowercase();
553 let sql_patterns = [
554 "select ", "insert ", "update ", "delete ", "drop ", "union ", "exec ", "--", "/*",
555 "*/", "'", "\"", " or ", " and ", "1=1", "1='1",
556 ];
557
558 for pattern in &sql_patterns {
559 if lower_value.contains(pattern) {
560 return Err(ValidationError::InvalidFormat(
561 "Suspicious parameter value: potential SQL injection".to_string(),
562 )
563 .into());
564 }
565 }
566
567 let dangerous_chars = ['|', '&', ';', '$', '`', '\n', '\0'];
569 if value.chars().any(|c| dangerous_chars.contains(&c)) {
570 return Err(ValidationError::InvalidFormat(
571 "Dangerous characters in parameter value".to_string(),
572 )
573 .into());
574 }
575 }
576
577 Ok(())
578 }
579}
580
581pub fn validate_config_value<T>(value: &str, min: Option<T>, max: Option<T>) -> P2pResult<T>
583where
584 T: std::str::FromStr + PartialOrd + std::fmt::Display,
585{
586 let parsed = value
587 .parse::<T>()
588 .map_err(|_| ValidationError::InvalidFormat(format!("Failed to parse value: {}", value)))?;
589
590 if let Some(min_val) = min
591 && parsed < min_val
592 {
593 return Err(ValidationError::InvalidFormat(format!(
594 "Value {} is less than minimum {}",
595 parsed, min_val
596 ))
597 .into());
598 }
599
600 if let Some(max_val) = max
601 && parsed > max_val
602 {
603 return Err(ValidationError::InvalidFormat(format!(
604 "Value {} is greater than maximum {}",
605 parsed, max_val
606 ))
607 .into());
608 }
609
610 Ok(parsed)
611}
612
613pub fn sanitize_string(input: &str, max_length: usize) -> String {
615 let mut cleaned = input
617 .replace(['<', '>'], "")
618 .replace("script", "")
619 .replace("javascript:", "")
620 .replace("onerror", "")
621 .replace("onload", "")
622 .replace("onclick", "")
623 .replace("alert", "")
624 .replace("iframe", "");
625
626 cleaned = cleaned.replace('\u{2060}', ""); cleaned = cleaned.replace('\u{ffa0}', ""); cleaned = cleaned.replace('\u{200b}', ""); cleaned = cleaned.replace('\u{200c}', ""); cleaned = cleaned.replace('\u{200d}', ""); cleaned
635 .chars()
636 .filter(|c| c.is_alphanumeric() || *c == '_' || *c == '-' || *c == '.')
637 .take(max_length)
638 .collect()
639}
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644
645 #[test]
646 fn test_peer_id_validation() {
647 assert!(validate_peer_id("valid_peer_id_123").is_ok());
649 assert!(validate_peer_id("PEER-ID-WITH-CAPS").is_ok());
650
651 assert!(validate_peer_id("short").is_err()); assert!(validate_peer_id(&"x".repeat(100)).is_err()); assert!(validate_peer_id("invalid peer id").is_err()); assert!(validate_peer_id("peer@id").is_err()); }
657
658 #[test]
659 fn test_network_address_validation() {
660 let ctx = ValidationContext::default();
661
662 let addr: SocketAddr = "8.8.8.8:53".parse().unwrap();
664 assert!(validate_network_address(&addr, &ctx).is_ok());
665
666 let localhost: SocketAddr = "127.0.0.1:80".parse().unwrap();
668 assert!(validate_network_address(&localhost, &ctx).is_err());
669
670 let ctx_localhost = ValidationContext::default().allow_localhost();
672 assert!(validate_network_address(&localhost, &ctx_localhost).is_ok());
673 }
674
675 #[test]
676 fn test_file_path_validation() {
677 assert!(validate_file_path(Path::new("data/file.txt")).is_ok());
679 assert!(validate_file_path(Path::new("/usr/local/bin")).is_ok());
680
681 assert!(validate_file_path(Path::new("../etc/passwd")).is_err());
683 assert!(validate_file_path(Path::new("file\0name")).is_err());
684 }
685
686 #[test]
687 fn test_rate_limiter() {
688 let config = RateLimitConfig {
689 window: Duration::from_millis(500), max_requests: 10,
691 burst_size: 5,
692 ..Default::default()
693 };
694
695 let limiter = RateLimiter::new(config);
696 let ip: IpAddr = "192.168.1.1".parse().unwrap();
697
698 for _ in 0..5 {
700 assert!(limiter.check_ip(&ip).is_ok());
701 }
702
703 assert!(limiter.check_ip(&ip).is_err()); std::thread::sleep(Duration::from_millis(600));
708 assert!(limiter.check_ip(&ip).is_ok());
709 }
710
711 #[test]
712 fn test_message_validation() {
713 let ctx = ValidationContext::default();
714
715 let valid_msg = NetworkMessage {
716 peer_id: "valid_peer_id_123".to_string(),
717 payload: vec![0u8; 1024],
718 timestamp: std::time::SystemTime::now()
719 .duration_since(std::time::UNIX_EPOCH)
720 .unwrap()
721 .as_secs(),
722 };
723
724 assert!(valid_msg.validate(&ctx).is_ok());
725
726 let invalid_msg = NetworkMessage {
728 peer_id: "short".to_string(),
729 payload: vec![0u8; 1024],
730 timestamp: 0,
731 };
732
733 assert!(invalid_msg.validate(&ctx).is_err());
734 }
735
736 #[test]
737 fn test_sanitization() {
738 assert_eq!(sanitize_string("hello world!", 20), "helloworld");
739
740 assert_eq!(sanitize_string("test@#$%123", 20), "test123");
741
742 assert_eq!(
743 sanitize_string("very_long_string_that_exceeds_limit", 10),
744 "very_long_"
745 );
746 }
747}