diff --git a/bin/api/epg_api.dart b/bin/api/epg_api.dart index 3da6d9d..d0aea4b 100644 --- a/bin/api/epg_api.dart +++ b/bin/api/epg_api.dart @@ -257,7 +257,12 @@ class EpgApi { .map((p) => p.toJson(channelId)) .toList(); } catch (e) { - print('[EpgApi] source XMLTV indisponible pour $channelId : $e'); + // L'exception peut recopier l'URL `player_api`/`xmltv.php`, identifiants + // compris. + print( + '[EpgApi] source XMLTV indisponible pour $channelId : ' + '${LogRedactor.redactUrl('$e')}', + ); return const []; } } diff --git a/bin/api/streaming_handler.dart b/bin/api/streaming_handler.dart index fa641d9..36b115d 100644 --- a/bin/api/streaming_handler.dart +++ b/bin/api/streaming_handler.dart @@ -307,7 +307,11 @@ Stream> _resilientLiveBody( yield chunk; } } catch (e) { - print('[Live Proxy] $streamId : coupure amont ($e)'); + // Un `ClientException` recopie l'URL amont, identifiants compris. + print( + '[Live Proxy] $streamId : coupure amont ' + '(${LogRedactor.redactUrl('$e')})', + ); } finally { upstream.client.close(); } @@ -494,7 +498,9 @@ Handler createLiveStreamHandler( } on ProcessException catch (e) { // Serveur saturé (plus de processus ou de mémoire) : un 503 que le // lecteur sait traiter, plutôt qu'une exception qui remonte en 500. - print('[Live Turbo] $streamId : FFmpeg n\'a pas démarré ($e)'); + // `ProcessException` liste les arguments, donc l'URL `-i` du panneau. + print('[Live Turbo] $streamId : FFmpeg n\'a pas démarré ' + '(${LogRedactor.redactUrl('$e')})'); return Response(503, body: 'FFmpeg start failed'); } diff --git a/bin/services/ffmpeg_session_manager.dart b/bin/services/ffmpeg_session_manager.dart index 53943ef..5e7f3a7 100644 --- a/bin/services/ffmpeg_session_manager.dart +++ b/bin/services/ffmpeg_session_manager.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import '../utils/log_redactor.dart'; + /// One running FFmpeg transcoding session (live, VOD or recording playback). class FfmpegSession { final String id; @@ -121,7 +123,9 @@ class FfmpegSessionManager { process.stderr.transform(utf8.decoder).listen((data) { session.recentStderr.add(data); if (session.recentStderr.length > 20) session.recentStderr.removeAt(0); - print('[FFmpeg $id] $data'); + // FFmpeg rappelle l'URL d'entrée (« Input #0 … from 'http://…' ») : + // masquer les identifiants Xtream avant de journaliser. + print('[FFmpeg $id] ${LogRedactor.redactUrl(data)}'); }); process.exitCode.then((code) { diff --git a/bin/services/recording_scheduler.dart b/bin/services/recording_scheduler.dart index 1615f7d..494c969 100644 --- a/bin/services/recording_scheduler.dart +++ b/bin/services/recording_scheduler.dart @@ -533,7 +533,12 @@ class RecordingScheduler { await _launchFfmpeg(active); } catch (e, st) { // Attraper TOUTES les exceptions pour éviter de crasher le serveur - print('[RecordingScheduler] ERREUR dans _startRecording: $e\n$st'); + // `ProcessException` liste les arguments FFmpeg, URL de la source + // comprise : masquer les identifiants avant de journaliser. + print( + '[RecordingScheduler] ERREUR dans _startRecording: ' + '${LogRedactor.redactUrl('$e')}\n$st', + ); _db.updateRecordingStatus( recording.id, 'failed', @@ -662,7 +667,10 @@ class RecordingScheduler { try { await _launchFfmpeg(active); } catch (e) { - print('[RecordingScheduler] Relance impossible: $e'); + print( + '[RecordingScheduler] Relance impossible: ' + '${LogRedactor.redactUrl('$e')}', + ); _active.remove(recording.id); await _finalize( active, diff --git a/bin/services/xmltv_epg_service.dart b/bin/services/xmltv_epg_service.dart index 06b750d..31b0efa 100644 --- a/bin/services/xmltv_epg_service.dart +++ b/bin/services/xmltv_epg_service.dart @@ -6,6 +6,8 @@ import 'dart:typed_data'; import 'package:http/http.dart' as http; import 'package:xml/xml_events.dart'; +import '../utils/log_redactor.dart'; + /// Guide TV construit à partir de dumps XMLTV publics. /// /// Beaucoup de panneaux Xtream servent un EPG figé depuis plusieurs jours, ou @@ -116,6 +118,11 @@ class XmltvEpgService { var ok = 0; for (final url in sourceUrls) { + // La source peut être le `xmltv.php` du panneau, dont l'URL porte les + // identifiants de l'abonné en clair : ne jamais la journaliser telle + // quelle (elle finirait dans `docker logs`). Le texte de l'exception + // est masqué aussi, un `ClientException` recopiant l'URL demandée. + final safeUrl = LogRedactor.redactUrl(url); try { final parsed = await _downloadAndParse(url); // Première source servie gagne : les suivantes ne comblent que les @@ -125,10 +132,12 @@ class XmltvEpgService { } ok++; print( - '[XmltvEpg] $url : ${parsed.length} chaînes indexées', + '[XmltvEpg] $safeUrl : ${parsed.length} chaînes indexées', ); } catch (e) { - print('[XmltvEpg] $url : échec ($e)'); + print( + '[XmltvEpg] $safeUrl : échec (${LogRedactor.redactUrl('$e')})', + ); } } diff --git a/bin/test/xmltv_epg_log_redaction_test.dart b/bin/test/xmltv_epg_log_redaction_test.dart new file mode 100644 index 0000000..8a1d206 --- /dev/null +++ b/bin/test/xmltv_epg_log_redaction_test.dart @@ -0,0 +1,75 @@ +import 'dart:async'; + +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:test/test.dart'; +import '../services/xmltv_epg_service.dart'; + +/// URL du dump `xmltv.php` d'un panneau Xtream : les identifiants de +/// l'abonné y figurent en clair, comme en production. +const _panelUrl = + 'http://panel.example:8080/xmltv.php?username=john&password=hunter2'; + +/// Exécute [body] en capturant tout ce qui passe par `print`. +Future> _capturePrints(Future Function() body) async { + final lines = []; + await runZoned( + body, + zoneSpecification: ZoneSpecification( + print: (self, parent, zone, line) => lines.add(line), + ), + ); + return lines; +} + +void main() { + group('XmltvEpgService — journalisation', () { + test('masque les identifiants de l\'URL quand la source répond', () async { + final service = XmltvEpgService( + sourceUrls: const [_panelUrl], + client: MockClient( + (_) async => http.Response('', 200), + ), + ); + + final lines = await _capturePrints(service.ensureFresh); + final xmltvLines = lines.where((l) => l.startsWith('[XmltvEpg]')); + + expect(xmltvLines, isNotEmpty); + for (final line in xmltvLines) { + expect(line, isNot(contains('john'))); + expect(line, isNot(contains('hunter2'))); + } + expect( + lines, + contains(contains('username=***&password=***')), + ); + }); + + test('masque les identifiants de l\'URL et de l\'exception en cas d\'échec', + () async { + // `ClientException` recopie l'URL demandée dans son message : c'est + // par là que les identifiants fuyaient aussi. + final service = XmltvEpgService( + sourceUrls: const [_panelUrl], + client: MockClient( + (request) async => throw http.ClientException( + 'Connection refused', + request.url, + ), + ), + ); + + final lines = await _capturePrints(service.ensureFresh); + final failure = lines.firstWhere( + (l) => l.contains('échec'), + orElse: () => fail('aucune ligne d\'échec journalisée : $lines'), + ); + + expect(failure, isNot(contains('john'))); + expect(failure, isNot(contains('hunter2'))); + expect(failure, contains('username=***&password=***')); + expect(failure, contains('Connection refused')); + }); + }); +}