fix(train): Jellyfin over https alone, but on the phone itself (ADR 0030, D54)
An http address to another machine is refused as typed, saying why: over it the password, the token and what the person listens to went through the network in clear, and the music job, held by Android to https, retried for ever without a track. A Jellyfin on the phone (localhost, 127.0.0.1) stays reachable over http, the network security config allowing those two alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
071899ce44
commit
ced82b1e75
12 changed files with 74 additions and 0 deletions
|
|
@ -61,6 +61,7 @@
|
|||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:targetApi="s">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Nothing in clear (ADR 0030, D47): https alone, as Android has it by
|
||||
default, but for a Jellyfin on the phone itself, which the music
|
||||
downloads reach over http. Only the system's certificates are trusted. -->
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="false">
|
||||
<trust-anchors>
|
||||
<certificates src="system"/>
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">localhost</domain>
|
||||
<domain includeSubdomains="false">127.0.0.1</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
|
|
@ -481,6 +481,7 @@
|
|||
"musicServer": "Jellyfin-Server",
|
||||
"musicConnect": "Verbinden",
|
||||
"musicBadAddress": "Gib die Adresse des Servers ein, etwa jellyfin.example.org.",
|
||||
"musicInClear": "Train verbindet sich mit Jellyfin nur über https: Über http ginge das Passwort unverschlüsselt durchs Netz. Gib die https-Adresse des Servers ein.",
|
||||
"musicUnreachable": "Der Server ist nicht erreichbar. Prüfe die Adresse und die Verbindung.",
|
||||
"musicNotJellyfin": "Unter dieser Adresse ist kein Jellyfin-Server.",
|
||||
"musicRefused": "Jellyfin hat abgelehnt. Prüfe Benutzername und Passwort.",
|
||||
|
|
|
|||
|
|
@ -607,6 +607,7 @@
|
|||
"@musicServer": {"description": "The label of the server's address."},
|
||||
"musicConnect": "Connect",
|
||||
"musicBadAddress": "Enter the server's address, such as jellyfin.example.org.",
|
||||
"musicInClear": "Train connects to Jellyfin over https only: http would send the password unencrypted. Use the server's https address.",
|
||||
"musicUnreachable": "The server can't be reached. Check the address and the connection.",
|
||||
"musicNotJellyfin": "There is no Jellyfin server at this address.",
|
||||
"musicRefused": "Jellyfin refused. Check the user name and password.",
|
||||
|
|
|
|||
|
|
@ -2908,6 +2908,12 @@ abstract class AppLocalizations {
|
|||
/// **'Enter the server\'s address, such as jellyfin.example.org.'**
|
||||
String get musicBadAddress;
|
||||
|
||||
/// No description provided for @musicInClear.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Train connects to Jellyfin over https only: http would send the password unencrypted. Use the server\'s https address.'**
|
||||
String get musicInClear;
|
||||
|
||||
/// No description provided for @musicUnreachable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1723,6 +1723,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get musicBadAddress =>
|
||||
'Gib die Adresse des Servers ein, etwa jellyfin.example.org.';
|
||||
|
||||
@override
|
||||
String get musicInClear =>
|
||||
'Train verbindet sich mit Jellyfin nur über https: Über http ginge das Passwort unverschlüsselt durchs Netz. Gib die https-Adresse des Servers ein.';
|
||||
|
||||
@override
|
||||
String get musicUnreachable =>
|
||||
'Der Server ist nicht erreichbar. Prüfe die Adresse und die Verbindung.';
|
||||
|
|
|
|||
|
|
@ -1713,6 +1713,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get musicBadAddress =>
|
||||
'Enter the server\'s address, such as jellyfin.example.org.';
|
||||
|
||||
@override
|
||||
String get musicInClear =>
|
||||
'Train connects to Jellyfin over https only: http would send the password unencrypted. Use the server\'s https address.';
|
||||
|
||||
@override
|
||||
String get musicUnreachable =>
|
||||
'The server can\'t be reached. Check the address and the connection.';
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ enum MusicStep() {
|
|||
/// Train's own.
|
||||
enum MusicFailure() {
|
||||
badAddress,
|
||||
|
||||
/// http to another machine than the phone (D47).
|
||||
inClear,
|
||||
unreachable,
|
||||
notJellyfin,
|
||||
refused,
|
||||
|
|
@ -126,6 +129,10 @@ class MusicCubit(
|
|||
emit(state.copyWith(failure: () => MusicFailure.badAddress));
|
||||
return;
|
||||
}
|
||||
if (jellyfinInClear(address)) {
|
||||
emit(state.copyWith(failure: () => MusicFailure.inClear));
|
||||
return;
|
||||
}
|
||||
emit(state.copyWith(step: MusicStep.finding, failure: () => null));
|
||||
try {
|
||||
final server = await _library.find(address);
|
||||
|
|
|
|||
|
|
@ -86,6 +86,14 @@ Uri? jellyfinAddress(String typed) {
|
|||
);
|
||||
}
|
||||
|
||||
/// Whether [address] would carry the password and the token in clear: http
|
||||
/// to anywhere but this phone itself (D47). Train asks Jellyfin over https
|
||||
/// alone, but for a server on the phone (`localhost`, `127.0.0.1`), which
|
||||
/// the app's network security config lets the downloads reach too.
|
||||
bool jellyfinInClear(Uri address) =>
|
||||
address.scheme == 'http' &&
|
||||
!const {'localhost', '127.0.0.1'}.contains(address.host.toLowerCase());
|
||||
|
||||
/// Jellyfin's API as Train asks it (D47), over plain HTTP: as the client
|
||||
/// "Halkyon Train" on the device `deviceId`, to which Jellyfin ties the
|
||||
/// tokens it gives.
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ class const MusicScreen({
|
|||
String _failureText(AppLocalizations l10n, MusicFailure failure) =>
|
||||
switch (failure) {
|
||||
MusicFailure.badAddress => l10n.musicBadAddress,
|
||||
MusicFailure.inClear => l10n.musicInClear,
|
||||
MusicFailure.unreachable => l10n.musicUnreachable,
|
||||
MusicFailure.notJellyfin => l10n.musicNotJellyfin,
|
||||
MusicFailure.refused => l10n.musicRefused,
|
||||
|
|
|
|||
|
|
@ -51,6 +51,18 @@ void main() {
|
|||
expect(jellyfinAddress('ftp://music.example.org'), isNull);
|
||||
expect(jellyfinAddress('https://'), isNull);
|
||||
});
|
||||
|
||||
test('in clear is http to anywhere but this phone itself (D47)', () {
|
||||
bool inClear(String typed) => jellyfinInClear(jellyfinAddress(typed)!);
|
||||
expect(inClear('http://192.168.1.4:8096'), isTrue);
|
||||
expect(inClear('http://music.example.org'), isTrue);
|
||||
expect(inClear('http://127.0.0.2:8096'), isTrue);
|
||||
expect(inClear('https://192.168.1.4:8920'), isFalse);
|
||||
expect(inClear('music.example.org'), isFalse);
|
||||
expect(inClear('http://localhost:8096'), isFalse);
|
||||
expect(inClear('http://LocalHost:8096'), isFalse);
|
||||
expect(inClear('http://127.0.0.1:8096'), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
test('a server says its name and whether Quick Connect is on', () async {
|
||||
|
|
|
|||
|
|
@ -192,6 +192,20 @@ void main() {
|
|||
expect(jellyfin.requests, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('an address in clear is refused, and nothing is asked', (
|
||||
tester,
|
||||
) async {
|
||||
final jellyfin = FakeJellyfin();
|
||||
await show(tester, jellyfin);
|
||||
|
||||
await tester.enterText(find.byType(TextField), 'http://192.168.1.4:8096');
|
||||
await tester.tap(find.text('Connect'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.textContaining('https'), findsOneWidget);
|
||||
expect(jellyfin.requests, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('a code not approved in time expires, and a new one is asked '
|
||||
'for', (tester) async {
|
||||
final jellyfin = FakeJellyfin();
|
||||
|
|
|
|||
Loading…
Reference in a new issue