torrust-actix 4.2.3

A rich, fast and efficient Bittorrent Tracker.
use crate::common::structs::custom_error::CustomError;
use rand::RngExt;

pub const MAX_PERCENT_DECODED_SIZE: usize = 1_048_576;
pub const MAX_PEER_MESSAGE_SIZE: usize = 262_144;
pub const MIN_API_KEY_LENGTH: usize = 32;
pub const DEFAULT_API_KEY_ENTROPY_BYTES: usize = 32;
pub const MAX_INFO_HASH_HEX_LENGTH: usize = 40;
pub const MAX_PEER_ID_HEX_LENGTH: usize = 40;
pub const MAX_SCRAPE_TORRENTS: usize = 100;
pub const MAX_OFFER_ID_LENGTH: usize = 128;
pub const MAX_QUERY_STRING_LENGTH: usize = 8192;

pub fn generate_secure_api_key() -> String {
    let mut rng = rand::rng();
    let bytes: Vec<u8> = (0..32).map(|_| rng.random()).collect();
    use base64::prelude::*;
    BASE64_URL_SAFE_NO_PAD.encode(&bytes)
}

pub fn validate_api_key_strength(api_key: &str) -> bool {
    if api_key.len() < MIN_API_KEY_LENGTH {
        return false;
    }
    let has_lower = api_key.chars().any(|c| c.is_ascii_lowercase());
    let has_upper = api_key.chars().any(|c| c.is_ascii_uppercase());
    let has_digit = api_key.chars().any(|c| c.is_ascii_digit());
    let has_special = api_key.chars().any(|c| !c.is_alphanumeric());
    let variety_count = [has_lower, has_upper, has_digit, has_special]
        .iter()
        .filter(|&&x| x)
        .count();
    variety_count >= 2
}

pub fn constant_time_eq(a: &str, b: &str) -> bool {
    if a.len() != b.len() {
        return false;
    }
    let a_bytes = a.as_bytes();
    let b_bytes = b.as_bytes();
    let mut result = 0u8;
    for (x, y) in a_bytes.iter().zip(b_bytes.iter()) {
        result |= x ^ y;
    }
    result == 0
}

pub fn validate_file_path(path: &str) -> Result<(), CustomError> {
    if path.contains("..") || path.contains("./") || path.contains(".\\") {
        return Err(CustomError::new("Path traversal detected in file path"));
    }
    if path.starts_with('/') || (path.len() > 2 && path[1..].starts_with(":\\")) {
        return Err(CustomError::new("Absolute paths not allowed in certificate configuration"));
    }
    if path.contains('\0') {
        return Err(CustomError::new("Null byte detected in file path"));
    }
    Ok(())
}

pub fn validate_peer_message(message: &str) -> Result<(), CustomError> {
    if message.len() > MAX_PEER_MESSAGE_SIZE {
        return Err(CustomError::new(&format!(
            "Peer message exceeds maximum size of {MAX_PEER_MESSAGE_SIZE} bytes"
        )));
    }
    let suspicious_patterns = ["<script", "javascript:", "data:", "vbscript:"];
    let message_lower = message.to_lowercase();
    for pattern in suspicious_patterns {
        if message_lower.contains(pattern) {
            return Err(CustomError::new("Suspicious content detected in peer message"));
        }
    }
    Ok(())
}

pub fn validate_info_hash_hex(info_hash: &str) -> Result<(), CustomError> {
    if info_hash.len() == 40 && info_hash.chars().all(|c| c.is_ascii_hexdigit()) {
        return Ok(());
    }
    if info_hash.len() >= 15 && info_hash.len() <= 60 {
        return Ok(());
    }

    Err(CustomError::new("info_hash has invalid format"))
}

pub fn validate_peer_id_hex(peer_id: &str) -> Result<(), CustomError> {
    if peer_id.len() == 40 && peer_id.chars().all(|c| c.is_ascii_hexdigit()) {
        return Ok(());
    }
    if peer_id.len() >= 15 && peer_id.len() <= 60 {
        return Ok(());
    }
    Err(CustomError::new("peer_id has invalid format"))
}

pub fn validate_query_string_length(query: &str) -> Result<(), CustomError> {
    if query.len() > MAX_QUERY_STRING_LENGTH {
        return Err(CustomError::new(&format!(
            "Query string exceeds maximum length of {MAX_QUERY_STRING_LENGTH} bytes"
        )));
    }
    Ok(())
}

pub fn validate_remote_ip(ip: &str, trusted_proxies_enabled: bool) -> Result<(), CustomError> {
    use std::net::IpAddr;

    let addr: IpAddr = ip.parse().map_err(|_| CustomError::new("Invalid IP address format"))?;
    if !trusted_proxies_enabled {
        let is_private = match addr {
            IpAddr::V4(ipv4) => {
                ipv4.is_loopback() || ipv4.is_private() || ipv4.is_link_local() || ipv4.is_unspecified()
            }
            IpAddr::V6(ipv6) => {
                ipv6.is_loopback() || ipv6.is_unspecified()
            }
        };
        if is_private {
            return Err(CustomError::new(
                "Private IP addresses not allowed in X-Real-IP header without trusted proxy configuration"
            ));
        }
    }
    Ok(())
}