feat: Implement audit logging and rate limiting middleware with SurrealDB integration

- Added audit logging middleware to track admin actions and save logs to SurrealDB.
- Introduced rate limiting middleware for public endpoints and authentication endpoints.
- Enhanced security headers middleware with nonce generation for CSP in development.
- Created utility functions for extracting real client IP addresses from headers.
- Updated Cargo.toml and Cargo.lock to include new dependencies.
- Added new schemas for audit logs and rate limiting in the entities module.
- Refactored permissions middleware to support new permission checks.
This commit is contained in:
MythEclipse
2025-10-12 01:54:34 +07:00
parent b6b5f48055
commit 6f596efadd
13 changed files with 732 additions and 111 deletions
+144
View File
@@ -0,0 +1,144 @@
use axum::http::HeaderMap;
/// Extract real client IP address from various headers commonly used in proxies
///
/// Priority order:
/// 1. X-Forwarded-For (first IP in the list)
/// 2. X-Real-IP
/// 3. CF-Connecting-IP (Cloudflare)
/// 4. True-Client-IP (Akamai and others)
/// 5. X-Cluster-Client-IP
/// 6. Forwarded (standard header)
/// 7. Direct connection IP (if available)
pub fn extract_real_ip(headers: &HeaderMap) -> Option<String> {
// Try different headers in priority order
if let Some(ip) = extract_from_x_forwarded_for(headers) {
return Some(ip);
}
if let Some(ip) = extract_header_value(headers, "x-real-ip") {
return Some(ip);
}
if let Some(ip) = extract_header_value(headers, "cf-connecting-ip") {
return Some(ip);
}
if let Some(ip) = extract_header_value(headers, "true-client-ip") {
return Some(ip);
}
if let Some(ip) = extract_header_value(headers, "x-cluster-client-ip") {
return Some(ip);
}
if let Some(ip) = extract_from_forwarded_header(headers) {
return Some(ip);
}
None
}
/// Extract the first IP from X-Forwarded-For header
fn extract_from_x_forwarded_for(headers: &HeaderMap) -> Option<String> {
let header_value = headers.get("x-forwarded-for")?;
let header_str = header_value.to_str().ok()?;
// X-Forwarded-For can contain multiple IPs separated by commas
// We take the first one (the original client IP)
header_str.split(',').next()
.map(|ip| ip.trim().to_string())
.filter(|ip| is_valid_ip(ip))
}
/// Extract IP from Forwarded header (RFC 7239)
fn extract_from_forwarded_header(headers: &HeaderMap) -> Option<String> {
let header_value = headers.get("forwarded")?;
let header_str = header_value.to_str().ok()?;
// Parse Forwarded header: for=192.0.2.60;proto=http;by=203.0.113.43
for part in header_str.split(';') {
if part.trim().starts_with("for=") {
let ip = part.trim().trim_start_matches("for=");
// Remove quotes and brackets if present
let ip = ip.trim_matches('"').trim_matches('[').trim_matches(']');
if is_valid_ip(ip) {
return Some(ip.to_string());
}
}
}
None
}
/// Extract value from a specific header
fn extract_header_value(headers: &HeaderMap, header_name: &str) -> Option<String> {
let header_value = headers.get(header_name)?;
let value_str = header_value.to_str().ok()?;
if is_valid_ip(value_str) {
Some(value_str.to_string())
} else {
None
}
}
/// Basic IP validation
fn is_valid_ip(ip: &str) -> bool {
// Simple validation - check if it looks like an IP address
if ip.is_empty() || ip == "unknown" || ip == "undefined" {
return false;
}
// Check for IPv4 pattern
if ip.split('.').count() == 4 && ip.chars().all(|c| c.is_ascii_digit() || c == '.') {
return true;
}
// Check for IPv6 pattern (simplified)
if ip.contains(':') {
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::HeaderValue;
#[test]
fn test_extract_from_x_forwarded_for() {
let mut headers = HeaderMap::new();
headers.insert("x-forwarded-for", HeaderValue::from_static("192.168.1.1, 10.0.0.1"));
assert_eq!(extract_from_x_forwarded_for(&headers), Some("192.168.1.1".to_string()));
}
#[test]
fn test_extract_from_forwarded_header() {
let mut headers = HeaderMap::new();
headers.insert("forwarded", HeaderValue::from_static("for=192.168.1.1;proto=https"));
assert_eq!(extract_from_forwarded_header(&headers), Some("192.168.1.1".to_string()));
}
#[test]
fn test_extract_real_ip_priority() {
let mut headers = HeaderMap::new();
headers.insert("x-forwarded-for", HeaderValue::from_static("192.168.1.1"));
headers.insert("x-real-ip", HeaderValue::from_static("10.0.0.1"));
// Should prefer x-forwarded-for
assert_eq!(extract_real_ip(&headers), Some("192.168.1.1".to_string()));
}
#[test]
fn test_invalid_ip_rejection() {
let mut headers = HeaderMap::new();
headers.insert("x-forwarded-for", HeaderValue::from_static("unknown"));
assert_eq!(extract_real_ip(&headers), None);
}
}
+2
View File
@@ -9,6 +9,7 @@
pub mod bind_filter;
pub mod csrf_token;
pub mod extract_email;
pub mod extract_ip;
pub mod generate_date;
pub mod generate_otp;
pub mod get_id;
@@ -25,6 +26,7 @@ pub mod validator;
pub use bind_filter::bind_filter_value;
pub use csrf_token::{generate_csrf_token, generate_oauth_csrf_token, validate_csrf_token, validate_oauth_csrf_token};
pub use extract_email::{extract_email, extract_email_async, extract_email_token, extract_email_token_async};
pub use extract_ip::extract_real_ip;
pub use generate_date::get_iso_date;
pub use generate_otp::OtpManager;
pub use get_id::{extract_id, get_id};