|
|
import base64 |
|
|
import json |
|
|
import logging |
|
|
import time |
|
|
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") |
|
|
|
|
|
def decrypt_data(self, token: str, client_ip: str) -> dict: |
|
|
try: |
|
|
encrypted_data = base64.urlsafe_b64decode(token.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"] |
|
|
|
|
|
if "ip" in data: |
|
|
if data["ip"] != client_ip: |
|
|
raise HTTPException(status_code=403, detail="IP address mismatch") |
|
|
del data["ip"] |
|
|
|
|
|
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): |
|
|
encrypted_token = request.query_params.get("token") |
|
|
if encrypted_token: |
|
|
try: |
|
|
client_ip = self.get_client_ip(request) |
|
|
decrypted_data = self.encryption_handler.decrypt_data(encrypted_token, client_ip) |
|
|
|
|
|
query_params = dict(request.query_params) |
|
|
query_params.pop("token") |
|
|
query_params.update(decrypted_data) |
|
|
query_params["has_encrypted"] = True |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
try: |
|
|
response = await call_next(request) |
|
|
except Exception as e: |
|
|
logging.exception("An error occurred while processing the request") |
|
|
return JSONResponse(content={"error": str(e)}, status_code=500) |
|
|
return response |
|
|
|
|
|
@staticmethod |
|
|
def get_client_ip(request: Request) -> str | None: |
|
|
""" |
|
|
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: |
|
|
|
|
|
|
|
|
|
|
|
return x_forwarded_for.split(",")[0].strip() |
|
|
|
|
|
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) |
|
|
|