Files
orion/middleware/cloudflare.py
Samir Boulahtit ff5b395cdd feat: add Sentry, Cloudflare R2, and CloudFlare CDN integrations
Production quick wins for improved observability and scalability:

Sentry Error Tracking:
- Add sentry-sdk[fastapi] dependency
- Initialize Sentry in main.py with FastAPI/SQLAlchemy integrations
- Add Celery integration for background task error tracking
- Feature-flagged via SENTRY_DSN (disabled when empty)

Cloudflare R2 Storage:
- Add boto3 dependency for S3-compatible API
- Create storage_service.py with StorageBackend abstraction
- LocalStorageBackend for development (default)
- R2StorageBackend for production cloud storage
- Feature-flagged via STORAGE_BACKEND setting

CloudFlare CDN/Proxy:
- Create middleware/cloudflare.py for CF header handling
- Extract real client IP from CF-Connecting-IP
- Support CF-IPCountry for geo features
- Feature-flagged via CLOUDFLARE_ENABLED setting

Documentation:
- Add docs/deployment/cloudflare.md setup guide
- Update infrastructure.md with dev vs prod requirements
- Add enterprise upgrade checklist for scaling beyond 1000 users
- Update installation.md with new environment variables

All features are optional and disabled by default for development.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 19:44:59 +01:00

150 lines
4.3 KiB
Python

# 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",
]