347 lines
14 KiB
Rust
347 lines
14 KiB
Rust
|
|
use actix_web::{HttpRequest, dev::ServiceRequest, middleware::Next, dev::ServiceResponse, error::ErrorTooManyRequests};
|
|||
|
|
use log::{warn, error, info};
|
|||
|
|
use redis::{AsyncCommands, aio::MultiplexedConnection};
|
|||
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|||
|
|
use std::collections::HashMap;
|
|||
|
|
use tokio::sync::RwLock;
|
|||
|
|
use std::sync::Arc;
|
|||
|
|
use serde::{Deserialize, Serialize};
|
|||
|
|
|
|||
|
|
/// Конфигурация лимитов запросов
|
|||
|
|
#[derive(Debug, Clone)]
|
|||
|
|
pub struct RateLimitConfig {
|
|||
|
|
/// Максимальное количество запросов в окне времени
|
|||
|
|
pub max_requests: u32,
|
|||
|
|
/// Окно времени в секундах
|
|||
|
|
pub window_seconds: u64,
|
|||
|
|
/// Блокировка на количество секунд при превышении лимита
|
|||
|
|
pub block_duration_seconds: u64,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
impl Default for RateLimitConfig {
|
|||
|
|
fn default() -> Self {
|
|||
|
|
Self {
|
|||
|
|
max_requests: 100, // 100 запросов
|
|||
|
|
window_seconds: 60, // в минуту
|
|||
|
|
block_duration_seconds: 300, // блокировка на 5 минут
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Конфигурация для разных типов запросов
|
|||
|
|
#[derive(Debug, Clone)]
|
|||
|
|
pub struct SecurityConfig {
|
|||
|
|
/// Общий лимит по IP
|
|||
|
|
pub general_rate_limit: RateLimitConfig,
|
|||
|
|
/// Лимит для загрузки файлов
|
|||
|
|
pub upload_rate_limit: RateLimitConfig,
|
|||
|
|
/// Лимит для аутентификации
|
|||
|
|
pub auth_rate_limit: RateLimitConfig,
|
|||
|
|
/// Максимальный размер тела запроса (байты)
|
|||
|
|
pub max_payload_size: usize,
|
|||
|
|
/// Таймаут запроса (секунды)
|
|||
|
|
pub request_timeout_seconds: u64,
|
|||
|
|
/// Максимальная длина пути
|
|||
|
|
pub max_path_length: usize,
|
|||
|
|
/// Максимальное количество заголовков
|
|||
|
|
pub max_headers_count: usize,
|
|||
|
|
/// Максимальная длина значения заголовка
|
|||
|
|
pub max_header_value_length: usize,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
impl Default for SecurityConfig {
|
|||
|
|
fn default() -> Self {
|
|||
|
|
Self {
|
|||
|
|
general_rate_limit: RateLimitConfig::default(),
|
|||
|
|
upload_rate_limit: RateLimitConfig {
|
|||
|
|
max_requests: 10, // 10 загрузок
|
|||
|
|
window_seconds: 300, // в 5 минут
|
|||
|
|
block_duration_seconds: 600, // блокировка на 10 минут
|
|||
|
|
},
|
|||
|
|
auth_rate_limit: RateLimitConfig {
|
|||
|
|
max_requests: 20, // 20 попыток аутентификации
|
|||
|
|
window_seconds: 900, // в 15 минут
|
|||
|
|
block_duration_seconds: 1800, // блокировка на 30 минут
|
|||
|
|
},
|
|||
|
|
max_payload_size: 4000 * 1024 * 1024, // 4000 МБ
|
|||
|
|
request_timeout_seconds: 300, // 5 минут
|
|||
|
|
max_path_length: 1000,
|
|||
|
|
max_headers_count: 50,
|
|||
|
|
max_header_value_length: 8192,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Структура для хранения информации о запросах
|
|||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|||
|
|
pub struct RequestInfo {
|
|||
|
|
pub count: u32,
|
|||
|
|
pub first_request_time: u64,
|
|||
|
|
pub blocked_until: Option<u64>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Менеджер безопасности
|
|||
|
|
pub struct SecurityManager {
|
|||
|
|
pub config: SecurityConfig,
|
|||
|
|
redis: MultiplexedConnection,
|
|||
|
|
// Локальный кэш для быстрых проверок
|
|||
|
|
local_cache: Arc<RwLock<HashMap<String, RequestInfo>>>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
impl SecurityManager {
|
|||
|
|
pub fn new(config: SecurityConfig, redis: MultiplexedConnection) -> Self {
|
|||
|
|
Self {
|
|||
|
|
config,
|
|||
|
|
redis,
|
|||
|
|
local_cache: Arc::new(RwLock::new(HashMap::new())),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Получает IP адрес из запроса, учитывая прокси
|
|||
|
|
pub fn extract_client_ip(req: &HttpRequest) -> String {
|
|||
|
|
// Проверяем заголовки прокси
|
|||
|
|
if let Some(forwarded_for) = req.headers().get("x-forwarded-for") {
|
|||
|
|
if let Ok(forwarded_str) = forwarded_for.to_str() {
|
|||
|
|
if let Some(first_ip) = forwarded_str.split(',').next() {
|
|||
|
|
return first_ip.trim().to_string();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if let Some(real_ip) = req.headers().get("x-real-ip") {
|
|||
|
|
if let Ok(ip_str) = real_ip.to_str() {
|
|||
|
|
return ip_str.to_string();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Fallback к connection info
|
|||
|
|
req.connection_info()
|
|||
|
|
.realip_remote_addr()
|
|||
|
|
.unwrap_or("unknown")
|
|||
|
|
.to_string()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Проверяет лимиты запросов для IP
|
|||
|
|
pub async fn check_rate_limit(&mut self, ip: &str, endpoint_type: &str) -> Result<(), actix_web::Error> {
|
|||
|
|
let config = match endpoint_type {
|
|||
|
|
"upload" => &self.config.upload_rate_limit,
|
|||
|
|
"auth" => &self.config.auth_rate_limit,
|
|||
|
|
_ => &self.config.general_rate_limit,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let current_time = SystemTime::now()
|
|||
|
|
.duration_since(UNIX_EPOCH)
|
|||
|
|
.unwrap()
|
|||
|
|
.as_secs();
|
|||
|
|
|
|||
|
|
let redis_key = format!("rate_limit:{}:{}", endpoint_type, ip);
|
|||
|
|
|
|||
|
|
// Проверяем локальный кэш
|
|||
|
|
{
|
|||
|
|
let cache = self.local_cache.read().await;
|
|||
|
|
if let Some(info) = cache.get(&redis_key) {
|
|||
|
|
if let Some(blocked_until) = info.blocked_until {
|
|||
|
|
if current_time < blocked_until {
|
|||
|
|
warn!("IP {} blocked until {}", ip, blocked_until);
|
|||
|
|
return Err(ErrorTooManyRequests("Rate limit exceeded, IP temporarily blocked"));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Проверяем в Redis
|
|||
|
|
let info_str: Option<String> = self.redis.get(&redis_key).await
|
|||
|
|
.map_err(|e| {
|
|||
|
|
error!("Redis error in rate limit check: {}", e);
|
|||
|
|
actix_web::error::ErrorInternalServerError("Service temporarily unavailable")
|
|||
|
|
})?;
|
|||
|
|
|
|||
|
|
let mut request_info = if let Some(info_str) = info_str {
|
|||
|
|
serde_json::from_str::<RequestInfo>(&info_str)
|
|||
|
|
.unwrap_or_else(|_| RequestInfo {
|
|||
|
|
count: 0,
|
|||
|
|
first_request_time: current_time,
|
|||
|
|
blocked_until: None,
|
|||
|
|
})
|
|||
|
|
} else {
|
|||
|
|
RequestInfo {
|
|||
|
|
count: 0,
|
|||
|
|
first_request_time: current_time,
|
|||
|
|
blocked_until: None,
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Проверяем блокировку
|
|||
|
|
if let Some(blocked_until) = request_info.blocked_until {
|
|||
|
|
if current_time < blocked_until {
|
|||
|
|
warn!("IP {} is blocked until {}", ip, blocked_until);
|
|||
|
|
return Err(ErrorTooManyRequests("Rate limit exceeded, IP temporarily blocked"));
|
|||
|
|
} else {
|
|||
|
|
// Блокировка истекла, сбрасываем
|
|||
|
|
request_info.blocked_until = None;
|
|||
|
|
request_info.count = 0;
|
|||
|
|
request_info.first_request_time = current_time;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Проверяем окно времени
|
|||
|
|
if current_time - request_info.first_request_time > config.window_seconds {
|
|||
|
|
// Новое окно времени, сбрасываем счетчик
|
|||
|
|
request_info.count = 0;
|
|||
|
|
request_info.first_request_time = current_time;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Увеличиваем счетчик
|
|||
|
|
request_info.count += 1;
|
|||
|
|
|
|||
|
|
// Проверяем лимит
|
|||
|
|
if request_info.count > config.max_requests {
|
|||
|
|
warn!("Rate limit exceeded for IP {}: {} requests in window", ip, request_info.count);
|
|||
|
|
|
|||
|
|
// Устанавливаем блокировку
|
|||
|
|
request_info.blocked_until = Some(current_time + config.block_duration_seconds);
|
|||
|
|
|
|||
|
|
// Сохраняем в Redis
|
|||
|
|
let info_str = serde_json::to_string(&request_info).unwrap();
|
|||
|
|
let _: () = self.redis.set_ex(&redis_key, info_str, config.block_duration_seconds).await
|
|||
|
|
.map_err(|e| {
|
|||
|
|
error!("Redis error saving rate limit: {}", e);
|
|||
|
|
actix_web::error::ErrorInternalServerError("Service temporarily unavailable")
|
|||
|
|
})?;
|
|||
|
|
|
|||
|
|
// Обновляем локальный кэш
|
|||
|
|
{
|
|||
|
|
let mut cache = self.local_cache.write().await;
|
|||
|
|
cache.insert(redis_key, request_info);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return Err(ErrorTooManyRequests("Rate limit exceeded, IP temporarily blocked"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Сохраняем обновленную информацию
|
|||
|
|
let info_str = serde_json::to_string(&request_info).unwrap();
|
|||
|
|
let _: () = self.redis.set_ex(&redis_key, info_str, config.window_seconds * 2).await
|
|||
|
|
.map_err(|e| {
|
|||
|
|
error!("Redis error updating rate limit: {}", e);
|
|||
|
|
actix_web::error::ErrorInternalServerError("Service temporarily unavailable")
|
|||
|
|
})?;
|
|||
|
|
|
|||
|
|
let count = request_info.count;
|
|||
|
|
|
|||
|
|
// Обновляем локальный кэш
|
|||
|
|
{
|
|||
|
|
let mut cache = self.local_cache.write().await;
|
|||
|
|
cache.insert(redis_key, request_info);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
info!("Rate limit check passed for IP {}: {}/{} requests", ip, count, config.max_requests);
|
|||
|
|
Ok(())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Проверяет безопасность запроса (размер, заголовки, путь)
|
|||
|
|
pub fn validate_request_security(&self, req: &HttpRequest) -> Result<(), actix_web::Error> {
|
|||
|
|
// Проверка длины пути
|
|||
|
|
let path = req.path();
|
|||
|
|
if path.len() > self.config.max_path_length {
|
|||
|
|
warn!("Request path too long: {} chars", path.len());
|
|||
|
|
return Err(actix_web::error::ErrorBadRequest("Request path too long"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Проверка количества заголовков
|
|||
|
|
if req.headers().len() > self.config.max_headers_count {
|
|||
|
|
warn!("Too many headers: {}", req.headers().len());
|
|||
|
|
return Err(actix_web::error::ErrorBadRequest("Too many headers"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Проверка длины значений заголовков
|
|||
|
|
for (name, value) in req.headers().iter() {
|
|||
|
|
if let Ok(value_str) = value.to_str() {
|
|||
|
|
if value_str.len() > self.config.max_header_value_length {
|
|||
|
|
warn!("Header value too long: {} = {} chars", name, value_str.len());
|
|||
|
|
return Err(actix_web::error::ErrorBadRequest("Header value too long"));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Проверка на подозрительные символы в пути
|
|||
|
|
if path.contains("..") || path.contains('\0') || path.contains('\r') || path.contains('\n') {
|
|||
|
|
warn!("Suspicious characters in path: {}", path);
|
|||
|
|
return Err(actix_web::error::ErrorBadRequest("Invalid characters in path"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Ok(())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Проверяет подозрительные паттерны в пути
|
|||
|
|
pub fn check_suspicious_patterns(&self, path: &str) -> bool {
|
|||
|
|
let suspicious_patterns = [
|
|||
|
|
"/admin", "/wp-admin", "/phpmyadmin", "/.env", "/config",
|
|||
|
|
"/.git", "/backup", "/db", "/sql", "/.well-known/acme-challenge",
|
|||
|
|
"/xmlrpc.php", "/wp-login.php", "/wp-config.php",
|
|||
|
|
"script>", "<iframe", "javascript:", "data:",
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
let path_lower = path.to_lowercase();
|
|||
|
|
for pattern in &suspicious_patterns {
|
|||
|
|
if path_lower.contains(pattern) {
|
|||
|
|
warn!("Suspicious pattern detected in path: {} (pattern: {})", path, pattern);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Очистка старых записей из локального кэша
|
|||
|
|
pub async fn cleanup_cache(&mut self) {
|
|||
|
|
let current_time = SystemTime::now()
|
|||
|
|
.duration_since(UNIX_EPOCH)
|
|||
|
|
.unwrap()
|
|||
|
|
.as_secs();
|
|||
|
|
|
|||
|
|
let mut cache = self.local_cache.write().await;
|
|||
|
|
let mut to_remove = Vec::new();
|
|||
|
|
|
|||
|
|
for (key, info) in cache.iter() {
|
|||
|
|
// Удаляем записи старше 1 часа
|
|||
|
|
if current_time - info.first_request_time > 3600 {
|
|||
|
|
to_remove.push(key.clone());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for key in to_remove {
|
|||
|
|
cache.remove(&key);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
info!("Cleaned {} old entries from security cache", cache.len());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Middleware для проверки безопасности
|
|||
|
|
pub async fn security_middleware(
|
|||
|
|
req: ServiceRequest,
|
|||
|
|
next: Next<impl actix_web::body::MessageBody>,
|
|||
|
|
) -> Result<ServiceResponse<impl actix_web::body::MessageBody>, actix_web::Error> {
|
|||
|
|
let path = req.path().to_string();
|
|||
|
|
let method = req.method().to_string();
|
|||
|
|
|
|||
|
|
// Быстрая проверка на известные атаки
|
|||
|
|
if path.contains("..") || path.contains('\0') || path.len() > 1000 {
|
|||
|
|
warn!("Blocked suspicious request: {} {}", method, path);
|
|||
|
|
return Err(actix_web::error::ErrorBadRequest("Invalid request"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Проверка на bot patterns
|
|||
|
|
if let Some(user_agent) = req.headers().get("user-agent") {
|
|||
|
|
if let Ok(ua_str) = user_agent.to_str() {
|
|||
|
|
let ua_lower = ua_str.to_lowercase();
|
|||
|
|
if ua_lower.contains("bot") || ua_lower.contains("crawler") || ua_lower.contains("spider") {
|
|||
|
|
// Для ботов применяем более строгие лимиты
|
|||
|
|
info!("Bot detected: {}", ua_str);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let res = next.call(req).await?;
|
|||
|
|
Ok(res)
|
|||
|
|
}
|