diff --git a/README.md b/README.md index b68f2b4..6d1baf5 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/mediaflow_proxy/handlers.py b/mediaflow_proxy/handlers.py index 1eac22e..1e0ecff 100644 --- a/mediaflow_proxy/handlers.py +++ b/mediaflow_proxy/handlers.py @@ -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) diff --git a/mediaflow_proxy/routes.py b/mediaflow_proxy/routes.py index 2e929fb..3feec5b 100644 --- a/mediaflow_proxy/routes.py +++ b/mediaflow_proxy/routes.py @@ -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") diff --git a/mediaflow_proxy/utils/cache_utils.py b/mediaflow_proxy/utils/cache_utils.py index 05e714e..83de2f3 100644 --- a/mediaflow_proxy/utils/cache_utils.py +++ b/mediaflow_proxy/utils/cache_utils.py @@ -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] diff --git a/mediaflow_proxy/utils/http_utils.py b/mediaflow_proxy/utils/http_utils.py index f9a0a39..4c9fd18 100644 --- a/mediaflow_proxy/utils/http_utils.py +++ b/mediaflow_proxy/utils/http_utils.py @@ -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 diff --git a/mediaflow_proxy/utils/mpd_utils.py b/mediaflow_proxy/utils/mpd_utils.py index 15400bd..d900758 100644 --- a/mediaflow_proxy/utils/mpd_utils.py +++ b/mediaflow_proxy/utils/mpd_utils.py @@ -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.