mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-19 23:52:10 +02:00
583 lines
24 KiB
Python
583 lines
24 KiB
Python
# streaming_providers/base/auth/credential_manager.py
|
|
from typing import Optional
|
|
from .credentials import BaseCredentials, UserPasswordCredentials, ClientCredentials
|
|
|
|
import time
|
|
from typing import Dict, Any, List
|
|
|
|
# Import centralized logger and VFS
|
|
from ..utils.logger import logger
|
|
|
|
|
|
class CredentialManager:
|
|
"""
|
|
Manages loading and saving credentials from/to persistent storage
|
|
Now supports country-specific credentials
|
|
"""
|
|
|
|
def __init__(self, config_dir: Optional[str] = None):
|
|
# Initialize VFS with config directory support
|
|
from ..utils.vfs import VFS
|
|
self.vfs = VFS(config_dir=config_dir)
|
|
|
|
# Credentials file is always in the root of the VFS base path
|
|
self.credentials_file = 'credentials.json'
|
|
|
|
# Ensure base directory exists
|
|
self.vfs.mkdirs('')
|
|
logger.debug(f"CredentialManager initialized with VFS base: {self.vfs.base_path}")
|
|
|
|
@staticmethod
|
|
def _get_credential_path(provider: str, country: Optional[str] = None) -> tuple:
|
|
"""
|
|
Determine the path to credential data based on country
|
|
|
|
Args:
|
|
provider: Provider name
|
|
country: Optional country code (e.g., 'de', 'at', 'ch')
|
|
|
|
Returns:
|
|
Tuple of (keys_path, is_nested) where keys_path is list of keys to navigate
|
|
"""
|
|
if country:
|
|
return [provider, country], True
|
|
else:
|
|
return [provider], False
|
|
|
|
def load_credentials(self, provider: str, country: Optional[str] = None) -> Optional[BaseCredentials]:
|
|
"""
|
|
Load credentials for a specific provider and optional country
|
|
|
|
Args:
|
|
provider: Provider name (e.g., 'rtlplus', 'joyn')
|
|
country: Optional country code (e.g., 'de', 'at', 'ch')
|
|
|
|
Returns:
|
|
BaseCredentials instance or None if not found
|
|
"""
|
|
country_str = f" (country: {country})" if country else ""
|
|
try:
|
|
logger.debug(f"CredentialManager: Loading credentials for '{provider}{country_str}'")
|
|
|
|
# Check if credentials file exists
|
|
if not self.vfs.exists(self.credentials_file):
|
|
logger.debug(f"CredentialManager: Credentials file does not exist: {self.credentials_file}")
|
|
return None
|
|
|
|
logger.debug(f"CredentialManager: Reading credentials file: {self.credentials_file}")
|
|
data = self.vfs.read_json(self.credentials_file)
|
|
|
|
if not data:
|
|
logger.debug(f"CredentialManager: Credentials file is empty or invalid JSON")
|
|
return None
|
|
|
|
logger.debug(f"CredentialManager: Available providers in file: {list(data.keys())}")
|
|
|
|
# Navigate to the correct credential data
|
|
keys_path, is_nested = self._get_credential_path(provider, country)
|
|
provider_data = data
|
|
|
|
for key in keys_path:
|
|
if not isinstance(provider_data, dict) or key not in provider_data:
|
|
logger.debug(f"CredentialManager: No data found at path: {' -> '.join(keys_path)}")
|
|
return None
|
|
provider_data = provider_data[key]
|
|
|
|
if not provider_data:
|
|
logger.debug(f"CredentialManager: No data found for provider '{provider}{country_str}'")
|
|
return None
|
|
|
|
logger.debug(f"CredentialManager: Found data for '{provider}{country_str}': {list(provider_data.keys())}")
|
|
|
|
credential_type = provider_data.get('type')
|
|
logger.debug(f"CredentialManager: Credential type for '{provider}{country_str}': {credential_type}")
|
|
|
|
if credential_type == 'user_password':
|
|
username = provider_data.get('username', '')
|
|
password_present = 'password' in provider_data
|
|
client_id = provider_data.get('client_id')
|
|
|
|
logger.debug(f"CredentialManager: Creating UserPasswordCredentials for '{provider}{country_str}'")
|
|
logger.debug(
|
|
f"CredentialManager: Username: '{username}', Password present: {password_present}, Client ID: {client_id}")
|
|
|
|
creds = UserPasswordCredentials(
|
|
username=username,
|
|
password=self._decode_password(provider_data.get('password', '')),
|
|
client_id=client_id,
|
|
grant_type=provider_data.get('grant_type', 'password')
|
|
)
|
|
|
|
logger.debug(
|
|
f"CredentialManager: Successfully created UserPasswordCredentials for '{provider}{country_str}'")
|
|
return creds
|
|
|
|
elif credential_type == 'client_credentials':
|
|
client_id = provider_data.get('client_id', '')
|
|
client_secret_present = 'client_secret' in provider_data
|
|
|
|
logger.debug(f"CredentialManager: Creating ClientCredentials for '{provider}{country_str}'")
|
|
logger.debug(
|
|
f"CredentialManager: Client ID: '{client_id}', Client secret present: {client_secret_present}")
|
|
|
|
creds = ClientCredentials(
|
|
client_id=client_id,
|
|
client_secret=self._decode_password(provider_data.get('client_secret', '')),
|
|
grant_type=provider_data.get('grant_type', 'client_credentials')
|
|
)
|
|
|
|
logger.debug(f"CredentialManager: Successfully created ClientCredentials for '{provider}{country_str}'")
|
|
return creds
|
|
|
|
else:
|
|
logger.error(
|
|
f"CredentialManager: Unknown credential type for '{provider}{country_str}': {credential_type}")
|
|
return None
|
|
|
|
except Exception as e:
|
|
country_str = f" (country: {country})" if country else ""
|
|
logger.error(f"CredentialManager: Error loading credentials for '{provider}{country_str}': {e}")
|
|
return None
|
|
|
|
def save_credentials(self, provider: str, credentials: BaseCredentials,
|
|
country: Optional[str] = None) -> bool:
|
|
"""
|
|
Save credentials for a specific provider and optional country
|
|
|
|
Args:
|
|
provider: Provider name
|
|
credentials: Credentials to save
|
|
country: Optional country code
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
country_str = f" (country: {country})" if country else ""
|
|
try:
|
|
logger.debug(f"Saving {credentials.credential_type} credentials for {provider}{country_str}")
|
|
|
|
# Load existing data
|
|
data = {}
|
|
if self.vfs.exists(self.credentials_file):
|
|
data = self.vfs.read_json(self.credentials_file) or {}
|
|
|
|
# Prepare provider data based on credential type
|
|
if isinstance(credentials, UserPasswordCredentials):
|
|
provider_data = {
|
|
'type': 'user_password',
|
|
'username': credentials.username,
|
|
'password': self._encode_password(credentials.password),
|
|
'grant_type': credentials.grant_type
|
|
}
|
|
if credentials.client_id:
|
|
provider_data['client_id'] = credentials.client_id
|
|
|
|
elif isinstance(credentials, ClientCredentials):
|
|
provider_data = {
|
|
'type': 'client_credentials',
|
|
'client_id': credentials.client_id,
|
|
'client_secret': self._encode_password(credentials.client_secret),
|
|
'grant_type': credentials.grant_type
|
|
}
|
|
else:
|
|
logger.error(f"Unsupported credential type: {type(credentials)}")
|
|
return False
|
|
|
|
# Navigate and create nested structure if needed
|
|
keys_path, is_nested = self._get_credential_path(provider, country)
|
|
|
|
# Build nested structure
|
|
current = data
|
|
for i, key in enumerate(keys_path[:-1]):
|
|
if key not in current:
|
|
current[key] = {}
|
|
elif not isinstance(current[key], dict):
|
|
logger.warning(f"Overwriting non-dict value at {key}")
|
|
current[key] = {}
|
|
current = current[key]
|
|
|
|
# Set the final value
|
|
current[keys_path[-1]] = provider_data
|
|
|
|
# Save to file using VFS
|
|
success = self.vfs.write_json(self.credentials_file, data)
|
|
|
|
if success:
|
|
logger.info(f"Successfully saved credentials for {provider}{country_str}")
|
|
|
|
# Verify by reading back
|
|
verify_data = self.vfs.read_json(self.credentials_file)
|
|
if verify_data:
|
|
verify_current = verify_data
|
|
found = True
|
|
for key in keys_path:
|
|
if not isinstance(verify_current, dict) or key not in verify_current:
|
|
found = False
|
|
break
|
|
verify_current = verify_current[key]
|
|
|
|
if found:
|
|
logger.debug(f"Verification successful: {provider}{country_str} data found in saved file")
|
|
else:
|
|
logger.error(f"Verification failed: {provider}{country_str} data NOT found in saved file")
|
|
return False
|
|
|
|
return True
|
|
else:
|
|
logger.error(f"Failed to save credentials for {provider}{country_str}")
|
|
return False
|
|
|
|
except Exception as e:
|
|
country_str = f" (country: {country})" if country else ""
|
|
logger.error(f"Error saving credentials for {provider}{country_str}: {e}")
|
|
return False
|
|
|
|
def delete_credentials(self, provider: str, country: Optional[str] = None) -> bool:
|
|
"""
|
|
Delete credentials for a specific provider and optional country
|
|
|
|
Args:
|
|
provider: Provider name
|
|
country: Optional country code (if None, deletes entire provider or all countries)
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
country_str = f" (country: {country})" if country else ""
|
|
try:
|
|
|
|
if not self.vfs.exists(self.credentials_file):
|
|
logger.debug(f"No credentials file exists, nothing to delete for {provider}{country_str}")
|
|
return True
|
|
|
|
data = self.vfs.read_json(self.credentials_file)
|
|
if not data:
|
|
logger.debug(f"Credentials file is empty, nothing to delete for {provider}{country_str}")
|
|
return True
|
|
|
|
if country:
|
|
# Delete specific country data
|
|
if provider in data and isinstance(data[provider], dict) and country in data[provider]:
|
|
del data[provider][country]
|
|
logger.info(f"Deleted stored credentials for {provider}{country_str}")
|
|
|
|
# If provider dict is now empty, remove it entirely
|
|
if not data[provider]:
|
|
del data[provider]
|
|
logger.debug(f"Provider {provider} had no more countries, removed entirely")
|
|
|
|
return self.vfs.write_json(self.credentials_file, data)
|
|
else:
|
|
logger.debug(f"No stored credentials found to delete for {provider}{country_str}")
|
|
else:
|
|
# Delete entire provider data (all countries)
|
|
if provider in data:
|
|
del data[provider]
|
|
logger.info(f"Deleted all stored credentials for {provider}")
|
|
return self.vfs.write_json(self.credentials_file, data)
|
|
else:
|
|
logger.debug(f"No stored credentials found to delete for {provider}")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
country_str = f" (country: {country})" if country else ""
|
|
logger.error(f"Error deleting credentials for {provider}{country_str}: {e}")
|
|
return False
|
|
|
|
def list_providers(self) -> List[str]:
|
|
"""
|
|
List all providers with stored credentials
|
|
|
|
Returns:
|
|
List of provider names
|
|
"""
|
|
try:
|
|
if not self.vfs.exists(self.credentials_file):
|
|
logger.debug("No credentials file exists, returning empty provider list")
|
|
return []
|
|
|
|
data = self.vfs.read_json(self.credentials_file)
|
|
if not data:
|
|
logger.debug("Credentials file is empty, returning empty provider list")
|
|
return []
|
|
|
|
providers = list(data.keys())
|
|
logger.debug(f"Found stored credentials for providers: {providers}")
|
|
return providers
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error listing providers: {e}")
|
|
return []
|
|
|
|
def get_all_countries(self, provider: str) -> List[str]:
|
|
"""
|
|
Get all countries that have credentials for a provider
|
|
|
|
Args:
|
|
provider: Provider name
|
|
|
|
Returns:
|
|
List of country codes
|
|
"""
|
|
try:
|
|
if not self.vfs.exists(self.credentials_file):
|
|
return []
|
|
|
|
data = self.vfs.read_json(self.credentials_file)
|
|
if not data or provider not in data:
|
|
return []
|
|
|
|
provider_data = data[provider]
|
|
|
|
# Check if this is a nested (country-aware) structure
|
|
if isinstance(provider_data, dict):
|
|
# Check if any keys look like country codes (2-3 char strings)
|
|
# and their values are dicts (credential data)
|
|
countries = []
|
|
for key, value in provider_data.items():
|
|
if isinstance(value, dict) and len(key) <= 3 and 'type' in value:
|
|
countries.append(key)
|
|
return countries
|
|
|
|
return []
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting countries for {provider}: {e}")
|
|
return []
|
|
|
|
def has_credentials(self, provider: str, country: Optional[str] = None) -> bool:
|
|
"""
|
|
Check if credentials exist for a provider and optional country
|
|
|
|
Args:
|
|
provider: Provider name
|
|
country: Optional country code
|
|
|
|
Returns:
|
|
True if credentials exist, False otherwise
|
|
"""
|
|
credentials = self.load_credentials(provider, country)
|
|
return credentials is not None and credentials.validate()
|
|
|
|
@staticmethod
|
|
def _encode_password(password: str) -> str:
|
|
"""No encoding - store as plaintext (TEMPORARY INSECURE SOLUTION)."""
|
|
logger.warning("Storing password in plaintext - this is insecure and should be replaced with proper encryption")
|
|
return password
|
|
|
|
@staticmethod
|
|
def _decode_password(encoded_password: str) -> str:
|
|
"""No decoding needed - passwords are stored in plaintext."""
|
|
return encoded_password
|
|
|
|
def export_config(self, provider: Optional[str] = None,
|
|
country: Optional[str] = None) -> Dict[str, Any]:
|
|
"""
|
|
Export credentials to a portable format
|
|
|
|
Args:
|
|
provider: Optional provider name to export (if None, exports all)
|
|
country: Optional country code (only used if provider is specified)
|
|
|
|
Returns:
|
|
Dictionary with credentials data
|
|
"""
|
|
export_data = {
|
|
'version': '1.1', # Bumped for country support
|
|
'exported_at': time.time(),
|
|
'credentials': {}
|
|
}
|
|
|
|
if provider:
|
|
# Export specific provider
|
|
if country:
|
|
# Export specific country
|
|
credentials = self.load_credentials(provider, country)
|
|
if credentials:
|
|
export_data['credentials'][provider] = {
|
|
country: self._credential_to_dict(credentials)
|
|
}
|
|
else:
|
|
# Export all countries for provider or non-country data
|
|
countries = self.get_all_countries(provider)
|
|
if countries:
|
|
# Provider has country-specific credentials
|
|
provider_data = {}
|
|
for ctry in countries:
|
|
credentials = self.load_credentials(provider, ctry)
|
|
if credentials:
|
|
provider_data[ctry] = self._credential_to_dict(credentials)
|
|
if provider_data:
|
|
export_data['credentials'][provider] = provider_data
|
|
else:
|
|
# Non-country provider
|
|
credentials = self.load_credentials(provider)
|
|
if credentials:
|
|
export_data['credentials'][provider] = self._credential_to_dict(credentials)
|
|
else:
|
|
# Export all providers
|
|
for prov in self.list_providers():
|
|
countries = self.get_all_countries(prov)
|
|
if countries:
|
|
# Provider has country-specific credentials
|
|
provider_data = {}
|
|
for ctry in countries:
|
|
credentials = self.load_credentials(prov, ctry)
|
|
if credentials:
|
|
provider_data[ctry] = self._credential_to_dict(credentials)
|
|
if provider_data:
|
|
export_data['credentials'][prov] = provider_data
|
|
else:
|
|
# Non-country provider
|
|
credentials = self.load_credentials(prov)
|
|
if credentials:
|
|
export_data['credentials'][prov] = self._credential_to_dict(credentials)
|
|
|
|
return export_data
|
|
|
|
@staticmethod
|
|
def _credential_to_dict(credentials: BaseCredentials) -> Dict[str, Any]:
|
|
"""Convert credentials object to dictionary for export"""
|
|
if isinstance(credentials, UserPasswordCredentials):
|
|
return {
|
|
'type': 'user_password',
|
|
'username': credentials.username,
|
|
'password': credentials.password,
|
|
'client_id': credentials.client_id
|
|
}
|
|
elif isinstance(credentials, ClientCredentials):
|
|
return {
|
|
'type': 'client_credentials',
|
|
'client_id': credentials.client_id,
|
|
'client_secret': credentials.client_secret
|
|
}
|
|
else:
|
|
return {}
|
|
|
|
def import_config(self, config_data: Dict[str, Any]) -> Dict[str, bool]:
|
|
"""
|
|
Import credentials from exported configuration
|
|
|
|
Args:
|
|
config_data: Configuration data from export_config()
|
|
|
|
Returns:
|
|
Dictionary mapping provider names (or provider_country) to import success status
|
|
"""
|
|
results = {}
|
|
|
|
# Validate config data
|
|
if not isinstance(config_data, dict) or 'credentials' not in config_data:
|
|
logger.error("Invalid credential config data format")
|
|
return results
|
|
|
|
credentials_data = config_data.get('credentials', {})
|
|
|
|
for provider, provider_data in credentials_data.items():
|
|
try:
|
|
# Check if this is country-aware structure
|
|
if isinstance(provider_data, dict) and not provider_data.get('type'):
|
|
# Nested structure with countries
|
|
for country, cred_data in provider_data.items():
|
|
if not isinstance(cred_data, dict):
|
|
continue
|
|
|
|
credentials = self._dict_to_credential(cred_data)
|
|
if credentials:
|
|
success = self.save_credentials(provider, credentials, country)
|
|
results[f"{provider}_{country}"] = success
|
|
|
|
if success:
|
|
logger.info(f"Successfully imported credentials for {provider} ({country})")
|
|
else:
|
|
logger.error(f"Failed to import credentials for {provider} ({country})")
|
|
else:
|
|
# Flat structure (no country)
|
|
credentials = self._dict_to_credential(provider_data)
|
|
if credentials:
|
|
success = self.save_credentials(provider, credentials)
|
|
results[provider] = success
|
|
|
|
if success:
|
|
logger.info(f"Successfully imported credentials for {provider}")
|
|
else:
|
|
logger.error(f"Failed to import credentials for {provider}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error importing credentials for {provider}: {e}")
|
|
results[provider] = False
|
|
|
|
return results
|
|
|
|
@staticmethod
|
|
def _dict_to_credential(cred_data: Dict[str, Any]) -> Optional[BaseCredentials]:
|
|
"""Convert dictionary to credentials object"""
|
|
try:
|
|
if cred_data.get('type') == 'user_password':
|
|
return UserPasswordCredentials(
|
|
username=cred_data.get('username', ''),
|
|
password=cred_data.get('password', ''),
|
|
client_id=cred_data.get('client_id')
|
|
)
|
|
elif cred_data.get('type') == 'client_credentials':
|
|
return ClientCredentials(
|
|
client_id=cred_data.get('client_id', ''),
|
|
client_secret=cred_data.get('client_secret', '')
|
|
)
|
|
else:
|
|
logger.error(f"Unknown credential type: {cred_data.get('type')}")
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Error creating credential from dict: {e}")
|
|
return None
|
|
|
|
def debug_credentials_file(self) -> None:
|
|
"""Debug method to log the current state of the credentials file"""
|
|
try:
|
|
logger.info(f"=== CREDENTIALS FILE DEBUG INFO ===")
|
|
|
|
credentials_file_path = self.vfs.join_path(self.credentials_file)
|
|
logger.info(f"Credentials file path: {credentials_file_path}")
|
|
logger.info(f"Credentials file exists: {self.vfs.exists(self.credentials_file)}")
|
|
|
|
if self.vfs.exists(self.credentials_file):
|
|
file_size = self.vfs.get_size(self.credentials_file)
|
|
logger.info(f"Credentials file size: {file_size} bytes")
|
|
|
|
if file_size and file_size > 0:
|
|
data = self.vfs.read_json(self.credentials_file)
|
|
if data:
|
|
logger.info(f"Credentials file contains {len(data)} providers: {list(data.keys())}")
|
|
|
|
for provider, provider_data in data.items():
|
|
if isinstance(provider_data, dict):
|
|
# Check if nested (has countries)
|
|
has_countries = any(
|
|
isinstance(v, dict) and len(k) <= 3 and 'type' in v
|
|
for k, v in provider_data.items()
|
|
)
|
|
|
|
if has_countries:
|
|
logger.info(f" {provider} (country-aware):")
|
|
for country, cred_data in provider_data.items():
|
|
if isinstance(cred_data, dict) and 'type' in cred_data:
|
|
cred_type = cred_data.get('type')
|
|
username = cred_data.get('username', 'N/A')
|
|
logger.info(f" {country}: type={cred_type}, username={username}")
|
|
else:
|
|
# Flat structure (no country)
|
|
cred_type = provider_data.get('type', 'unknown')
|
|
username = provider_data.get('username', 'N/A')
|
|
logger.info(f" {provider} (no country): type={cred_type}, username={username}")
|
|
else:
|
|
logger.error("Credentials file contains invalid JSON or is empty")
|
|
else:
|
|
logger.info("Credentials file is empty")
|
|
else:
|
|
logger.info("Credentials file does not exist yet")
|
|
|
|
logger.info(f"=== END CREDENTIALS FILE DEBUG ===")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error during credentials file debug: {e}") |