""" Security middleware for rate limiting and security headers. """ import time import logging from typing import Dict, Tuple from collections import defaultdict from datetime import datetime, timedelta from fastapi import Request, HTTPException from starlette.middleware.base import BaseHTTPMiddleware from starlette.responses import Response logger = logging.getLogger(__name__) class RateLimiter: """ Simple in-memory rate limiter. For production, use Redis-based rate limiting. """ def __init__(self, requests_per_minute: int = 60): self.requests_per_minute = requests_per_minute self._requests: Dict[str, list] = defaultdict(list) def _cleanup_old_requests(self, key: str) -> None: """Remove requests older than 1 minute.""" cutoff = time.time() - 60 self._requests[key] = [ts for ts in self._requests[key] if ts > cutoff] def check(self, key: str) -> bool: """Check if request is allowed.""" self._cleanup_old_requests(key) if len(self._requests[key]) >= self.requests_per_minute: return False self._requests[key].append(time.time()) return True def get_remaining(self, key: str) -> int: """Get remaining requests for this minute.""" self._cleanup_old_requests(key) return max(0, self.requests_per_minute - len(self._requests[key])) class RateLimitMiddleware(BaseHTTPMiddleware): """ Middleware for rate limiting based on IP address. """ def __init__(self, app, requests_per_minute: int = 60): super().__init__(app) self.rate_limiter = RateLimiter(requests_per_minute) # Endpoints to exclude from rate limiting self.exclude_paths = ["/health", "/ready", "/startup", "/metrics"] async def dispatch(self, request: Request, call_next) -> Response: # Skip rate limiting for excluded paths if any(request.url.path.startswith(path) for path in self.exclude_paths): return await call_next(request) # Get client IP client_ip = request.client.host if request.client else "unknown" # Check rate limit if not self.rate_limiter.check(client_ip): logger.warning(f"Rate limit exceeded for IP: {client_ip}") raise HTTPException( status_code=429, detail="Too many requests. Please try again later." ) # Add rate limit headers remaining = self.rate_limiter.get_remaining(client_ip) response = await call_next(request) response.headers["X-RateLimit-Limit"] = str(self.rate_limiter.requests_per_minute) response.headers["X-RateLimit-Remaining"] = str(remaining) return response class SecurityHeadersMiddleware(BaseHTTPMiddleware): """ Middleware for adding security headers to responses. """ async def dispatch(self, request: Request, call_next) -> Response: response = await call_next(request) # Add security headers response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" response.headers["X-XSS-Protection"] = "1; mode=block" response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" return response def create_rate_limiter(requests_per_minute: int = 60) -> RateLimitMiddleware: """Factory function to create rate limiter middleware.""" return lambda app: RateLimitMiddleware(app, requests_per_minute)