Skip to main content

ipfrs_interface/
middleware.rs

1//! HTTP Middleware for IPFRS Gateway
2//!
3//! Provides:
4//! - Authentication and authorization middleware using JWT tokens and API keys
5//! - CORS middleware for cross-origin requests
6//! - Rate limiting middleware for DoS prevention
7//! - Compression middleware for bandwidth optimization
8//! - Caching middleware for HTTP caching headers
9
10use crate::auth::{AuthError, AuthState, Claims, Permission};
11use axum::{
12    body::Body,
13    extract::{Request, State},
14    http::{header, HeaderMap, HeaderValue, Method, StatusCode},
15    middleware::Next,
16    response::{IntoResponse, Response},
17};
18use std::collections::HashSet;
19use std::sync::Arc;
20use std::time::{Duration, Instant};
21use tokio::sync::Mutex;
22use uuid::Uuid;
23
24// ============================================================================
25// CORS Configuration
26// ============================================================================
27
28/// CORS configuration
29#[derive(Debug, Clone)]
30pub struct CorsConfig {
31    /// Allowed origins (use "*" for any origin)
32    pub allowed_origins: HashSet<String>,
33    /// Allowed HTTP methods
34    pub allowed_methods: HashSet<Method>,
35    /// Allowed headers
36    pub allowed_headers: HashSet<String>,
37    /// Headers to expose to the client
38    pub exposed_headers: HashSet<String>,
39    /// Allow credentials (cookies, authorization headers)
40    pub allow_credentials: bool,
41    /// Max age for preflight cache (seconds)
42    pub max_age: u64,
43}
44
45impl Default for CorsConfig {
46    fn default() -> Self {
47        let mut methods = HashSet::new();
48        methods.insert(Method::GET);
49        methods.insert(Method::POST);
50        methods.insert(Method::PUT);
51        methods.insert(Method::DELETE);
52        methods.insert(Method::OPTIONS);
53        methods.insert(Method::HEAD);
54
55        let mut headers = HashSet::new();
56        headers.insert("content-type".to_string());
57        headers.insert("authorization".to_string());
58        headers.insert("accept".to_string());
59        headers.insert("origin".to_string());
60        headers.insert("x-requested-with".to_string());
61
62        Self {
63            allowed_origins: HashSet::new(), // Empty = allow all
64            allowed_methods: methods,
65            allowed_headers: headers,
66            exposed_headers: HashSet::new(),
67            allow_credentials: false,
68            max_age: 86400, // 24 hours
69        }
70    }
71}
72
73impl CorsConfig {
74    /// Create a permissive CORS config (allows all origins)
75    pub fn permissive() -> Self {
76        let mut config = Self::default();
77        config.allowed_origins.insert("*".to_string());
78        config
79    }
80
81    /// Allow specific origin
82    pub fn allow_origin(mut self, origin: impl Into<String>) -> Self {
83        self.allowed_origins.insert(origin.into());
84        self
85    }
86
87    /// Allow credentials
88    pub fn allow_credentials(mut self, allow: bool) -> Self {
89        self.allow_credentials = allow;
90        self
91    }
92
93    /// Check if origin is allowed
94    fn is_origin_allowed(&self, origin: &str) -> bool {
95        if self.allowed_origins.is_empty() || self.allowed_origins.contains("*") {
96            true
97        } else {
98            self.allowed_origins.contains(origin)
99        }
100    }
101
102    /// Get allowed methods as comma-separated string
103    fn methods_string(&self) -> String {
104        self.allowed_methods
105            .iter()
106            .map(|m| m.as_str())
107            .collect::<Vec<_>>()
108            .join(", ")
109    }
110
111    /// Get allowed headers as comma-separated string
112    fn headers_string(&self) -> String {
113        self.allowed_headers
114            .iter()
115            .cloned()
116            .collect::<Vec<_>>()
117            .join(", ")
118    }
119}
120
121/// CORS middleware state
122#[derive(Clone)]
123pub struct CorsState {
124    pub config: CorsConfig,
125}
126
127/// CORS middleware
128///
129/// Handles preflight requests and adds CORS headers to responses.
130pub async fn cors_middleware(
131    State(cors_state): State<CorsState>,
132    req: Request,
133    next: Next,
134) -> Response {
135    let origin = req
136        .headers()
137        .get(header::ORIGIN)
138        .and_then(|h| h.to_str().ok())
139        .map(|s| s.to_string());
140
141    // Handle preflight (OPTIONS) requests
142    if req.method() == Method::OPTIONS {
143        return build_preflight_response(&cors_state.config, origin.as_deref());
144    }
145
146    // Process the request
147    let mut response = next.run(req).await;
148
149    // Add CORS headers to response
150    add_cors_headers(
151        response.headers_mut(),
152        &cors_state.config,
153        origin.as_deref(),
154    );
155
156    response
157}
158
159/// Build preflight response for OPTIONS requests
160fn build_preflight_response(config: &CorsConfig, origin: Option<&str>) -> Response {
161    let mut response = Response::builder()
162        .status(StatusCode::NO_CONTENT)
163        .body(Body::empty())
164        .expect("building NO_CONTENT response with empty body is infallible");
165
166    add_cors_headers(response.headers_mut(), config, origin);
167
168    // Add preflight-specific headers
169    if let Ok(value) = HeaderValue::from_str(&config.methods_string()) {
170        response
171            .headers_mut()
172            .insert(header::ACCESS_CONTROL_ALLOW_METHODS, value);
173    }
174    if let Ok(value) = HeaderValue::from_str(&config.headers_string()) {
175        response
176            .headers_mut()
177            .insert(header::ACCESS_CONTROL_ALLOW_HEADERS, value);
178    }
179    if let Ok(value) = HeaderValue::from_str(&config.max_age.to_string()) {
180        response
181            .headers_mut()
182            .insert(header::ACCESS_CONTROL_MAX_AGE, value);
183    }
184
185    response
186}
187
188/// Add CORS headers to a response
189fn add_cors_headers(headers: &mut HeaderMap, config: &CorsConfig, origin: Option<&str>) {
190    // Access-Control-Allow-Origin
191    let origin_value = if let Some(origin) = origin {
192        if config.is_origin_allowed(origin) {
193            if config.allowed_origins.contains("*") && !config.allow_credentials {
194                "*"
195            } else {
196                origin
197            }
198        } else {
199            return; // Origin not allowed, don't add CORS headers
200        }
201    } else if config.allowed_origins.contains("*") {
202        "*"
203    } else {
204        return;
205    };
206
207    if let Ok(value) = HeaderValue::from_str(origin_value) {
208        headers.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, value);
209    }
210
211    // Access-Control-Allow-Credentials
212    if config.allow_credentials {
213        headers.insert(
214            header::ACCESS_CONTROL_ALLOW_CREDENTIALS,
215            HeaderValue::from_static("true"),
216        );
217    }
218
219    // Access-Control-Expose-Headers
220    if !config.exposed_headers.is_empty() {
221        let exposed = config
222            .exposed_headers
223            .iter()
224            .cloned()
225            .collect::<Vec<_>>()
226            .join(", ");
227        if let Ok(value) = HeaderValue::from_str(&exposed) {
228            headers.insert(header::ACCESS_CONTROL_EXPOSE_HEADERS, value);
229        }
230    }
231}
232
233// ============================================================================
234// Rate Limiting
235// ============================================================================
236
237/// Rate limiter configuration
238#[derive(Debug, Clone)]
239pub struct RateLimitConfig {
240    /// Maximum requests per window
241    pub max_requests: u32,
242    /// Time window duration
243    pub window: Duration,
244    /// Burst capacity (token bucket max tokens)
245    pub burst_capacity: u32,
246}
247
248impl Default for RateLimitConfig {
249    fn default() -> Self {
250        Self {
251            max_requests: 100,
252            window: Duration::from_secs(60),
253            burst_capacity: 10,
254        }
255    }
256}
257
258impl RateLimitConfig {
259    /// Validate configuration
260    pub fn validate(&self) -> Result<(), String> {
261        if self.max_requests == 0 {
262            return Err("Maximum requests must be greater than 0".to_string());
263        }
264
265        if self.window.as_secs() == 0 {
266            return Err("Time window must be greater than 0".to_string());
267        }
268
269        if self.burst_capacity == 0 {
270            return Err("Burst capacity must be greater than 0".to_string());
271        }
272
273        if self.burst_capacity > self.max_requests {
274            return Err(format!(
275                "Burst capacity ({}) cannot exceed max requests ({})",
276                self.burst_capacity, self.max_requests
277            ));
278        }
279
280        Ok(())
281    }
282}
283
284/// Token bucket for rate limiting
285#[derive(Debug)]
286struct TokenBucket {
287    tokens: f64,
288    last_update: Instant,
289    capacity: f64,
290    refill_rate: f64, // tokens per second
291}
292
293impl TokenBucket {
294    fn new(capacity: u32, refill_rate: f64) -> Self {
295        Self {
296            tokens: capacity as f64,
297            last_update: Instant::now(),
298            capacity: capacity as f64,
299            refill_rate,
300        }
301    }
302
303    fn try_acquire(&mut self) -> bool {
304        self.refill();
305        if self.tokens >= 1.0 {
306            self.tokens -= 1.0;
307            true
308        } else {
309            false
310        }
311    }
312
313    fn refill(&mut self) {
314        let now = Instant::now();
315        let elapsed = now.duration_since(self.last_update).as_secs_f64();
316        self.tokens = (self.tokens + elapsed * self.refill_rate).min(self.capacity);
317        self.last_update = now;
318    }
319
320    fn tokens_remaining(&self) -> u32 {
321        self.tokens as u32
322    }
323}
324
325/// Rate limiter state (per-IP buckets)
326#[derive(Clone)]
327pub struct RateLimitState {
328    config: RateLimitConfig,
329    buckets: Arc<Mutex<std::collections::HashMap<String, TokenBucket>>>,
330}
331
332impl RateLimitState {
333    /// Create a new rate limiter state
334    pub fn new(config: RateLimitConfig) -> Self {
335        Self {
336            config,
337            buckets: Arc::new(Mutex::new(std::collections::HashMap::new())),
338        }
339    }
340
341    /// Get or create a token bucket for an IP
342    async fn get_bucket(&self, ip: &str) -> (bool, u32) {
343        let mut buckets = self.buckets.lock().await;
344
345        let refill_rate = self.config.max_requests as f64 / self.config.window.as_secs_f64();
346
347        let bucket = buckets
348            .entry(ip.to_string())
349            .or_insert_with(|| TokenBucket::new(self.config.burst_capacity, refill_rate));
350
351        let allowed = bucket.try_acquire();
352        let remaining = bucket.tokens_remaining();
353
354        (allowed, remaining)
355    }
356}
357
358/// Rate limiting middleware
359///
360/// Limits requests per IP using token bucket algorithm.
361pub async fn rate_limit_middleware(
362    State(rate_state): State<RateLimitState>,
363    req: Request,
364    next: Next,
365) -> Result<Response, RateLimitError> {
366    // Extract client IP from headers or connection
367    let ip = extract_client_ip(&req);
368
369    let (allowed, remaining) = rate_state.get_bucket(&ip).await;
370
371    if !allowed {
372        return Err(RateLimitError::TooManyRequests);
373    }
374
375    let mut response = next.run(req).await;
376
377    // Add rate limit headers
378    let headers = response.headers_mut();
379    if let Ok(value) = HeaderValue::from_str(&rate_state.config.max_requests.to_string()) {
380        headers.insert("X-RateLimit-Limit", value);
381    }
382    if let Ok(value) = HeaderValue::from_str(&remaining.to_string()) {
383        headers.insert("X-RateLimit-Remaining", value);
384    }
385
386    Ok(response)
387}
388
389/// Extract client IP from request
390fn extract_client_ip(req: &Request) -> String {
391    // Check X-Forwarded-For first (for proxied requests)
392    if let Some(forwarded) = req.headers().get("x-forwarded-for") {
393        if let Ok(s) = forwarded.to_str() {
394            if let Some(ip) = s.split(',').next() {
395                return ip.trim().to_string();
396            }
397        }
398    }
399
400    // Check X-Real-IP
401    if let Some(real_ip) = req.headers().get("x-real-ip") {
402        if let Ok(s) = real_ip.to_str() {
403            return s.to_string();
404        }
405    }
406
407    // Fallback to unknown
408    "unknown".to_string()
409}
410
411/// Rate limit error
412#[derive(Debug)]
413pub enum RateLimitError {
414    TooManyRequests,
415}
416
417impl IntoResponse for RateLimitError {
418    fn into_response(self) -> Response {
419        let (status, message) = match self {
420            RateLimitError::TooManyRequests => (
421                StatusCode::TOO_MANY_REQUESTS,
422                "Rate limit exceeded. Please retry later.",
423            ),
424        };
425
426        let mut response = (status, message).into_response();
427
428        // Add Retry-After header (60 seconds)
429        response
430            .headers_mut()
431            .insert(header::RETRY_AFTER, HeaderValue::from_static("60"));
432
433        response
434    }
435}
436
437// ============================================================================
438// Compression Configuration
439// ============================================================================
440
441/// Compression level - balances speed vs compression ratio
442#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
443pub enum CompressionLevel {
444    /// Fastest compression, larger files (level 1)
445    Fastest,
446    /// Balanced compression (level 5-6)
447    #[default]
448    Balanced,
449    /// Best compression, slower (level 9)
450    Best,
451    /// Custom compression level (0-9)
452    Custom(u32),
453}
454
455impl CompressionLevel {
456    /// Get the numeric compression level for gzip/deflate
457    pub fn to_level(self) -> u32 {
458        match self {
459            CompressionLevel::Fastest => 1,
460            CompressionLevel::Balanced => 5,
461            CompressionLevel::Best => 9,
462            CompressionLevel::Custom(level) => level.min(9),
463        }
464    }
465}
466
467/// Compression configuration
468#[derive(Debug, Clone)]
469pub struct CompressionConfig {
470    /// Enable gzip compression
471    pub enable_gzip: bool,
472    /// Compression level (speed vs size trade-off)
473    pub level: CompressionLevel,
474    /// Minimum size in bytes to compress (smaller files not compressed)
475    pub min_size: usize,
476}
477
478impl Default for CompressionConfig {
479    fn default() -> Self {
480        Self {
481            enable_gzip: true,
482            level: CompressionLevel::Balanced,
483            min_size: 1024, // Don't compress files smaller than 1KB
484        }
485    }
486}
487
488impl CompressionConfig {
489    /// Create a fast compression config (prioritize speed)
490    pub fn fast() -> Self {
491        Self {
492            level: CompressionLevel::Fastest,
493            ..Default::default()
494        }
495    }
496
497    /// Create a best compression config (prioritize size)
498    pub fn best() -> Self {
499        Self {
500            level: CompressionLevel::Best,
501            ..Default::default()
502        }
503    }
504
505    /// Set compression level
506    pub fn with_level(mut self, level: CompressionLevel) -> Self {
507        self.level = level;
508        self
509    }
510
511    /// Set minimum size threshold
512    pub fn with_min_size(mut self, min_size: usize) -> Self {
513        self.min_size = min_size;
514        self
515    }
516
517    /// Enable/disable gzip compression
518    pub fn with_gzip(mut self, gzip: bool) -> Self {
519        self.enable_gzip = gzip;
520        self
521    }
522
523    /// Validate configuration
524    pub fn validate(&self) -> Result<(), String> {
525        // Gzip must be enabled (only supported algorithm per COOLJAPAN OxiARC policy)
526        if !self.enable_gzip {
527            return Err("At least one compression algorithm must be enabled".to_string());
528        }
529
530        // Minimum size should be reasonable
531        if self.min_size > 100 * 1024 * 1024 {
532            return Err(format!(
533                "Minimum compression size {} is too large (max: 100MB)",
534                self.min_size
535            ));
536        }
537
538        Ok(())
539    }
540}
541
542// ============================================================================
543// HTTP Caching
544// ============================================================================
545
546/// Cache configuration
547#[derive(Debug, Clone)]
548pub struct CacheConfig {
549    /// Default max-age for cacheable responses (seconds)
550    pub default_max_age: u64,
551    /// Whether responses are public (can be cached by CDNs)
552    pub public: bool,
553    /// Whether to mark CID responses as immutable
554    pub immutable_cids: bool,
555}
556
557impl Default for CacheConfig {
558    fn default() -> Self {
559        Self {
560            default_max_age: 3600, // 1 hour
561            public: true,
562            immutable_cids: true, // CID content is immutable by definition
563        }
564    }
565}
566
567impl CacheConfig {
568    /// Validate configuration
569    pub fn validate(&self) -> Result<(), String> {
570        // Max age should be reasonable (not more than 1 year)
571        const MAX_AGE_LIMIT: u64 = 365 * 24 * 3600; // 1 year in seconds
572
573        if self.default_max_age > MAX_AGE_LIMIT {
574            return Err(format!(
575                "Max age {} exceeds maximum {} (1 year)",
576                self.default_max_age, MAX_AGE_LIMIT
577            ));
578        }
579
580        Ok(())
581    }
582}
583
584/// Add caching headers to a response for a given CID
585pub fn add_caching_headers(headers: &mut HeaderMap, cid: &str, config: &CacheConfig) {
586    // ETag based on CID (content-addressed = perfect ETag)
587    if let Ok(etag) = HeaderValue::from_str(&format!("\"{}\"", cid)) {
588        headers.insert(header::ETAG, etag);
589    }
590
591    // Cache-Control
592    let mut cache_control = String::new();
593    if config.public {
594        cache_control.push_str("public, ");
595    } else {
596        cache_control.push_str("private, ");
597    }
598    cache_control.push_str(&format!("max-age={}", config.default_max_age));
599
600    // CID content is immutable - it will never change
601    if config.immutable_cids {
602        cache_control.push_str(", immutable");
603    }
604
605    if let Ok(value) = HeaderValue::from_str(&cache_control) {
606        headers.insert(header::CACHE_CONTROL, value);
607    }
608}
609
610/// Check if request has a matching ETag (for conditional requests)
611pub fn check_etag_match(headers: &HeaderMap, cid: &str) -> bool {
612    if let Some(if_none_match) = headers.get(header::IF_NONE_MATCH) {
613        if let Ok(value) = if_none_match.to_str() {
614            // Remove quotes and compare
615            let etag = value.trim().trim_matches('"');
616            return etag == cid;
617        }
618    }
619    false
620}
621
622/// Build a 304 Not Modified response
623pub fn not_modified_response(cid: &str, config: &CacheConfig) -> Response {
624    let mut response = Response::builder()
625        .status(StatusCode::NOT_MODIFIED)
626        .body(Body::empty())
627        .expect("building NOT_MODIFIED response with empty body is infallible");
628
629    add_caching_headers(response.headers_mut(), cid, config);
630
631    response
632}
633
634/// Authenticated user context
635#[derive(Debug, Clone)]
636pub struct AuthUser {
637    pub user_id: Uuid,
638    pub username: String,
639    pub claims: Option<Claims>,
640}
641
642/// Authenticate user from Authorization header
643///
644/// Supports both JWT tokens (Bearer <token>) and API keys (ipfrs_...)
645fn authenticate_user(req: &Request, auth_state: &AuthState) -> Result<AuthUser, AuthError> {
646    let auth_header = req
647        .headers()
648        .get(header::AUTHORIZATION)
649        .and_then(|h| h.to_str().ok())
650        .ok_or(AuthError::InvalidToken(
651            "Missing Authorization header".to_string(),
652        ))?;
653
654    // Try JWT token first (Bearer <token>)
655    if let Some(token) = auth_header.strip_prefix("Bearer ") {
656        let claims = auth_state.jwt_manager.validate_token(token)?;
657        let user = auth_state.user_store.get_user(&claims.username)?;
658
659        return Ok(AuthUser {
660            user_id: user.id,
661            username: user.username,
662            claims: Some(claims),
663        });
664    }
665
666    // Try API key (ipfrs_...)
667    if auth_header.starts_with("ipfrs_") {
668        let (_api_key, user_id) = auth_state.api_key_store.authenticate(auth_header)?;
669        let user = auth_state.user_store.get_by_id(&user_id)?;
670
671        return Ok(AuthUser {
672            user_id: user.id,
673            username: user.username,
674            claims: None,
675        });
676    }
677
678    Err(AuthError::InvalidToken(
679        "Authorization header must be either 'Bearer <token>' or 'ipfrs_<key>'".to_string(),
680    ))
681}
682
683/// Authentication middleware
684///
685/// Validates JWT token or API key from Authorization header and injects authenticated user into request extensions.
686pub async fn auth_middleware(
687    State(auth_state): State<AuthState>,
688    mut req: Request,
689    next: Next,
690) -> Result<Response, AuthMiddlewareError> {
691    // Authenticate user (JWT or API key)
692    let auth_user = authenticate_user(&req, &auth_state)?;
693
694    // Inject authenticated user into request extensions
695    req.extensions_mut().insert(auth_user);
696
697    Ok(next.run(req).await)
698}
699
700/// Type alias for the permission check middleware future
701type PermissionCheckFuture = std::pin::Pin<
702    Box<dyn std::future::Future<Output = Result<Response, AuthMiddlewareError>> + Send>,
703>;
704
705/// Authorization middleware factory
706///
707/// Creates middleware that checks if the authenticated user has required permissions.
708pub fn require_permission(
709    required: Permission,
710) -> impl Fn(State<AuthState>, Request, Next) -> PermissionCheckFuture + Clone {
711    move |State(auth_state): State<AuthState>, req: Request, next: Next| {
712        let required = required;
713        Box::pin(async move {
714            // Get authenticated user from extensions
715            let auth_user = req
716                .extensions()
717                .get::<AuthUser>()
718                .ok_or_else(|| AuthError::InvalidToken("User not authenticated".to_string()))?;
719
720            // Get user from store to check permissions
721            let user = auth_state.user_store.get_by_id(&auth_user.user_id)?;
722
723            // Check if user has required permission
724            if !user.has_permission(required) {
725                return Err(AuthMiddlewareError::from(
726                    AuthError::InsufficientPermissions,
727                ));
728            }
729
730            Ok(next.run(req).await)
731        })
732    }
733}
734
735/// Middleware error wrapper
736#[derive(Debug)]
737pub struct AuthMiddlewareError {
738    error: AuthError,
739}
740
741impl From<AuthError> for AuthMiddlewareError {
742    fn from(error: AuthError) -> Self {
743        Self { error }
744    }
745}
746
747impl IntoResponse for AuthMiddlewareError {
748    fn into_response(self) -> Response {
749        let (status, message) = match self.error {
750            AuthError::InvalidToken(_) | AuthError::TokenExpired => {
751                (StatusCode::UNAUTHORIZED, "Authentication required")
752            }
753            AuthError::InsufficientPermissions => {
754                (StatusCode::FORBIDDEN, "Insufficient permissions")
755            }
756            AuthError::UserNotFound | AuthError::InvalidCredentials => {
757                (StatusCode::UNAUTHORIZED, "Invalid credentials")
758            }
759            _ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"),
760        };
761
762        (status, message).into_response()
763    }
764}
765
766// ============================================================================
767// Request Validation Middleware
768// ============================================================================
769
770/// Request validation configuration
771#[derive(Debug, Clone)]
772pub struct ValidationConfig {
773    /// Maximum request body size (bytes)
774    pub max_body_size: usize,
775    /// Maximum CID length
776    pub max_cid_length: usize,
777    /// Validate CID format
778    pub validate_cid_format: bool,
779    /// Required content types for specific endpoints
780    pub content_type_validation: bool,
781    /// Maximum batch size for batch operations
782    pub max_batch_size: usize,
783}
784
785impl Default for ValidationConfig {
786    fn default() -> Self {
787        Self {
788            max_body_size: 100 * 1024 * 1024, // 100 MB
789            max_cid_length: 100,
790            validate_cid_format: true,
791            content_type_validation: true,
792            max_batch_size: 1000,
793        }
794    }
795}
796
797impl ValidationConfig {
798    /// Create a strict validation config
799    pub fn strict() -> Self {
800        Self {
801            max_body_size: 10 * 1024 * 1024, // 10 MB
802            max_cid_length: 64,
803            validate_cid_format: true,
804            content_type_validation: true,
805            max_batch_size: 100,
806        }
807    }
808
809    /// Create a permissive validation config
810    pub fn permissive() -> Self {
811        Self {
812            max_body_size: 1024 * 1024 * 1024, // 1 GB
813            max_cid_length: 200,
814            validate_cid_format: false,
815            content_type_validation: false,
816            max_batch_size: 10000,
817        }
818    }
819}
820
821/// Validation error types
822#[derive(Debug)]
823pub enum ValidationError {
824    /// Request body too large
825    BodyTooLarge { size: usize, max: usize },
826    /// Invalid CID format
827    InvalidCid(String),
828    /// Invalid content type
829    InvalidContentType { expected: String, actual: String },
830    /// Missing required parameter
831    MissingParameter(String),
832    /// Batch size exceeds limit
833    BatchTooLarge { size: usize, max: usize },
834    /// Invalid parameter value
835    InvalidParameter { name: String, reason: String },
836}
837
838impl std::fmt::Display for ValidationError {
839    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
840        match self {
841            ValidationError::BodyTooLarge { size, max } => {
842                write!(
843                    f,
844                    "Request body too large: {} bytes (max: {} bytes)",
845                    size, max
846                )
847            }
848            ValidationError::InvalidCid(cid) => {
849                write!(f, "Invalid CID format: {}", cid)
850            }
851            ValidationError::InvalidContentType { expected, actual } => {
852                write!(
853                    f,
854                    "Invalid content type: expected {}, got {}",
855                    expected, actual
856                )
857            }
858            ValidationError::MissingParameter(param) => {
859                write!(f, "Missing required parameter: {}", param)
860            }
861            ValidationError::BatchTooLarge { size, max } => {
862                write!(
863                    f,
864                    "Batch size too large: {} items (max: {} items)",
865                    size, max
866                )
867            }
868            ValidationError::InvalidParameter { name, reason } => {
869                write!(f, "Invalid parameter '{}': {}", name, reason)
870            }
871        }
872    }
873}
874
875impl std::error::Error for ValidationError {}
876
877impl IntoResponse for ValidationError {
878    fn into_response(self) -> Response {
879        let request_id = Uuid::new_v4();
880        let error_message = self.to_string();
881
882        let (status, code) = match self {
883            ValidationError::BodyTooLarge { .. } => {
884                (StatusCode::PAYLOAD_TOO_LARGE, "BODY_TOO_LARGE")
885            }
886            ValidationError::InvalidCid(_) => (StatusCode::BAD_REQUEST, "INVALID_CID"),
887            ValidationError::InvalidContentType { .. } => {
888                (StatusCode::UNSUPPORTED_MEDIA_TYPE, "INVALID_CONTENT_TYPE")
889            }
890            ValidationError::MissingParameter(_) => (StatusCode::BAD_REQUEST, "MISSING_PARAMETER"),
891            ValidationError::BatchTooLarge { .. } => (StatusCode::BAD_REQUEST, "BATCH_TOO_LARGE"),
892            ValidationError::InvalidParameter { .. } => {
893                (StatusCode::BAD_REQUEST, "INVALID_PARAMETER")
894            }
895        };
896
897        let body = serde_json::json!({
898            "error": error_message,
899            "code": code,
900            "request_id": request_id.to_string(),
901        });
902
903        (
904            status,
905            serde_json::to_string(&body).expect("serializing JSON Value is infallible"),
906        )
907            .into_response()
908    }
909}
910
911/// Validate CID format
912///
913/// Basic validation: CIDv0 starts with "Qm" and is 46 chars, CIDv1 is base32/base58
914pub fn validate_cid(cid: &str, config: &ValidationConfig) -> Result<(), ValidationError> {
915    // Empty CID is always invalid, regardless of validation settings
916    if cid.is_empty() {
917        return Err(ValidationError::InvalidCid(
918            "CID cannot be empty".to_string(),
919        ));
920    }
921
922    if !config.validate_cid_format {
923        return Ok(());
924    }
925
926    if cid.len() > config.max_cid_length {
927        return Err(ValidationError::InvalidCid(format!(
928            "CID too long: {} chars (max: {})",
929            cid.len(),
930            config.max_cid_length
931        )));
932    }
933
934    // Basic format check: CIDv0 or CIDv1
935    if cid.starts_with("Qm") && cid.len() == 46 {
936        // CIDv0 (base58btc encoded SHA-256 hash)
937        Ok(())
938    } else if cid.starts_with("b") || cid.starts_with("z") || cid.starts_with("f") {
939        // CIDv1 (multibase prefix)
940        Ok(())
941    } else {
942        Err(ValidationError::InvalidCid(
943            "Invalid CID format: must be CIDv0 (Qm...) or CIDv1 (b..., z..., f...)".to_string(),
944        ))
945    }
946}
947
948/// Validate batch size
949pub fn validate_batch_size(size: usize, config: &ValidationConfig) -> Result<(), ValidationError> {
950    if size == 0 {
951        return Err(ValidationError::InvalidParameter {
952            name: "batch".to_string(),
953            reason: "Batch cannot be empty".to_string(),
954        });
955    }
956
957    if size > config.max_batch_size {
958        return Err(ValidationError::BatchTooLarge {
959            size,
960            max: config.max_batch_size,
961        });
962    }
963
964    Ok(())
965}
966
967/// Validate content type
968pub fn validate_content_type(
969    headers: &HeaderMap,
970    expected: &str,
971    config: &ValidationConfig,
972) -> Result<(), ValidationError> {
973    if !config.content_type_validation {
974        return Ok(());
975    }
976
977    let content_type = headers
978        .get(header::CONTENT_TYPE)
979        .and_then(|h| h.to_str().ok())
980        .unwrap_or("");
981
982    if !content_type.starts_with(expected) {
983        return Err(ValidationError::InvalidContentType {
984            expected: expected.to_string(),
985            actual: content_type.to_string(),
986        });
987    }
988
989    Ok(())
990}
991
992/// Validation middleware state
993#[derive(Clone)]
994pub struct ValidationState {
995    pub config: ValidationConfig,
996}
997
998/// Request validation middleware
999///
1000/// Validates request size and basic parameters before processing
1001pub async fn validation_middleware(
1002    State(_validation_state): State<ValidationState>,
1003    req: Request,
1004    next: Next,
1005) -> Result<Response, ValidationError> {
1006    let (parts, body) = req.into_parts();
1007
1008    // Validate content-type for POST/PUT requests
1009    if parts.method == Method::POST || parts.method == Method::PUT {
1010        // Skip validation for multipart/form-data (handled by body parser)
1011        if let Some(content_type) = parts.headers.get(header::CONTENT_TYPE) {
1012            if let Ok(ct_str) = content_type.to_str() {
1013                if ct_str.contains("multipart/form-data") {
1014                    // Skip body size validation for multipart
1015                    let req = Request::from_parts(parts, body);
1016                    return Ok(next.run(req).await);
1017                }
1018            }
1019        }
1020    }
1021
1022    // Reconstruct request and continue
1023    let req = Request::from_parts(parts, body);
1024    Ok(next.run(req).await)
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029    use super::*;
1030
1031    #[test]
1032    fn test_cors_config_default() {
1033        let config = CorsConfig::default();
1034        assert!(config.allowed_origins.is_empty());
1035        assert!(config.allowed_methods.contains(&Method::GET));
1036        assert!(config.allowed_methods.contains(&Method::POST));
1037        assert!(!config.allow_credentials);
1038        assert_eq!(config.max_age, 86400);
1039    }
1040
1041    #[test]
1042    fn test_cors_config_permissive() {
1043        let config = CorsConfig::permissive();
1044        assert!(config.allowed_origins.contains("*"));
1045        assert!(config.is_origin_allowed("https://example.com"));
1046        assert!(config.is_origin_allowed("http://localhost:3000"));
1047    }
1048
1049    #[test]
1050    fn test_cors_config_allow_origin() {
1051        let config = CorsConfig::default()
1052            .allow_origin("https://example.com")
1053            .allow_origin("https://api.example.com");
1054
1055        assert!(config.is_origin_allowed("https://example.com"));
1056        assert!(config.is_origin_allowed("https://api.example.com"));
1057        assert!(!config.is_origin_allowed("https://other.com"));
1058    }
1059
1060    #[test]
1061    fn test_rate_limit_config_default() {
1062        let config = RateLimitConfig::default();
1063        assert_eq!(config.max_requests, 100);
1064        assert_eq!(config.window, Duration::from_secs(60));
1065        assert_eq!(config.burst_capacity, 10);
1066    }
1067
1068    #[test]
1069    fn test_cache_config_default() {
1070        let config = CacheConfig::default();
1071        assert_eq!(config.default_max_age, 3600);
1072        assert!(config.public);
1073        assert!(config.immutable_cids);
1074    }
1075
1076    #[test]
1077    fn test_add_caching_headers() {
1078        let mut headers = HeaderMap::new();
1079        let config = CacheConfig::default();
1080
1081        add_caching_headers(&mut headers, "QmTest123", &config);
1082
1083        assert!(headers.contains_key(header::ETAG));
1084        assert!(headers.contains_key(header::CACHE_CONTROL));
1085
1086        let etag = headers
1087            .get(header::ETAG)
1088            .expect("test: ETAG header must be present")
1089            .to_str()
1090            .expect("test: ETAG header value must be valid UTF-8");
1091        assert_eq!(etag, "\"QmTest123\"");
1092
1093        let cache_control = headers
1094            .get(header::CACHE_CONTROL)
1095            .expect("test: CACHE_CONTROL header must be present")
1096            .to_str()
1097            .expect("test: CACHE_CONTROL header value must be valid UTF-8");
1098        assert!(cache_control.contains("public"));
1099        assert!(cache_control.contains("max-age=3600"));
1100        assert!(cache_control.contains("immutable"));
1101    }
1102
1103    #[test]
1104    fn test_check_etag_match() {
1105        let mut headers = HeaderMap::new();
1106
1107        // No If-None-Match header
1108        assert!(!check_etag_match(&headers, "QmTest123"));
1109
1110        // With matching ETag
1111        headers.insert(
1112            header::IF_NONE_MATCH,
1113            HeaderValue::from_static("\"QmTest123\""),
1114        );
1115        assert!(check_etag_match(&headers, "QmTest123"));
1116
1117        // With non-matching ETag
1118        assert!(!check_etag_match(&headers, "QmOther456"));
1119    }
1120
1121    #[tokio::test]
1122    async fn test_rate_limit_state() {
1123        let config = RateLimitConfig {
1124            max_requests: 5,
1125            window: Duration::from_secs(1),
1126            burst_capacity: 3,
1127        };
1128        let state = RateLimitState::new(config);
1129
1130        // First 3 requests should succeed (burst capacity)
1131        for _ in 0..3 {
1132            let (allowed, _) = state.get_bucket("127.0.0.1").await;
1133            assert!(allowed);
1134        }
1135    }
1136
1137    #[test]
1138    fn test_compression_level_to_level() {
1139        assert_eq!(CompressionLevel::Fastest.to_level(), 1);
1140        assert_eq!(CompressionLevel::Balanced.to_level(), 5);
1141        assert_eq!(CompressionLevel::Best.to_level(), 9);
1142        assert_eq!(CompressionLevel::Custom(7).to_level(), 7);
1143        assert_eq!(CompressionLevel::Custom(15).to_level(), 9); // Capped at 9
1144    }
1145
1146    #[test]
1147    fn test_compression_config_default() {
1148        let config = CompressionConfig::default();
1149        assert!(config.enable_gzip);
1150        assert_eq!(config.level, CompressionLevel::Balanced);
1151        assert_eq!(config.min_size, 1024);
1152    }
1153
1154    #[test]
1155    fn test_compression_config_fast() {
1156        let config = CompressionConfig::fast();
1157        assert_eq!(config.level, CompressionLevel::Fastest);
1158        assert!(config.enable_gzip);
1159    }
1160
1161    #[test]
1162    fn test_compression_config_best() {
1163        let config = CompressionConfig::best();
1164        assert_eq!(config.level, CompressionLevel::Best);
1165    }
1166
1167    #[test]
1168    fn test_compression_config_builder() {
1169        let config = CompressionConfig::default()
1170            .with_level(CompressionLevel::Custom(7))
1171            .with_min_size(2048)
1172            .with_gzip(true);
1173
1174        assert_eq!(config.level, CompressionLevel::Custom(7));
1175        assert_eq!(config.min_size, 2048);
1176        assert!(config.enable_gzip);
1177    }
1178
1179    #[test]
1180    fn test_compression_config_validation_valid() {
1181        let config = CompressionConfig::default();
1182        assert!(config.validate().is_ok());
1183
1184        let config = CompressionConfig::default().with_gzip(true);
1185        assert!(config.validate().is_ok());
1186    }
1187
1188    #[test]
1189    fn test_compression_config_validation_invalid() {
1190        // No algorithms enabled
1191        let config = CompressionConfig::default().with_gzip(false);
1192        assert!(config.validate().is_err());
1193
1194        // Min size too large
1195        let config = CompressionConfig::default().with_min_size(200 * 1024 * 1024);
1196        assert!(config.validate().is_err());
1197    }
1198
1199    #[test]
1200    fn test_rate_limit_config_validation_valid() {
1201        let config = RateLimitConfig::default();
1202        assert!(config.validate().is_ok());
1203
1204        let config = RateLimitConfig {
1205            max_requests: 100,
1206            window: Duration::from_secs(60),
1207            burst_capacity: 50,
1208        };
1209        assert!(config.validate().is_ok());
1210    }
1211
1212    #[test]
1213    fn test_rate_limit_config_validation_invalid() {
1214        // Zero max requests
1215        let config = RateLimitConfig {
1216            max_requests: 0,
1217            window: Duration::from_secs(60),
1218            burst_capacity: 10,
1219        };
1220        assert!(config.validate().is_err());
1221
1222        // Zero window
1223        let config = RateLimitConfig {
1224            max_requests: 100,
1225            window: Duration::from_secs(0),
1226            burst_capacity: 10,
1227        };
1228        assert!(config.validate().is_err());
1229
1230        // Burst exceeds max
1231        let config = RateLimitConfig {
1232            max_requests: 100,
1233            window: Duration::from_secs(60),
1234            burst_capacity: 200,
1235        };
1236        assert!(config.validate().is_err());
1237    }
1238
1239    #[test]
1240    fn test_cache_config_validation_valid() {
1241        let config = CacheConfig::default();
1242        assert!(config.validate().is_ok());
1243
1244        let config = CacheConfig {
1245            default_max_age: 86400, // 1 day
1246            public: true,
1247            immutable_cids: true,
1248        };
1249        assert!(config.validate().is_ok());
1250    }
1251
1252    #[test]
1253    fn test_cache_config_validation_invalid() {
1254        // Max age too large (more than 1 year)
1255        let config = CacheConfig {
1256            default_max_age: 400 * 24 * 3600, // More than 1 year
1257            public: true,
1258            immutable_cids: true,
1259        };
1260        assert!(config.validate().is_err());
1261    }
1262
1263    // Validation middleware tests
1264
1265    #[test]
1266    fn test_validation_config_default() {
1267        let config = ValidationConfig::default();
1268        assert_eq!(config.max_body_size, 100 * 1024 * 1024);
1269        assert_eq!(config.max_cid_length, 100);
1270        assert!(config.validate_cid_format);
1271        assert!(config.content_type_validation);
1272        assert_eq!(config.max_batch_size, 1000);
1273    }
1274
1275    #[test]
1276    fn test_validation_config_strict() {
1277        let config = ValidationConfig::strict();
1278        assert_eq!(config.max_body_size, 10 * 1024 * 1024);
1279        assert_eq!(config.max_cid_length, 64);
1280        assert_eq!(config.max_batch_size, 100);
1281    }
1282
1283    #[test]
1284    fn test_validation_config_permissive() {
1285        let config = ValidationConfig::permissive();
1286        assert_eq!(config.max_body_size, 1024 * 1024 * 1024);
1287        assert_eq!(config.max_cid_length, 200);
1288        assert!(!config.validate_cid_format);
1289        assert!(!config.content_type_validation);
1290        assert_eq!(config.max_batch_size, 10000);
1291    }
1292
1293    #[test]
1294    fn test_validate_cid_v0() {
1295        let config = ValidationConfig::default();
1296
1297        // Valid CIDv0
1298        assert!(validate_cid("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco", &config).is_ok());
1299
1300        // Invalid CIDv0 (wrong length)
1301        assert!(validate_cid("QmShort", &config).is_err());
1302
1303        // Empty CID
1304        assert!(validate_cid("", &config).is_err());
1305    }
1306
1307    #[test]
1308    fn test_validate_cid_v1() {
1309        let config = ValidationConfig::default();
1310
1311        // Valid CIDv1 prefixes
1312        assert!(validate_cid(
1313            "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
1314            &config
1315        )
1316        .is_ok());
1317        assert!(validate_cid("zb2rhk6GMPQF8p1kqXvhYnCMp3hGGUQVvqp6qjdvNLKqCqKCo", &config).is_ok());
1318
1319        // Invalid format
1320        assert!(validate_cid("invalid_cid_format", &config).is_err());
1321    }
1322
1323    #[test]
1324    fn test_validate_cid_disabled() {
1325        let config = ValidationConfig {
1326            validate_cid_format: false,
1327            ..Default::default()
1328        };
1329
1330        // Should accept any string when validation is disabled
1331        assert!(validate_cid("invalid_format", &config).is_ok());
1332        assert!(validate_cid("", &config).is_err()); // Empty still fails
1333    }
1334
1335    #[test]
1336    fn test_validate_batch_size_valid() {
1337        let config = ValidationConfig::default();
1338
1339        assert!(validate_batch_size(1, &config).is_ok());
1340        assert!(validate_batch_size(100, &config).is_ok());
1341        assert!(validate_batch_size(1000, &config).is_ok());
1342    }
1343
1344    #[test]
1345    fn test_validate_batch_size_invalid() {
1346        let config = ValidationConfig::default();
1347
1348        // Empty batch
1349        assert!(validate_batch_size(0, &config).is_err());
1350
1351        // Too large
1352        assert!(validate_batch_size(1001, &config).is_err());
1353        assert!(validate_batch_size(10000, &config).is_err());
1354    }
1355
1356    #[test]
1357    fn test_validate_content_type_valid() {
1358        let config = ValidationConfig::default();
1359        let mut headers = HeaderMap::new();
1360        headers.insert(
1361            header::CONTENT_TYPE,
1362            HeaderValue::from_static("application/json"),
1363        );
1364
1365        assert!(validate_content_type(&headers, "application/json", &config).is_ok());
1366    }
1367
1368    #[test]
1369    fn test_validate_content_type_invalid() {
1370        let config = ValidationConfig::default();
1371        let mut headers = HeaderMap::new();
1372        headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/plain"));
1373
1374        assert!(validate_content_type(&headers, "application/json", &config).is_err());
1375    }
1376
1377    #[test]
1378    fn test_validate_content_type_disabled() {
1379        let config = ValidationConfig {
1380            content_type_validation: false,
1381            ..Default::default()
1382        };
1383
1384        let mut headers = HeaderMap::new();
1385        headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/plain"));
1386
1387        // Should accept any content type when validation is disabled
1388        assert!(validate_content_type(&headers, "application/json", &config).is_ok());
1389    }
1390
1391    #[test]
1392    fn test_validation_error_display() {
1393        let err = ValidationError::InvalidCid("test".to_string());
1394        assert_eq!(err.to_string(), "Invalid CID format: test");
1395
1396        let err = ValidationError::BodyTooLarge {
1397            size: 200,
1398            max: 100,
1399        };
1400        assert!(err.to_string().contains("200 bytes"));
1401        assert!(err.to_string().contains("100 bytes"));
1402
1403        let err = ValidationError::BatchTooLarge {
1404            size: 2000,
1405            max: 1000,
1406        };
1407        assert!(err.to_string().contains("2000 items"));
1408        assert!(err.to_string().contains("1000 items"));
1409    }
1410}