diff --git a/Cargo.lock b/Cargo.lock index fe88725..3e3eb40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -414,7 +414,9 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", "windows-link", ] @@ -440,6 +442,7 @@ version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ + "anstream", "anstyle", "clap_lex", "strsim", @@ -630,13 +633,11 @@ dependencies = [ "prometheus 0.14.0", "prometheus_exporter", "rand", - "regex", "rlua", "serde", "serde_json", "tempfile", "tokio", - "toml", ] [[package]] @@ -1639,15 +1640,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1904,47 +1896,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" - [[package]] name = "tracing" version = "0.1.41" @@ -2256,15 +2207,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e27d6ad3dac991091e4d35de9ba2d2d00647c5d0fc26c5496dee55984ae111b" -dependencies = [ - "memchr", -] - [[package]] name = "winsafe" version = "0.0.19" diff --git a/Cargo.toml b/Cargo.toml index 7efec6b..c02e277 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,9 +4,9 @@ version = "0.1.0" edition = "2024" [dependencies] -actix-web = { version = "4.3.1" } -chrono = { version = "0.4.41", default-features = false, features = ["std", "clock"] } -clap = { version = "4.5", default-features = false, features = ["std", "derive", "help", "usage", "suggestions"] } +actix-web = "4.3.1" +clap = { version = "4.3", features = ["derive"] } +chrono = "0.4.24" futures = "0.3.28" ipnetwork = "0.21.1" lazy_static = "1.4.0" @@ -20,5 +20,3 @@ tokio = { version = "1.28.0", features = ["full"] } log = "0.4.27" env_logger = "0.11.8" tempfile = "3.19.1" -regex = "1.11.1" -toml = "0.8.22" diff --git a/src/lua/mod.rs b/src/lua/mod.rs index fc96382..ea0c712 100644 --- a/src/lua/mod.rs +++ b/src/lua/mod.rs @@ -472,7 +472,7 @@ impl ScriptManager { result } - /// Generate a deceptive response, calling all `response_gen` handlers + /// Generate a deceptive response, calling all response_gen handlers pub fn generate_response( &self, path: &str, diff --git a/src/main.rs b/src/main.rs index 185c258..197c0a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ use actix_web::{App, HttpResponse, HttpServer, web}; use clap::Parser; use ipnetwork::IpNetwork; -use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::env; @@ -97,7 +96,7 @@ struct Args { #[clap( long, - help = "Path to configuration file (JSON or TOML, overrides command line options)" + help = "Path to JSON configuration file (overrides command line options)" )] config_file: Option, @@ -109,44 +108,6 @@ struct Args { log_level: String, } -// Trap pattern structure that can be either a plain string or regex -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(untagged)] -enum TrapPattern { - Plain(String), - Regex { pattern: String, regex: bool }, -} - -impl TrapPattern { - fn as_plain(value: &str) -> Self { - Self::Plain(value.to_string()) - } - - fn as_regex(value: &str) -> Self { - Self::Regex { - pattern: value.to_string(), - regex: true, - } - } - - fn matches(&self, path: &str) -> bool { - match self { - Self::Plain(pattern) => path.contains(pattern), - Self::Regex { - pattern, - regex: true, - } => { - if let Ok(re) = Regex::new(pattern) { - re.is_match(path) - } else { - false - } - } - _ => false, - } - } -} - // Configuration structure #[derive(Clone, Debug, Deserialize, Serialize)] struct Config { @@ -158,7 +119,7 @@ struct Config { max_delay: u64, max_tarpit_time: u64, block_threshold: u32, - trap_patterns: Vec, + trap_patterns: Vec, whitelist_networks: Vec, markov_corpora_dir: String, lua_scripts_dir: String, @@ -179,37 +140,17 @@ impl Default for Config { max_tarpit_time: 600, block_threshold: 3, trap_patterns: vec![ - // Basic attack patterns as plain strings - TrapPattern::as_plain("/vendor/phpunit"), - TrapPattern::as_plain("eval-stdin.php"), - TrapPattern::as_plain("/wp-admin"), - TrapPattern::as_plain("/wp-login.php"), - TrapPattern::as_plain("/xmlrpc.php"), - TrapPattern::as_plain("/phpMyAdmin"), - TrapPattern::as_plain("/solr/"), - TrapPattern::as_plain("/.env"), - TrapPattern::as_plain("/config"), - TrapPattern::as_plain("/actuator/"), - // More aggressive patterns for various PHP exploits - TrapPattern::as_regex(r"/.*phpunit.*eval-stdin\.php"), - TrapPattern::as_regex(r"/index\.php\?s=/index/\\think\\app/invokefunction"), - TrapPattern::as_regex(r".*%ADd\+auto_prepend_file%3dphp://input.*"), - TrapPattern::as_regex(r".*%ADd\+allow_url_include%3d1.*"), - TrapPattern::as_regex(r".*/wp-content/plugins/.*\.php"), - TrapPattern::as_regex(r".*/wp-content/themes/.*\.php"), - TrapPattern::as_regex(r".*eval\(.*\).*"), - TrapPattern::as_regex(r".*/adminer\.php.*"), - TrapPattern::as_regex(r".*/admin\.php.*"), - TrapPattern::as_regex(r".*/administrator/.*"), - TrapPattern::as_regex(r".*/wp-json/.*"), - TrapPattern::as_regex(r".*/api/.*\.php.*"), - TrapPattern::as_regex(r".*/cgi-bin/.*"), - TrapPattern::as_regex(r".*/owa/.*"), - TrapPattern::as_regex(r".*/ecp/.*"), - TrapPattern::as_regex(r".*/webshell\.php.*"), - TrapPattern::as_regex(r".*/shell\.php.*"), - TrapPattern::as_regex(r".*/cmd\.php.*"), - TrapPattern::as_regex(r".*/struts.*"), + "/vendor/phpunit".to_string(), + "eval-stdin.php".to_string(), + "/wp-admin".to_string(), + "/wp-login.php".to_string(), + "/xmlrpc.php".to_string(), + "/phpMyAdmin".to_string(), + "/solr/".to_string(), + "/.env".to_string(), + "/config".to_string(), + "/api/".to_string(), + "/actuator/".to_string(), ], whitelist_networks: vec![ "192.168.0.0/16".to_string(), @@ -293,65 +234,19 @@ impl Config { } } - // Load configuration from a file (JSON or TOML) + // Load configuration from a JSON file fn load_from_file(path: &Path) -> std::io::Result { let content = fs::read_to_string(path)?; - - let extension = path - .extension() - .map(|ext| ext.to_string_lossy().to_lowercase()) - .unwrap_or_default(); - - let config = match extension.as_str() { - "toml" => toml::from_str(&content).map_err(|e| { - std::io::Error::new( - std::io::ErrorKind::InvalidData, - format!("Failed to parse TOML: {e}"), - ) - })?, - _ => { - // Default to JSON for any other extension - serde_json::from_str(&content).map_err(|e| { - std::io::Error::new( - std::io::ErrorKind::InvalidData, - format!("Failed to parse JSON: {e}"), - ) - })? - } - }; - + let config = serde_json::from_str(&content)?; Ok(config) } - // Save configuration to a file (JSON or TOML) + // Save configuration to a JSON file fn save_to_file(&self, path: &Path) -> std::io::Result<()> { if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } - - let extension = path - .extension() - .map(|ext| ext.to_string_lossy().to_lowercase()) - .unwrap_or_default(); - - let content = match extension.as_str() { - "toml" => toml::to_string_pretty(self).map_err(|e| { - std::io::Error::new( - std::io::ErrorKind::InvalidData, - format!("Failed to serialize to TOML: {e}"), - ) - })?, - _ => { - // Default to JSON for any other extension - serde_json::to_string_pretty(self).map_err(|e| { - std::io::Error::new( - std::io::ErrorKind::InvalidData, - format!("Failed to serialize to JSON: {e}"), - ) - })? - } - }; - + let content = serde_json::to_string_pretty(self)?; fs::write(path, content)?; Ok(()) } @@ -493,20 +388,16 @@ fn find_header_end(data: &[u8]) -> Option { // Extract path from raw request data fn extract_path_from_request(data: &[u8]) -> Option<&str> { - // Get first line from request - let first_line = data + let request_line = data .split(|&b| b == b'\r' || b == b'\n') .next() .filter(|line| !line.is_empty())?; - // Split by spaces and ensure we have at least 3 parts (METHOD PATH VERSION) - let parts: Vec<&[u8]> = first_line.split(|&b| b == b' ').collect(); - if parts.len() < 3 || !parts[2].starts_with(b"HTTP/") { - return None; - } + let mut parts = request_line.split(|&b| b == b' '); + let _ = parts.next()?; // Skip HTTP method + let path = parts.next()?; - // Return the path (second element) - std::str::from_utf8(parts[1]).ok() + std::str::from_utf8(path).ok() } // Extract header value from raw request data @@ -991,9 +882,10 @@ async fn should_tarpit(path: &str, ip: &IpAddr, config: &Config) -> bool { } } - // Use pattern matching based on the trap pattern type (plain string or regex) + // Use a more efficient pattern matching approach + let path_lower = path.to_lowercase(); for pattern in &config.trap_patterns { - if pattern.matches(path) { + if path_lower.contains(pattern) { return true; } } @@ -1196,23 +1088,12 @@ async fn main() -> std::io::Result<()> { if let Err(e) = fs::create_dir_all(&config.config_dir) { log::warn!("Failed to create config directory: {e}"); } else { - // Save both JSON and TOML versions of the config for user reference - let config_path_json = Path::new(&config.config_dir).join("config.json"); - let config_path_toml = Path::new(&config.config_dir).join("config.toml"); - - if !config_path_json.exists() { - if let Err(e) = config.save_to_file(&config_path_json) { - log::warn!("Failed to save JSON configuration: {e}"); + let config_path = Path::new(&config.config_dir).join("config.json"); + if !config_path.exists() { + if let Err(e) = config.save_to_file(&config_path) { + log::warn!("Failed to save default configuration: {e}"); } else { - log::info!("Saved JSON configuration to {config_path_json:?}"); - } - } - - if !config_path_toml.exists() { - if let Err(e) = config.save_to_file(&config_path_toml) { - log::warn!("Failed to save TOML configuration: {e}"); - } else { - log::info!("Saved TOML configuration to {config_path_toml:?}"); + log::info!("Saved default configuration to {config_path:?}"); } } } @@ -1433,27 +1314,6 @@ mod tests { assert_eq!(config.cache_dir, "/tmp/eris/cache"); } - #[test] - fn test_trap_pattern_matching() { - // Test plain string pattern - let plain = TrapPattern::as_plain("phpunit"); - assert!(plain.matches("path/to/phpunit/test")); - assert!(!plain.matches("path/to/something/else")); - - // Test regex pattern - let regex = TrapPattern::as_regex(r".*eval-stdin\.php.*"); - assert!(regex.matches("/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php")); - assert!(regex.matches("/tests/eval-stdin.php?param")); - assert!(!regex.matches("/normal/path")); - - // Test invalid regex pattern (should return false) - let invalid = TrapPattern::Regex { - pattern: "(invalid[regex".to_string(), - regex: true, - }; - assert!(!invalid.matches("anything")); - } - #[tokio::test] async fn test_should_tarpit() { let config = Config::default(); @@ -1512,25 +1372,6 @@ mod tests { ) .await ); - - // Test regex patterns - assert!( - should_tarpit( - "/index.php?s=/index/\\think\\app/invokefunction&function=call_user_func_array&vars[0]=md5&vars[1][]=Hello", - &IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), - &config - ) - .await - ); - - assert!( - should_tarpit( - "/hello.world?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input", - &IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), - &config - ) - .await - ); } #[tokio::test] @@ -1558,7 +1399,6 @@ mod tests { state.blocked.insert(ip1); assert!(state.blocked.contains(&ip1)); assert!(!state.blocked.contains(&ip2)); - drop(state); } // Test active connections @@ -1572,14 +1412,13 @@ mod tests { assert_eq!(state.active_connections.len(), 1); assert!(!state.active_connections.contains(&ip1)); assert!(state.active_connections.contains(&ip2)); - drop(state); } } #[test] fn test_find_header_end() { let data = b"GET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: test\r\n\r\nBody content"; - assert_eq!(find_header_end(data), Some(55)); + assert_eq!(find_header_end(data), Some(53)); let incomplete = b"GET / HTTP/1.1\r\nHost: example.com\r\n"; assert_eq!(find_header_end(incomplete), None); @@ -1634,30 +1473,4 @@ mod tests { assert_eq!(choose_response_type("/api/v1/users"), "api"); assert_eq!(choose_response_type("/index.html"), "generic"); } - - #[test] - fn test_config_file_formats() { - // Create temporary JSON config file - let temp_dir = std::env::temp_dir(); - let json_path = temp_dir.join("temp_config.json"); - let toml_path = temp_dir.join("temp_config.toml"); - - let config = Config::default(); - - // Test JSON serialization and deserialization - config.save_to_file(&json_path).unwrap(); - let loaded_json = Config::load_from_file(&json_path).unwrap(); - assert_eq!(loaded_json.listen_addr, config.listen_addr); - assert_eq!(loaded_json.min_delay, config.min_delay); - - // Test TOML serialization and deserialization - config.save_to_file(&toml_path).unwrap(); - let loaded_toml = Config::load_from_file(&toml_path).unwrap(); - assert_eq!(loaded_toml.listen_addr, config.listen_addr); - assert_eq!(loaded_toml.min_delay, config.min_delay); - - // Clean up - let _ = std::fs::remove_file(json_path); - let _ = std::fs::remove_file(toml_path); - } }