# [file name]: auth.py # [file content begin] # streaming_providers/providers/hrti/auth.py import base64 import json import time import uuid from datetime import datetime, timedelta from typing import Any, Dict, Optional from ...base.auth.base_auth import BaseAuthenticator, BaseAuthToken, TokenAuthLevel from ...base.models.proxy_models import ProxyConfig from ...base.utils.logger import logger from .constants import HRTiConfig from .models import HRTiAuthToken, HRTiCredentials class HRTiAuthenticator(BaseAuthenticator): def __init__( self, credentials=None, config_dir=None, proxy_config: Optional[ProxyConfig] = None, http_manager=None, ): # Initialize configuration FIRST self._config = HRTiConfig() # Get proxy_config if not provided if proxy_config is None: from ...base.network import ProxyConfigManager proxy_mgr = ProxyConfigManager(config_dir) proxy_config = proxy_mgr.get_proxy_config("hrti") # Store HTTP manager and proxy config locally (like MagentaEU) self._http_manager = http_manager self._proxy_config = proxy_config # Require http_manager like MagentaEU does if http_manager is None: raise ValueError("http_manager is required for HRTiAuthenticator") # Initialize HRTi-specific properties BEFORE calling parent init self._ip_address = None self._device_id = None self._user_id = None # Initialize _user_id here # Call parent init WITHOUT proxy_config and http_manager (like MagentaEU) super().__init__( provider_name="hrti", credentials=credentials, country=None, # Changed from 'HR' to None for flat credential structure config_dir=config_dir, # NO proxy_config or http_manager passed to parent ) # Load or initialize device ID self._initialize_device() @property def config(self): """Safe access to config""" return self._config @property def http_manager(self): """Safe access to http_manager - required by provider""" if self._http_manager is None: # This should never happen since we validate in __init__ raise ValueError( "HTTP manager not available - this should have been set during initialization" ) return self._http_manager @http_manager.setter def http_manager(self, value): """Allow setting http_manager""" self._http_manager = value @property def auth_endpoint(self) -> str: """HRTi authentication endpoint""" return self.config.api_endpoints["grant_access"] def _get_auth_headers(self) -> Dict[str, str]: """Get headers specifically for authentication endpoint""" if not self._ip_address: self._get_ip_address() device_id = self.get_device_id() # Special headers for authentication endpoint with /signin referer headers = { "User-Agent": self.config.user_agent, "Accept": "application/json", "Content-Type": "application/json", "deviceid": device_id, "devicetypeid": self.config.device_reference_id, # REMOVED: 'host': 'hrti.hrt.hr', # Let HTTP library set this automatically "ipaddress": self._ip_address, "operatorreferenceid": self.config.operator_reference_id, "origin": self.config.base_website, "referer": f"{self.config.base_website}/signin", } return headers def _get_api_headers(self, bearer_token: str = None, referer: str = None) -> Dict[str, str]: """Get headers for regular API endpoints with proper session support""" if not self._ip_address: self._get_ip_address() device_id = self.get_device_id() if referer is None: referer = f"{self.config.base_website}/login" headers = { "connection": "keep-alive", "User-Agent": self.config.user_agent, "Accept": "application/json, text/plain, */*", "Content-Type": "application/json", "deviceid": device_id, "devicetypeid": self.config.device_reference_id, # REMOVED: 'host': 'hrti.hrt.hr', # Let HTTP library set this automatically "ipaddress": self._ip_address, "operatorreferenceid": self.config.operator_reference_id, "origin": self.config.base_website, "referer": referer, "accept-encoding": "gzip, deflate, br", "accept-language": "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7", } # Add authorization header with Client prefix if bearer_token: headers["authorization"] = f"Client {bearer_token}" logger.debug(f"Added authorization header with token: {bearer_token[:20]}...") return headers def _build_auth_payload(self) -> Dict[str, Any]: """Build HRTi-specific authentication payload""" # HRTi uses specific format: {"Username":"...","Password":"...","OperatorReferenceId":"hrt"} if hasattr(self.credentials, "username") and hasattr(self.credentials, "password"): return { "Username": self.credentials.username, "Password": self.credentials.password, "OperatorReferenceId": self.config.operator_reference_id, } else: # Fallback for any other credential type return { "Username": getattr(self.credentials, "username", ""), "Password": getattr(self.credentials, "password", ""), "OperatorReferenceId": self.config.operator_reference_id, } def _create_token_from_response(self, response_data: Dict[str, Any]) -> HRTiAuthToken: """Create HRTi-specific token from API response""" result = response_data.get("Result", {}) # Debug the response structure logger.debug(f"GrantAccess response Result keys: {list(result.keys())}") # Extract token access_token = result.get("Token", "") logger.debug(f"Extracted access_token: {'present' if access_token else 'MISSING'}") if access_token: logger.debug(f"Token length: {len(access_token)}, starts with: {access_token[:20]}...") # Store user ID if available if "Customer" in result: self._user_id = result["Customer"].get("CustomerId", "") logger.debug(f"Extracted user_id: {self._user_id}") else: logger.warning("No Customer data in response") token = HRTiAuthToken( access_token=access_token, token_type="Client", expires_in=86400, # 24 hours default issued_at=time.time(), user_id=self._user_id or "", # Ensure user_id is never None valid_from=result.get("ValidFrom", ""), valid_to=result.get("ValidTo", ""), ) # Classify the token immediately after creation token.auth_level = self._classify_token(token) logger.debug( f"Created HRTiAuthToken - access_token: {bool(token.access_token)}, user_id: {token.user_id}" ) return token def get_fallback_credentials(self): """Get fallback credentials (anonymous access)""" return HRTiCredentials(username="anonymoushrt", password="an0nPasshrt") def _perform_authentication(self) -> HRTiAuthToken: """Perform HRTi custom authentication flow""" logger.info("Starting HRTi authentication flow") # Step 1: Get IP address self._get_ip_address() # Step 2: Get environment configuration self._get_environment_config() # Step 3: Perform grant access token_data = self._perform_grant_access() # Step 4: Create token and SET IT AS CURRENT TOKEN token = self._create_token_from_response(token_data) self._current_token = token # THIS IS THE CRITICAL MISSING LINE # Debug: Verify token is available logger.debug(f"Token created - access_token present: {bool(token.access_token)}") if token.access_token: logger.debug(f"Token value: {token.access_token[:20]}...") else: logger.warning("Token created but access_token is empty!") # Step 5: Register device (now with token available in self._current_token) self._register_device() # Step 6: Get content rating and profiles self._get_initial_data() logger.info( f"HRTi authentication successful - user_id: {token.user_id}, auth_level: {token.auth_level.value}" ) # Save the session after successful authentication self._save_session() return token def _get_ip_address(self) -> str: """Get public IP address - prefer configured IP from settings, fallback to API""" if self._ip_address: return self._ip_address # Priority 1: Try to get configured IP from Kodi settings try: configured_ip = self._get_configured_ip_from_settings() if configured_ip: self._ip_address = configured_ip logger.info(f"Using configured IP address from Kodi settings: {self._ip_address}") return self._ip_address except Exception as e: logger.debug(f"Could not read configured IP from settings: {e}") # Priority 2: Fall back to API fetch if no configured IP try: logger.debug("No configured IP found, fetching from API...") response = self.http_manager.get(self.config.api_endpoints["get_ip"], operation="api") response.raise_for_status() self._ip_address = response.text.strip().strip('"') # Remove quotes if present logger.info(f"Retrieved IP address from API: {self._ip_address}") return self._ip_address except Exception as e: logger.error(f"Error getting IP address from API: {e}") self._ip_address = "0.0.0.0" # Fallback return self._ip_address def _get_configured_ip_from_settings(self) -> Optional[str]: """ Get configured IP address from Kodi settings via settings_manager Returns: Configured IP address or None if not available """ # Access settings_manager through parent class if not hasattr(self, "settings_manager") or not self.settings_manager: logger.debug("Settings manager not available for IP configuration") return None kodi_bridge = self.settings_manager.kodi_bridge if not kodi_bridge or not kodi_bridge.is_kodi_environment(): logger.debug("Kodi bridge not available or not in Kodi environment") return None # Try to read IP from Kodi settings # HRTi doesn't use country in this implementation, pass None configured_ip = kodi_bridge.read_ip_address_from_kodi("hrti", None) if configured_ip: logger.debug(f"Found configured IP in Kodi settings: {configured_ip}") return configured_ip return None def _get_environment_config(self): """Get HRTi environment configuration""" try: # Get env config env_response = self.http_manager.get(self.config.env_endpoint, operation="api") env_response.raise_for_status() env_data = env_response.json() # Get main config config_response = self.http_manager.get(self.config.config_endpoint, operation="api") config_response.raise_for_status() config_data = config_response.json() # Update config with retrieved values self.config.update_from_api(env_data, config_data) logger.debug("HRTi environment configuration loaded") except Exception as e: logger.warning(f"Error loading HRTi environment config: {e}") def _perform_grant_access(self) -> Dict[str, Any]: """Perform grant access authentication with proper headers and debugging""" try: # Ensure we have IP and device ID if not self._ip_address: self._get_ip_address() device_id = self.get_device_id() logger.debug(f"Performing grant access with username: {self.credentials.username}") logger.debug(f"Using device ID: {device_id}") logger.debug(f"Using IP address: {self._ip_address}") # Use auth-specific headers with /signin referer headers = self._get_auth_headers() payload = self._build_auth_payload() # Log the request details (safely) safe_headers = headers.copy() if "deviceid" in safe_headers: safe_headers["deviceid"] = f"{safe_headers['deviceid'][:8]}..." logger.debug(f"HRTi Auth Headers: {safe_headers}") safe_payload = payload.copy() if "Password" in safe_payload: safe_payload["Password"] = "***" if safe_payload["Password"] else "" logger.debug(f"HRTi Auth Payload: {safe_payload}") response = self.http_manager.post( self.auth_endpoint, operation="auth", headers=headers, data=json.dumps(payload), ) logger.debug(f"HRTi Auth Response - Status: {response.status_code}") response.raise_for_status() result = response.json() if "Result" not in result: raise Exception("No result in grant access response") customer_id = result.get("Result", {}).get("Customer", {}).get("CustomerId", "unknown") logger.debug(f"Grant access successful - user: {customer_id}") return result except Exception as e: logger.error(f"HRTi grant access failed: {e}") # Only fallback to anonymous if we're not already using it if ( not isinstance(self.credentials, HRTiCredentials) or self.credentials.username != "anonymoushrt" ): logger.info("Falling back to anonymous credentials") self.credentials = self.get_fallback_credentials() return self._perform_grant_access() else: raise e def _register_device(self): """Register device with HRTi using API headers with proper authorization""" try: bearer_token = self._current_token.access_token if self._current_token else "" if not bearer_token: logger.warning("No bearer token available for device registration") return # Use API headers with proper authorization and referer headers = self._get_api_headers(bearer_token) # Update the referer to root path for device registration headers["referer"] = f"{self.config.base_website}/" # Add cache-control and pragma headers to match working example headers["cache-control"] = "no-cache" headers["pragma"] = "no-cache" payload = { "DeviceSerial": self._device_id, "DeviceReferenceId": self.config.device_reference_id, "IpAddress": self._ip_address, "ConnectionType": self.config.connection_type, "ApplicationVersion": self.config.application_version, "DrmId": self._device_id, "OsVersion": self.config.os_version, "ClientType": self.config.client_type, } logger.debug(f"HRTi Device Registration Payload: {payload}") response = self.http_manager.post( self.config.api_endpoints["register_device"], operation="api", headers=headers, data=json.dumps(payload), ) logger.debug(f"Device registration response status: {response.status_code}") response.raise_for_status() result = response.json() logger.debug(f"HRTi device registration successful: {result}") except Exception as e: logger.warning(f"HRTi device registration failed: {e}") def _get_initial_data(self): """Get initial content rating and profiles using API headers with proper authorization""" try: bearer_token = self._current_token.access_token if self._current_token else "" headers = self._get_api_headers(bearer_token) # Update the referer to root path for these API calls headers["referer"] = f"{self.config.base_website}/" # Get content ratings content_response = self.http_manager.post( self.config.api_endpoints["content_ratings"], operation="api", headers=headers, data=json.dumps({}), ) content_response.raise_for_status() # Get profiles profiles_response = self.http_manager.post( self.config.api_endpoints["profiles"], operation="api", headers=headers, data=json.dumps({}), ) profiles_response.raise_for_status() logger.debug("HRTi initial data loaded") except Exception as e: logger.debug(f"Error loading HRTi initial data: {e}") def _initialize_device(self): """Initialize or load device ID - ensure it's a proper UUID""" try: self._device_id = self.settings_manager.get_device_id(self.provider_name, self.country) if not self._device_id: self._device_id = str(uuid.uuid4()) logger.debug(f"Generated new device ID: {self._device_id}") # Ensure it's a valid UUID format if not self._validate_uuid(self._device_id): logger.warning(f"Invalid device ID format, generating new one: {self._device_id}") self._device_id = str(uuid.uuid4()) except Exception as e: logger.error(f"Error initializing device ID: {e}") self._device_id = str(uuid.uuid4()) @staticmethod def _validate_uuid(uuid_string): """Validate UUID format""" try: uuid.UUID(uuid_string) return True except ValueError: return False def get_device_id(self) -> str: """Get device ID""" return self._device_id def get_ip_address(self) -> str: """Get IP address""" if not self._ip_address: self._get_ip_address() return self._ip_address def _load_session(self) -> None: """Override to ensure _user_id is set when loading session""" try: # Call parent method first super()._load_session() # If we have a current token and it's an HRTiAuthToken, set _user_id if self._current_token and isinstance(self._current_token, HRTiAuthToken): self._user_id = self._current_token.user_id logger.debug(f"Set _user_id from loaded session: {self._user_id}") except Exception as e: logger.error(f"Error in HRTi session loading: {e}") # Ensure _user_id is at least an empty string self._user_id = self._user_id or "" def authorize_session( self, content_type: str, content_ref_id: str, content_drm_id: str = None, video_store_ids: list = None, channel_id: str = None, start_time: str = None, end_time: str = None, ) -> Optional[Dict[str, Any]]: """Authorize a playback session""" try: bearer_token = self._current_token.access_token if self._current_token else "" headers = self._get_api_headers(bearer_token) # Set referer based on content type if channel_id is None: headers["referer"] = f"{self.config.base_website}/videostore" else: if content_type == "tlive": headers["referer"] = f"{self.config.base_website}/live/tv?channel={channel_id}" elif content_type == "rlive": headers["referer"] = f"{self.config.base_website}/live/radio" else: headers["referer"] = f"{self.config.base_website}/live/" payload = { "ContentType": content_type, "ContentReferenceId": content_ref_id, "ContentDrmId": content_drm_id or f"{content_ref_id}_drm", "VideostoreReferenceIds": video_store_ids, "ChannelReferenceId": channel_id, "StartTime": start_time, "EndTime": end_time, } logger.debug( f"Authorizing session - type: {content_type}, ref: {content_ref_id}, drm: {content_drm_id}" ) response = self.http_manager.post( self.config.api_endpoints["authorize_session"], operation="api", headers=headers, data=json.dumps(payload), ) response.raise_for_status() result = response.json() if "Result" in result: authorized = result["Result"].get("Authorized", False) session_id = result["Result"].get("SessionId") or result["Result"].get("DrmId") logger.debug( f"Session authorization result - authorized: {authorized}, session_id: {session_id}" ) return result["Result"] else: logger.warning("No result in session authorization response") return None except Exception as e: logger.error(f"HRTi session authorization failed: {e}") return None def report_session_event(self, session_id: str, channel_id: str = None) -> bool: """Report session event (like play start)""" try: bearer_token = self._current_token.access_token if self._current_token else "" headers = self._get_api_headers(bearer_token) # Set referer based on channel if channel_id is None: headers["referer"] = f"{self.config.base_website}/videostore" else: headers["referer"] = f"{self.config.base_website}/live/tv?channel={channel_id}" payload = {"SessionEventId": 1, "SessionId": session_id} # 1 = play start response = self.http_manager.post( self.config.api_endpoints["report_session"], operation="api", headers=headers, data=json.dumps(payload), ) response.raise_for_status() logger.debug(f"Session event reported for session {session_id}") return True except Exception as e: logger.warning(f"Session event reporting failed: {e}") return False def get_license_data(self, session_id: str) -> str: """Generate license data for DRM - returns base64 encoded string""" try: drm_license = { "userId": self._user_id or "", "sessionId": session_id, "merchant": self.config.merchant, } logger.debug( f"Creating license data - userId: {self._user_id}, sessionId: {session_id}, merchant: {self.config.merchant}" ) # Encode to JSON then to base64 license_bytes = json.dumps(drm_license).encode("utf-8") license_b64 = base64.b64encode(license_bytes).decode("utf-8") logger.debug(f"License data (base64, first 30 chars): {license_b64[:30]}...") return license_b64 except Exception as e: logger.error(f"Error generating license data: {e}") return "" @staticmethod def get_time_offset(hours_offset: int) -> int: """Get timestamp with offset in milliseconds""" target_time = datetime.now() + timedelta(hours=hours_offset) return int(target_time.timestamp() * 1000) def _classify_token(self, token: BaseAuthToken) -> TokenAuthLevel: """Classify HRTi token authentication level""" if not token or not token.access_token: return TokenAuthLevel.ANONYMOUS # Check if it's an HRTiAuthToken and has user_id attribute if isinstance(token, HRTiAuthToken): # Check if it's an anonymous token by user_id if token.user_id == "anonymoushrt": return TokenAuthLevel.ANONYMOUS # Check if we have user credentials available from ...base.auth.credentials import UserPasswordCredentials # Check stored credentials first try: stored_creds = self.settings_manager.get_provider_credentials( self.provider_name, self.country ) except TypeError: stored_creds = self.settings_manager.get_provider_credentials(self.provider_name) has_user_creds = ( isinstance(stored_creds, UserPasswordCredentials) and stored_creds.validate() ) # Check current credentials if not has_user_creds: has_user_creds = ( isinstance(self.credentials, UserPasswordCredentials) and self.credentials.validate() ) # If we have user credentials and token has a real user_id, it's user authenticated if has_user_creds and token.user_id and token.user_id != "anonymoushrt": return TokenAuthLevel.USER_AUTHENTICATED # Check if using anonymous credentials if hasattr(self.credentials, "username") and self.credentials.username == "anonymoushrt": return TokenAuthLevel.ANONYMOUS # If we have user credentials, consider it user authenticated if ( hasattr(self.credentials, "username") and self.credentials.username and self.credentials.username != "anonymoushrt" ): return TokenAuthLevel.USER_AUTHENTICATED return TokenAuthLevel.CLIENT_CREDENTIALS def should_upgrade_token(self, token: BaseAuthToken) -> bool: """ Determine if token should be upgraded from anonymous to user credentials """ if not token: return False # Classify token if not already classified if token.auth_level == TokenAuthLevel.UNKNOWN: token.auth_level = self._classify_token(token) # Check if token is anonymous and we have user credentials if token.auth_level == TokenAuthLevel.ANONYMOUS: from ...base.auth.credentials import UserPasswordCredentials # Check if we have stored user credentials try: stored_creds = self.settings_manager.get_provider_credentials( self.provider_name, self.country ) except TypeError: stored_creds = self.settings_manager.get_provider_credentials(self.provider_name) has_stored_user_creds = ( isinstance(stored_creds, UserPasswordCredentials) and stored_creds.validate() ) # Check current credentials has_current_user_creds = ( isinstance(self.credentials, UserPasswordCredentials) and self.credentials.validate() ) # Upgrade if we have any user credentials return has_stored_user_creds or has_current_user_creds return False def get_bearer_token(self, force_refresh: bool = False, force_upgrade: bool = False) -> str: """ Get bearer token with automatic upgrade from anonymous to user credentials """ logger.debug( f"HRTi get_bearer_token: force_refresh={force_refresh}, force_upgrade={force_upgrade}" ) # Get current token (authenticate if needed) current_token = self.authenticate(force_refresh=force_refresh) # Check if upgrade is needed/requested should_upgrade = force_upgrade or self.should_upgrade_token(current_token) if should_upgrade: logger.info( f"HRTi token upgrade triggered (force={force_upgrade}, should_upgrade={self.should_upgrade_token(current_token)})" ) # Store original credentials at the beginning original_credentials = self.credentials try: # Get user credentials from settings manager try: user_creds = self.settings_manager.get_provider_credentials( self.provider_name, self.country ) except TypeError: user_creds = self.settings_manager.get_provider_credentials(self.provider_name) if not user_creds or not user_creds.validate(): logger.debug("No valid user credentials available for upgrade") return current_token.bearer_token # Switch to user credentials self.credentials = user_creds # Perform authentication with user credentials logger.info("Performing authentication with user credentials for upgrade") user_token = self._perform_authentication() if user_token and not user_token.is_expired: # Verify it's actually a user token (not anonymous) user_token.auth_level = self._classify_token(user_token) if user_token.auth_level == TokenAuthLevel.USER_AUTHENTICATED: logger.info("Successfully upgraded to user token") self._current_token = user_token self._save_session() return user_token.bearer_token else: logger.warning( "Authentication succeeded but token is still anonymous level" ) self.credentials = original_credentials return current_token.bearer_token else: logger.warning("User authentication failed during upgrade") self.credentials = original_credentials return current_token.bearer_token except Exception as e: logger.error(f"Token upgrade failed: {e}") # Restore original credentials on failure self.credentials = original_credentials return current_token.bearer_token return current_token.bearer_token if current_token else "" def _refresh_token(self) -> Optional[BaseAuthToken]: """Refresh HRTi token - reauthenticate since it's custom auth""" logger.info("Refreshing HRTi token via reauthentication") try: return self._perform_authentication() except Exception as e: logger.error(f"HRTi token refresh failed: {e}") return None # [file content end]