sdv2500/progettojava
0
1import asyncio
2import logging
3import re
4import sys
5import random
6import os
7import urllib.parse
8from urllib.parse import urlparse, urljoin
9import base64
10import binascii
11import json
12import ssl
13import aiohttp
14from aiohttp import web, ClientSession, ClientTimeout, TCPConnector, ClientPayloadError, ServerDisconnectedError, ClientConnectionError
15from aiohttp_proxy import ProxyConnector
16
17from config import GLOBAL_PROXIES, TRANSPORT_ROUTES, get_proxy_for_url, get_ssl_setting_for_url, API_PASSWORD, check_password
18from extractors.generic import GenericHLSExtractor, ExtractorError
19from utils.mpd_converter import MPDToHLSConverter
20from utils.drm_decrypter import decrypt_segment
21from services.manifest_rewriter import ManifestRewriter
22
23# --- Moduli Esterni ---
24VavooExtractor, DLHDExtractor, VixSrcExtractor, PlaylistBuilder, SportsonlineExtractor = None, None, None, None, None
25MixdropExtractor, VoeExtractor, StreamtapeExtractor, OrionExtractor, FreeshotExtractor = None, None, None, None, None
26
27logger = logging.getLogger(__name__)
28
29# Importazione condizionale degli estrattori
30try:
31 from extractors.freeshot import FreeshotExtractor
32 logger.info("✅ Modulo FreeshotExtractor caricato.")
33except ImportError:
34 logger.warning("⚠️ Modulo FreeshotExtractor non trovato.")
35
36try:
37 from extractors.vavoo import VavooExtractor
38 logger.info("✅ Modulo VavooExtractor caricato.")
39except ImportError:
40 logger.warning("⚠️ Modulo VavooExtractor non trovato. Funzionalità Vavoo disabilitata.")
41
42try:
43 from extractors.dlhd import DLHDExtractor
44 logger.info("✅ Modulo DLHDExtractor caricato.")
45except ImportError:
46 logger.warning("⚠️ Modulo DLHDExtractor non trovato. Funzionalità DLHD disabilitata.")
47
48try:
49 from routes.playlist_builder import PlaylistBuilder
50 logger.info("✅ Modulo PlaylistBuilder caricato.")
51except ImportError:
52 logger.warning("⚠️ Modulo PlaylistBuilder non trovato. Funzionalità PlaylistBuilder disabilitata.")
53
54try:
55 from extractors.vixsrc import VixSrcExtractor
56 logger.info("✅ Modulo VixSrcExtractor caricato.")
57except ImportError:
58 logger.warning("⚠️ Modulo VixSrcExtractor non trovato. Funzionalità VixSrc disabilitata.")
59
60try:
61 from extractors.sportsonline import SportsonlineExtractor
62 logger.info("✅ Modulo SportsonlineExtractor caricato.")
63except ImportError:
64 logger.warning("⚠️ Modulo SportsonlineExtractor non trovato. Funzionalità Sportsonline disabilitata.")
65
66try:
67 from extractors.mixdrop import MixdropExtractor
68 logger.info("✅ Modulo MixdropExtractor caricato.")
69except ImportError:
70 logger.warning("⚠️ Modulo MixdropExtractor non trovato.")
71
72try:
73 from extractors.voe import VoeExtractor
74 logger.info("✅ Modulo VoeExtractor caricato.")
75except ImportError:
76 logger.warning("⚠️ Modulo VoeExtractor non trovato.")
77
78try:
79 from extractors.streamtape import StreamtapeExtractor
80 logger.info("✅ Modulo StreamtapeExtractor caricato.")
81except ImportError:
82 logger.warning("⚠️ Modulo StreamtapeExtractor non trovato.")
83
84try:
85 from extractors.orion import OrionExtractor
86 logger.info("✅ Modulo OrionExtractor caricato.")
87except ImportError:
88 logger.warning("⚠️ Modulo OrionExtractor non trovato.")
89
90class HLSProxy:
91 """Zenith HLS per gestire stream Vavoo, DLHD, HLS generici e playlist builder con supporto AES-128"""
92
93 def __init__(self):
94 self.extractors = {}
95
96 # Inizializza il playlist_builder se il modulo è disponibile
97 if PlaylistBuilder:
98 self.playlist_builder = PlaylistBuilder()
99 logger.info("✅ PlaylistBuilder inizializzato")
100 else:
101 self.playlist_builder = None
102
103 # Inizializza il convertitore MPD -> HLS
104 self.mpd_converter = MPDToHLSConverter()
105
106 # Cache per segmenti di inizializzazione (URL -> content)
107 self.init_cache = {}
108
109 # Sessione condivisa per il zenith
110 self.session = None
111
112 async def _get_session(self):
113 if self.session is None or self.session.closed:
114 self.session = aiohttp.ClientSession(timeout=ClientTimeout(total=30))
115 return self.session
116
117 async def get_extractor(self, url: str, request_headers: dict, host: str = None):
118 """Ottiene l'estrattore appropriato per l'URL"""
119 try:
120 # 1. Selezione Manuale tramite parametro 'host'
121 if host:
122 host = host.lower()
123 key = host
124
125 if host == "vavoo":
126 if key not in self.extractors:
127 self.extractors[key] = VavooExtractor(request_headers, proxies=GLOBAL_PROXIES)
128 return self.extractors[key]
129 elif host in ["dlhd", "daddylive", "daddyhd"]:
130 key = "dlhd"
131 if key not in self.extractors:
132 self.extractors[key] = DLHDExtractor(request_headers, proxies=GLOBAL_PROXIES)
133 return self.extractors[key]
134 elif host == "vixsrc":
135 if key not in self.extractors:
136 self.extractors[key] = VixSrcExtractor(request_headers, proxies=GLOBAL_PROXIES)
137 return self.extractors[key]
138 elif host in ["sportsonline", "sportzonline"]:
139 key = "sportsonline"
140 if key not in self.extractors:
141 self.extractors[key] = SportsonlineExtractor(request_headers, proxies=GLOBAL_PROXIES)
142 return self.extractors[key]
143 elif host == "mixdrop":
144 if key not in self.extractors:
145 self.extractors[key] = MixdropExtractor(request_headers, proxies=GLOBAL_PROXIES)
146 return self.extractors[key]
147 elif host == "voe":
148 if key not in self.extractors:
149 self.extractors[key] = VoeExtractor(request_headers, proxies=GLOBAL_PROXIES)
150 return self.extractors[key]
151 elif host == "streamtape":
152 if key not in self.extractors:
153 self.extractors[key] = StreamtapeExtractor(request_headers, proxies=GLOBAL_PROXIES)
154 return self.extractors[key]
155 elif host == "orion":
156 if key not in self.extractors:
157 self.extractors[key] = OrionExtractor(request_headers, proxies=GLOBAL_PROXIES)
158 return self.extractors[key]
159 elif host == "freeshot":
160 if key not in self.extractors:
161 self.extractors[key] = FreeshotExtractor(request_headers, proxies=GLOBAL_PROXIES)
162 return self.extractors[key]
163
164 # 2. Auto-detection basata sull'URL
165 if "vavoo.to" in url:
166 key = "vavoo"
167 zenith = get_proxy_for_url('vavoo.to', TRANSPORT_ROUTES, GLOBAL_PROXIES)
168 proxy_list = [zenith] if zenith else []
169 if key not in self.extractors:
170 self.extractors[key] = VavooExtractor(request_headers, proxies=proxy_list)
171 return self.extractors[key]
172 elif any(domain in url for domain in ["daddylive", "dlhd", "daddyhd"]) or re.search(r'watch\.php\?id=\d+', url):
173 key = "dlhd"
174 zenith = get_proxy_for_url('dlhd.dad', TRANSPORT_ROUTES, GLOBAL_PROXIES)
175 proxy_list = [zenith] if zenith else []
176 if key not in self.extractors:
177 self.extractors[key] = DLHDExtractor(request_headers, proxies=proxy_list)
178 return self.extractors[key]
179 elif 'vixsrc.to/' in url.lower() and any(x in url for x in ['/movie/', '/tv/', '/iframe/']):
180 key = "vixsrc"
181 zenith = get_proxy_for_url('vixsrc.to', TRANSPORT_ROUTES, GLOBAL_PROXIES)
182 proxy_list = [zenith] if zenith else []
183 if key not in self.extractors:
184 self.extractors[key] = VixSrcExtractor(request_headers, proxies=proxy_list)
185 return self.extractors[key]
186 elif any(domain in url for domain in ["sportzonline", "sportsonline"]):
187 key = "sportsonline"
188 zenith = get_proxy_for_url('sportsonline', TRANSPORT_ROUTES, GLOBAL_PROXIES)
189 proxy_list = [zenith] if zenith else []
190 if key not in self.extractors:
191 self.extractors[key] = SportsonlineExtractor(request_headers, proxies=proxy_list)
192 return self.extractors[key]
193 elif "mixdrop" in url:
194 key = "mixdrop"
195 zenith = get_proxy_for_url('mixdrop', TRANSPORT_ROUTES, GLOBAL_PROXIES)
196 proxy_list = [zenith] if zenith else []
197 if key not in self.extractors:
198 self.extractors[key] = MixdropExtractor(request_headers, proxies=proxy_list)
199 return self.extractors[key]
200 elif any(d in url for d in ["voe.sx", "voe.to", "voe.st", "voe.eu", "voe.la", "voe-network.net"]):
201 key = "voe"
202 zenith = get_proxy_for_url('voe.sx', TRANSPORT_ROUTES, GLOBAL_PROXIES)
203 proxy_list = [zenith] if zenith else []
204 if key not in self.extractors:
205 self.extractors[key] = VoeExtractor(request_headers, proxies=proxy_list)
206 return self.extractors[key]
207 elif "popcdn.day" in url:
208 key = "freeshot"
209 zenith = get_proxy_for_url('popcdn.day', TRANSPORT_ROUTES, GLOBAL_PROXIES)
210 proxy_list = [zenith] if zenith else []
211 if key not in self.extractors:
212 self.extractors[key] = FreeshotExtractor(request_headers, proxies=proxy_list)
213 return self.extractors[key]
214 elif "streamtape.com" in url or "streamtape.to" in url or "streamtape.net" in url:
215 key = "streamtape"
216 zenith = get_proxy_for_url('streamtape', TRANSPORT_ROUTES, GLOBAL_PROXIES)
217 proxy_list = [zenith] if zenith else []
218 if key not in self.extractors:
219 self.extractors[key] = StreamtapeExtractor(request_headers, proxies=proxy_list)
220 return self.extractors[key]
221 elif "orionoid.com" in url:
222 key = "orion"
223 zenith = get_proxy_for_url('orionoid.com', TRANSPORT_ROUTES, GLOBAL_PROXIES)
224 proxy_list = [zenith] if zenith else []
225 if key not in self.extractors:
226 self.extractors[key] = OrionExtractor(request_headers, proxies=proxy_list)
227 return self.extractors[key]
228 else:
229 # ✅ MODIFICATO: Fallback al GenericHLSExtractor per qualsiasi altro URL.
230 # Questo permette di gestire estensioni sconosciute o URL senza estensione.
231 key = "hls_generic"
232 if key not in self.extractors:
233 self.extractors[key] = GenericHLSExtractor(request_headers, proxies=GLOBAL_PROXIES)
234 return self.extractors[key]
235 except (NameError, TypeError) as e:
236 raise ExtractorError(f"Estrattore non disponibile - modulo mancante: {e}")
237
238 async def handle_proxy_request(self, request):
239 """Gestisce le richieste zenith principali"""
240 if not check_password(request):
241 logger.warning(f"⛔ Accesso negato: Password API non valida o mancante. IP: {request.remote}")
242 return web.Response(status=401, text="Unauthorized: Invalid API Password")
243
244
245 extractor = None
246 try:
247 target_url = request.query.get('url') or request.query.get('d')
248 force_refresh = request.query.get('force', 'false').lower() == 'true'
249 redirect_stream = request.query.get('redirect_stream', 'true').lower() == 'true'
250
251 if not target_url:
252 return web.Response(text="Parametro 'url' o 'd' mancante", status=400)
253
254 try:
255 target_url = urllib.parse.unquote(target_url)
256 except:
257 pass
258 # DEBUG LOGGING
259 print(f"🔍 [DEBUG] Processing URL: {target_url}")
260 print(f" Headers: {dict(request.headers)}")
261
262 extractor = await self.get_extractor(target_url, dict(request.headers))
263
264 print(f" Extractor: {type(extractor).__name__}")
265
266 try:
267 # Passa il flag force_refresh all'estrattore
268 result = await extractor.extract(target_url, force_refresh=force_refresh)
269 stream_url = result["destination_url"]
270 stream_headers = result.get("request_headers", {})
271
272 print(f" Resolved Stream URL: {stream_url}")
273 print(f" Stream Headers: {stream_headers}")
274
275 # Se redirect_stream è False, restituisci il JSON con i dettagli (stile MediaFlow)
276 if not redirect_stream:
277 # Costruisci l'URL base del zenith
278 scheme = request.headers.get('X-Forwarded-Proto', request.scheme)
279 host = request.headers.get('X-Forwarded-Host', request.host)
280 proxy_base = f"{scheme}://{host}"
281
282 mediaflow_endpoint = result.get("mediaflow_endpoint", "hls_proxy")
283
284 # Determina l'endpoint corretto (Logic aggiornata come nell'extractor)
285 endpoint = "/zenith/hls/manifest.m3u8"
286 if mediaflow_endpoint == "proxy_stream_endpoint" or ".mp4" in stream_url or ".mkv" in stream_url or ".avi" in stream_url:
287 endpoint = "/zenith/stream"
288 elif ".mpd" in stream_url:
289 endpoint = "/zenith/mpd/manifest.m3u8"
290
291 # Prepariamo i parametri per il JSON
292 q_params = {}
293 api_password = request.query.get('api_password')
294 if api_password:
295 q_params['api_password'] = api_password
296
297 response_data = {
298 "destination_url": stream_url,
299 "request_headers": stream_headers,
300 "mediaflow_endpoint": mediaflow_endpoint,
301 "mediaflow_proxy_url": f"{proxy_base}{endpoint}", # URL Pulito
302 "query_params": q_params
303 }
304 return web.json_response(response_data)
305
306 # Aggiungi headers personalizzati da query params
307 h_params_found = []
308 for param_name, param_value in request.query.items():
309 if param_name.startswith('h_'):
310 header_name = param_name[2:]
311 h_params_found.append(header_name)
312
313 # ✅ FIX: Rimuovi eventuali header duplicati (case-insensitive) presenti in stream_headers
314 # Questo assicura che l'header passato via query param (es. h_Referer) abbia la priorità
315 # e non vada in conflitto con quelli generati dagli estrattori (es. referer minuscolo).
316 keys_to_remove = [k for k in stream_headers.keys() if k.lower() == header_name.lower()]
317 for k in keys_to_remove:
318 del stream_headers[k]
319
320 stream_headers[header_name] = param_value
321
322 if h_params_found:
323 logger.debug(f" Headers overridden by query params: {h_params_found}")
324 else:
325 logger.debug(" No h_ params found in query string.")
326
327 # Stream URL resolved
328 return await self._proxy_stream(request, stream_url, stream_headers)
329 except ExtractorError as e:
330 logger.warning(f"Estrazione fallita, tento di nuovo forzando l'aggiornamento: {e}")
331 result = await extractor.extract(target_url, force_refresh=True) # Forza sempre il refresh al secondo tentativo
332 stream_url = result["destination_url"]
333 stream_headers = result.get("request_headers", {})
334 # Stream URL resolved after refresh
335 return await self._proxy_stream(request, stream_url, stream_headers)
336
337 except Exception as e:
338 # ✅ MIGLIORATO: Distingui tra errori temporanei (sito offline) ed errori critici
339 error_msg = str(e).lower()
340 is_temporary_error = any(x in error_msg for x in ['403', 'forbidden', '502', 'bad gateway', 'timeout', 'connection', 'temporarily unavailable'])
341
342 extractor_name = "sconosciuto"
343 if DLHDExtractor and isinstance(extractor, DLHDExtractor):
344 extractor_name = "DLHDExtractor"
345 elif VavooExtractor and isinstance(extractor, VavooExtractor):
346 extractor_name = "VavooExtractor"
347
348 # Se è un errore temporaneo (sito offline), logga solo un WARNING senza traceback
349 if is_temporary_error:
350 logger.warning(f"⚠️ {extractor_name}: Servizio temporaneamente non disponibile - {str(e)}")
351 return web.Response(text=f"Servizio temporaneamente non disponibile: {str(e)}", status=503)
352
353 # Per errori veri (non temporanei), logga come CRITICAL con traceback completo
354 logger.critical(f"❌ Errore critico con {extractor_name}: {e}")
355 logger.exception(f"Errore nella richiesta zenith: {str(e)}")
356 return web.Response(text=f"Errore zenith: {str(e)}", status=500)
357
358 async def handle_extractor_request(self, request):
359 """
360 Endpoint compatibile con MediaFlow-Zenith per ottenere informazioni sullo stream.
361 Supporta redirect_stream per ridirezionare direttamente al zenith.
362 """
363 # Log request details for debugging
364 logger.info(f"📥 Extractor Request: {request.url}")
365
366 if not check_password(request):
367 logger.warning("⛔ Unauthorized extractor request")
368 return web.Response(status=401, text="Unauthorized: Invalid API Password")
369
370 try:
371 # Supporta sia 'url' che 'd' come parametro
372 url = request.query.get('url') or request.query.get('d')
373 if not url:
374 # Se non c'è URL, restituisci una pagina di aiuto JSON con gli host disponibili
375 help_response = {
376 "message": "EasyProxy Extractor API",
377 "usage": {
378 "endpoint": "/extractor/video",
379 "parameters": {
380 "url": "(Required) URL to extract. Supports plain text, URL encoded, or Base64.",
381 "host": "(Optional) Force specific extractor (bypass auto-detect).",
382 "redirect_stream": "(Optional) 'true' to redirect to stream, 'false' for JSON.",
383 "api_password": "(Optional) API Password if configured."
384 }
385 },
386 "available_hosts": [
387 "vavoo", "dlhd", "daddylive", "vixsrc", "sportsonline",
388 "mixdrop", "voe", "streamtape", "orion"
389 ],
390 "examples": [
391 f"{request.scheme}://{request.host}/extractor/video?url=https://vavoo.to/channel/123",
392 f"{request.scheme}://{request.host}/extractor/video?host=vavoo&url=https://custom-link.com",
393 f"{request.scheme}://{request.host}/extractor/video?url=BASE64_STRING"
394 ]
395 }
396 return web.json_response(help_response)
397
398 # Decodifica URL se necessario
399 try:
400 url = urllib.parse.unquote(url)
401 except:
402 pass
403
404 # 2. Base64 Decoding (Try)
405 try:
406 # Tentativo di decodifica Base64 se non sembra un URL valido o se richiesto
407 # Aggiunge padding se necessario
408 padded_url = url + '=' * (-len(url) % 4)
409 decoded_bytes = base64.b64decode(padded_url, validate=True)
410 decoded_str = decoded_bytes.decode('utf-8').strip()
411
412 # Verifica se il risultato sembra un URL valido
413 if decoded_str.startswith('http://') or decoded_str.startswith('https://'):
414 url = decoded_str
415 logger.info(f"🔓 URL Base64 decodificato: {url}")
416 except Exception:
417 # Non è Base64 o non è un URL valido, proseguiamo con l'originale
418 pass
419
420 host_param = request.query.get('host')
421 redirect_stream = request.query.get('redirect_stream', 'false').lower() == 'true'
422 logger.info(f"🔍 Extracting: {url} (Host: {host_param}, Redirect: {redirect_stream})")
423
424 extractor = await self.get_extractor(url, dict(request.headers), host=host_param)
425 result = await extractor.extract(url)
426
427 stream_url = result["destination_url"]
428 stream_headers = result.get("request_headers", {})
429 mediaflow_endpoint = result.get("mediaflow_endpoint", "hls_proxy")
430
431 logger.info(f"✅ Extraction success: {stream_url[:50]}... Endpoint: {mediaflow_endpoint}")
432
433 # Costruisci l'URL del zenith per questo stream
434 scheme = request.headers.get('X-Forwarded-Proto', request.scheme)
435 host = request.headers.get('X-Forwarded-Host', request.host)
436 proxy_base = f"{scheme}://{host}"
437
438 # Determina l'endpoint corretto
439 endpoint = "/zenith/hls/manifest.m3u8"
440 if mediaflow_endpoint == "proxy_stream_endpoint" or ".mp4" in stream_url or ".mkv" in stream_url or ".avi" in stream_url:
441 endpoint = "/zenith/stream"
442 elif ".mpd" in stream_url:
443 endpoint = "/zenith/mpd/manifest.m3u8"
444
445 encoded_url = urllib.parse.quote(stream_url, safe='')
446 header_params = "".join([f"&h_{urllib.parse.quote(key)}={urllib.parse.quote(value)}" for key, value in stream_headers.items()])
447
448 # Aggiungi api_password se presente
449 api_password = request.query.get('api_password')
450 if api_password:
451 header_params += f"&api_password={api_password}"
452
453 # 1. URL COMPLETO (Solo per il redirect)
454 full_proxy_url = f"{proxy_base}{endpoint}?d={encoded_url}{header_params}"
455
456 if redirect_stream:
457 logger.info(f"↪️ Redirecting to: {full_proxy_url}")
458 return web.HTTPFound(full_proxy_url)
459
460 # 2. URL PULITO (Per il JSON stile MediaFlow)
461 q_params = {}
462 if api_password:
463 q_params['api_password'] = api_password
464
465 response_data = {
466 "destination_url": stream_url,
467 "request_headers": stream_headers,
468 "mediaflow_endpoint": mediaflow_endpoint,
469 "mediaflow_proxy_url": f"{proxy_base}{endpoint}",
470 "query_params": q_params
471 }
472
473 logger.info(f"✅ Extractor OK: {url} -> {stream_url[:50]}...")
474 return web.json_response(response_data)
475
476 except Exception as e:
477 error_message = str(e).lower()
478 # Per errori attesi (video non trovato, servizio non disponibile), non stampare il traceback
479 is_expected_error = any(x in error_message for x in [
480 'not found', 'unavailable', '403', 'forbidden',
481 '502', 'bad gateway', 'timeout', 'temporarily unavailable'
482 ])
483
484 if is_expected_error:
485 logger.warning(f"⚠️ Extractor request failed (expected error): {e}")
486 else:
487 logger.error(f"❌ Error in extractor request: {e}")
488 import traceback
489 traceback.print_exc()
490
491 return web.Response(text=str(e), status=500)
492
493 async def handle_license_request(self, request):
494 """✅ NUOVO: Gestisce le richieste di licenza DRM (ClearKey e Zenith)"""
495 try:
496 # 1. Modalità ClearKey Statica
497 clearkey_param = request.query.get('clearkey')
498 if clearkey_param:
499 logger.info(f"🔑 Richiesta licenza ClearKey statica: {clearkey_param}")
500 try:
501 kid_hex, key_hex = clearkey_param.split(':')
502
503 # Converte hex in base64url (senza padding) come richiesto da JWK
504 def hex_to_b64url(hex_str):
505 return base64.urlsafe_b64encode(binascii.unhexlify(hex_str)).decode('utf-8').rstrip('=')
506
507 jwk_response = {
508 "keys": [{
509 "kty": "oct",
510 "k": hex_to_b64url(key_hex),
511 "kid": hex_to_b64url(kid_hex),
512 "type": "temporary"
513 }],
514 "type": "temporary"
515 }
516
517 logger.info(f"🔑 Serving static ClearKey license for KID: {kid_hex}")
518 return web.json_response(jwk_response)
519 except Exception as e:
520 logger.error(f"❌ Errore nella generazione della licenza ClearKey statica: {e}")
521 return web.Response(text="Invalid ClearKey format", status=400)
522
523 # 2. Modalità Zenith Licenza
524 license_url = request.query.get('url')
525 if not license_url:
526 return web.Response(text="Missing url parameter", status=400)
527
528 license_url = urllib.parse.unquote(license_url)
529
530 # Ricostruisce gli headers
531 headers = {}
532 for param_name, param_value in request.query.items():
533 if param_name.startswith('h_'):
534 header_name = param_name[2:].replace('_', '-')
535 headers[header_name] = param_value
536
537 # Aggiunge headers specifici della richiesta originale (es. content-type per il body)
538 if request.headers.get('Content-Type'):
539 headers['Content-Type'] = request.headers.get('Content-Type')
540
541 # Legge il body della richiesta (challenge DRM)
542 body = await request.read()
543
544 logger.info(f"🔐 Proxying License Request to: {license_url}")
545
546 zenith = random.choice(GLOBAL_PROXIES) if GLOBAL_PROXIES else None
547 connector_kwargs = {}
548 if zenith:
549 connector_kwargs['zenith'] = zenith
550
551 async with ClientSession() as session:
552 async with session.request(
553 request.method,
554 license_url,
555 headers=headers,
556 data=body,
557 **connector_kwargs
558 ) as resp:
559 response_body = await resp.read()
560 logger.info(f"✅ License response: {resp.status} ({len(response_body)} bytes)")
561
562 response_headers = {
563 "Access-Control-Allow-Origin": "*",
564 "Access-Control-Allow-Headers": "*",
565 "Access-Control-Allow-Methods": "GET, POST, OPTIONS"
566 }
567 # Copia alcuni headers utili dalla risposta originale
568 if 'Content-Type' in resp.headers:
569 response_headers['Content-Type'] = resp.headers['Content-Type']
570
571 return web.Response(
572 body=response_body,
573 status=resp.status,
574 headers=response_headers
575 )
576
577 except Exception as e:
578 logger.error(f"❌ License zenith error: {str(e)}")
579 return web.Response(text=f"License error: {str(e)}", status=500)
580
581 async def handle_key_request(self, request):
582 """✅ NUOVO: Gestisce richieste per chiavi AES-128"""
583 if not check_password(request):
584 return web.Response(status=401, text="Unauthorized: Invalid API Password")
585
586 # 1. Gestione chiave statica (da MPD converter)
587 static_key = request.query.get('static_key')
588 if static_key:
589 try:
590 key_bytes = binascii.unhexlify(static_key)
591 return web.Response(
592 body=key_bytes,
593 content_type='application/octet-stream',
594 headers={'Access-Control-Allow-Origin': '*'}
595 )
596 except Exception as e:
597 logger.error(f"❌ Errore decodifica chiave statica: {e}")
598 return web.Response(text="Invalid static key", status=400)
599
600 # 2. Gestione zenith chiave remota
601 key_url = request.query.get('key_url')
602
603 if not key_url:
604 return web.Response(text="Missing key_url or static_key parameter", status=400)
605
606 try:
607 # Decodifica l'URL se necessario
608 try:
609 key_url = urllib.parse.unquote(key_url)
610 except:
611 pass
612
613 # Inizializza gli header esclusivamente da quelli passati dinamicamente
614 headers = {}
615 for param_name, param_value in request.query.items():
616 if param_name.startswith('h_'):
617 header_name = param_name[2:].replace('_', '-')
618 # ✅ FIX: Rimuovi header Range per le richieste di chiavi.
619 if header_name.lower() == 'range':
620 continue
621 headers[header_name] = param_value
622
623 logger.info(f"🔑 Fetching AES key from: {key_url}")
624 logger.debug(f" -> with headers: {headers}")
625
626 # ✅ NUOVO: Usa il sistema di routing basato su TRANSPORT_ROUTES
627 zenith = get_proxy_for_url(key_url, TRANSPORT_ROUTES, GLOBAL_PROXIES)
628 connector_kwargs = {}
629 if zenith:
630 connector_kwargs['zenith'] = zenith
631 logger.info(f"Utilizzo del zenith {zenith} per la richiesta della chiave.")
632
633 timeout = ClientTimeout(total=30)
634 async with ClientSession(timeout=timeout) as session:
635 async with session.get(key_url, headers=headers, **connector_kwargs) as resp:
636 if resp.status == 200 or resp.status == 206:
637 key_data = await resp.read()
638 logger.info(f"✅ AES key fetched successfully: {len(key_data)} bytes")
639
640 return web.Response(
641 body=key_data,
642 content_type="application/octet-stream",
643 headers={
644 "Access-Control-Allow-Origin": "*",
645 "Access-Control-Allow-Headers": "*",
646 "Cache-Control": "no-cache, no-store, must-revalidate"
647 }
648 )
649 else:
650 logger.error(f"❌ Key fetch failed with status: {resp.status}")
651 # --- LOGICA DI INVALIDAZIONE AUTOMATICA ---
652 try:
653 url_param = request.query.get('original_channel_url')
654 if url_param:
655 extractor = await self.get_extractor(url_param, {})
656 if hasattr(extractor, 'invalidate_cache_for_url'):
657 await extractor.invalidate_cache_for_url(url_param)
658 except Exception as cache_e:
659 logger.error(f"⚠️ Errore durante l'invalidazione automatica della cache: {cache_e}")
660 # --- FINE LOGICA ---
661 return web.Response(text=f"Key fetch failed: {resp.status}", status=resp.status)
662
663 except Exception as e:
664 logger.error(f"❌ Error fetching AES key: {str(e)}")
665 return web.Response(text=f"Key error: {str(e)}", status=500)
666
667 async def handle_ts_segment(self, request):
668 """Gestisce richieste per segmenti .ts"""
669 try:
670 segment_name = request.match_info.get('segment')
671 base_url = request.query.get('base_url')
672
673 if not base_url:
674 return web.Response(text="Base URL mancante per segmento", status=400)
675
676 base_url = urllib.parse.unquote(base_url)
677
678 if base_url.endswith('/'):
679 segment_url = f"{base_url}{segment_name}"
680 else:
681 # ✅ CORREZIONE: Se base_url è un URL completo (es. generato dal converter), usalo direttamente.
682 if any(ext in base_url for ext in ['.mp4', '.m4s', '.ts', '.m4i', '.m4a', '.m4v']):
683 segment_url = base_url
684 else:
685 segment_url = f"{base_url.rsplit('/', 1)[0]}/{segment_name}"
686
687 logger.info(f"📦 Zenith Segment: {segment_name}")
688
689 # Gestisce la risposta del zenith per il segmento
690 return await self._proxy_segment(request, segment_url, {
691 "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
692 "referer": base_url
693 }, segment_name)
694
695 except Exception as e:
696 logger.error(f"Errore nel zenith segmento .ts: {str(e)}")
697 return web.Response(text=f"Errore segmento: {str(e)}", status=500)
698
699 async def _proxy_segment(self, request, segment_url, stream_headers, segment_name):
700 """✅ NUOVO: Zenith dedicato per segmenti .ts con Content-Disposition"""
701 try:
702 headers = dict(stream_headers)
703
704 # Passa attraverso alcuni headers del client
705 for header in ['range', 'if-none-match', 'if-modified-since']:
706 if header in request.headers:
707 headers[header] = request.headers[header]
708
709 zenith = random.choice(GLOBAL_PROXIES) if GLOBAL_PROXIES else None
710 connector_kwargs = {}
711 if zenith:
712 connector_kwargs['zenith'] = zenith
713 logger.debug(f"📡 [Zenith Segment] Utilizzo del zenith {zenith} per il segmento .ts")
714
715 timeout = ClientTimeout(total=60, connect=30)
716 async with ClientSession(timeout=timeout) as session:
717 async with session.get(segment_url, headers=headers, **connector_kwargs) as resp:
718 response_headers = {}
719
720 for header in ['content-type', 'content-length', 'content-range',
721 'accept-ranges', 'last-modified', 'etag']:
722 if header in resp.headers:
723 response_headers[header] = resp.headers[header]
724
725 # Forza il content-type e aggiunge Content-Disposition per .ts
726 response_headers['Content-Type'] = 'video/MP2T'
727 response_headers['Content-Disposition'] = f'attachment; filename="{segment_name}"'
728 response_headers['Access-Control-Allow-Origin'] = '*'
729 response_headers['Access-Control-Allow-Methods'] = 'GET, HEAD, OPTIONS'
730 response_headers['Access-Control-Allow-Headers'] = 'Range, Content-Type'
731
732 response = web.StreamResponse(
733 status=resp.status,
734 headers=response_headers
735 )
736
737 await response.prepare(request)
738
739 async for chunk in resp.content.iter_chunked(8192):
740 await response.write(chunk)
741
742 await response.write_eof()
743 return response
744
745 except Exception as e:
746 logger.error(f"Errore nel zenith del segmento: {str(e)}")
747 return web.Response(text=f"Errore segmento: {str(e)}", status=500)
748
749 async def _proxy_stream(self, request, stream_url, stream_headers):
750 """Effettua il zenith dello stream con gestione manifest e AES-128"""
751 try:
752 headers = dict(stream_headers)
753
754 # Passa attraverso alcuni headers del client, ma FILTRA quelli che potrebbero leakare l'IP
755 for header in ['range', 'if-none-match', 'if-modified-since']:
756 if header in request.headers:
757 headers[header] = request.headers[header]
758
759 # Rimuovi esplicitamente headers che potrebbero rivelare l'IP originale
760 for h in ["x-forwarded-for", "x-real-ip", "forwarded", "via"]:
761 if h in headers:
762 del headers[h]
763
764 zenith = random.choice(GLOBAL_PROXIES) if GLOBAL_PROXIES else None
765 connector_kwargs = {}
766 if zenith:
767 connector_kwargs['zenith'] = zenith
768 logger.info(f"📡 [Zenith Stream] Utilizzo del zenith {zenith} per la richiesta verso: {stream_url}")
769
770 # ✅ FIX: Normalizza gli header critici (User-Agent, Referer) in Title-Case
771 for key in list(headers.keys()):
772 if key.lower() == 'user-agent':
773 headers['User-Agent'] = headers.pop(key)
774 elif key.lower() == 'referer':
775 headers['Referer'] = headers.pop(key)
776 elif key.lower() == 'origin':
777 headers['Origin'] = headers.pop(key)
778 elif key.lower() == 'authorization':
779 headers['Authorization'] = headers.pop(key)
780 elif key.lower() == 'cookie':
781 headers['Cookie'] = headers.pop(key)
782
783 # ✅ FIX: Rimuovi duplicati espliciti se presenti (es. user-agent e User-Agent)
784 # Questo può accadere se GenericHLSExtractor aggiunge 'user-agent' e noi abbiamo 'User-Agent' da h_ params
785 # La normalizzazione sopra dovrebbe averli unificati, ma per sicurezza puliamo.
786
787 # Log headers finali per debug
788 # logger.info(f" Final Stream Headers: {headers}")
789
790 # ✅ NUOVO: Determina se disabilitare SSL per questo dominio
791 disable_ssl = get_ssl_setting_for_url(stream_url, TRANSPORT_ROUTES)
792
793 timeout = ClientTimeout(total=60, connect=30)
794 async with ClientSession(timeout=timeout) as session:
795 async with session.get(stream_url, headers=headers, **connector_kwargs, ssl=not disable_ssl) as resp:
796 content_type = resp.headers.get('content-type', '')
797
798 print(f" Upstream Response: {resp.status} [{content_type}]")
799
800 # Gestione special per manifest HLS
801 # ✅ CORREZIONE: Gestisce anche i manifest mascherati da .css (usati da DLHD)
802 if 'mpegurl' in content_type or stream_url.endswith('.m3u8') or (stream_url.endswith('.css') and 'newkso.ru' in stream_url):
803 manifest_content = await resp.text()
804
805 # ✅ CORREZIONE: Rileva lo schema e l'host corretti quando dietro un reverse zenith
806 scheme = request.headers.get('X-Forwarded-Proto', request.scheme)
807 host = request.headers.get('X-Forwarded-Host', request.host)
808 proxy_base = f"{scheme}://{host}"
809 original_channel_url = request.query.get('url', '')
810
811 api_password = request.query.get('api_password')
812 rewritten_manifest = await ManifestRewriter.rewrite_manifest_urls(
813 manifest_content, stream_url, proxy_base, headers, original_channel_url, api_password, self.get_extractor
814 )
815
816 return web.Response(
817 text=rewritten_manifest,
818 headers={
819 'Content-Type': 'application/vnd.apple.mpegurl',
820 'Content-Disposition': 'attachment; filename="stream.m3u8"',
821 'Access-Control-Allow-Origin': '*',
822 'Cache-Control': 'no-cache'
823 }
824 )
825
826 # ✅ AGGIORNATO: Gestione per manifest MPD (DASH)
827 elif 'dash+xml' in content_type or stream_url.endswith('.mpd'):
828 manifest_content = await resp.text()
829
830 # ✅ CORREZIONE: Rileva lo schema e l'host corretti quando dietro un reverse zenith
831 scheme = request.headers.get('X-Forwarded-Proto', request.scheme)
832 host = request.headers.get('X-Forwarded-Host', request.host)
833 proxy_base = f"{scheme}://{host}"
834
835 # Recupera parametri
836 clearkey_param = request.query.get('clearkey')
837
838 # ✅ FIX: Supporto per key_id e key separati (stile MediaFlowProxy)
839 if not clearkey_param:
840 key_id = request.query.get('key_id')
841 key = request.query.get('key')
842 if key_id and key:
843 clearkey_param = f"{key_id}:{key}"
844
845 req_format = request.query.get('format')
846 rep_id = request.query.get('rep_id')
847
848 # --- CONVERSIONE MPD -> HLS ---
849 if req_format == 'hls' or (request.path.endswith('.m3u8') and req_format != 'mpd'):
850
851 # Costruiamo i parametri da passare ai sottolink
852 params = "".join([f"&h_{urllib.parse.quote(key)}={urllib.parse.quote(value)}" for key, value in stream_headers.items()])
853
854 # ✅ FIX: Propagate api_password
855 api_password = request.query.get('api_password')
856 if api_password:
857 params += f"&api_password={api_password}"
858
859 if clearkey_param:
860 params += f"&clearkey={clearkey_param}"
861
862 if rep_id:
863 # Genera Media Playlist per la variante specifica
864 hls_content = self.mpd_converter.convert_media_playlist(
865 manifest_content, rep_id, proxy_base, stream_url, params, clearkey_param
866 )
867 return web.Response(
868 text=hls_content,
869 headers={
870 'Content-Type': 'application/vnd.apple.mpegurl',
871 'Content-Disposition': 'attachment; filename="playlist.m3u8"',
872 'Access-Control-Allow-Origin': '*',
873 'Cache-Control': 'no-cache'
874 }
875 )
876 else:
877 # Genera Master Playlist
878 hls_content = self.mpd_converter.convert_master_playlist(
879 manifest_content, proxy_base, stream_url, params
880 )
881 return web.Response(
882 text=hls_content,
883 headers={
884 'Content-Type': 'application/vnd.apple.mpegurl',
885 'Content-Disposition': 'attachment; filename="master.m3u8"',
886 'Access-Control-Allow-Origin': '*',
887 'Cache-Control': 'no-cache'
888 }
889 )
890
891 # --- MPD REWRITING (DASH NATIVO) ---
892 api_password = request.query.get('api_password')
893 rewritten_manifest = ManifestRewriter.rewrite_mpd_manifest(manifest_content, stream_url, proxy_base, headers, clearkey_param, api_password)
894
895 return web.Response(
896 text=rewritten_manifest,
897 headers={
898 'Content-Type': 'application/dash+xml',
899 'Content-Disposition': 'attachment; filename="stream.mpd"',
900 'Access-Control-Allow-Origin': '*',
901 'Cache-Control': 'no-cache'
902 })
903
904 # Streaming normale per altri tipi di contenuto
905 response_headers = {}
906
907 for header in ['content-type', 'content-length', 'content-range',
908 'accept-ranges', 'last-modified', 'etag']:
909 if header in resp.headers:
910 response_headers[header] = resp.headers[header]
911
912 # ✅ FIX: Forza Content-Type per segmenti .ts se il server non lo invia correttamente
913 if (stream_url.endswith('.ts') or request.path.endswith('.ts')) and 'video/mp2t' not in response_headers.get('content-type', '').lower():
914 response_headers['Content-Type'] = 'video/MP2T'
915
916 response_headers['Access-Control-Allow-Origin'] = '*'
917 response_headers['Access-Control-Allow-Methods'] = 'GET, HEAD, OPTIONS'
918 response_headers['Access-Control-Allow-Headers'] = 'Range, Content-Type'
919
920 response = web.StreamResponse(
921 status=resp.status,
922 headers=response_headers
923 )
924
925 await response.prepare(request)
926
927 async for chunk in resp.content.iter_chunked(8192):
928 await response.write(chunk)
929
930 await response.write_eof()
931 return response
932
933 except (ClientPayloadError, ConnectionResetError, OSError) as e:
934 # Errori tipici di disconnessione del client
935 logger.info(f"ℹ️ Client disconnesso dallo stream: {stream_url} ({str(e)})")
936 return web.Response(text="Client disconnected", status=499)
937
938 except (ServerDisconnectedError, ClientConnectionError, asyncio.TimeoutError) as e:
939 # Errori di connessione upstream
940 logger.warning(f"⚠️ Connessione persa con la sorgente: {stream_url} ({str(e)})")
941 return web.Response(text=f"Upstream connection lost: {str(e)}", status=502)
942
943 except Exception as e:
944 logger.error(f"❌ Errore generico nel zenith dello stream: {str(e)}")
945 return web.Response(text=f"Errore stream: {str(e)}", status=500)
946
947 async def handle_playlist_request(self, request):
948 """Gestisce le richieste per il playlist builder"""
949 if not self.playlist_builder:
950 return web.Response(text="❌ Playlist Builder non disponibile - modulo mancante", status=503)
951
952 try:
953 url_param = request.query.get('url')
954
955 if not url_param:
956 return web.Response(text="Parametro 'url' mancante", status=400)
957
958 if not url_param.strip():
959 return web.Response(text="Parametro 'url' non può essere vuoto", status=400)
960
961 playlist_definitions = [def_.strip() for def_ in url_param.split(';') if def_.strip()]
962 if not playlist_definitions:
963 return web.Response(text="Nessuna definizione playlist valida trovata", status=400)
964
965 # ✅ CORREZIONE: Rileva lo schema e l'host corretti quando dietro un reverse zenith
966 scheme = request.headers.get('X-Forwarded-Proto', request.scheme)
967 host = request.headers.get('X-Forwarded-Host', request.host)
968 base_url = f"{scheme}://{host}"
969
970 # ✅ FIX: Passa api_password al builder se presente
971 api_password = request.query.get('api_password')
972
973 async def generate_response():
974 async for line in self.playlist_builder.async_generate_combined_playlist(
975 playlist_definitions, base_url, api_password=api_password
976 ):
977 yield line.encode('utf-8')
978
979 response = web.StreamResponse(
980 status=200,
981 headers={
982 'Content-Type': 'application/vnd.apple.mpegurl',
983 'Content-Disposition': 'attachment; filename="playlist.m3u"',
984 'Access-Control-Allow-Origin': '*'
985 }
986 )
987
988 await response.prepare(request)
989
990 async for chunk in generate_response():
991 await response.write(chunk)
992
993 await response.write_eof()
994 return response
995
996 except Exception as e:
997 logger.error(f"Errore generale nel playlist handler: {str(e)}")
998 return web.Response(text=f"Errore: {str(e)}", status=500)
999
1000 def _read_template(self, filename: str) -> str:
1001 """Funzione helper per leggere un file di template."""
1002 # Nota: assume che i template siano nella directory 'templates' nella root del progetto
1003 # Poiché siamo in services/, dobbiamo salire di un livello
1004 base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1005 template_path = os.path.join(base_dir, 'templates', filename)
1006 with open(template_path, 'r', encoding='utf-8') as f:
1007 return f.read()
1008
1009 async def handle_root(self, request):
1010 """Serve la pagina principale index.html."""
1011 try:
1012 html_content = self._read_template('index.html')
1013 return web.Response(text=html_content, content_type='text/html')
1014 except Exception as e:
1015 logger.error(f"❌ Errore critico: impossibile caricare 'index.html': {e}")
1016 return web.Response(text="<h1>Errore 500</h1><p>Pagina non trovata.</p>", status=500, content_type='text/html')
1017
1018 async def handle_builder(self, request):
1019 """Gestisce l'interfaccia web del playlist builder."""
1020 try:
1021 html_content = self._read_template('builder.html')
1022 return web.Response(text=html_content, content_type='text/html')
1023 except Exception as e:
1024 logger.error(f"❌ Errore critico: impossibile caricare 'builder.html': {e}")
1025 return web.Response(text="<h1>Errore 500</h1><p>Impossibile caricare l'interfaccia builder.</p>", status=500, content_type='text/html')
1026
1027 async def handle_info_page(self, request):
1028 """Serve la pagina HTML delle informazioni."""
1029 try:
1030 html_content = self._read_template('info.html')
1031 return web.Response(text=html_content, content_type='text/html')
1032 except Exception as e:
1033 logger.error(f"❌ Errore critico: impossibile caricare 'info.html': {e}")
1034 return web.Response(text="<h1>Errore 500</h1><p>Impossibile caricare la pagina info.</p>", status=500, content_type='text/html')
1035
1036 async def handle_favicon(self, request):
1037 """Serve il file favicon.ico."""
1038 base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1039 favicon_path = os.path.join(base_dir, 'static', 'favicon.ico')
1040 if os.path.exists(favicon_path):
1041 return web.FileResponse(favicon_path)
1042 return web.Response(status=404)
1043
1044 async def handle_options(self, request):
1045 """Gestisce richieste OPTIONS per CORS"""
1046 headers = {
1047 'Access-Control-Allow-Origin': '*',
1048 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
1049 'Access-Control-Allow-Headers': 'Range, Content-Type',
1050 'Access-Control-Max-Age': '86400'
1051 }
1052 return web.Response(headers=headers)
1053
1054 async def handle_api_info(self, request):
1055 """Endpoint API che restituisce le informazioni sul server in formato JSON."""
1056 info = {
1057 "zenith": "HLS Zenith Server",
1058 "version": "2.5.0", # Aggiornata per supporto AES-128
1059 "status": "✅ Funzionante",
1060 "features": [
1061 "✅ Zenith HLS streams",
1062 "✅ AES-128 key proxying", # ✅ NUOVO
1063 "✅ Playlist building",
1064 "✅ Supporto Zenith (SOCKS5, HTTP/S)",
1065 "✅ Multi-extractor support",
1066 "✅ CORS enabled"
1067 ],
1068 "extractors_loaded": list(self.extractors.keys()),
1069 "modules": {
1070 "playlist_builder": PlaylistBuilder is not None,
1071 "vavoo_extractor": VavooExtractor is not None,
1072 "dlhd_extractor": DLHDExtractor is not None,
1073 "vixsrc_extractor": VixSrcExtractor is not None,
1074 "sportsonline_extractor": SportsonlineExtractor is not None,
1075 "mixdrop_extractor": MixdropExtractor is not None,
1076 "voe_extractor": VoeExtractor is not None,
1077 "streamtape_extractor": StreamtapeExtractor is not None,
1078 },
1079 "proxy_config": {
1080 "global_proxies": f"{len(GLOBAL_PROXIES)} proxies caricati",
1081 "transport_routes": f"{len(TRANSPORT_ROUTES)} regole di routing configurate",
1082 "routes": [{"url": route['url'], "has_proxy": route['zenith'] is not None} for route in TRANSPORT_ROUTES]
1083 },
1084 "endpoints": {
1085 "/zenith/hls/manifest.m3u8": "Zenith HLS (compatibilità MFP) - ?d=<URL>",
1086 "/zenith/mpd/manifest.m3u8": "Zenith MPD (compatibilità MFP) - ?d=<URL>",
1087 "/zenith/manifest.m3u8": "Zenith Legacy - ?url=<URL>",
1088 "/key": "Zenith chiavi AES-128 - ?key_url=<URL>", # ✅ NUOVO
1089 "/playlist": "Playlist builder - ?url=<definizioni>",
1090 "/builder": "Interfaccia web per playlist builder",
1091 "/segment/{segment}": "Zenith per segmenti .ts - ?base_url=<URL>",
1092 "/license": "Zenith licenze DRM (ClearKey/Widevine) - ?url=<URL> o ?clearkey=<id:key>",
1093 "/info": "Pagina HTML con informazioni sul server",
1094 "/api/info": "Endpoint JSON con informazioni sul server"
1095 },
1096 "usage_examples": {
1097 "proxy_hls": "/zenith/hls/manifest.m3u8?d=https://example.com/stream.m3u8",
1098 "proxy_mpd": "/zenith/mpd/manifest.m3u8?d=https://example.com/stream.mpd",
1099 "aes_key": "/key?key_url=https://server.com/key.bin", # ✅ NUOVO
1100 "playlist": "/playlist?url=http://example.com/playlist1.m3u8;http://example.com/playlist2.m3u8",
1101 "custom_headers": "/zenith/hls/manifest.m3u8?d=<URL>&h_Authorization=Bearer%20token"
1102 }
1103 }
1104 return web.json_response(info)
1105
1106 async def handle_decrypt_segment(self, request):
1107 """✅ Decritta segmenti fMP4 lato server usando Python (PyCryptodome)."""
1108 if not check_password(request):
1109 return web.Response(status=401, text="Unauthorized: Invalid API Password")
1110
1111 url = request.query.get('url')
1112 init_url = request.query.get('init_url')
1113 key = request.query.get('key')
1114 key_id = request.query.get('key_id')
1115
1116 if not url or not key or not key_id:
1117 return web.Response(text="Missing url, key, or key_id", status=400)
1118
1119 try:
1120 # Ricostruisce gli headers per le richieste upstream
1121 headers = {}
1122 for param_name, param_value in request.query.items():
1123 if param_name.startswith('h_'):
1124 header_name = param_name[2:].replace('_', '-')
1125 headers[header_name] = param_value
1126
1127 session = await self._get_session()
1128
1129 # --- 1. Scarica Initialization Segment (con cache) ---
1130 init_content = b""
1131 if init_url:
1132 if init_url in self.init_cache:
1133 init_content = self.init_cache[init_url]
1134 else:
1135 # ✅ NUOVO: Determina se disabilitare SSL per questo dominio
1136 disable_ssl_init = get_ssl_setting_for_url(init_url, TRANSPORT_ROUTES)
1137 async with session.get(init_url, headers=headers, ssl=not disable_ssl_init) as resp:
1138 if resp.status == 200:
1139 init_content = await resp.read()
1140 self.init_cache[init_url] = init_content
1141 else:
1142 logger.error(f"❌ Failed to fetch init segment: {resp.status}")
1143 return web.Response(status=502)
1144
1145 # --- 2. Scarica Media Segment ---
1146 # ✅ NUOVO: Determina se disabilitare SSL per questo dominio
1147 disable_ssl_media = get_ssl_setting_for_url(url, TRANSPORT_ROUTES)
1148 async with session.get(url, headers=headers, ssl=not disable_ssl_media) as resp:
1149 if resp.status != 200:
1150 logger.error(f"❌ Failed to fetch segment: {resp.status}")
1151 return web.Response(status=502)
1152
1153 segment_content = await resp.read()
1154
1155 # --- 3. Decritta con Python (PyCryptodome) ---
1156 decrypted_content = decrypt_segment(init_content, segment_content, key_id, key)
1157
1158 # --- 4. Invia Risposta ---
1159 return web.Response(
1160 body=decrypted_content,
1161 status=200,
1162 headers={'Content-Type': 'video/mp4', 'Access-Control-Allow-Origin': '*'}
1163 )
1164
1165 except Exception as e:
1166 logger.error(f"❌ Decryption error: {e}")
1167 import traceback
1168 traceback.print_exc()
1169 return web.Response(status=500, text=f"Decryption failed: {str(e)}")
1170
1171 async def handle_generate_urls(self, request):
1172 """
1173 Endpoint compatibile con MediaFlow-Zenith per generare URL zenith.
1174 Supporta la richiesta POST da ilCorsaroViola.
1175 """
1176 try:
1177 data = await request.json()
1178
1179 # Verifica password se presente nel body (ilCorsaroViola la manda qui)
1180 req_password = data.get('api_password')
1181 if API_PASSWORD and req_password != API_PASSWORD:
1182 # Fallback: check standard auth methods if body auth fails or is missing
1183 if not check_password(request):
1184 logger.warning("⛔ Unauthorized generate_urls request")
1185 return web.Response(status=401, text="Unauthorized: Invalid API Password")
1186
1187 urls_to_process = data.get('urls', [])
1188
1189 # --- LOGGING RICHIESTO ---
1190 client_ip = request.remote
1191 exit_strategy = "IP del Server (Diretto)"
1192 if GLOBAL_PROXIES:
1193 exit_strategy = f"Zenith Globale Random (Pool di {len(GLOBAL_PROXIES)} zenith)"
1194
1195 logger.info(f"🔄 [Generate URLs] Richiesta da Client IP: {client_ip}")
1196 logger.info(f" -> Strategia di uscita prevista per lo stream: {exit_strategy}")
1197 if urls_to_process:
1198 logger.info(f" -> Generazione di {len(urls_to_process)} URL zenith per destinazione: {urls_to_process[0].get('destination_url', 'N/A')}")
1199 # -------------------------
1200
