From 95195c9c9b87c0d910b6920a712487580d27eba1 Mon Sep 17 00:00:00 2001 From: lostf1sh Date: Fri, 26 Jun 2026 20:11:30 +0300 Subject: [PATCH] Allow Tailscale Jellyfin HTTP URLs --- .../data/jellyfin/model/JellyfinCredentials.kt | 17 +++++++++++------ .../data/stream/CloudStreamSecurity.kt | 1 + .../jellyfin/model/JellyfinCredentialsTest.kt | 5 ++++- .../data/stream/CloudStreamSecurityIdTest.kt | 14 ++++++++++++-- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/lostf1sh/pixelplayeross/data/jellyfin/model/JellyfinCredentials.kt b/app/src/main/java/com/lostf1sh/pixelplayeross/data/jellyfin/model/JellyfinCredentials.kt index 8ac9b52..850fd43 100644 --- a/app/src/main/java/com/lostf1sh/pixelplayeross/data/jellyfin/model/JellyfinCredentials.kt +++ b/app/src/main/java/com/lostf1sh/pixelplayeross/data/jellyfin/model/JellyfinCredentials.kt @@ -55,16 +55,21 @@ data class JellyfinCredentials( // Warn about cleartext HTTP on public hosts if (!parsed.isHttps) { - val host = parsed.host - val isPrivate = host == "localhost" || - host == "127.0.0.1" || - host.endsWith(".local") || - CloudStreamSecurity.isPrivateIpv4Literal(host) - if (!isPrivate) { + val host = parsed.host.lowercase() + if (!isHttpAllowedHost(host)) { return "Use https:// for remote Jellyfin servers. HTTP is only allowed for local network addresses." } } return null } + + private fun isHttpAllowedHost(host: String): Boolean { + return host == "localhost" || + host == "127.0.0.1" || + host.endsWith(".local") || + host.endsWith(".ts.net") || + !host.contains('.') || + CloudStreamSecurity.isPrivateIpv4Literal(host) + } } diff --git a/app/src/main/java/com/lostf1sh/pixelplayeross/data/stream/CloudStreamSecurity.kt b/app/src/main/java/com/lostf1sh/pixelplayeross/data/stream/CloudStreamSecurity.kt index 34c8da9..5f440e1 100644 --- a/app/src/main/java/com/lostf1sh/pixelplayeross/data/stream/CloudStreamSecurity.kt +++ b/app/src/main/java/com/lostf1sh/pixelplayeross/data/stream/CloudStreamSecurity.kt @@ -174,6 +174,7 @@ object CloudStreamSecurity { return first == 0 || first == 10 || first == 127 || + (first == 100 && second in 64..127) || (first == 169 && second == 254) || (first == 172 && second in 16..31) || (first == 192 && second == 168) diff --git a/app/src/test/java/com/lostf1sh/pixelplayeross/data/jellyfin/model/JellyfinCredentialsTest.kt b/app/src/test/java/com/lostf1sh/pixelplayeross/data/jellyfin/model/JellyfinCredentialsTest.kt index ac39984..6ca00ba 100644 --- a/app/src/test/java/com/lostf1sh/pixelplayeross/data/jellyfin/model/JellyfinCredentialsTest.kt +++ b/app/src/test/java/com/lostf1sh/pixelplayeross/data/jellyfin/model/JellyfinCredentialsTest.kt @@ -25,14 +25,17 @@ class JellyfinCredentialsTest { @Test fun `http is allowed on local network addresses`() { - // localhost, loopback, private RFC1918 ranges, and .local hosts may be HTTP-only. + // localhost, loopback, private RFC1918 ranges, Tailscale, and local DNS names may be HTTP-only. listOf( "http://localhost:8096", "http://127.0.0.1:8096", "http://192.168.1.50:8096", "http://10.0.0.5", "http://172.16.4.4", + "http://100.64.12.34:8096", + "http://musicbox:8096", "http://media.local", + "http://jellyfin.tailnet.ts.net:8096", ).forEach { url -> assertNull(creds(url).connectionValidationError(), "expected $url to be allowed over http") } diff --git a/app/src/test/java/com/lostf1sh/pixelplayeross/data/stream/CloudStreamSecurityIdTest.kt b/app/src/test/java/com/lostf1sh/pixelplayeross/data/stream/CloudStreamSecurityIdTest.kt index f91559f..6c0ed94 100644 --- a/app/src/test/java/com/lostf1sh/pixelplayeross/data/stream/CloudStreamSecurityIdTest.kt +++ b/app/src/test/java/com/lostf1sh/pixelplayeross/data/stream/CloudStreamSecurityIdTest.kt @@ -33,13 +33,23 @@ class CloudStreamSecurityIdTest { @Test fun `private and loopback ipv4 literals are detected`() { - listOf("10.0.0.1", "192.168.1.1", "172.16.0.1", "172.31.255.255", "127.0.0.1", "169.254.1.1", "0.0.0.0") + listOf( + "10.0.0.1", + "192.168.1.1", + "172.16.0.1", + "172.31.255.255", + "127.0.0.1", + "169.254.1.1", + "0.0.0.0", + "100.64.0.1", + "100.127.255.254", + ) .forEach { assertTrue(CloudStreamSecurity.isPrivateIpv4Literal(it), "$it should be private") } } @Test fun `public ipv4 literals and non-ip hosts are not flagged private`() { - listOf("8.8.8.8", "1.2.3.4", "172.15.0.1", "172.32.0.1", "203.0.113.7") + listOf("8.8.8.8", "1.2.3.4", "172.15.0.1", "172.32.0.1", "203.0.113.7", "100.63.255.255", "100.128.0.1") .forEach { assertFalse(CloudStreamSecurity.isPrivateIpv4Literal(it), "$it should be public") } // Not dotted-quad IPv4 at all. assertFalse(CloudStreamSecurity.isPrivateIpv4Literal("example.com"))