mirror of
https://github.com/Viren070/mediaflow-proxy.git
synced 2025-12-01 23:22:12 +01:00
Add support for obfuscating parameters by encrypting & support ip, exp time restriction for generated url
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user