Add Support for play expired or self-signed SSL certificates server streams

This commit is contained in:
mhdzumair
2024-08-31 15:31:45 +05:30
parent 9c4a1a8858
commit 0e2d63a838
6 changed files with 79 additions and 26 deletions
+8 -1
View File
@@ -30,7 +30,7 @@ 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
## Configuration
@@ -157,6 +157,13 @@ Once the server is running, for more details on the available endpoints and thei
mpv "http://localhost:8888/proxy/stream?d=https://jsoncompare.org/LearningContainer/SampleFiles/Video/MP4/sample-mp4-file.mp4&api_password=your_password"
```
#### Proxy HTTPS self-signed certificate Stream
```bash
mpv "http://localhost:8888/proxy/stream?d=https://self-signed.badssl.com/&api_password=your_password&verify_ssl=false"
```
#### Proxy HLS Stream with Headers
```bash
+41 -11
View File
@@ -24,7 +24,9 @@ from .utils.mpd_utils import pad_base64
logger = logging.getLogger(__name__)
async def handle_hls_stream_proxy(request: Request, destination: str, headers: dict, key_url: HttpUrl = None):
async def handle_hls_stream_proxy(
request: Request, destination: str, headers: dict, key_url: HttpUrl = None, verify_ssl: bool = True
):
"""
Handles the HLS stream proxy request, fetching and processing the m3u8 playlist or streaming the content.
@@ -33,6 +35,7 @@ async def handle_hls_stream_proxy(request: Request, destination: str, headers: d
destination (str): The destination URL to fetch the content from.
headers (dict): The headers to include in the request.
key_url (str, optional): The HLS Key URL to replace the original key URL. Defaults to None.
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
Returns:
Response: The HTTP response with the processed m3u8 playlist or streamed content.
@@ -42,6 +45,7 @@ async def handle_hls_stream_proxy(request: Request, destination: str, headers: d
timeout=httpx.Timeout(30.0),
limits=httpx.Limits(max_keepalive_connections=10, max_connections=20),
proxy=settings.proxy_url,
verify=verify_ssl,
)
streamer = Streamer(client)
try:
@@ -83,7 +87,7 @@ async def handle_hls_stream_proxy(request: Request, destination: str, headers: d
return Response(status_code=502, content=f"Internal server error: {e}")
async def proxy_stream(method: str, video_url: str, headers: dict):
async def proxy_stream(method: str, video_url: str, headers: dict, verify_ssl: bool = True):
"""
Proxies the stream request to the given video URL.
@@ -91,14 +95,15 @@ async def proxy_stream(method: str, video_url: str, headers: dict):
method (str): The HTTP method (e.g., GET, HEAD).
video_url (str): The URL of the video to stream.
headers (dict): The headers to include in the request.
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
Returns:
Response: The HTTP response with the streamed content.
"""
return await handle_stream_request(method, video_url, headers)
return await handle_stream_request(method, video_url, headers, verify_ssl)
async def handle_stream_request(method: str, video_url: str, headers: dict):
async def handle_stream_request(method: str, video_url: str, headers: dict, verify_ssl: bool = True):
"""
Handles the stream request, fetching the content from the video URL and streaming it.
@@ -106,6 +111,7 @@ async def handle_stream_request(method: str, video_url: str, headers: dict):
method (str): The HTTP method (e.g., GET, HEAD).
video_url (str): The URL of the video to stream.
headers (dict): The headers to include in the request.
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
Returns:
Response: The HTTP response with the streamed content.
@@ -115,6 +121,7 @@ async def handle_stream_request(method: str, video_url: str, headers: dict):
timeout=httpx.Timeout(30.0),
limits=httpx.Limits(max_keepalive_connections=10, max_connections=20),
proxy=settings.proxy_url,
verify=verify_ssl,
)
streamer = Streamer(client)
try:
@@ -223,7 +230,9 @@ async def handle_drm_key_data(key_id, key, drm_info):
return key_id, key
async def get_manifest(request: Request, mpd_url: str, headers: dict, key_id: str = None, key: str = None):
async def get_manifest(
request: Request, mpd_url: str, headers: dict, key_id: str = None, key: str = None, verify_ssl: bool = True
):
"""
Retrieves and processes the MPD manifest, converting it to an HLS manifest.
@@ -233,12 +242,15 @@ async def get_manifest(request: Request, mpd_url: str, headers: dict, key_id: st
headers (dict): The headers to include in the request.
key_id (str, optional): The DRM key ID. Defaults to None.
key (str, optional): The DRM key. Defaults to None.
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
Returns:
Response: The HTTP response with the HLS manifest.
"""
try:
mpd_dict = await get_cached_mpd(mpd_url, headers=headers, parse_drm=not key_id and not key)
mpd_dict = await get_cached_mpd(
mpd_url, headers=headers, parse_drm=not key_id and not key, verify_ssl=verify_ssl
)
except DownloadError as e:
raise HTTPException(status_code=e.status_code, detail=f"Failed to download MPD: {e.message}")
drm_info = mpd_dict.get("drmInfo", {})
@@ -259,7 +271,13 @@ async def get_manifest(request: Request, mpd_url: str, headers: dict, key_id: st
async def get_playlist(
request: Request, mpd_url: str, profile_id: str, headers: dict, key_id: str = None, key: str = None
request: Request,
mpd_url: str,
profile_id: str,
headers: dict,
key_id: str = None,
key: str = None,
verify_ssl: bool = True,
):
"""
Retrieves and processes the MPD manifest, converting it to an HLS playlist for a specific profile.
@@ -271,18 +289,29 @@ async def get_playlist(
headers (dict): The headers to include in the request.
key_id (str, optional): The DRM key ID. Defaults to None.
key (str, optional): The DRM key. Defaults to None.
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
Returns:
Response: The HTTP response with the HLS playlist.
"""
mpd_dict = await get_cached_mpd(
mpd_url, headers=headers, parse_drm=not key_id and not key, parse_segment_profile_id=profile_id
mpd_url,
headers=headers,
parse_drm=not key_id and not key,
parse_segment_profile_id=profile_id,
verify_ssl=verify_ssl,
)
return await process_playlist(request, mpd_dict, profile_id)
async def get_segment(
init_url: str, segment_url: str, mimetype: str, headers: dict, key_id: str = None, key: str = None
init_url: str,
segment_url: str,
mimetype: str,
headers: dict,
key_id: str = None,
key: str = None,
verify_ssl: bool = True,
):
"""
Retrieves and processes a media segment, decrypting it if necessary.
@@ -294,13 +323,14 @@ async def get_segment(
headers (dict): The headers to include in the request.
key_id (str, optional): The DRM key ID. Defaults to None.
key (str, optional): The DRM key. Defaults to None.
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
Returns:
Response: The HTTP response with the processed segment.
"""
try:
init_content = await get_cached_init_segment(init_url, headers)
segment_content = await download_file_with_retry(segment_url, headers)
init_content = await get_cached_init_segment(init_url, headers, verify_ssl)
segment_content = await download_file_with_retry(segment_url, headers, verify_ssl=verify_ssl)
except DownloadError as e:
raise HTTPException(status_code=e.status_code, detail=f"Failed to download segment: {e.message}")
return await process_segment(init_content, segment_content, mimetype, key_id, key)
+17 -6
View File
@@ -14,6 +14,7 @@ async def hls_stream_proxy(
d: HttpUrl,
headers: dict = Depends(get_proxy_headers),
key_url: HttpUrl | None = None,
verify_ssl: bool = True,
):
"""
Proxify HLS stream requests, fetching and processing the m3u8 playlist or streaming the content.
@@ -23,17 +24,20 @@ async def hls_stream_proxy(
d (HttpUrl): The destination URL to fetch the content from.
key_url (HttpUrl, optional): The HLS Key URL to replace the original key URL. Defaults to None. (Useful for bypassing some sneaky protection)
headers (dict): The headers to include in the request.
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
Returns:
Response: The HTTP response with the processed m3u8 playlist or streamed content.
"""
destination = str(d)
return await handle_hls_stream_proxy(request, destination, headers, key_url)
return await handle_hls_stream_proxy(request, destination, headers, key_url, verify_ssl)
@proxy_router.head("/stream")
@proxy_router.get("/stream")
async def proxy_stream_endpoint(request: Request, d: HttpUrl, headers: dict = Depends(get_proxy_headers)):
async def proxy_stream_endpoint(
request: Request, d: HttpUrl, headers: dict = Depends(get_proxy_headers), verify_ssl: bool = True
):
"""
Proxies stream requests to the given video URL.
@@ -41,12 +45,13 @@ async def proxy_stream_endpoint(request: Request, d: HttpUrl, headers: dict = De
request (Request): The incoming HTTP request.
d (HttpUrl): The URL of the video to stream.
headers (dict): The headers to include in the request.
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
Returns:
Response: The HTTP response with the streamed content.
"""
headers.update({"range": headers.get("range", "bytes=0-")})
return await proxy_stream(request.method, str(d), headers)
return await proxy_stream(request.method, str(d), headers, verify_ssl)
@proxy_router.get("/mpd/manifest")
@@ -56,6 +61,7 @@ async def manifest_endpoint(
headers: dict = Depends(get_proxy_headers),
key_id: str = None,
key: str = None,
verify_ssl: bool = True,
):
"""
Retrieves and processes the MPD manifest, converting it to an HLS manifest.
@@ -66,11 +72,12 @@ async def manifest_endpoint(
headers (dict): The headers to include in the request.
key_id (str, optional): The DRM key ID. Defaults to None.
key (str, optional): The DRM key. Defaults to None.
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
Returns:
Response: The HTTP response with the HLS manifest.
"""
return await get_manifest(request, str(d), headers, key_id, key)
return await get_manifest(request, str(d), headers, key_id, key, verify_ssl)
@proxy_router.get("/mpd/playlist")
@@ -81,6 +88,7 @@ async def playlist_endpoint(
headers: dict = Depends(get_proxy_headers),
key_id: str = None,
key: str = None,
verify_ssl: bool = True,
):
"""
Retrieves and processes the MPD manifest, converting it to an HLS playlist for a specific profile.
@@ -92,11 +100,12 @@ async def playlist_endpoint(
headers (dict): The headers to include in the request.
key_id (str, optional): The DRM key ID. Defaults to None.
key (str, optional): The DRM key. Defaults to None.
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
Returns:
Response: The HTTP response with the HLS playlist.
"""
return await get_playlist(request, str(d), profile_id, headers, key_id, key)
return await get_playlist(request, str(d), profile_id, headers, key_id, key, verify_ssl)
@proxy_router.get("/mpd/segment")
@@ -107,6 +116,7 @@ async def segment_endpoint(
headers: dict = Depends(get_proxy_headers),
key_id: str = None,
key: str = None,
verify_ssl: bool = True,
):
"""
Retrieves and processes a media segment, decrypting it if necessary.
@@ -118,11 +128,12 @@ async def segment_endpoint(
headers (dict): The headers to include in the request.
key_id (str, optional): The DRM key ID. Defaults to None.
key (str, optional): The DRM key. Defaults to None.
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
Returns:
Response: The HTTP response with the processed segment.
"""
return await get_segment(str(init_url), str(segment_url), mime_type, headers, key_id, key)
return await get_segment(str(init_url), str(segment_url), mime_type, headers, key_id, key, verify_ssl)
@proxy_router.get("/ip")
+6 -4
View File
@@ -14,7 +14,7 @@ init_segment_cache = TTLCache(maxsize=100, ttl=3600) # 1 hour default TTL
async def get_cached_mpd(
mpd_url: str, headers: dict, parse_drm: bool, parse_segment_profile_id: str | None = None
mpd_url: str, headers: dict, parse_drm: bool, parse_segment_profile_id: str | None = None, verify_ssl: bool = True
) -> dict:
"""
Retrieves and caches the MPD manifest, parsing it if not already cached.
@@ -24,6 +24,7 @@ async def get_cached_mpd(
headers (dict): The headers to include in the request.
parse_drm (bool): Whether to parse DRM information.
parse_segment_profile_id (str, optional): The profile ID to parse segments for. Defaults to None.
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
Returns:
dict: The parsed MPD manifest data.
@@ -33,7 +34,7 @@ async def get_cached_mpd(
logger.info(f"Using cached MPD for {mpd_url}")
return parse_mpd_dict(mpd_cache[mpd_url]["mpd"], mpd_url, parse_drm, parse_segment_profile_id)
mpd_dict = parse_mpd(await download_file_with_retry(mpd_url, headers))
mpd_dict = parse_mpd(await download_file_with_retry(mpd_url, headers, verify_ssl=verify_ssl))
parsed_mpd_dict = parse_mpd_dict(mpd_dict, mpd_url, parse_drm, parse_segment_profile_id)
current_time = datetime.datetime.now(datetime.UTC)
expiration_time = current_time + datetime.timedelta(seconds=parsed_mpd_dict.get("minimumUpdatePeriod", 300))
@@ -41,18 +42,19 @@ async def get_cached_mpd(
return parsed_mpd_dict
async def get_cached_init_segment(init_url: str, headers: dict) -> bytes:
async def get_cached_init_segment(init_url: str, headers: dict, verify_ssl: bool = True) -> bytes:
"""
Retrieves and caches the initialization segment.
Args:
init_url (str): The URL of the initialization segment.
headers (dict): The headers to include in the request.
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
Returns:
bytes: The initialization segment content.
"""
if init_url not in init_segment_cache:
init_content = await download_file_with_retry(init_url, headers)
init_content = await download_file_with_retry(init_url, headers, verify_ssl=verify_ssl)
init_segment_cache[init_url] = init_content
return init_segment_cache[init_url]
+5 -2
View File
@@ -137,7 +137,7 @@ class Streamer:
await self.client.aclose()
async def download_file_with_retry(url: str, headers: dict, timeout: float = 10.0):
async def download_file_with_retry(url: str, headers: dict, timeout: float = 10.0, verify_ssl: bool = True):
"""
Downloads a file with retry logic.
@@ -145,6 +145,7 @@ async def download_file_with_retry(url: str, headers: dict, timeout: float = 10.
url (str): The URL of the file to download.
headers (dict): The headers to include in the request.
timeout (float, optional): The request timeout. Defaults to 10.0.
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
Returns:
bytes: The downloaded file content.
@@ -152,7 +153,9 @@ async def download_file_with_retry(url: str, headers: dict, timeout: float = 10.
Raises:
DownloadError: If the download fails after retries.
"""
async with httpx.AsyncClient(follow_redirects=True, timeout=timeout, proxy=settings.proxy_url) as client:
async with httpx.AsyncClient(
follow_redirects=True, timeout=timeout, proxy=settings.proxy_url, verify=verify_ssl
) as client:
try:
response = await fetch_with_retry(client, "GET", url, headers)
return response.content
+2 -2
View File
@@ -10,12 +10,12 @@ import xmltodict
logger = logging.getLogger(__name__)
def parse_mpd(mpd_content: str) -> dict:
def parse_mpd(mpd_content: str | bytes) -> dict:
"""
Parses the MPD content into a dictionary.
Args:
mpd_content (str): The MPD content as a string.
mpd_content (str | bytes): The MPD content to parse.
Returns:
dict: The parsed MPD content as a dictionary.