mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-17 14:42:18 +02:00
697 lines
28 KiB
Python
697 lines
28 KiB
Python
# streaming_providers/base/auth/credential_manager.py
|
|
import time
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
# Import centralized logger and VFS
|
|
from ..utils.logger import logger
|
|
from .credentials import BaseCredentials, ClientCredentials, UserPasswordCredentials
|
|
|
|
|
|
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.
|
|
|
|
Now with fallback: tries with country first, then without country
|
|
for backward compatibility with credentials stored without country nesting.
|
|
|
|
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())}")
|
|
|
|
# Try with country first (new format: {"provider": {"country": {...}}})
|
|
if country:
|
|
keys_path, _ = self._get_credential_path(provider, country)
|
|
provider_data = data
|
|
found = True
|
|
|
|
for key in keys_path:
|
|
if not isinstance(provider_data, dict) or key not in provider_data:
|
|
found = False
|
|
break
|
|
provider_data = provider_data[key]
|
|
|
|
if found and provider_data:
|
|
logger.debug(
|
|
f"CredentialManager: Found credentials with country nesting for '{provider}{country_str}'"
|
|
)
|
|
return self._create_credential_from_data(provider, country, provider_data)
|
|
|
|
# Fallback: try without country (legacy format: {"provider": {...}})
|
|
logger.debug(
|
|
f"CredentialManager: No country-nested credentials found, trying legacy format"
|
|
)
|
|
keys_path, _ = self._get_credential_path(provider, None)
|
|
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 credentials found at path: {' -> '.join(keys_path)}"
|
|
)
|
|
return None
|
|
provider_data = provider_data[key]
|
|
|
|
if provider_data:
|
|
logger.debug(
|
|
f"CredentialManager: Found credentials in legacy format for '{provider}{country_str}'"
|
|
)
|
|
return self._create_credential_from_data(provider, country, provider_data)
|
|
|
|
return None
|
|
|
|
# No country specified - direct lookup
|
|
keys_path, _ = self._get_credential_path(provider, None)
|
|
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}'")
|
|
return None
|
|
|
|
return self._create_credential_from_data(provider, None, provider_data)
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
f"CredentialManager: Error loading credentials for '{provider}{country_str}': {e}"
|
|
)
|
|
return None
|
|
|
|
def _create_credential_from_data(
|
|
self, provider: str, country: Optional[str], provider_data: dict
|
|
) -> Optional[BaseCredentials]:
|
|
"""
|
|
Helper method to create credential object from loaded data.
|
|
|
|
Args:
|
|
provider: Provider name
|
|
country: Optional country code (for logging)
|
|
provider_data: Dictionary containing credential data
|
|
|
|
Returns:
|
|
BaseCredentials instance or None
|
|
"""
|
|
country_str = f" (country: {country})" if country else ""
|
|
|
|
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
|
|
|
|
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}")
|