import base64 import json import logging import time import traceback from typing import Optional from urllib.parse import urlencode from Crypto.Cipher import AES from Crypto.Random import get_random_bytes from Crypto.Util.Padding import pad, unpad from fastapi import HTTPException, Request from starlette.middleware.base import BaseHTTPMiddleware from starlette.responses import JSONResponse from mediaflow_proxy.configs import settings class EncryptionHandler: def __init__(self, secret_key: str): self.secret_key = secret_key.encode("utf-8").ljust(32)[:32] def encrypt_data(self, data: dict, expiration: int = None, ip: str = None) -> str: if expiration: data["exp"] = int(time.time()) + expiration if ip: data["ip"] = ip json_data = json.dumps(data).encode("utf-8") iv = get_random_bytes(16) cipher = AES.new(self.secret_key, AES.MODE_CBC, iv) encrypted_data = cipher.encrypt(pad(json_data, AES.block_size)) return base64.urlsafe_b64encode(iv + encrypted_data).decode("utf-8").rstrip("=") def decrypt_data(self, token: str, client_ip: str) -> dict: try: padding_needed = (4 - len(token) % 4) % 4 encrypted_token_b64_padded = token + ("=" * padding_needed) encrypted_data = base64.urlsafe_b64decode(encrypted_token_b64_padded.encode("utf-8")) iv = encrypted_data[:16] cipher = AES.new(self.secret_key, AES.MODE_CBC, iv) decrypted_data = unpad(cipher.decrypt(encrypted_data[16:]), AES.block_size) data = json.loads(decrypted_data) if "exp" in data: if data["exp"] < time.time(): raise HTTPException(status_code=401, detail="Token has expired") del data["exp"] # Remove expiration from the data if "ip" in data: if data["ip"] != client_ip: raise HTTPException(status_code=403, detail="IP address mismatch") del data["ip"] # Remove IP from the data return data except Exception as e: raise HTTPException(status_code=401, detail="Invalid or expired token") class EncryptionMiddleware(BaseHTTPMiddleware): def __init__(self, app): super().__init__(app) self.encryption_handler = encryption_handler async def dispatch(self, request: Request, call_next): path = request.url.path token_marker = "/_token_" encrypted_token = None # Check for token in path if token_marker in path and self.encryption_handler: try: # Extract token from path token_start = path.find(token_marker) + len(token_marker) token_end = path.find("/", token_start) if token_end == -1: # No trailing slash (no filename after token) token_end = len(path) filename_part = "" else: # There's something after the token (likely a filename) filename_part = path[token_end:] # Get the encrypted token encrypted_token = path[token_start:token_end] # Modify the path to remove the token part but preserve the filename original_path = path[: path.find(token_marker)] original_path += filename_part # Add back the filename part request.scope["path"] = original_path # Update the raw path as well request.scope["raw_path"] = original_path.encode() except Exception as e: logging.error(f"Error processing token in path: {str(e)}") return JSONResponse(content={"error": f"Invalid token in path: {str(e)}"}, status_code=400) # Check for token in query parameters (original method) if not encrypted_token: # Only check if we didn't already find a token in the path encrypted_token = request.query_params.get("token") # Process the token if found (from either source) if encrypted_token and self.encryption_handler: try: client_ip = self.get_client_ip(request) decrypted_data = self.encryption_handler.decrypt_data(encrypted_token, client_ip) # Modify request query parameters with decrypted data query_params = dict(request.query_params) if "token" in query_params: query_params.pop("token") # Remove the encrypted token from query params query_params.update(decrypted_data) # Add decrypted data to query params query_params["has_encrypted"] = True # Create a new request scope with updated query parameters new_query_string = urlencode(query_params) request.scope["query_string"] = new_query_string.encode() request._query_params = query_params except HTTPException as e: return JSONResponse(content={"error": str(e.detail)}, status_code=e.status_code) except Exception as e: logging.error(f"Error decrypting token: {str(e)}") return JSONResponse(content={"error": f"Invalid token: {str(e)}"}, status_code=400) try: response = await call_next(request) except Exception: exc = traceback.format_exc(chain=False) logging.error("An error occurred while processing the request, error: %s", exc) return JSONResponse( content={"error": "An error occurred while processing the request, check the server for logs"}, status_code=500, ) return response @staticmethod def get_client_ip(request: Request) -> Optional[str]: """ Extract the client's real IP address from the request headers or fallback to the client host. """ x_forwarded_for = request.headers.get("X-Forwarded-For") if x_forwarded_for: # In some cases, this header can contain multiple IPs # separated by commas. # The first one is the original client's IP. return x_forwarded_for.split(",")[0].strip() # Fallback to X-Real-IP if X-Forwarded-For is not available x_real_ip = request.headers.get("X-Real-IP") if x_real_ip: return x_real_ip return request.client.host if request.client else "127.0.0.1" encryption_handler = EncryptionHandler(settings.api_password) if settings.api_password else None