# middleware/cloudflare.py """ CloudFlare proxy middleware. When enabled, this middleware handles CloudFlare-specific headers: - CF-Connecting-IP: Real client IP address - CF-IPCountry: Client's country code - CF-Ray: CloudFlare Ray ID for request tracing - CF-Visitor: Original request scheme Usage: Enable by setting CLOUDFLARE_ENABLED=true in .env The middleware will: 1. Extract real client IP from CF-Connecting-IP header 2. Store country code in request.state.client_country 3. Log CF-Ray for request correlation """ import logging from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import Response from app.core.config import settings logger = logging.getLogger(__name__) class CloudFlareMiddleware(BaseHTTPMiddleware): """ Middleware to handle CloudFlare proxy headers. Only active when CLOUDFLARE_ENABLED=true. """ async def dispatch(self, request: Request, call_next) -> Response: """Process request with CloudFlare headers.""" if not settings.cloudflare_enabled: return await call_next(request) # Extract CloudFlare headers cf_connecting_ip = request.headers.get("CF-Connecting-IP") cf_country = request.headers.get("CF-IPCountry") cf_ray = request.headers.get("CF-Ray") cf_visitor = request.headers.get("CF-Visitor") # {"scheme":"https"} # Store real client IP if cf_connecting_ip: # Override the client host with real IP # Note: Starlette's request.client is immutable, so we store in state request.state.real_ip = cf_connecting_ip else: # Fallback to standard client IP request.state.real_ip = ( request.client.host if request.client else "unknown" ) # Store country code for geo features if cf_country: request.state.client_country = cf_country # Store Ray ID for request tracing if cf_ray: request.state.cf_ray = cf_ray # Determine original scheme if cf_visitor: try: import json visitor_info = json.loads(cf_visitor) request.state.original_scheme = visitor_info.get("scheme", "https") except (json.JSONDecodeError, TypeError): request.state.original_scheme = "https" # Log request with CloudFlare context logger.debug( f"CloudFlare request: ip={cf_connecting_ip}, " f"country={cf_country}, ray={cf_ray}" ) # Process request response = await call_next(request) # Add CF-Ray to response for debugging if cf_ray: response.headers["X-CF-Ray"] = cf_ray return response def get_real_client_ip(request: Request) -> str: """ Get the real client IP address. When behind CloudFlare, this returns CF-Connecting-IP. Otherwise, returns the standard client IP. Args: request: Starlette Request object Returns: Client IP address string """ # First check if CloudFlare middleware set real_ip if hasattr(request.state, "real_ip"): return request.state.real_ip # Check for CF-Connecting-IP header directly cf_ip = request.headers.get("CF-Connecting-IP") if cf_ip: return cf_ip # Check for X-Forwarded-For (generic proxy header) forwarded_for = request.headers.get("X-Forwarded-For") if forwarded_for: # Take the first IP (client IP) return forwarded_for.split(",")[0].strip() # Fallback to request client return request.client.host if request.client else "unknown" def get_client_country(request: Request) -> str | None: """ Get the client's country code from CloudFlare. Args: request: Starlette Request object Returns: ISO 3166-1 alpha-2 country code, or None if not available """ if hasattr(request.state, "client_country"): return request.state.client_country return request.headers.get("CF-IPCountry") # ============================================================================= # PUBLIC API # ============================================================================= __all__ = [ "CloudFlareMiddleware", "get_real_client_ip", "get_client_country", ]