"""RMI Backend - Core Middleware.""" import json import os import uuid from fastapi import Request from fastapi.responses import JSONResponse # ═══════════════════════════════════════════════════════════════ # Rate-limit config # ═══════════════════════════════════════════════════════════════ CACHEABLE_TOOLS = {"token_price", "token_metadata", "wallet_tokens", "entity_intel"} # ═══════════════════════════════════════════════════════════════ # Payload size limit # ═══════════════════════════════════════════════════════════════ MAX_PAYLOAD_SIZE = 1_048_576 # 1MB for standard JSON APIs async def cache_middleware(request: Request, call_next): """Check Redis cache before executing tool. Store result after.""" path = request.url.path if not path.startswith("/api/v1/x402-tools/"): return await call_next(request) if request.method != "POST": return await call_next(request) tool = path.rstrip("/").split("/")[-1] if tool not in CACHEABLE_TOOLS: return await call_next(request) try: body = await request.body() params = json.loads(body) if body else {} except Exception: return await call_next(request) try: from app.routers.x402_advanced_tools import get_cached, set_cached cached = get_cached(tool, params) if cached: return JSONResponse(content=cached, headers={"X-Cache": "HIT", "X-Cache-TTL": "60"}) except Exception: pass response = await call_next(request) if response.status_code == 200: try: resp_body = b"" async for chunk in response.body_iterator: resp_body += chunk result = json.loads(resp_body) set_cached(tool, params, result) return JSONResponse( content=result, status_code=response.status_code, headers={**dict(response.headers), "X-Cache": "MISS"}, ) except Exception: pass return response async def emergency_lockdown_middleware(request: Request, call_next): """Check for emergency lockdown status. Block non-admin routes if active.""" if request.url.path in ["/health", "/api/v1/admin/emergency-lockdown", "/api/v1/admin/emergency-status"]: return await call_next(request) try: import redis.asyncio as redis_lib r = redis_lib.Redis( host=os.getenv("REDIS_HOST", "localhost"), port=int(os.getenv("REDIS_PORT", "6379")), password=os.getenv("REDIS_PASSWORD", ""), decode_responses=True, ) is_locked = await r.exists("rmi:emergency_lockdown") if is_locked: auth_header = request.headers.get("Authorization", "") session_token = request.headers.get("X-Admin-Session", "") if not auth_header and not session_token: return JSONResponse( status_code=503, content={"error": "Service Unavailable", "detail": "System is in emergency lockdown."}, ) except Exception: pass return await call_next(request) async def hsts_middleware(request: Request, call_next): """Force HTTPS and prevent protocol downgrade attacks.""" response = await call_next(request) response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload" return response async def request_id_middleware(request: Request, call_next): """Generate unique request ID for log correlation.""" request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4()) response = await call_next(request) response.headers["X-Request-ID"] = request_id return response async def payload_size_limit_middleware(request: Request, call_next): """Enforce 1MB payload limit on non-upload routes.""" content_length = request.headers.get("content-length") if content_length and int(content_length) > MAX_PAYLOAD_SIZE: if not request.url.path.startswith("/api/v1/admin/backend/upload/"): return JSONResponse( status_code=413, content={"error": "Payload Too Large", "detail": "Maximum payload size is 1MB"}, ) return await call_next(request) async def secure_cookie_middleware(request: Request, call_next): """Enforce secure cookie flags for admin sessions.""" response = await call_next(request) if "set-cookie" in response.headers: cookie_val = response.headers["set-cookie"] if "HttpOnly" not in cookie_val: cookie_val += "; HttpOnly" if "Secure" not in cookie_val: cookie_val += "; Secure" if "SameSite=Strict" not in cookie_val and "SameSite=Lax" not in cookie_val: cookie_val += "; SameSite=Strict" response.headers["set-cookie"] = cookie_val return response