Initial upload: iptv-android-tv-hermes
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
# Gestion d'erreurs — iMedia TV+
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
API[Serveur Xtream] -->|Timeout / 4xx / 5xx| Interceptor[OkHttp ErrorInterceptor]
|
||||
Interceptor -->|Exception typée| Repository
|
||||
Repository -->|Result / UiState.Error| ViewModel
|
||||
ViewModel -->|Message localisé| UI[Écran Compose]
|
||||
UI -->|Snackbar / Toast / Overlay| User[Utilisateur]
|
||||
```
|
||||
|
||||
## Hiérarchie d'erreurs
|
||||
|
||||
```kotlin
|
||||
/**
|
||||
* Toutes les erreurs Xtream sont des sealed classes
|
||||
* pour un `when` exhaustif dans l'UI.
|
||||
*/
|
||||
sealed class XtreamException(message: String, cause: Throwable? = null) : Exception(message, cause) {
|
||||
class NetworkException(message: String) : XtreamException(message)
|
||||
class TimeoutException(message: String) : XtreamException(message)
|
||||
class AuthException(message: String) : XtreamException(message)
|
||||
class ServerException(val code: Int, message: String) : XtreamException(message)
|
||||
class ParseException(message: String, cause: Throwable) : XtreamException(message, cause)
|
||||
class StreamUnavailableException(message: String) : XtreamException(message)
|
||||
}
|
||||
```
|
||||
|
||||
## Table des erreurs
|
||||
|
||||
| N° | Condition | Exception | Code UI | Message FR | Message EN |
|
||||
|----|-----------|-----------|---------|------------|------------|
|
||||
| 1 | Serveur injoignable | `NetworkException` | Erreur réseau | "Impossible de se connecter au serveur. Vérifiez votre réseau." | "Cannot reach server. Check your network." |
|
||||
| 2 | Timeout dépassé (15s) | `TimeoutException` | Erreur réseau | "Le serveur ne répond pas. Réessayez plus tard." | "Server is not responding. Try again later." |
|
||||
| 3 | Auth invalide | `AuthException` | Login bloqué | "Identifiants incorrects ou serveur invalide." | "Invalid credentials or server." |
|
||||
| 4 | Flux vidéo cassé | `StreamUnavailableException` | Overlay erreur | "Flux indisponible pour cette chaîne." | "Stream unavailable for this channel." |
|
||||
| 5 | HTTP 4xx | `ServerException` | Toast | "Erreur serveur (code X). Contactez votre fournisseur." | "Server error (code X). Contact your provider." |
|
||||
| 6 | HTTP 5xx | `ServerException` | Toast | "Le serveur Xtream rencontre un problème." | "Xtream server is having issues." |
|
||||
| 7 | JSON mal formé | `ParseException` | Fallback silencieux | "Données reçues incorrectes." | "Received data is malformed." |
|
||||
| 8 | EPG vide | N/A (pas une erreur) | Texte vide | "Aucun programme trouvé pour cette chaîne." | "No program found for this channel." |
|
||||
| 9 | Cache corrompu | N/A | Suppression + reload | "Mise à jour des données..." | "Updating data..." |
|
||||
|
||||
## État loading global
|
||||
|
||||
```kotlin
|
||||
/**
|
||||
* État unique pour TOUS les écrans.
|
||||
* Permet un traitement uniforme dans les composants UI.
|
||||
*/
|
||||
sealed interface UiState<out T> {
|
||||
data object Idle : UiState<Nothing>
|
||||
data object Loading : UiState<Nothing>
|
||||
data class Success<T>(val data: T) : UiState<T>
|
||||
data class Error(
|
||||
val message: String, // Message affiché à l'utilisateur
|
||||
val retry: Boolean = true, // Afficher un bouton "Réessayer" ?
|
||||
val exception: XtreamException? = null // Pour logging uniquement
|
||||
) : UiState<Nothing>
|
||||
}
|
||||
```
|
||||
|
||||
## Intercepteur OkHttp
|
||||
|
||||
```kotlin
|
||||
class XtreamErrorInterceptor : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
return try {
|
||||
val response = chain.proceed(request)
|
||||
if (!response.isSuccessful) {
|
||||
when (response.code) {
|
||||
401, 403 -> throw XtreamException.AuthException(
|
||||
"Authentication failed"
|
||||
)
|
||||
in 400..499 -> throw XtreamException.ServerException(
|
||||
response.code, "Client error"
|
||||
)
|
||||
in 500..599 -> throw XtreamException.ServerException(
|
||||
response.code, "Server error"
|
||||
)
|
||||
}
|
||||
}
|
||||
response
|
||||
} catch (e: SocketTimeoutException) {
|
||||
throw XtreamException.TimeoutException("Connection timed out")
|
||||
} catch (e: ConnectException) {
|
||||
throw XtreamException.NetworkException("Cannot connect")
|
||||
} catch (e: UnknownHostException) {
|
||||
throw XtreamException.NetworkException("DNS resolution failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Comportement UI par erreur
|
||||
|
||||
### Écran de connexion
|
||||
```
|
||||
[Champs vides]
|
||||
❌ Bouton Connexion désactivé
|
||||
|
||||
[Erreur réseau]
|
||||
✅ Bouton actif, message rouge sous le formulaire
|
||||
✅ Champs conservés (pas de reset)
|
||||
|
||||
[Auth refusée]
|
||||
✅ Message : "Identifiants incorrects"
|
||||
✅ Focus revient sur le champ mot de passe
|
||||
```
|
||||
|
||||
### Écran Live TV
|
||||
```
|
||||
[Erreur chargement chaînes]
|
||||
✅ Toast 3s : "Impossible de charger les chaînes"
|
||||
✅ Cache affiché si disponible
|
||||
|
||||
[Chaîne sans flux]
|
||||
✅ Overlay : "Flux indisponible" + bouton "Chaîne suivante"
|
||||
```
|
||||
|
||||
### Lecteur vidéo
|
||||
```
|
||||
[Flux mort pendant lecture]
|
||||
✅ Remplacer par écran noir + message
|
||||
✅ Bouton "Chaîne suivante" visible
|
||||
✅ Retour possible vers le menu
|
||||
```
|
||||
|
||||
## Règles de logging
|
||||
|
||||
| Niveau | Usage | Exemple |
|
||||
|--------|-------|---------|
|
||||
| `Timber.d` | Debug uniquement (pas en prod) | "Loading 42 live channels" |
|
||||
| `Timber.i` | Événements importants | "User logged in: user123" |
|
||||
| `Timber.w` | Anomalies non bloquantes | "EPG cache expired for stream 99" |
|
||||
| `Timber.e` | Erreurs utilisateur | "Stream unavailable for stream 42" |
|
||||
|
||||
**❌ Interdit** : `Log.d("PASSWORD", password)` ou tout log contenant des credentials.
|
||||
|
||||
## Annexes
|
||||
|
||||
### Codes Xtream que l'on peut rencontrer
|
||||
|
||||
| Code | Signification | Action |
|
||||
|------|--------------|--------|
|
||||
| `auth = "0"` | Compte invalide | Message login |
|
||||
| `status = "Expired"` | Abonnement expiré | Message distinct |
|
||||
| `status = "Disabled"` | Compte désactivé | Message distinct |
|
||||
| `tv_archive = 0` | Pas d'archive | Cacher bouton Replay |
|
||||
| `has_archive = false` | Pas d'archive | Cacher bouton Replay |
|
||||
| Réponse vide | Serveur HS ou mauvaise URL | Erreur réseau générique |
|
||||
@@ -0,0 +1,159 @@
|
||||
# Sécurité — iMedia TV+
|
||||
|
||||
## Principes
|
||||
|
||||
1. **Aucun secret en clair sur le disque ou dans les logs**
|
||||
2. **Chiffrement de bout en bout** du stockage local des credentials
|
||||
3. **Isolation** : l'application ne partage pas les credentials avec d'autres apps
|
||||
4. **Principle of least privilege** : permissions Android minimales
|
||||
|
||||
## Stockage des credentials
|
||||
|
||||
### Implémentation
|
||||
|
||||
```kotlin
|
||||
// data/storage/SecureStorage.kt
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
|
||||
class SecureStorage(context: Context) {
|
||||
private val masterKey = MasterKey.Builder(context)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
|
||||
private val prefs = EncryptedSharedPreferences.create(
|
||||
context,
|
||||
"imedia_secure_prefs",
|
||||
masterKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
|
||||
fun saveCredentials(server: String, username: String, password: String) {
|
||||
prefs.edit()
|
||||
.putString("server_url", server)
|
||||
.putString("username", username)
|
||||
.putString("password", password)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun getCredentials(): Credentials? {
|
||||
val server = prefs.getString("server_url", null)
|
||||
val username = prefs.getString("username", null)
|
||||
val password = prefs.getString("password", null)
|
||||
return if (server != null && username != null && password != null) {
|
||||
Credentials(server, username, password)
|
||||
} else null
|
||||
}
|
||||
|
||||
fun clearCredentials() {
|
||||
prefs.edit().clear().apply()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Pour les favoris et la chaîne épinglée (données non sensibles), la version 1 utilisait des `SharedPreferences` classiques — acceptable. Si on veut tout unifier, on utilise `EncryptedSharedPreferences` partout.
|
||||
|
||||
### Ce qui est chiffré
|
||||
|
||||
| Donnée | Niveau | Sensibilité |
|
||||
|--------|--------|-------------|
|
||||
| Server URL | AES-256 GCM | Haute |
|
||||
| Username | AES-256 GCM | Haute |
|
||||
| Password | AES-256 GCM | Critique |
|
||||
| Favoris | AES-256 SIV keys | Moyenne |
|
||||
| Chaîne épinglée | AES-256 SIV keys | Basse |
|
||||
|
||||
## Fichier de mapping des endpoints
|
||||
|
||||
**Important :** les URLs Xtream construites avec le builder ne doivent pas non plus être logguées.
|
||||
|
||||
```kotlin
|
||||
// util/XtreamUrlBuilder.kt
|
||||
class XtreamUrlBuilder(private val credentials: Credentials) {
|
||||
|
||||
fun liveUrl(streamId: Int): String =
|
||||
"${credentials.server}/live/${credentials.username}/${credentials.password}/$streamId.m3u8"
|
||||
|
||||
// ⚠️ Attention : cette méthode est appelée par PlayerScreen
|
||||
// — NE PAS logger son résultat
|
||||
}
|
||||
```
|
||||
|
||||
## Règles strictes
|
||||
|
||||
### ✅ À faire
|
||||
|
||||
- Utiliser `Timber.d` pour les logs de debug (supprimés en release par ProGuard)
|
||||
- Vérifier les inputs utilisateur avant de les envoyer (URL bien formée)
|
||||
- Timeout réseau configuré (15s connect, 30s read)
|
||||
- Nettoyer les credentials à la déconnexion
|
||||
- Minify + ProGuard en release (obfuscation)
|
||||
|
||||
### ❌ Interdit
|
||||
|
||||
```kotlin
|
||||
// JAMAIS :
|
||||
Log.d("Auth", "Login with $username / $password")
|
||||
Timber.d("Server URL: $serverUrl")
|
||||
println("Password is: $password")
|
||||
Log.e("API", "Stream URL: ${urlBuilder.liveUrl(42)}")
|
||||
|
||||
// En release, ces logs ne devraient pas survivre,
|
||||
// mais ne prenez pas le risque en debug non plus.
|
||||
```
|
||||
|
||||
## ProGuard
|
||||
|
||||
```pro
|
||||
# app/proguard-rules.pro
|
||||
# Sécurité : ne pas exposer les données chiffrées
|
||||
-keepclassmembers class ma.tkgraph.xtreamtv.data.storage.** { *; }
|
||||
|
||||
# OkHttp / Retrofit déjà couverts par leurs propres rules
|
||||
# Ne pas garder de logs en release
|
||||
-assumenosideeffects class timber.log.Timber {
|
||||
public static *** d(...);
|
||||
public static *** v(...);
|
||||
}
|
||||
```
|
||||
|
||||
## Permissions Android
|
||||
|
||||
```xml
|
||||
<!-- AndroidManifest.xml — le strict minimum -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<!-- ⚠️ PAS de READ_EXTERNAL_STORAGE ni WRITE_EXTERNAL_STORAGE -->
|
||||
```
|
||||
|
||||
L'application ne lit pas de fichiers locaux et ne sauvegarde que des données dans son propre espace de stockage privé — pas besoin de permissions de stockage.
|
||||
|
||||
## HTTPS
|
||||
|
||||
Bien que le protocole Xtream utilise souvent HTTP (non chiffré), l'application doit :
|
||||
1. **Accepter HTTP** pour la compatibilité
|
||||
2. **Préférer HTTPS** si le serveur le supporte
|
||||
3. **Ne pas désactiver la validation** SSL/TLS (pas de `TrustAllCertificates`)
|
||||
|
||||
Si le fournisseur Xtream le permet, les credentials transitent en HTTPS, ce qui protège le mot de passe en transit.
|
||||
|
||||
## Audit
|
||||
|
||||
Une amélioration future possible : `Sentry` ou `Firebase Crashlytics` pour monitorer les crashs. Attention cependant à ne pas envoyer de données sensibles dans les rapports :
|
||||
|
||||
```kotlin
|
||||
// Dans Application.onCreate()
|
||||
FirebaseCrashlytics.getInstance().setCustomKey("screen", "login")
|
||||
// ❌ PAS FirebaseCrashlytics.getInstance().setCustomKey("username", username)
|
||||
```
|
||||
|
||||
## Checklist de sécurité avant release
|
||||
|
||||
- [ ] Aucun log de credentials dans le code (grep "password" dans les fichiers source)
|
||||
- [ ] EncryptedSharedPreferences en place, pas de fallback SharedPreferences
|
||||
- [ ] Timeouts réseau configurés
|
||||
- [ ] Permissions minimales (seulement INTERNET + ACCESS_NETWORK_STATE)
|
||||
- [ ] ProGuard activé en release
|
||||
- [ ] Pas de validation SSL désactivée
|
||||
- [ ] Gestion de la déconnexion efface TOUTES les données
|
||||
Reference in New Issue
Block a user