168 lines
5.3 KiB
Rust
168 lines
5.3 KiB
Rust
use std::collections::HashMap;
|
|
use std::hash::Hasher;
|
|
|
|
// Find end of HTTP headers
|
|
pub fn find_header_end(data: &[u8]) -> Option<usize> {
|
|
data.windows(4)
|
|
.position(|window| window == b"\r\n\r\n")
|
|
.map(|pos| pos + 4)
|
|
}
|
|
|
|
// Extract path from raw request data
|
|
pub fn extract_path_from_request(data: &[u8]) -> Option<&str> {
|
|
// Get first line from request
|
|
let first_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;
|
|
}
|
|
|
|
// Return the path (second element)
|
|
std::str::from_utf8(parts[1]).ok()
|
|
}
|
|
|
|
// Extract header value from raw request data
|
|
pub fn extract_header_value(data: &[u8], header_name: &str) -> Option<String> {
|
|
let data_str = std::str::from_utf8(data).ok()?;
|
|
let header_prefix = format!("{header_name}: ").to_lowercase();
|
|
|
|
for line in data_str.lines() {
|
|
let line_lower = line.to_lowercase();
|
|
if line_lower.starts_with(&header_prefix) {
|
|
return Some(line[header_prefix.len()..].trim().to_string());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
// Extract all headers from request data
|
|
pub fn extract_all_headers(data: &[u8]) -> HashMap<String, String> {
|
|
let mut headers = HashMap::new();
|
|
|
|
if let Ok(data_str) = std::str::from_utf8(data) {
|
|
let mut lines = data_str.lines();
|
|
|
|
// Skip the request line
|
|
let _ = lines.next();
|
|
|
|
// Parse headers until empty line
|
|
for line in lines {
|
|
if line.is_empty() {
|
|
break;
|
|
}
|
|
|
|
if let Some(colon_pos) = line.find(':') {
|
|
let key = line[..colon_pos].trim().to_lowercase();
|
|
let value = line[colon_pos + 1..].trim().to_string();
|
|
headers.insert(key, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
headers
|
|
}
|
|
|
|
// Determine response type based on request path
|
|
pub fn choose_response_type(path: &str) -> &'static str {
|
|
if path.contains("phpunit") || path.contains("eval") {
|
|
"php_exploit"
|
|
} else if path.contains("wp-") {
|
|
"wordpress"
|
|
} else if path.contains("api") {
|
|
"api"
|
|
} else {
|
|
"generic"
|
|
}
|
|
}
|
|
|
|
// Get current timestamp in seconds
|
|
pub fn get_timestamp() -> u64 {
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_secs()
|
|
}
|
|
|
|
// Create a unique session ID for tracking a connection
|
|
pub fn generate_session_id(ip: &str, user_agent: &str) -> String {
|
|
let timestamp = get_timestamp();
|
|
let random = rand::random::<u32>();
|
|
|
|
// XXX: Is this fast enough for our case? I don't think hashing is a huge
|
|
// bottleneck, but it's worth revisiting in the future to see if there is
|
|
// an objectively faster algorithm that we can try.
|
|
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
|
std::hash::Hash::hash(&format!("{ip}_{user_agent}_{timestamp}"), &mut hasher);
|
|
let hash = hasher.finish();
|
|
|
|
format!("SID_{hash:x}_{random:x}")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[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));
|
|
|
|
let incomplete = b"GET / HTTP/1.1\r\nHost: example.com\r\n";
|
|
assert_eq!(find_header_end(incomplete), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_path_from_request() {
|
|
let data = b"GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n";
|
|
assert_eq!(extract_path_from_request(data), Some("/index.html"));
|
|
|
|
let bad_data = b"INVALID DATA";
|
|
assert_eq!(extract_path_from_request(bad_data), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_header_value() {
|
|
let data = b"GET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: TestBot/1.0\r\n\r\n";
|
|
assert_eq!(
|
|
extract_header_value(data, "user-agent"),
|
|
Some("TestBot/1.0".to_string())
|
|
);
|
|
assert_eq!(
|
|
extract_header_value(data, "Host"),
|
|
Some("example.com".to_string())
|
|
);
|
|
assert_eq!(extract_header_value(data, "nonexistent"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_all_headers() {
|
|
let data = b"GET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: TestBot/1.0\r\nAccept: */*\r\n\r\n";
|
|
let headers = extract_all_headers(data);
|
|
|
|
assert_eq!(headers.len(), 3);
|
|
assert_eq!(headers.get("host").unwrap(), "example.com");
|
|
assert_eq!(headers.get("user-agent").unwrap(), "TestBot/1.0");
|
|
assert_eq!(headers.get("accept").unwrap(), "*/*");
|
|
}
|
|
|
|
#[test]
|
|
fn test_choose_response_type() {
|
|
assert_eq!(
|
|
choose_response_type("/vendor/phpunit/whatever"),
|
|
"php_exploit"
|
|
);
|
|
assert_eq!(
|
|
choose_response_type("/path/to/eval-stdin.php"),
|
|
"php_exploit"
|
|
);
|
|
assert_eq!(choose_response_type("/wp-admin/login.php"), "wordpress");
|
|
assert_eq!(choose_response_type("/wp-login.php"), "wordpress");
|
|
assert_eq!(choose_response_type("/api/v1/users"), "api");
|
|
assert_eq!(choose_response_type("/index.html"), "generic");
|
|
}
|
|
}
|