Files
pyramid/lib/core/livekit_token.dart
T
Bernd SteckmeisterandClaude Opus 5.5 756486d1fe security: LiveKit-Token vom Server statt Secret in der App
Die App signierte LiveKit-Tokens selbst – dafür steckte das SFU-Secret im
App-Code (jeder mit der APK konnte sich Tokens für beliebige Räume und
Identitäten ausstellen). Jetzt holt die App das Token von der bereits
vorhandenen Server-Route /api/livekit-token (whoami-Prüfung, Identität =
geprüfte Matrix-ID). Secret und JWT-Signieren sind aus dem Client entfernt.

- Lesbare Fehlermeldungen (offline, 401, Serverfehler) statt stiller Fehler.
- Gruppenanruf aus dem Chat-Kopf übergibt jetzt den Matrix-Client.
- Tests: Unit-Test mit Mock-HTTP + echter LiveKit-Beitritt (Testkonto).
- Offen: Handy-Anruftest; danach Secret-Rotation durch Bernd (Doku).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 22:12:15 +02:00

90 lines
3.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
/// LiveKit-Beitritts-Token vom Pyramid-Server holen.
///
/// Früher hat die App die Tokens selbst signiert – dafür steckte das
/// LiveKit-Secret im App-Code, und wer die App hatte, konnte sich für jeden
/// Raum und jede Identität ein Token ausstellen. Jetzt signiert nur noch der
/// Server (`/api/livekit-token` im Dashboard-Server auf dem Pi): Er prüft den
/// Matrix-Login per whoami und setzt die LiveKit-Identität selbst auf die
/// geprüfte Matrix-ID. Siehe docs/LIVEKIT_TOKEN_MIGRATION.md.
class LiveKitTokenService {
static const endpoint =
'https://dashboard.steggi-matrix.work/api/livekit-token';
static const fallbackUrl = 'wss://livekit.steggi-matrix.work';
static const _timeout = Duration(seconds: 15);
/// Liefert Token und LiveKit-Adresse für [room]. Wirft
/// [LiveKitTokenException] mit einer verständlichen Meldung.
static Future<({String token, String url})> fetch({
required String room,
required String matrixToken,
http.Client? httpClient,
}) async {
final client = httpClient ?? http.Client();
try {
final res = await client
.post(
Uri.parse(endpoint),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'room': room, 'matrix_token': matrixToken}),
)
.timeout(_timeout);
if (res.statusCode == 401) {
throw const LiveKitTokenException(
'Anmeldung vom Anrufserver abgelehnt – bitte neu anmelden.',
);
}
if (res.statusCode != 200) {
throw LiveKitTokenException(
'Anrufserver antwortet nicht richtig (HTTP ${res.statusCode}).',
);
}
final data = jsonDecode(res.body);
final token = data is Map ? data['token'] : null;
if (token is! String || token.isEmpty) {
throw const LiveKitTokenException(
'Anrufserver hat kein Zugangstoken geliefert.',
);
}
final url = (data as Map)['url'];
return (
token: token,
url: url is String && url.startsWith('wss://') ? url : fallbackUrl,
);
} on LiveKitTokenException {
rethrow;
} on TimeoutException {
throw const LiveKitTokenException(
'Anrufserver nicht erreichbar (Zeitüberschreitung).',
);
} on SocketException {
throw const LiveKitTokenException(
'Anrufserver nicht erreichbar – Internetverbindung prüfen.',
);
} on http.ClientException {
throw const LiveKitTokenException(
'Anrufserver nicht erreichbar – Internetverbindung prüfen.',
);
} on FormatException {
throw const LiveKitTokenException(
'Anrufserver hat eine unlesbare Antwort geschickt.',
);
} finally {
if (httpClient == null) client.close();
}
}
}
class LiveKitTokenException implements Exception {
final String message;
const LiveKitTokenException(this.message);
@override
String toString() => message;
}