payload-loader 0.1.0

Production-ready Rust loader with cryptography, configuration, and security features
Documentation
//! Security utilities: key sanitization, rate limiting, etc.

use anyhow::{bail, Result};
use std::time::{Duration, Instant};
use tracing::warn;

/// Rate limiter for network requests
pub struct RateLimiter {
    min_interval: Duration,
    last_request: Option<Instant>,
}

impl RateLimiter {
    /// Create a new rate limiter with minimum interval between requests
    pub fn new(min_interval: Duration) -> Self {
        Self {
            min_interval,
            last_request: None,
        }
    }

    /// Check if request is allowed; returns true if allowed, false if rate-limited
    pub fn check_and_update(&mut self) -> bool {
        match self.last_request {
            None => {
                self.last_request = Some(Instant::now());
                true
            }
            Some(last) => {
                if last.elapsed() >= self.min_interval {
                    self.last_request = Some(Instant::now());
                    true
                } else {
                    false
                }
            }
        }
    }

    /// Wait until next request is allowed
    pub async fn wait_if_needed(&mut self) {
        if !self.check_and_update() {
            if let Some(last) = self.last_request {
                let elapsed = last.elapsed();
                let wait_time = self.min_interval.saturating_sub(elapsed);
                if wait_time.as_millis() > 0 {
                    tokio::time::sleep(wait_time).await;
                }
            }
        }
    }
}

/// Validate sensitive operations and sanitize for logging
pub struct SensitiveData;

impl SensitiveData {
    /// Mask a key for logging (shows only first 4 and last 4 chars)
    pub fn mask_key(key_b64: &str) -> String {
        if key_b64.len() <= 8 {
            "***".to_string()
        } else {
            format!(
                "{}...{}",
                &key_b64[..4],
                &key_b64[key_b64.len() - 4..]
            )
        }
    }

    /// Check if URL looks like HTTPS (strict security)
    pub fn validate_url_scheme(url: &str) -> Result<()> {
        if !url.starts_with("https://") {
            warn!("URL does not use HTTPS: {}", url);
            // In production, this could be a hard failure:
            // bail!("only HTTPS URLs are allowed");
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_mask_key() {
        let key = "abcdefghijklmnop";
        let masked = SensitiveData::mask_key(key);
        assert_eq!(masked, "abcd...mnop");

        let short_key = "abc";
        let masked_short = SensitiveData::mask_key(short_key);
        assert_eq!(masked_short, "***");
    }

    #[test]
    fn test_rate_limiter() {
        let mut limiter = RateLimiter::new(Duration::from_millis(100));

        assert!(limiter.check_and_update());
        assert!(!limiter.check_and_update()); // Too soon
    }

    #[tokio::test]
    async fn test_rate_limiter_async() {
        let mut limiter = RateLimiter::new(Duration::from_millis(50));

        assert!(limiter.check_and_update());
        assert!(!limiter.check_and_update());

        limiter.wait_if_needed().await; // Should sleep ~50ms
        assert!(limiter.check_and_update());
    }

    #[test]
    fn test_validate_url() {
        assert!(SensitiveData::validate_url_scheme("https://example.com").is_ok());
        // HTTP should warn but not fail by default
        assert!(SensitiveData::validate_url_scheme("http://example.com").is_ok());
    }
}