Spaces:
Paused
Paused
Add support for response proxy headers & flag to use request proxy via use_request_proxy
Browse files- mediaflow_proxy/handlers.py +84 -39
- mediaflow_proxy/main.py +8 -0
- mediaflow_proxy/mpd_processor.py +14 -6
- mediaflow_proxy/routes.py +36 -19
- mediaflow_proxy/utils/cache_utils.py +17 -4
- mediaflow_proxy/utils/http_utils.py +36 -7
mediaflow_proxy/handlers.py
CHANGED
|
@@ -16,6 +16,7 @@ from .utils.http_utils import (
|
|
| 16 |
download_file_with_retry,
|
| 17 |
request_with_retry,
|
| 18 |
EnhancedStreamingResponse,
|
|
|
|
| 19 |
)
|
| 20 |
from .utils.m3u8_processor import M3U8Processor
|
| 21 |
from .utils.mpd_utils import pad_base64
|
|
@@ -24,7 +25,12 @@ logger = logging.getLogger(__name__)
|
|
| 24 |
|
| 25 |
|
| 26 |
async def handle_hls_stream_proxy(
|
| 27 |
-
request: Request,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
):
|
| 29 |
"""
|
| 30 |
Handles the HLS stream proxy request, fetching and processing the m3u8 playlist or streaming the content.
|
|
@@ -32,9 +38,10 @@ async def handle_hls_stream_proxy(
|
|
| 32 |
Args:
|
| 33 |
request (Request): The incoming HTTP request.
|
| 34 |
destination (str): The destination URL to fetch the content from.
|
| 35 |
-
|
| 36 |
key_url (str, optional): The HLS Key URL to replace the original key URL. Defaults to None.
|
| 37 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
|
|
|
| 38 |
|
| 39 |
Returns:
|
| 40 |
Response: The HTTP response with the processed m3u8 playlist or streamed content.
|
|
@@ -43,19 +50,19 @@ async def handle_hls_stream_proxy(
|
|
| 43 |
follow_redirects=True,
|
| 44 |
timeout=httpx.Timeout(30.0),
|
| 45 |
limits=httpx.Limits(max_keepalive_connections=10, max_connections=20),
|
| 46 |
-
proxy=settings.proxy_url,
|
| 47 |
verify=verify_ssl,
|
| 48 |
)
|
| 49 |
streamer = Streamer(client)
|
| 50 |
try:
|
| 51 |
if destination.endswith((".m3u", ".m3u8")):
|
| 52 |
-
return await fetch_and_process_m3u8(streamer, destination,
|
| 53 |
|
| 54 |
-
response = await streamer.head(destination,
|
| 55 |
if "mpegurl" in response.headers.get("content-type", "").lower():
|
| 56 |
-
return await fetch_and_process_m3u8(streamer, destination,
|
| 57 |
|
| 58 |
-
|
| 59 |
# clean up the headers to only include the necessary headers and remove acl headers
|
| 60 |
response_headers = {k: v for k, v in response.headers.multi_items() if k in SUPPORTED_RESPONSE_HEADERS}
|
| 61 |
|
|
@@ -65,9 +72,10 @@ async def handle_hls_stream_proxy(
|
|
| 65 |
else:
|
| 66 |
transfer_encoding = "chunked"
|
| 67 |
response_headers["transfer-encoding"] = transfer_encoding
|
|
|
|
| 68 |
|
| 69 |
return EnhancedStreamingResponse(
|
| 70 |
-
streamer.stream_content(destination,
|
| 71 |
status_code=response.status_code,
|
| 72 |
headers=response_headers,
|
| 73 |
background=BackgroundTask(streamer.close),
|
|
@@ -86,31 +94,45 @@ async def handle_hls_stream_proxy(
|
|
| 86 |
return Response(status_code=502, content=f"Internal server error: {e}")
|
| 87 |
|
| 88 |
|
| 89 |
-
async def proxy_stream(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
"""
|
| 91 |
Proxies the stream request to the given video URL.
|
| 92 |
|
| 93 |
Args:
|
| 94 |
method (str): The HTTP method (e.g., GET, HEAD).
|
| 95 |
video_url (str): The URL of the video to stream.
|
| 96 |
-
|
| 97 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
|
|
|
| 98 |
|
| 99 |
Returns:
|
| 100 |
Response: The HTTP response with the streamed content.
|
| 101 |
"""
|
| 102 |
-
return await handle_stream_request(method, video_url,
|
| 103 |
|
| 104 |
|
| 105 |
-
async def handle_stream_request(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
"""
|
| 107 |
Handles the stream request, fetching the content from the video URL and streaming it.
|
| 108 |
|
| 109 |
Args:
|
| 110 |
method (str): The HTTP method (e.g., GET, HEAD).
|
| 111 |
video_url (str): The URL of the video to stream.
|
| 112 |
-
|
| 113 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
|
|
|
| 114 |
|
| 115 |
Returns:
|
| 116 |
Response: The HTTP response with the streamed content.
|
|
@@ -119,12 +141,12 @@ async def handle_stream_request(method: str, video_url: str, headers: dict, veri
|
|
| 119 |
follow_redirects=True,
|
| 120 |
timeout=httpx.Timeout(30.0),
|
| 121 |
limits=httpx.Limits(max_keepalive_connections=10, max_connections=20),
|
| 122 |
-
proxy=settings.proxy_url,
|
| 123 |
verify=verify_ssl,
|
| 124 |
)
|
| 125 |
streamer = Streamer(client)
|
| 126 |
try:
|
| 127 |
-
response = await streamer.head(video_url,
|
| 128 |
# clean up the headers to only include the necessary headers and remove acl headers
|
| 129 |
response_headers = {k: v for k, v in response.headers.multi_items() if k in SUPPORTED_RESPONSE_HEADERS}
|
| 130 |
if transfer_encoding := response_headers.get("transfer-encoding"):
|
|
@@ -133,13 +155,14 @@ async def handle_stream_request(method: str, video_url: str, headers: dict, veri
|
|
| 133 |
else:
|
| 134 |
transfer_encoding = "chunked"
|
| 135 |
response_headers["transfer-encoding"] = transfer_encoding
|
|
|
|
| 136 |
|
| 137 |
if method == "HEAD":
|
| 138 |
await streamer.close()
|
| 139 |
return Response(headers=response_headers, status_code=response.status_code)
|
| 140 |
else:
|
| 141 |
return EnhancedStreamingResponse(
|
| 142 |
-
streamer.stream_content(video_url,
|
| 143 |
headers=response_headers,
|
| 144 |
status_code=response.status_code,
|
| 145 |
background=BackgroundTask(streamer.close),
|
|
@@ -159,7 +182,7 @@ async def handle_stream_request(method: str, video_url: str, headers: dict, veri
|
|
| 159 |
|
| 160 |
|
| 161 |
async def fetch_and_process_m3u8(
|
| 162 |
-
streamer: Streamer, url: str,
|
| 163 |
):
|
| 164 |
"""
|
| 165 |
Fetches and processes the m3u8 playlist, converting it to an HLS playlist.
|
|
@@ -167,7 +190,7 @@ async def fetch_and_process_m3u8(
|
|
| 167 |
Args:
|
| 168 |
streamer (Streamer): The HTTP client to use for streaming.
|
| 169 |
url (str): The URL of the m3u8 playlist.
|
| 170 |
-
|
| 171 |
request (Request): The incoming HTTP request.
|
| 172 |
key_url (HttpUrl, optional): The HLS Key URL to replace the original key URL. Defaults to None.
|
| 173 |
|
|
@@ -175,16 +198,15 @@ async def fetch_and_process_m3u8(
|
|
| 175 |
Response: The HTTP response with the processed m3u8 playlist.
|
| 176 |
"""
|
| 177 |
try:
|
| 178 |
-
content = await streamer.get_text(url,
|
| 179 |
processor = M3U8Processor(request, key_url)
|
| 180 |
processed_content = await processor.process_m3u8(content, str(streamer.response.url))
|
|
|
|
|
|
|
| 181 |
return Response(
|
| 182 |
content=processed_content,
|
| 183 |
media_type="application/vnd.apple.mpegurl",
|
| 184 |
-
headers=
|
| 185 |
-
"Content-Disposition": "inline",
|
| 186 |
-
"Accept-Ranges": "none",
|
| 187 |
-
},
|
| 188 |
)
|
| 189 |
except httpx.HTTPStatusError as e:
|
| 190 |
logger.error(f"HTTP error while fetching m3u8: {e}")
|
|
@@ -229,7 +251,13 @@ async def handle_drm_key_data(key_id, key, drm_info):
|
|
| 229 |
|
| 230 |
|
| 231 |
async def get_manifest(
|
| 232 |
-
request: Request,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
):
|
| 234 |
"""
|
| 235 |
Retrieves and processes the MPD manifest, converting it to an HLS manifest.
|
|
@@ -237,17 +265,22 @@ async def get_manifest(
|
|
| 237 |
Args:
|
| 238 |
request (Request): The incoming HTTP request.
|
| 239 |
mpd_url (str): The URL of the MPD manifest.
|
| 240 |
-
|
| 241 |
key_id (str, optional): The DRM key ID. Defaults to None.
|
| 242 |
key (str, optional): The DRM key. Defaults to None.
|
| 243 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
|
|
|
| 244 |
|
| 245 |
Returns:
|
| 246 |
Response: The HTTP response with the HLS manifest.
|
| 247 |
"""
|
| 248 |
try:
|
| 249 |
mpd_dict = await get_cached_mpd(
|
| 250 |
-
mpd_url,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
)
|
| 252 |
except DownloadError as e:
|
| 253 |
raise HTTPException(status_code=e.status_code, detail=f"Failed to download MPD: {e.message}")
|
|
@@ -255,7 +288,7 @@ async def get_manifest(
|
|
| 255 |
|
| 256 |
if drm_info and not drm_info.get("isDrmProtected"):
|
| 257 |
# For non-DRM protected MPD, we still create an HLS manifest
|
| 258 |
-
return await process_manifest(request, mpd_dict, None, None)
|
| 259 |
|
| 260 |
key_id, key = await handle_drm_key_data(key_id, key, drm_info)
|
| 261 |
|
|
@@ -265,17 +298,18 @@ async def get_manifest(
|
|
| 265 |
if key and len(key) != 32:
|
| 266 |
key = base64.urlsafe_b64decode(pad_base64(key)).hex()
|
| 267 |
|
| 268 |
-
return await process_manifest(request, mpd_dict, key_id, key)
|
| 269 |
|
| 270 |
|
| 271 |
async def get_playlist(
|
| 272 |
request: Request,
|
| 273 |
mpd_url: str,
|
| 274 |
profile_id: str,
|
| 275 |
-
|
| 276 |
key_id: str = None,
|
| 277 |
key: str = None,
|
| 278 |
verify_ssl: bool = True,
|
|
|
|
| 279 |
):
|
| 280 |
"""
|
| 281 |
Retrieves and processes the MPD manifest, converting it to an HLS playlist for a specific profile.
|
|
@@ -284,32 +318,35 @@ async def get_playlist(
|
|
| 284 |
request (Request): The incoming HTTP request.
|
| 285 |
mpd_url (str): The URL of the MPD manifest.
|
| 286 |
profile_id (str): The profile ID to generate the playlist for.
|
| 287 |
-
|
| 288 |
key_id (str, optional): The DRM key ID. Defaults to None.
|
| 289 |
key (str, optional): The DRM key. Defaults to None.
|
| 290 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
|
|
|
| 291 |
|
| 292 |
Returns:
|
| 293 |
Response: The HTTP response with the HLS playlist.
|
| 294 |
"""
|
| 295 |
mpd_dict = await get_cached_mpd(
|
| 296 |
mpd_url,
|
| 297 |
-
headers=
|
| 298 |
parse_drm=not key_id and not key,
|
| 299 |
parse_segment_profile_id=profile_id,
|
| 300 |
verify_ssl=verify_ssl,
|
|
|
|
| 301 |
)
|
| 302 |
-
return await process_playlist(request, mpd_dict, profile_id)
|
| 303 |
|
| 304 |
|
| 305 |
async def get_segment(
|
| 306 |
init_url: str,
|
| 307 |
segment_url: str,
|
| 308 |
mimetype: str,
|
| 309 |
-
|
| 310 |
key_id: str = None,
|
| 311 |
key: str = None,
|
| 312 |
verify_ssl: bool = True,
|
|
|
|
| 313 |
):
|
| 314 |
"""
|
| 315 |
Retrieves and processes a media segment, decrypting it if necessary.
|
|
@@ -318,28 +355,36 @@ async def get_segment(
|
|
| 318 |
init_url (str): The URL of the initialization segment.
|
| 319 |
segment_url (str): The URL of the media segment.
|
| 320 |
mimetype (str): The MIME type of the segment.
|
| 321 |
-
|
| 322 |
key_id (str, optional): The DRM key ID. Defaults to None.
|
| 323 |
key (str, optional): The DRM key. Defaults to None.
|
| 324 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
|
|
|
| 325 |
|
| 326 |
Returns:
|
| 327 |
Response: The HTTP response with the processed segment.
|
| 328 |
"""
|
| 329 |
try:
|
| 330 |
-
init_content = await get_cached_init_segment(init_url,
|
| 331 |
-
segment_content = await download_file_with_retry(
|
|
|
|
|
|
|
| 332 |
except DownloadError as e:
|
| 333 |
raise HTTPException(status_code=e.status_code, detail=f"Failed to download segment: {e.message}")
|
| 334 |
-
return await process_segment(init_content, segment_content, mimetype, key_id, key)
|
| 335 |
|
| 336 |
|
| 337 |
-
async def get_public_ip():
|
| 338 |
"""
|
| 339 |
Retrieves the public IP address of the MediaFlow proxy.
|
| 340 |
|
|
|
|
|
|
|
|
|
|
| 341 |
Returns:
|
| 342 |
Response: The HTTP response with the public IP address.
|
| 343 |
"""
|
| 344 |
-
ip_address_data = await request_with_retry(
|
|
|
|
|
|
|
| 345 |
return ip_address_data.json()
|
|
|
|
| 16 |
download_file_with_retry,
|
| 17 |
request_with_retry,
|
| 18 |
EnhancedStreamingResponse,
|
| 19 |
+
ProxyRequestHeaders,
|
| 20 |
)
|
| 21 |
from .utils.m3u8_processor import M3U8Processor
|
| 22 |
from .utils.mpd_utils import pad_base64
|
|
|
|
| 25 |
|
| 26 |
|
| 27 |
async def handle_hls_stream_proxy(
|
| 28 |
+
request: Request,
|
| 29 |
+
destination: str,
|
| 30 |
+
proxy_headers: ProxyRequestHeaders,
|
| 31 |
+
key_url: HttpUrl = None,
|
| 32 |
+
verify_ssl: bool = True,
|
| 33 |
+
use_request_proxy: bool = True,
|
| 34 |
):
|
| 35 |
"""
|
| 36 |
Handles the HLS stream proxy request, fetching and processing the m3u8 playlist or streaming the content.
|
|
|
|
| 38 |
Args:
|
| 39 |
request (Request): The incoming HTTP request.
|
| 40 |
destination (str): The destination URL to fetch the content from.
|
| 41 |
+
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
| 42 |
key_url (str, optional): The HLS Key URL to replace the original key URL. Defaults to None.
|
| 43 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
| 44 |
+
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
| 45 |
|
| 46 |
Returns:
|
| 47 |
Response: The HTTP response with the processed m3u8 playlist or streamed content.
|
|
|
|
| 50 |
follow_redirects=True,
|
| 51 |
timeout=httpx.Timeout(30.0),
|
| 52 |
limits=httpx.Limits(max_keepalive_connections=10, max_connections=20),
|
| 53 |
+
proxy=settings.proxy_url if use_request_proxy else None,
|
| 54 |
verify=verify_ssl,
|
| 55 |
)
|
| 56 |
streamer = Streamer(client)
|
| 57 |
try:
|
| 58 |
if destination.endswith((".m3u", ".m3u8")):
|
| 59 |
+
return await fetch_and_process_m3u8(streamer, destination, proxy_headers, request, key_url)
|
| 60 |
|
| 61 |
+
response = await streamer.head(destination, proxy_headers.request)
|
| 62 |
if "mpegurl" in response.headers.get("content-type", "").lower():
|
| 63 |
+
return await fetch_and_process_m3u8(streamer, destination, proxy_headers, request, key_url)
|
| 64 |
|
| 65 |
+
proxy_headers.request.update({"range": proxy_headers.request.get("range", "bytes=0-")})
|
| 66 |
# clean up the headers to only include the necessary headers and remove acl headers
|
| 67 |
response_headers = {k: v for k, v in response.headers.multi_items() if k in SUPPORTED_RESPONSE_HEADERS}
|
| 68 |
|
|
|
|
| 72 |
else:
|
| 73 |
transfer_encoding = "chunked"
|
| 74 |
response_headers["transfer-encoding"] = transfer_encoding
|
| 75 |
+
response_headers.update(proxy_headers.response)
|
| 76 |
|
| 77 |
return EnhancedStreamingResponse(
|
| 78 |
+
streamer.stream_content(destination, proxy_headers.request),
|
| 79 |
status_code=response.status_code,
|
| 80 |
headers=response_headers,
|
| 81 |
background=BackgroundTask(streamer.close),
|
|
|
|
| 94 |
return Response(status_code=502, content=f"Internal server error: {e}")
|
| 95 |
|
| 96 |
|
| 97 |
+
async def proxy_stream(
|
| 98 |
+
method: str,
|
| 99 |
+
video_url: str,
|
| 100 |
+
proxy_headers: ProxyRequestHeaders,
|
| 101 |
+
verify_ssl: bool = True,
|
| 102 |
+
use_request_proxy: bool = True,
|
| 103 |
+
):
|
| 104 |
"""
|
| 105 |
Proxies the stream request to the given video URL.
|
| 106 |
|
| 107 |
Args:
|
| 108 |
method (str): The HTTP method (e.g., GET, HEAD).
|
| 109 |
video_url (str): The URL of the video to stream.
|
| 110 |
+
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
| 111 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
| 112 |
+
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
| 113 |
|
| 114 |
Returns:
|
| 115 |
Response: The HTTP response with the streamed content.
|
| 116 |
"""
|
| 117 |
+
return await handle_stream_request(method, video_url, proxy_headers, verify_ssl, use_request_proxy)
|
| 118 |
|
| 119 |
|
| 120 |
+
async def handle_stream_request(
|
| 121 |
+
method: str,
|
| 122 |
+
video_url: str,
|
| 123 |
+
proxy_headers: ProxyRequestHeaders,
|
| 124 |
+
verify_ssl: bool = True,
|
| 125 |
+
use_request_proxy: bool = True,
|
| 126 |
+
):
|
| 127 |
"""
|
| 128 |
Handles the stream request, fetching the content from the video URL and streaming it.
|
| 129 |
|
| 130 |
Args:
|
| 131 |
method (str): The HTTP method (e.g., GET, HEAD).
|
| 132 |
video_url (str): The URL of the video to stream.
|
| 133 |
+
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
| 134 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
| 135 |
+
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
| 136 |
|
| 137 |
Returns:
|
| 138 |
Response: The HTTP response with the streamed content.
|
|
|
|
| 141 |
follow_redirects=True,
|
| 142 |
timeout=httpx.Timeout(30.0),
|
| 143 |
limits=httpx.Limits(max_keepalive_connections=10, max_connections=20),
|
| 144 |
+
proxy=settings.proxy_url if use_request_proxy else None,
|
| 145 |
verify=verify_ssl,
|
| 146 |
)
|
| 147 |
streamer = Streamer(client)
|
| 148 |
try:
|
| 149 |
+
response = await streamer.head(video_url, proxy_headers.request)
|
| 150 |
# clean up the headers to only include the necessary headers and remove acl headers
|
| 151 |
response_headers = {k: v for k, v in response.headers.multi_items() if k in SUPPORTED_RESPONSE_HEADERS}
|
| 152 |
if transfer_encoding := response_headers.get("transfer-encoding"):
|
|
|
|
| 155 |
else:
|
| 156 |
transfer_encoding = "chunked"
|
| 157 |
response_headers["transfer-encoding"] = transfer_encoding
|
| 158 |
+
response_headers.update(proxy_headers.response)
|
| 159 |
|
| 160 |
if method == "HEAD":
|
| 161 |
await streamer.close()
|
| 162 |
return Response(headers=response_headers, status_code=response.status_code)
|
| 163 |
else:
|
| 164 |
return EnhancedStreamingResponse(
|
| 165 |
+
streamer.stream_content(video_url, proxy_headers.request),
|
| 166 |
headers=response_headers,
|
| 167 |
status_code=response.status_code,
|
| 168 |
background=BackgroundTask(streamer.close),
|
|
|
|
| 182 |
|
| 183 |
|
| 184 |
async def fetch_and_process_m3u8(
|
| 185 |
+
streamer: Streamer, url: str, proxy_headers: ProxyRequestHeaders, request: Request, key_url: HttpUrl = None
|
| 186 |
):
|
| 187 |
"""
|
| 188 |
Fetches and processes the m3u8 playlist, converting it to an HLS playlist.
|
|
|
|
| 190 |
Args:
|
| 191 |
streamer (Streamer): The HTTP client to use for streaming.
|
| 192 |
url (str): The URL of the m3u8 playlist.
|
| 193 |
+
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
| 194 |
request (Request): The incoming HTTP request.
|
| 195 |
key_url (HttpUrl, optional): The HLS Key URL to replace the original key URL. Defaults to None.
|
| 196 |
|
|
|
|
| 198 |
Response: The HTTP response with the processed m3u8 playlist.
|
| 199 |
"""
|
| 200 |
try:
|
| 201 |
+
content = await streamer.get_text(url, proxy_headers.request)
|
| 202 |
processor = M3U8Processor(request, key_url)
|
| 203 |
processed_content = await processor.process_m3u8(content, str(streamer.response.url))
|
| 204 |
+
response_headers = {"Content-Disposition": "inline", "Accept-Ranges": "none"}
|
| 205 |
+
response_headers.update(proxy_headers.response)
|
| 206 |
return Response(
|
| 207 |
content=processed_content,
|
| 208 |
media_type="application/vnd.apple.mpegurl",
|
| 209 |
+
headers=response_headers,
|
|
|
|
|
|
|
|
|
|
| 210 |
)
|
| 211 |
except httpx.HTTPStatusError as e:
|
| 212 |
logger.error(f"HTTP error while fetching m3u8: {e}")
|
|
|
|
| 251 |
|
| 252 |
|
| 253 |
async def get_manifest(
|
| 254 |
+
request: Request,
|
| 255 |
+
mpd_url: str,
|
| 256 |
+
proxy_headers: ProxyRequestHeaders,
|
| 257 |
+
key_id: str = None,
|
| 258 |
+
key: str = None,
|
| 259 |
+
verify_ssl: bool = True,
|
| 260 |
+
use_request_proxy: bool = True,
|
| 261 |
):
|
| 262 |
"""
|
| 263 |
Retrieves and processes the MPD manifest, converting it to an HLS manifest.
|
|
|
|
| 265 |
Args:
|
| 266 |
request (Request): The incoming HTTP request.
|
| 267 |
mpd_url (str): The URL of the MPD manifest.
|
| 268 |
+
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
| 269 |
key_id (str, optional): The DRM key ID. Defaults to None.
|
| 270 |
key (str, optional): The DRM key. Defaults to None.
|
| 271 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
| 272 |
+
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
| 273 |
|
| 274 |
Returns:
|
| 275 |
Response: The HTTP response with the HLS manifest.
|
| 276 |
"""
|
| 277 |
try:
|
| 278 |
mpd_dict = await get_cached_mpd(
|
| 279 |
+
mpd_url,
|
| 280 |
+
headers=proxy_headers.request,
|
| 281 |
+
parse_drm=not key_id and not key,
|
| 282 |
+
verify_ssl=verify_ssl,
|
| 283 |
+
use_request_proxy=use_request_proxy,
|
| 284 |
)
|
| 285 |
except DownloadError as e:
|
| 286 |
raise HTTPException(status_code=e.status_code, detail=f"Failed to download MPD: {e.message}")
|
|
|
|
| 288 |
|
| 289 |
if drm_info and not drm_info.get("isDrmProtected"):
|
| 290 |
# For non-DRM protected MPD, we still create an HLS manifest
|
| 291 |
+
return await process_manifest(request, mpd_dict, proxy_headers, None, None)
|
| 292 |
|
| 293 |
key_id, key = await handle_drm_key_data(key_id, key, drm_info)
|
| 294 |
|
|
|
|
| 298 |
if key and len(key) != 32:
|
| 299 |
key = base64.urlsafe_b64decode(pad_base64(key)).hex()
|
| 300 |
|
| 301 |
+
return await process_manifest(request, mpd_dict, proxy_headers, key_id, key)
|
| 302 |
|
| 303 |
|
| 304 |
async def get_playlist(
|
| 305 |
request: Request,
|
| 306 |
mpd_url: str,
|
| 307 |
profile_id: str,
|
| 308 |
+
proxy_headers: ProxyRequestHeaders,
|
| 309 |
key_id: str = None,
|
| 310 |
key: str = None,
|
| 311 |
verify_ssl: bool = True,
|
| 312 |
+
use_request_proxy: bool = True,
|
| 313 |
):
|
| 314 |
"""
|
| 315 |
Retrieves and processes the MPD manifest, converting it to an HLS playlist for a specific profile.
|
|
|
|
| 318 |
request (Request): The incoming HTTP request.
|
| 319 |
mpd_url (str): The URL of the MPD manifest.
|
| 320 |
profile_id (str): The profile ID to generate the playlist for.
|
| 321 |
+
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
| 322 |
key_id (str, optional): The DRM key ID. Defaults to None.
|
| 323 |
key (str, optional): The DRM key. Defaults to None.
|
| 324 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
| 325 |
+
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
| 326 |
|
| 327 |
Returns:
|
| 328 |
Response: The HTTP response with the HLS playlist.
|
| 329 |
"""
|
| 330 |
mpd_dict = await get_cached_mpd(
|
| 331 |
mpd_url,
|
| 332 |
+
headers=proxy_headers.request,
|
| 333 |
parse_drm=not key_id and not key,
|
| 334 |
parse_segment_profile_id=profile_id,
|
| 335 |
verify_ssl=verify_ssl,
|
| 336 |
+
use_request_proxy=use_request_proxy,
|
| 337 |
)
|
| 338 |
+
return await process_playlist(request, mpd_dict, profile_id, proxy_headers)
|
| 339 |
|
| 340 |
|
| 341 |
async def get_segment(
|
| 342 |
init_url: str,
|
| 343 |
segment_url: str,
|
| 344 |
mimetype: str,
|
| 345 |
+
proxy_headers: ProxyRequestHeaders,
|
| 346 |
key_id: str = None,
|
| 347 |
key: str = None,
|
| 348 |
verify_ssl: bool = True,
|
| 349 |
+
use_request_proxy: bool = True,
|
| 350 |
):
|
| 351 |
"""
|
| 352 |
Retrieves and processes a media segment, decrypting it if necessary.
|
|
|
|
| 355 |
init_url (str): The URL of the initialization segment.
|
| 356 |
segment_url (str): The URL of the media segment.
|
| 357 |
mimetype (str): The MIME type of the segment.
|
| 358 |
+
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
| 359 |
key_id (str, optional): The DRM key ID. Defaults to None.
|
| 360 |
key (str, optional): The DRM key. Defaults to None.
|
| 361 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
| 362 |
+
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
| 363 |
|
| 364 |
Returns:
|
| 365 |
Response: The HTTP response with the processed segment.
|
| 366 |
"""
|
| 367 |
try:
|
| 368 |
+
init_content = await get_cached_init_segment(init_url, proxy_headers.request, verify_ssl, use_request_proxy)
|
| 369 |
+
segment_content = await download_file_with_retry(
|
| 370 |
+
segment_url, proxy_headers.request, verify_ssl=verify_ssl, use_request_proxy=use_request_proxy
|
| 371 |
+
)
|
| 372 |
except DownloadError as e:
|
| 373 |
raise HTTPException(status_code=e.status_code, detail=f"Failed to download segment: {e.message}")
|
| 374 |
+
return await process_segment(init_content, segment_content, mimetype, proxy_headers, key_id, key)
|
| 375 |
|
| 376 |
|
| 377 |
+
async def get_public_ip(use_request_proxy: bool = True):
|
| 378 |
"""
|
| 379 |
Retrieves the public IP address of the MediaFlow proxy.
|
| 380 |
|
| 381 |
+
Args:
|
| 382 |
+
use_request_proxy (bool, optional): Whether to use the proxy configuration from the user's MediaFlow config. Defaults to True.
|
| 383 |
+
|
| 384 |
Returns:
|
| 385 |
Response: The HTTP response with the public IP address.
|
| 386 |
"""
|
| 387 |
+
ip_address_data = await request_with_retry(
|
| 388 |
+
"GET", "https://api.ipify.org?format=json", {}, use_request_proxy=use_request_proxy
|
| 389 |
+
)
|
| 390 |
return ip_address_data.json()
|
mediaflow_proxy/main.py
CHANGED
|
@@ -3,6 +3,7 @@ from importlib import resources
|
|
| 3 |
|
| 4 |
from fastapi import FastAPI, Depends, Security, HTTPException
|
| 5 |
from fastapi.security import APIKeyQuery, APIKeyHeader
|
|
|
|
| 6 |
from starlette.responses import RedirectResponse
|
| 7 |
from starlette.staticfiles import StaticFiles
|
| 8 |
|
|
@@ -13,6 +14,13 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(level
|
|
| 13 |
app = FastAPI()
|
| 14 |
api_password_query = APIKeyQuery(name="api_password", auto_error=False)
|
| 15 |
api_password_header = APIKeyHeader(name="api_password", auto_error=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
async def verify_api_key(api_key: str = Security(api_password_query), api_key_alt: str = Security(api_password_header)):
|
|
|
|
| 3 |
|
| 4 |
from fastapi import FastAPI, Depends, Security, HTTPException
|
| 5 |
from fastapi.security import APIKeyQuery, APIKeyHeader
|
| 6 |
+
from starlette.middleware.cors import CORSMiddleware
|
| 7 |
from starlette.responses import RedirectResponse
|
| 8 |
from starlette.staticfiles import StaticFiles
|
| 9 |
|
|
|
|
| 14 |
app = FastAPI()
|
| 15 |
api_password_query = APIKeyQuery(name="api_password", auto_error=False)
|
| 16 |
api_password_header = APIKeyHeader(name="api_password", auto_error=False)
|
| 17 |
+
app.add_middleware(
|
| 18 |
+
CORSMiddleware,
|
| 19 |
+
allow_origins=["*"],
|
| 20 |
+
allow_credentials=True,
|
| 21 |
+
allow_methods=["*"],
|
| 22 |
+
allow_headers=["*"],
|
| 23 |
+
)
|
| 24 |
|
| 25 |
|
| 26 |
async def verify_api_key(api_key: str = Security(api_password_query), api_key_alt: str = Security(api_password_header)):
|
mediaflow_proxy/mpd_processor.py
CHANGED
|
@@ -7,18 +7,21 @@ from fastapi import Request, Response, HTTPException
|
|
| 7 |
|
| 8 |
from mediaflow_proxy.configs import settings
|
| 9 |
from mediaflow_proxy.drm.decrypter import decrypt_segment
|
| 10 |
-
from mediaflow_proxy.utils.http_utils import encode_mediaflow_proxy_url, get_original_scheme
|
| 11 |
|
| 12 |
logger = logging.getLogger(__name__)
|
| 13 |
|
| 14 |
|
| 15 |
-
async def process_manifest(
|
|
|
|
|
|
|
| 16 |
"""
|
| 17 |
Processes the MPD manifest and converts it to an HLS manifest.
|
| 18 |
|
| 19 |
Args:
|
| 20 |
request (Request): The incoming HTTP request.
|
| 21 |
mpd_dict (dict): The MPD manifest data.
|
|
|
|
| 22 |
key_id (str, optional): The DRM key ID. Defaults to None.
|
| 23 |
key (str, optional): The DRM key. Defaults to None.
|
| 24 |
|
|
@@ -26,10 +29,12 @@ async def process_manifest(request: Request, mpd_dict: dict, key_id: str = None,
|
|
| 26 |
Response: The HLS manifest as an HTTP response.
|
| 27 |
"""
|
| 28 |
hls_content = build_hls(mpd_dict, request, key_id, key)
|
| 29 |
-
return Response(content=hls_content, media_type="application/vnd.apple.mpegurl")
|
| 30 |
|
| 31 |
|
| 32 |
-
async def process_playlist(
|
|
|
|
|
|
|
| 33 |
"""
|
| 34 |
Processes the MPD manifest and converts it to an HLS playlist for a specific profile.
|
| 35 |
|
|
@@ -37,6 +42,7 @@ async def process_playlist(request: Request, mpd_dict: dict, profile_id: str) ->
|
|
| 37 |
request (Request): The incoming HTTP request.
|
| 38 |
mpd_dict (dict): The MPD manifest data.
|
| 39 |
profile_id (str): The profile ID to generate the playlist for.
|
|
|
|
| 40 |
|
| 41 |
Returns:
|
| 42 |
Response: The HLS playlist as an HTTP response.
|
|
@@ -49,13 +55,14 @@ async def process_playlist(request: Request, mpd_dict: dict, profile_id: str) ->
|
|
| 49 |
raise HTTPException(status_code=404, detail="Profile not found")
|
| 50 |
|
| 51 |
hls_content = build_hls_playlist(mpd_dict, matching_profiles, request)
|
| 52 |
-
return Response(content=hls_content, media_type="application/vnd.apple.mpegurl")
|
| 53 |
|
| 54 |
|
| 55 |
async def process_segment(
|
| 56 |
init_content: bytes,
|
| 57 |
segment_content: bytes,
|
| 58 |
mimetype: str,
|
|
|
|
| 59 |
key_id: str = None,
|
| 60 |
key: str = None,
|
| 61 |
) -> Response:
|
|
@@ -66,6 +73,7 @@ async def process_segment(
|
|
| 66 |
init_content (bytes): The initialization segment content.
|
| 67 |
segment_content (bytes): The media segment content.
|
| 68 |
mimetype (str): The MIME type of the segment.
|
|
|
|
| 69 |
key_id (str, optional): The DRM key ID. Defaults to None.
|
| 70 |
key (str, optional): The DRM key. Defaults to None.
|
| 71 |
|
|
@@ -81,7 +89,7 @@ async def process_segment(
|
|
| 81 |
# For non-DRM protected content, we just concatenate init and segment content
|
| 82 |
decrypted_content = init_content + segment_content
|
| 83 |
|
| 84 |
-
return Response(content=decrypted_content, media_type=mimetype)
|
| 85 |
|
| 86 |
|
| 87 |
def build_hls(mpd_dict: dict, request: Request, key_id: str = None, key: str = None) -> str:
|
|
|
|
| 7 |
|
| 8 |
from mediaflow_proxy.configs import settings
|
| 9 |
from mediaflow_proxy.drm.decrypter import decrypt_segment
|
| 10 |
+
from mediaflow_proxy.utils.http_utils import encode_mediaflow_proxy_url, get_original_scheme, ProxyRequestHeaders
|
| 11 |
|
| 12 |
logger = logging.getLogger(__name__)
|
| 13 |
|
| 14 |
|
| 15 |
+
async def process_manifest(
|
| 16 |
+
request: Request, mpd_dict: dict, proxy_headers: ProxyRequestHeaders, key_id: str = None, key: str = None
|
| 17 |
+
) -> Response:
|
| 18 |
"""
|
| 19 |
Processes the MPD manifest and converts it to an HLS manifest.
|
| 20 |
|
| 21 |
Args:
|
| 22 |
request (Request): The incoming HTTP request.
|
| 23 |
mpd_dict (dict): The MPD manifest data.
|
| 24 |
+
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
| 25 |
key_id (str, optional): The DRM key ID. Defaults to None.
|
| 26 |
key (str, optional): The DRM key. Defaults to None.
|
| 27 |
|
|
|
|
| 29 |
Response: The HLS manifest as an HTTP response.
|
| 30 |
"""
|
| 31 |
hls_content = build_hls(mpd_dict, request, key_id, key)
|
| 32 |
+
return Response(content=hls_content, media_type="application/vnd.apple.mpegurl", headers=proxy_headers.response)
|
| 33 |
|
| 34 |
|
| 35 |
+
async def process_playlist(
|
| 36 |
+
request: Request, mpd_dict: dict, profile_id: str, proxy_headers: ProxyRequestHeaders
|
| 37 |
+
) -> Response:
|
| 38 |
"""
|
| 39 |
Processes the MPD manifest and converts it to an HLS playlist for a specific profile.
|
| 40 |
|
|
|
|
| 42 |
request (Request): The incoming HTTP request.
|
| 43 |
mpd_dict (dict): The MPD manifest data.
|
| 44 |
profile_id (str): The profile ID to generate the playlist for.
|
| 45 |
+
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
| 46 |
|
| 47 |
Returns:
|
| 48 |
Response: The HLS playlist as an HTTP response.
|
|
|
|
| 55 |
raise HTTPException(status_code=404, detail="Profile not found")
|
| 56 |
|
| 57 |
hls_content = build_hls_playlist(mpd_dict, matching_profiles, request)
|
| 58 |
+
return Response(content=hls_content, media_type="application/vnd.apple.mpegurl", headers=proxy_headers.response)
|
| 59 |
|
| 60 |
|
| 61 |
async def process_segment(
|
| 62 |
init_content: bytes,
|
| 63 |
segment_content: bytes,
|
| 64 |
mimetype: str,
|
| 65 |
+
proxy_headers: ProxyRequestHeaders,
|
| 66 |
key_id: str = None,
|
| 67 |
key: str = None,
|
| 68 |
) -> Response:
|
|
|
|
| 73 |
init_content (bytes): The initialization segment content.
|
| 74 |
segment_content (bytes): The media segment content.
|
| 75 |
mimetype (str): The MIME type of the segment.
|
| 76 |
+
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
| 77 |
key_id (str, optional): The DRM key ID. Defaults to None.
|
| 78 |
key (str, optional): The DRM key. Defaults to None.
|
| 79 |
|
|
|
|
| 89 |
# For non-DRM protected content, we just concatenate init and segment content
|
| 90 |
decrypted_content = init_content + segment_content
|
| 91 |
|
| 92 |
+
return Response(content=decrypted_content, media_type=mimetype, headers=proxy_headers.response)
|
| 93 |
|
| 94 |
|
| 95 |
def build_hls(mpd_dict: dict, request: Request, key_id: str = None, key: str = None) -> str:
|
mediaflow_proxy/routes.py
CHANGED
|
@@ -2,7 +2,7 @@ from fastapi import Request, Depends, APIRouter
|
|
| 2 |
from pydantic import HttpUrl
|
| 3 |
|
| 4 |
from .handlers import handle_hls_stream_proxy, proxy_stream, get_manifest, get_playlist, get_segment, get_public_ip
|
| 5 |
-
from .utils.http_utils import get_proxy_headers
|
| 6 |
|
| 7 |
proxy_router = APIRouter()
|
| 8 |
|
|
@@ -12,9 +12,10 @@ proxy_router = APIRouter()
|
|
| 12 |
async def hls_stream_proxy(
|
| 13 |
request: Request,
|
| 14 |
d: HttpUrl,
|
| 15 |
-
|
| 16 |
key_url: HttpUrl | None = None,
|
| 17 |
verify_ssl: bool = False,
|
|
|
|
| 18 |
):
|
| 19 |
"""
|
| 20 |
Proxify HLS stream requests, fetching and processing the m3u8 playlist or streaming the content.
|
|
@@ -23,20 +24,25 @@ async def hls_stream_proxy(
|
|
| 23 |
request (Request): The incoming HTTP request.
|
| 24 |
d (HttpUrl): The destination URL to fetch the content from.
|
| 25 |
key_url (HttpUrl, optional): The HLS Key URL to replace the original key URL. Defaults to None. (Useful for bypassing some sneaky protection)
|
| 26 |
-
|
| 27 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to False.
|
|
|
|
| 28 |
|
| 29 |
Returns:
|
| 30 |
Response: The HTTP response with the processed m3u8 playlist or streamed content.
|
| 31 |
"""
|
| 32 |
destination = str(d)
|
| 33 |
-
return await handle_hls_stream_proxy(request, destination,
|
| 34 |
|
| 35 |
|
| 36 |
@proxy_router.head("/stream")
|
| 37 |
@proxy_router.get("/stream")
|
| 38 |
async def proxy_stream_endpoint(
|
| 39 |
-
request: Request,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
):
|
| 41 |
"""
|
| 42 |
Proxies stream requests to the given video URL.
|
|
@@ -44,24 +50,26 @@ async def proxy_stream_endpoint(
|
|
| 44 |
Args:
|
| 45 |
request (Request): The incoming HTTP request.
|
| 46 |
d (HttpUrl): The URL of the video to stream.
|
| 47 |
-
|
| 48 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to False.
|
|
|
|
| 49 |
|
| 50 |
Returns:
|
| 51 |
Response: The HTTP response with the streamed content.
|
| 52 |
"""
|
| 53 |
-
|
| 54 |
-
return await proxy_stream(request.method, str(d),
|
| 55 |
|
| 56 |
|
| 57 |
@proxy_router.get("/mpd/manifest")
|
| 58 |
async def manifest_endpoint(
|
| 59 |
request: Request,
|
| 60 |
d: HttpUrl,
|
| 61 |
-
|
| 62 |
key_id: str = None,
|
| 63 |
key: str = None,
|
| 64 |
verify_ssl: bool = False,
|
|
|
|
| 65 |
):
|
| 66 |
"""
|
| 67 |
Retrieves and processes the MPD manifest, converting it to an HLS manifest.
|
|
@@ -69,15 +77,16 @@ async def manifest_endpoint(
|
|
| 69 |
Args:
|
| 70 |
request (Request): The incoming HTTP request.
|
| 71 |
d (HttpUrl): The URL of the MPD manifest.
|
| 72 |
-
|
| 73 |
key_id (str, optional): The DRM key ID. Defaults to None.
|
| 74 |
key (str, optional): The DRM key. Defaults to None.
|
| 75 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to False.
|
|
|
|
| 76 |
|
| 77 |
Returns:
|
| 78 |
Response: The HTTP response with the HLS manifest.
|
| 79 |
"""
|
| 80 |
-
return await get_manifest(request, str(d),
|
| 81 |
|
| 82 |
|
| 83 |
@proxy_router.get("/mpd/playlist")
|
|
@@ -85,10 +94,11 @@ async def playlist_endpoint(
|
|
| 85 |
request: Request,
|
| 86 |
d: HttpUrl,
|
| 87 |
profile_id: str,
|
| 88 |
-
|
| 89 |
key_id: str = None,
|
| 90 |
key: str = None,
|
| 91 |
verify_ssl: bool = False,
|
|
|
|
| 92 |
):
|
| 93 |
"""
|
| 94 |
Retrieves and processes the MPD manifest, converting it to an HLS playlist for a specific profile.
|
|
@@ -97,15 +107,16 @@ async def playlist_endpoint(
|
|
| 97 |
request (Request): The incoming HTTP request.
|
| 98 |
d (HttpUrl): The URL of the MPD manifest.
|
| 99 |
profile_id (str): The profile ID to generate the playlist for.
|
| 100 |
-
|
| 101 |
key_id (str, optional): The DRM key ID. Defaults to None.
|
| 102 |
key (str, optional): The DRM key. Defaults to None.
|
| 103 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to False.
|
|
|
|
| 104 |
|
| 105 |
Returns:
|
| 106 |
Response: The HTTP response with the HLS playlist.
|
| 107 |
"""
|
| 108 |
-
return await get_playlist(request, str(d), profile_id,
|
| 109 |
|
| 110 |
|
| 111 |
@proxy_router.get("/mpd/segment")
|
|
@@ -113,10 +124,11 @@ async def segment_endpoint(
|
|
| 113 |
init_url: HttpUrl,
|
| 114 |
segment_url: HttpUrl,
|
| 115 |
mime_type: str,
|
| 116 |
-
|
| 117 |
key_id: str = None,
|
| 118 |
key: str = None,
|
| 119 |
verify_ssl: bool = False,
|
|
|
|
| 120 |
):
|
| 121 |
"""
|
| 122 |
Retrieves and processes a media segment, decrypting it if necessary.
|
|
@@ -125,23 +137,28 @@ async def segment_endpoint(
|
|
| 125 |
init_url (HttpUrl): The URL of the initialization segment.
|
| 126 |
segment_url (HttpUrl): The URL of the media segment.
|
| 127 |
mime_type (str): The MIME type of the segment.
|
| 128 |
-
|
| 129 |
key_id (str, optional): The DRM key ID. Defaults to None.
|
| 130 |
key (str, optional): The DRM key. Defaults to None.
|
| 131 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to False.
|
|
|
|
| 132 |
|
| 133 |
Returns:
|
| 134 |
Response: The HTTP response with the processed segment.
|
| 135 |
"""
|
| 136 |
-
return await get_segment(
|
|
|
|
|
|
|
| 137 |
|
| 138 |
|
| 139 |
@proxy_router.get("/ip")
|
| 140 |
-
async def get_mediaflow_proxy_public_ip(
|
|
|
|
|
|
|
| 141 |
"""
|
| 142 |
Retrieves the public IP address of the MediaFlow proxy server.
|
| 143 |
|
| 144 |
Returns:
|
| 145 |
Response: The HTTP response with the public IP address in the form of a JSON object. {"ip": "xxx.xxx.xxx.xxx"}
|
| 146 |
"""
|
| 147 |
-
return await get_public_ip()
|
|
|
|
| 2 |
from pydantic import HttpUrl
|
| 3 |
|
| 4 |
from .handlers import handle_hls_stream_proxy, proxy_stream, get_manifest, get_playlist, get_segment, get_public_ip
|
| 5 |
+
from .utils.http_utils import get_proxy_headers, ProxyRequestHeaders
|
| 6 |
|
| 7 |
proxy_router = APIRouter()
|
| 8 |
|
|
|
|
| 12 |
async def hls_stream_proxy(
|
| 13 |
request: Request,
|
| 14 |
d: HttpUrl,
|
| 15 |
+
proxy_headers: ProxyRequestHeaders = Depends(get_proxy_headers),
|
| 16 |
key_url: HttpUrl | None = None,
|
| 17 |
verify_ssl: bool = False,
|
| 18 |
+
use_request_proxy: bool = True,
|
| 19 |
):
|
| 20 |
"""
|
| 21 |
Proxify HLS stream requests, fetching and processing the m3u8 playlist or streaming the content.
|
|
|
|
| 24 |
request (Request): The incoming HTTP request.
|
| 25 |
d (HttpUrl): The destination URL to fetch the content from.
|
| 26 |
key_url (HttpUrl, optional): The HLS Key URL to replace the original key URL. Defaults to None. (Useful for bypassing some sneaky protection)
|
| 27 |
+
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
| 28 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to False.
|
| 29 |
+
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
| 30 |
|
| 31 |
Returns:
|
| 32 |
Response: The HTTP response with the processed m3u8 playlist or streamed content.
|
| 33 |
"""
|
| 34 |
destination = str(d)
|
| 35 |
+
return await handle_hls_stream_proxy(request, destination, proxy_headers, key_url, verify_ssl, use_request_proxy)
|
| 36 |
|
| 37 |
|
| 38 |
@proxy_router.head("/stream")
|
| 39 |
@proxy_router.get("/stream")
|
| 40 |
async def proxy_stream_endpoint(
|
| 41 |
+
request: Request,
|
| 42 |
+
d: HttpUrl,
|
| 43 |
+
proxy_headers: ProxyRequestHeaders = Depends(get_proxy_headers),
|
| 44 |
+
verify_ssl: bool = False,
|
| 45 |
+
use_request_proxy: bool = True,
|
| 46 |
):
|
| 47 |
"""
|
| 48 |
Proxies stream requests to the given video URL.
|
|
|
|
| 50 |
Args:
|
| 51 |
request (Request): The incoming HTTP request.
|
| 52 |
d (HttpUrl): The URL of the video to stream.
|
| 53 |
+
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
| 54 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to False.
|
| 55 |
+
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
| 56 |
|
| 57 |
Returns:
|
| 58 |
Response: The HTTP response with the streamed content.
|
| 59 |
"""
|
| 60 |
+
proxy_headers.request.update({"range": proxy_headers.request.get("range", "bytes=0-")})
|
| 61 |
+
return await proxy_stream(request.method, str(d), proxy_headers, verify_ssl, use_request_proxy)
|
| 62 |
|
| 63 |
|
| 64 |
@proxy_router.get("/mpd/manifest")
|
| 65 |
async def manifest_endpoint(
|
| 66 |
request: Request,
|
| 67 |
d: HttpUrl,
|
| 68 |
+
proxy_headers: ProxyRequestHeaders = Depends(get_proxy_headers),
|
| 69 |
key_id: str = None,
|
| 70 |
key: str = None,
|
| 71 |
verify_ssl: bool = False,
|
| 72 |
+
use_request_proxy: bool = True,
|
| 73 |
):
|
| 74 |
"""
|
| 75 |
Retrieves and processes the MPD manifest, converting it to an HLS manifest.
|
|
|
|
| 77 |
Args:
|
| 78 |
request (Request): The incoming HTTP request.
|
| 79 |
d (HttpUrl): The URL of the MPD manifest.
|
| 80 |
+
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
| 81 |
key_id (str, optional): The DRM key ID. Defaults to None.
|
| 82 |
key (str, optional): The DRM key. Defaults to None.
|
| 83 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to False.
|
| 84 |
+
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
| 85 |
|
| 86 |
Returns:
|
| 87 |
Response: The HTTP response with the HLS manifest.
|
| 88 |
"""
|
| 89 |
+
return await get_manifest(request, str(d), proxy_headers, key_id, key, verify_ssl, use_request_proxy)
|
| 90 |
|
| 91 |
|
| 92 |
@proxy_router.get("/mpd/playlist")
|
|
|
|
| 94 |
request: Request,
|
| 95 |
d: HttpUrl,
|
| 96 |
profile_id: str,
|
| 97 |
+
proxy_headers: ProxyRequestHeaders = Depends(get_proxy_headers),
|
| 98 |
key_id: str = None,
|
| 99 |
key: str = None,
|
| 100 |
verify_ssl: bool = False,
|
| 101 |
+
use_request_proxy: bool = True,
|
| 102 |
):
|
| 103 |
"""
|
| 104 |
Retrieves and processes the MPD manifest, converting it to an HLS playlist for a specific profile.
|
|
|
|
| 107 |
request (Request): The incoming HTTP request.
|
| 108 |
d (HttpUrl): The URL of the MPD manifest.
|
| 109 |
profile_id (str): The profile ID to generate the playlist for.
|
| 110 |
+
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
| 111 |
key_id (str, optional): The DRM key ID. Defaults to None.
|
| 112 |
key (str, optional): The DRM key. Defaults to None.
|
| 113 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to False.
|
| 114 |
+
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
| 115 |
|
| 116 |
Returns:
|
| 117 |
Response: The HTTP response with the HLS playlist.
|
| 118 |
"""
|
| 119 |
+
return await get_playlist(request, str(d), profile_id, proxy_headers, key_id, key, verify_ssl, use_request_proxy)
|
| 120 |
|
| 121 |
|
| 122 |
@proxy_router.get("/mpd/segment")
|
|
|
|
| 124 |
init_url: HttpUrl,
|
| 125 |
segment_url: HttpUrl,
|
| 126 |
mime_type: str,
|
| 127 |
+
proxy_headers: ProxyRequestHeaders = Depends(get_proxy_headers),
|
| 128 |
key_id: str = None,
|
| 129 |
key: str = None,
|
| 130 |
verify_ssl: bool = False,
|
| 131 |
+
use_request_proxy: bool = True,
|
| 132 |
):
|
| 133 |
"""
|
| 134 |
Retrieves and processes a media segment, decrypting it if necessary.
|
|
|
|
| 137 |
init_url (HttpUrl): The URL of the initialization segment.
|
| 138 |
segment_url (HttpUrl): The URL of the media segment.
|
| 139 |
mime_type (str): The MIME type of the segment.
|
| 140 |
+
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
| 141 |
key_id (str, optional): The DRM key ID. Defaults to None.
|
| 142 |
key (str, optional): The DRM key. Defaults to None.
|
| 143 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to False.
|
| 144 |
+
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
| 145 |
|
| 146 |
Returns:
|
| 147 |
Response: The HTTP response with the processed segment.
|
| 148 |
"""
|
| 149 |
+
return await get_segment(
|
| 150 |
+
str(init_url), str(segment_url), mime_type, proxy_headers, key_id, key, verify_ssl, use_request_proxy
|
| 151 |
+
)
|
| 152 |
|
| 153 |
|
| 154 |
@proxy_router.get("/ip")
|
| 155 |
+
async def get_mediaflow_proxy_public_ip(
|
| 156 |
+
use_request_proxy: bool = True,
|
| 157 |
+
):
|
| 158 |
"""
|
| 159 |
Retrieves the public IP address of the MediaFlow proxy server.
|
| 160 |
|
| 161 |
Returns:
|
| 162 |
Response: The HTTP response with the public IP address in the form of a JSON object. {"ip": "xxx.xxx.xxx.xxx"}
|
| 163 |
"""
|
| 164 |
+
return await get_public_ip(use_request_proxy)
|
mediaflow_proxy/utils/cache_utils.py
CHANGED
|
@@ -14,7 +14,12 @@ init_segment_cache = TTLCache(maxsize=100, ttl=3600) # 1 hour default TTL
|
|
| 14 |
|
| 15 |
|
| 16 |
async def get_cached_mpd(
|
| 17 |
-
mpd_url: str,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
) -> dict:
|
| 19 |
"""
|
| 20 |
Retrieves and caches the MPD manifest, parsing it if not already cached.
|
|
@@ -25,6 +30,7 @@ async def get_cached_mpd(
|
|
| 25 |
parse_drm (bool): Whether to parse DRM information.
|
| 26 |
parse_segment_profile_id (str, optional): The profile ID to parse segments for. Defaults to None.
|
| 27 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
|
|
|
| 28 |
|
| 29 |
Returns:
|
| 30 |
dict: The parsed MPD manifest data.
|
|
@@ -34,7 +40,9 @@ async def get_cached_mpd(
|
|
| 34 |
logger.info(f"Using cached MPD for {mpd_url}")
|
| 35 |
return parse_mpd_dict(mpd_cache[mpd_url]["mpd"], mpd_url, parse_drm, parse_segment_profile_id)
|
| 36 |
|
| 37 |
-
mpd_dict = parse_mpd(
|
|
|
|
|
|
|
| 38 |
parsed_mpd_dict = parse_mpd_dict(mpd_dict, mpd_url, parse_drm, parse_segment_profile_id)
|
| 39 |
current_time = datetime.datetime.now(datetime.UTC)
|
| 40 |
expiration_time = current_time + datetime.timedelta(seconds=parsed_mpd_dict.get("minimumUpdatePeriod", 300))
|
|
@@ -42,7 +50,9 @@ async def get_cached_mpd(
|
|
| 42 |
return parsed_mpd_dict
|
| 43 |
|
| 44 |
|
| 45 |
-
async def get_cached_init_segment(
|
|
|
|
|
|
|
| 46 |
"""
|
| 47 |
Retrieves and caches the initialization segment.
|
| 48 |
|
|
@@ -50,11 +60,14 @@ async def get_cached_init_segment(init_url: str, headers: dict, verify_ssl: bool
|
|
| 50 |
init_url (str): The URL of the initialization segment.
|
| 51 |
headers (dict): The headers to include in the request.
|
| 52 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
|
|
|
| 53 |
|
| 54 |
Returns:
|
| 55 |
bytes: The initialization segment content.
|
| 56 |
"""
|
| 57 |
if init_url not in init_segment_cache:
|
| 58 |
-
init_content = await download_file_with_retry(
|
|
|
|
|
|
|
| 59 |
init_segment_cache[init_url] = init_content
|
| 60 |
return init_segment_cache[init_url]
|
|
|
|
| 14 |
|
| 15 |
|
| 16 |
async def get_cached_mpd(
|
| 17 |
+
mpd_url: str,
|
| 18 |
+
headers: dict,
|
| 19 |
+
parse_drm: bool,
|
| 20 |
+
parse_segment_profile_id: str | None = None,
|
| 21 |
+
verify_ssl: bool = True,
|
| 22 |
+
use_request_proxy: bool = True,
|
| 23 |
) -> dict:
|
| 24 |
"""
|
| 25 |
Retrieves and caches the MPD manifest, parsing it if not already cached.
|
|
|
|
| 30 |
parse_drm (bool): Whether to parse DRM information.
|
| 31 |
parse_segment_profile_id (str, optional): The profile ID to parse segments for. Defaults to None.
|
| 32 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
| 33 |
+
use_request_proxy (bool, optional): Whether to use the proxy configuration from the user's MediaFlow config. Defaults to True.
|
| 34 |
|
| 35 |
Returns:
|
| 36 |
dict: The parsed MPD manifest data.
|
|
|
|
| 40 |
logger.info(f"Using cached MPD for {mpd_url}")
|
| 41 |
return parse_mpd_dict(mpd_cache[mpd_url]["mpd"], mpd_url, parse_drm, parse_segment_profile_id)
|
| 42 |
|
| 43 |
+
mpd_dict = parse_mpd(
|
| 44 |
+
await download_file_with_retry(mpd_url, headers, verify_ssl=verify_ssl, use_request_proxy=use_request_proxy)
|
| 45 |
+
)
|
| 46 |
parsed_mpd_dict = parse_mpd_dict(mpd_dict, mpd_url, parse_drm, parse_segment_profile_id)
|
| 47 |
current_time = datetime.datetime.now(datetime.UTC)
|
| 48 |
expiration_time = current_time + datetime.timedelta(seconds=parsed_mpd_dict.get("minimumUpdatePeriod", 300))
|
|
|
|
| 50 |
return parsed_mpd_dict
|
| 51 |
|
| 52 |
|
| 53 |
+
async def get_cached_init_segment(
|
| 54 |
+
init_url: str, headers: dict, verify_ssl: bool = True, use_request_proxy: bool = True
|
| 55 |
+
) -> bytes:
|
| 56 |
"""
|
| 57 |
Retrieves and caches the initialization segment.
|
| 58 |
|
|
|
|
| 60 |
init_url (str): The URL of the initialization segment.
|
| 61 |
headers (dict): The headers to include in the request.
|
| 62 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
| 63 |
+
use_request_proxy (bool, optional): Whether to use the proxy configuration from the user's MediaFlow config. Defaults to True.
|
| 64 |
|
| 65 |
Returns:
|
| 66 |
bytes: The initialization segment content.
|
| 67 |
"""
|
| 68 |
if init_url not in init_segment_cache:
|
| 69 |
+
init_content = await download_file_with_retry(
|
| 70 |
+
init_url, headers, verify_ssl=verify_ssl, use_request_proxy=use_request_proxy
|
| 71 |
+
)
|
| 72 |
init_segment_cache[init_url] = init_content
|
| 73 |
return init_segment_cache[init_url]
|
mediaflow_proxy/utils/http_utils.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import logging
|
| 2 |
import typing
|
|
|
|
| 3 |
from functools import partial
|
| 4 |
from urllib import parse
|
| 5 |
|
|
@@ -137,7 +138,13 @@ class Streamer:
|
|
| 137 |
await self.client.aclose()
|
| 138 |
|
| 139 |
|
| 140 |
-
async def download_file_with_retry(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
"""
|
| 142 |
Downloads a file with retry logic.
|
| 143 |
|
|
@@ -146,6 +153,7 @@ async def download_file_with_retry(url: str, headers: dict, timeout: float = 10.
|
|
| 146 |
headers (dict): The headers to include in the request.
|
| 147 |
timeout (float, optional): The request timeout. Defaults to 10.0.
|
| 148 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
|
|
|
| 149 |
|
| 150 |
Returns:
|
| 151 |
bytes: The downloaded file content.
|
|
@@ -154,7 +162,10 @@ async def download_file_with_retry(url: str, headers: dict, timeout: float = 10.
|
|
| 154 |
DownloadError: If the download fails after retries.
|
| 155 |
"""
|
| 156 |
async with httpx.AsyncClient(
|
| 157 |
-
follow_redirects=True,
|
|
|
|
|
|
|
|
|
|
| 158 |
) as client:
|
| 159 |
try:
|
| 160 |
response = await fetch_with_retry(client, "GET", url, headers)
|
|
@@ -166,7 +177,9 @@ async def download_file_with_retry(url: str, headers: dict, timeout: float = 10.
|
|
| 166 |
raise DownloadError(502, f"Failed to download file: {e.last_attempt.result()}")
|
| 167 |
|
| 168 |
|
| 169 |
-
async def request_with_retry(
|
|
|
|
|
|
|
| 170 |
"""
|
| 171 |
Sends an HTTP request with retry logic.
|
| 172 |
|
|
@@ -175,6 +188,7 @@ async def request_with_retry(method: str, url: str, headers: dict, timeout: floa
|
|
| 175 |
url (str): The URL to send the request to.
|
| 176 |
headers (dict): The headers to include in the request.
|
| 177 |
timeout (float, optional): The request timeout. Defaults to 10.0.
|
|
|
|
| 178 |
**kwargs: Additional arguments to pass to the request.
|
| 179 |
|
| 180 |
Returns:
|
|
@@ -183,7 +197,9 @@ async def request_with_retry(method: str, url: str, headers: dict, timeout: floa
|
|
| 183 |
Raises:
|
| 184 |
DownloadError: If the request fails after retries.
|
| 185 |
"""
|
| 186 |
-
async with httpx.AsyncClient(
|
|
|
|
|
|
|
| 187 |
try:
|
| 188 |
response = await fetch_with_retry(client, method, url, headers, **kwargs)
|
| 189 |
return response
|
|
@@ -198,6 +214,7 @@ def encode_mediaflow_proxy_url(
|
|
| 198 |
destination_url: str | None = None,
|
| 199 |
query_params: dict | None = None,
|
| 200 |
request_headers: dict | None = None,
|
|
|
|
| 201 |
) -> str:
|
| 202 |
"""
|
| 203 |
Encodes a MediaFlow proxy URL with query parameters and headers.
|
|
@@ -208,6 +225,7 @@ def encode_mediaflow_proxy_url(
|
|
| 208 |
destination_url (str, optional): The destination URL to include in the query parameters. Defaults to None.
|
| 209 |
query_params (dict, optional): Additional query parameters to include. Defaults to None.
|
| 210 |
request_headers (dict, optional): Headers to include as query parameters. Defaults to None.
|
|
|
|
| 211 |
|
| 212 |
Returns:
|
| 213 |
str: The encoded MediaFlow proxy URL.
|
|
@@ -221,6 +239,10 @@ def encode_mediaflow_proxy_url(
|
|
| 221 |
query_params.update(
|
| 222 |
{key if key.startswith("h_") else f"h_{key}": value for key, value in request_headers.items()}
|
| 223 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
# Encode the query parameters
|
| 225 |
encoded_params = parse.urlencode(query_params, quote_via=parse.quote)
|
| 226 |
|
|
@@ -263,7 +285,13 @@ def get_original_scheme(request: Request) -> str:
|
|
| 263 |
return "http"
|
| 264 |
|
| 265 |
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
"""
|
| 268 |
Extracts proxy headers from the request query parameters.
|
| 269 |
|
|
@@ -271,11 +299,12 @@ def get_proxy_headers(request: Request) -> dict:
|
|
| 271 |
request (Request): The incoming HTTP request.
|
| 272 |
|
| 273 |
Returns:
|
| 274 |
-
|
| 275 |
"""
|
| 276 |
request_headers = {k: v for k, v in request.headers.items() if k in SUPPORTED_REQUEST_HEADERS}
|
| 277 |
request_headers.update({k[2:].lower(): v for k, v in request.query_params.items() if k.startswith("h_")})
|
| 278 |
-
|
|
|
|
| 279 |
|
| 280 |
|
| 281 |
class EnhancedStreamingResponse(Response):
|
|
|
|
| 1 |
import logging
|
| 2 |
import typing
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
from functools import partial
|
| 5 |
from urllib import parse
|
| 6 |
|
|
|
|
| 138 |
await self.client.aclose()
|
| 139 |
|
| 140 |
|
| 141 |
+
async def download_file_with_retry(
|
| 142 |
+
url: str,
|
| 143 |
+
headers: dict,
|
| 144 |
+
timeout: float = 10.0,
|
| 145 |
+
verify_ssl: bool = True,
|
| 146 |
+
use_request_proxy: bool = True,
|
| 147 |
+
):
|
| 148 |
"""
|
| 149 |
Downloads a file with retry logic.
|
| 150 |
|
|
|
|
| 153 |
headers (dict): The headers to include in the request.
|
| 154 |
timeout (float, optional): The request timeout. Defaults to 10.0.
|
| 155 |
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
| 156 |
+
use_request_proxy (bool, optional): Whether to use the proxy configuration from the user's MediaFlow config. Defaults to True.
|
| 157 |
|
| 158 |
Returns:
|
| 159 |
bytes: The downloaded file content.
|
|
|
|
| 162 |
DownloadError: If the download fails after retries.
|
| 163 |
"""
|
| 164 |
async with httpx.AsyncClient(
|
| 165 |
+
follow_redirects=True,
|
| 166 |
+
timeout=timeout,
|
| 167 |
+
proxy=settings.proxy_url if use_request_proxy else None,
|
| 168 |
+
verify=verify_ssl,
|
| 169 |
) as client:
|
| 170 |
try:
|
| 171 |
response = await fetch_with_retry(client, "GET", url, headers)
|
|
|
|
| 177 |
raise DownloadError(502, f"Failed to download file: {e.last_attempt.result()}")
|
| 178 |
|
| 179 |
|
| 180 |
+
async def request_with_retry(
|
| 181 |
+
method: str, url: str, headers: dict, timeout: float = 10.0, use_request_proxy: bool = True, **kwargs
|
| 182 |
+
):
|
| 183 |
"""
|
| 184 |
Sends an HTTP request with retry logic.
|
| 185 |
|
|
|
|
| 188 |
url (str): The URL to send the request to.
|
| 189 |
headers (dict): The headers to include in the request.
|
| 190 |
timeout (float, optional): The request timeout. Defaults to 10.0.
|
| 191 |
+
use_request_proxy (bool, optional): Whether to use the proxy configuration from the user's MediaFlow config. Defaults to True.
|
| 192 |
**kwargs: Additional arguments to pass to the request.
|
| 193 |
|
| 194 |
Returns:
|
|
|
|
| 197 |
Raises:
|
| 198 |
DownloadError: If the request fails after retries.
|
| 199 |
"""
|
| 200 |
+
async with httpx.AsyncClient(
|
| 201 |
+
follow_redirects=True, timeout=timeout, proxy=settings.proxy_url if use_request_proxy else None
|
| 202 |
+
) as client:
|
| 203 |
try:
|
| 204 |
response = await fetch_with_retry(client, method, url, headers, **kwargs)
|
| 205 |
return response
|
|
|
|
| 214 |
destination_url: str | None = None,
|
| 215 |
query_params: dict | None = None,
|
| 216 |
request_headers: dict | None = None,
|
| 217 |
+
response_headers: dict | None = None,
|
| 218 |
) -> str:
|
| 219 |
"""
|
| 220 |
Encodes a MediaFlow proxy URL with query parameters and headers.
|
|
|
|
| 225 |
destination_url (str, optional): The destination URL to include in the query parameters. Defaults to None.
|
| 226 |
query_params (dict, optional): Additional query parameters to include. Defaults to None.
|
| 227 |
request_headers (dict, optional): Headers to include as query parameters. Defaults to None.
|
| 228 |
+
response_headers (dict, optional): Headers to include as query parameters. Defaults to None.
|
| 229 |
|
| 230 |
Returns:
|
| 231 |
str: The encoded MediaFlow proxy URL.
|
|
|
|
| 239 |
query_params.update(
|
| 240 |
{key if key.startswith("h_") else f"h_{key}": value for key, value in request_headers.items()}
|
| 241 |
)
|
| 242 |
+
if response_headers:
|
| 243 |
+
query_params.update(
|
| 244 |
+
{key if key.startswith("r_") else f"r_{key}": value for key, value in response_headers.items()}
|
| 245 |
+
)
|
| 246 |
# Encode the query parameters
|
| 247 |
encoded_params = parse.urlencode(query_params, quote_via=parse.quote)
|
| 248 |
|
|
|
|
| 285 |
return "http"
|
| 286 |
|
| 287 |
|
| 288 |
+
@dataclass
|
| 289 |
+
class ProxyRequestHeaders:
|
| 290 |
+
request: dict
|
| 291 |
+
response: dict
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
def get_proxy_headers(request: Request) -> ProxyRequestHeaders:
|
| 295 |
"""
|
| 296 |
Extracts proxy headers from the request query parameters.
|
| 297 |
|
|
|
|
| 299 |
request (Request): The incoming HTTP request.
|
| 300 |
|
| 301 |
Returns:
|
| 302 |
+
ProxyRequest: A named tuple containing the request headers and response headers.
|
| 303 |
"""
|
| 304 |
request_headers = {k: v for k, v in request.headers.items() if k in SUPPORTED_REQUEST_HEADERS}
|
| 305 |
request_headers.update({k[2:].lower(): v for k, v in request.query_params.items() if k.startswith("h_")})
|
| 306 |
+
response_headers = {k[2:].lower(): v for k, v in request.query_params.items() if k.startswith("r_")}
|
| 307 |
+
return ProxyRequestHeaders(request_headers, response_headers)
|
| 308 |
|
| 309 |
|
| 310 |
class EnhancedStreamingResponse(Response):
|