CoolFace
Apppublic

sdv2500/progettojava

sourceHugging Faceupdated 10mo agoView on Hugging Face
0likes
hls_proxy.py1286 linesDownload Raw Back to services
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

Showing the first 1,200 of 1286 lines. Download the file for the rest.