Add support for obfuscating parameters by encrypting & support ip, exp time restriction for generated url

This commit is contained in:
mhdzumair
2024-09-19 07:32:01 +05:30
parent 9b1476658f
commit a71fd48bf4
7 changed files with 201 additions and 6 deletions
+38 -3
View File
@@ -30,7 +30,10 @@ MediaFlow Proxy is a powerful and flexible solution for proxifying various types
- Retrieve public IP address of the MediaFlow Proxy server for use with Debrid services
- Support for HTTP/HTTPS/SOCKS5 proxy forwarding
- Protect against unauthorized access and network bandwidth abuses
- Support for play expired or self-signed SSL certificates server streams
- Support for play expired or self-signed SSL certificates server streams `(verify_ssl=false)` default is `false`
- Flexible request proxy usage control per request `(use_request_proxy=true/false)` default is `true`
- Obfuscating endpoint parameters by encrypting them to hide sensitive information from third-party.
- Optional IP-based access control restriction & expiration for encrypted URLs to prevent unauthorized access
## Configuration
@@ -151,10 +154,10 @@ Once the server is running, for more details on the available endpoints and thei
### Examples
#### Proxy HTTPS Stream
#### Proxy HTTPS Stream (without using configured proxy)
```bash
mpv "http://localhost:8888/proxy/stream?d=https://jsoncompare.org/LearningContainer/SampleFiles/Video/MP4/sample-mp4-file.mp4&api_password=your_password"
mpv "http://localhost:8888/proxy/stream?d=https://jsoncompare.org/LearningContainer/SampleFiles/Video/MP4/sample-mp4-file.mp4&api_password=your_password&use_request_proxy=false"
```
#### Proxy HTTPS self-signed certificate Stream
@@ -217,6 +220,38 @@ This will output a properly encoded URL that can be used with players like VLC.
vlc "http://127.0.0.1:8888/proxy/mpd/manifest?key_id=nrQFDeRLSAKTLifXUIPiZg&key=FmY0xnWCPCNaSpRG-tUuTQ&api_password=dedsec&d=https%3A%2F%2Fmedia.axprod.net%2FTestVectors%2Fv7-MultiDRM-SingleKey%2FManifest_1080p_ClearKey.mpd"
```
### Generating Encrypted URLs
To generate an encrypted URL with optional IP restriction and expiration, Use the `/generate_encrypted_or_encoded_url` endpoint via swagger UI or programmatically as shown below:
```python
import requests
url = "http://localhost:8888/generate_encrypted_or_encoded_url"
data = {
"mediaflow_proxy_url": "http://localhost:8888",
"endpoint": "/proxy/mpd/manifest",
"destination_url": "https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p_ClearKey.mpd",
"query_params": {
"key_id": "nrQFDeRLSAKTLifXUIPiZg",
"key": "FmY0xnWCPCNaSpRG-tUuTQ"
},
"request_headers": {
"referer": "https://media.axprod.net/",
"origin": "https://media.axprod.net",
},
"expiration": 3600, # URL will expire in 1 hour
"ip": "123.123.123.123", # Optional: Restrict access to this IP
"api_password": "your_password"
}
response = requests.post(url, json=data)
encrypted_url = response.json()["encoded_url"]
print(encrypted_url)
```
You can then use the `encoded_url` in your player or application to access the media stream.
### Using MediaFlow Proxy with Debrid Services and Stremio Addons
MediaFlow Proxy can be particularly useful when working with Debrid services (like Real-Debrid, AllDebrid) and Stremio addons. The `/proxy/ip` endpoint allows you to retrieve the public IP address of the MediaFlow Proxy server, which is crucial for routing Debrid streams correctly.
+23
View File
@@ -9,6 +9,9 @@ from starlette.staticfiles import StaticFiles
from mediaflow_proxy.configs import settings
from mediaflow_proxy.routes import proxy_router
from mediaflow_proxy.schemas import GenerateUrlRequest
from mediaflow_proxy.utils.crypto_utils import EncryptionHandler, EncryptionMiddleware
from mediaflow_proxy.utils.http_utils import encode_mediaflow_proxy_url
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
app = FastAPI()
@@ -21,6 +24,7 @@ app.add_middleware(
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(EncryptionMiddleware)
async def verify_api_key(api_key: str = Security(api_password_query), api_key_alt: str = Security(api_password_header)):
@@ -50,6 +54,25 @@ async def get_favicon():
return RedirectResponse(url="/logo.png")
@app.post("/generate_encrypted_or_encoded_url")
async def generate_encrypted_or_encoded_url(request: GenerateUrlRequest):
if "api_password" not in request.query_params:
request.query_params["api_password"] = request.api_password
encoded_url = encode_mediaflow_proxy_url(
request.mediaflow_proxy_url,
request.endpoint,
request.destination_url,
request.query_params,
request.request_headers,
request.response_headers,
EncryptionHandler(request.api_password) if request.api_password else None,
request.expiration,
str(request.ip) if request.ip else None,
)
return {"encoded_url": encoded_url}
app.include_router(proxy_router, prefix="/proxy", tags=["proxy"], dependencies=[Depends(verify_api_key)])
static_path = resources.files("mediaflow_proxy").joinpath("static")
+5
View File
@@ -7,6 +7,7 @@ from fastapi import Request, Response, HTTPException
from mediaflow_proxy.configs import settings
from mediaflow_proxy.drm.decrypter import decrypt_segment
from mediaflow_proxy.utils.crypto_utils import encryption_handler
from mediaflow_proxy.utils.http_utils import encode_mediaflow_proxy_url, get_original_scheme, ProxyRequestHeaders
logger = logging.getLogger(__name__)
@@ -107,6 +108,7 @@ def build_hls(mpd_dict: dict, request: Request, key_id: str = None, key: str = N
"""
hls = ["#EXTM3U", "#EXT-X-VERSION:6"]
query_params = dict(request.query_params)
has_encrypted = query_params.pop("has_encrypted", False)
video_profiles = {}
audio_profiles = {}
@@ -120,6 +122,7 @@ def build_hls(mpd_dict: dict, request: Request, key_id: str = None, key: str = N
playlist_url = encode_mediaflow_proxy_url(
proxy_url,
query_params=query_params,
encryption_handler=encryption_handler if has_encrypted else None,
)
if "video" in profile["mimeType"]:
@@ -193,6 +196,7 @@ def build_hls_playlist(mpd_dict: dict, profiles: list[dict], request: Request) -
query_params = dict(request.query_params)
query_params.pop("profile_id", None)
query_params.pop("d", None)
has_encrypted = query_params.pop("has_encrypted", False)
for segment in segments:
if mpd_dict["isLive"]:
@@ -207,6 +211,7 @@ def build_hls_playlist(mpd_dict: dict, profiles: list[dict], request: Request) -
encode_mediaflow_proxy_url(
proxy_url,
query_params=query_params,
encryption_handler=encryption_handler if has_encrypted else None,
)
)
added_segments += 1
+17
View File
@@ -0,0 +1,17 @@
from pydantic import BaseModel, Field, IPvAnyAddress
class GenerateUrlRequest(BaseModel):
mediaflow_proxy_url: str = Field(..., description="The base URL for the mediaflow proxy.")
endpoint: str | None = Field(None, description="The specific endpoint to be appended to the base URL.")
destination_url: str | None = Field(None, description="The destination URL to which the request will be proxied.")
query_params: dict | None = Field(None, description="Query parameters to be included in the request.")
request_headers: dict | None = Field(None, description="Headers to be included in the request.")
response_headers: dict | None = Field(None, description="Headers to be included in the response.")
expiration: int | None = Field(
None, description="Expiration time for the URL in seconds. If not provided, the URL will not expire."
)
api_password: str | None = Field(
None, description="API password for encryption. If not provided, the URL will only be encoded."
)
ip: IPvAnyAddress | None = Field(None, description="The IP address to restrict the URL to.")
+99
View File
@@ -0,0 +1,99 @@
import base64
import json
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"] # 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):
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)
# Modify request query parameters with decrypted data
query_params = dict(request.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)
response = await call_next(request)
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:
# 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)
+15 -3
View File
@@ -3,6 +3,7 @@ import typing
from dataclasses import dataclass
from functools import partial
from urllib import parse
from urllib.parse import urlencode
import anyio
import httpx
@@ -16,6 +17,7 @@ from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_excep
from mediaflow_proxy.configs import settings
from mediaflow_proxy.const import SUPPORTED_REQUEST_HEADERS
from mediaflow_proxy.utils.crypto_utils import EncryptionHandler
logger = logging.getLogger(__name__)
@@ -215,9 +217,12 @@ def encode_mediaflow_proxy_url(
query_params: dict | None = None,
request_headers: dict | None = None,
response_headers: dict | None = None,
encryption_handler: EncryptionHandler = None,
expiration: int = None,
ip: str = None,
) -> str:
"""
Encodes a MediaFlow proxy URL with query parameters and headers.
Encodes & Encrypt (Optional) a MediaFlow proxy URL with query parameters and headers.
Args:
mediaflow_proxy_url (str): The base MediaFlow proxy URL.
@@ -226,6 +231,9 @@ def encode_mediaflow_proxy_url(
query_params (dict, optional): Additional query parameters to include. Defaults to None.
request_headers (dict, optional): Headers to include as query parameters. Defaults to None.
response_headers (dict, optional): Headers to include as query parameters. Defaults to None.
encryption_handler (EncryptionHandler, optional): The encryption handler to use. Defaults to None.
expiration (int, optional): The expiration time for the encrypted token. Defaults to None.
ip (str, optional): The public IP address to include in the query parameters. Defaults to None.
Returns:
str: The encoded MediaFlow proxy URL.
@@ -243,8 +251,12 @@ def encode_mediaflow_proxy_url(
query_params.update(
{key if key.startswith("r_") else f"r_{key}": value for key, value in response_headers.items()}
)
# Encode the query parameters
encoded_params = parse.urlencode(query_params, quote_via=parse.quote)
if encryption_handler:
encrypted_token = encryption_handler.encrypt_data(query_params, expiration, ip)
encoded_params = urlencode({"token": encrypted_token})
else:
encoded_params = urlencode(query_params)
# Construct the full URL
if endpoint is None:
+4
View File
@@ -3,6 +3,7 @@ from urllib import parse
from pydantic import HttpUrl
from mediaflow_proxy.utils.crypto_utils import encryption_handler
from mediaflow_proxy.utils.http_utils import encode_mediaflow_proxy_url, get_original_scheme
@@ -74,10 +75,13 @@ class M3U8Processor:
str: The proxied URL.
"""
full_url = parse.urljoin(base_url, url)
query_params = dict(self.request.query_params)
has_encrypted = query_params.pop("has_encrypted", False)
return encode_mediaflow_proxy_url(
self.mediaflow_proxy_url,
"",
full_url,
query_params=dict(self.request.query_params),
encryption_handler=encryption_handler if has_encrypted else None,
)