mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-28 20:12:19 +02:00
830 lines
32 KiB
Python
830 lines
32 KiB
Python
# 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.get("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,
|
|
"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,
|
|
"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...")
|
|
get_ip_endpoint = self.config.api_endpoints.get("get_ip", "")
|
|
if not get_ip_endpoint:
|
|
logger.warning("No get_ip endpoint configured, using fallback")
|
|
self._ip_address = "0.0.0.0"
|
|
return self._ip_address
|
|
|
|
response = self.http_manager.get(get_ip_endpoint, 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}")
|
|
# Ensure API endpoints are built even if config fetch fails
|
|
self.config._build_fallback_endpoints()
|
|
|
|
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 "<empty>"
|
|
logger.debug(f"HRTi Auth Payload: {safe_payload}")
|
|
|
|
auth_endpoint = self.auth_endpoint
|
|
if not auth_endpoint:
|
|
raise Exception("No grant_access endpoint configured")
|
|
|
|
response = self.http_manager.post(
|
|
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}")
|
|
|
|
register_endpoint = self.config.api_endpoints.get("register_device", "")
|
|
if not register_endpoint:
|
|
logger.warning("No register_device endpoint configured")
|
|
return
|
|
|
|
response = self.http_manager.post(
|
|
register_endpoint,
|
|
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_endpoint = self.config.api_endpoints.get("content_ratings", "")
|
|
if content_endpoint:
|
|
content_response = self.http_manager.post(
|
|
content_endpoint,
|
|
operation="api",
|
|
headers=headers,
|
|
data=json.dumps({}),
|
|
)
|
|
content_response.raise_for_status()
|
|
|
|
# Get profiles
|
|
profiles_endpoint = self.config.api_endpoints.get("profiles", "")
|
|
if profiles_endpoint:
|
|
profiles_response = self.http_manager.post(
|
|
profiles_endpoint,
|
|
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}"
|
|
)
|
|
|
|
auth_endpoint = self.config.api_endpoints.get("authorize_session", "")
|
|
if not auth_endpoint:
|
|
logger.error("No authorize_session endpoint configured")
|
|
return None
|
|
|
|
response = self.http_manager.post(
|
|
auth_endpoint,
|
|
operation="api",
|
|
headers=headers,
|
|
data=json.dumps(payload),
|
|
)
|
|
response.raise_for_status()
|
|
|
|
result = response.json()
|
|
session_result = result.get("Result")
|
|
if session_result:
|
|
authorized = session_result.get("Authorized", False)
|
|
session_id = session_result.get("SessionId") or session_result.get("DrmId")
|
|
logger.debug(
|
|
f"Session authorization result - authorized: {authorized}, session_id: {session_id}"
|
|
)
|
|
return session_result
|
|
else:
|
|
logger.warning(
|
|
f"No result in session authorization response "
|
|
f"(ErrorCode={result.get('ErrorCode')}: {result.get('ErrorDescription')})"
|
|
)
|
|
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
|
|
|
|
report_endpoint = self.config.api_endpoints.get("report_session", "")
|
|
if not report_endpoint:
|
|
logger.warning("No report_session endpoint configured")
|
|
return False
|
|
|
|
response = self.http_manager.post(
|
|
report_endpoint,
|
|
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:
|
|
# Use merchant from config (now dynamically loaded)
|
|
merchant = self.config.merchant or "aviion2"
|
|
|
|
drm_license = {
|
|
"userId": self._user_id or "",
|
|
"sessionId": session_id,
|
|
"merchant": merchant,
|
|
}
|
|
|
|
logger.debug(
|
|
f"Creating license data - userId: {self._user_id}, sessionId: {session_id}, merchant: {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 |