mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
Preserve port when matching relays against the auth whitelist
The whitelist check used java.net.URI(url).host, which drops the port, so a "wss://relay.example.com:8080" event tag would not match a "relay.example.com:8080" whitelist entry. Worse, URI parses a bare "relay.example.com:8080" with the host as a scheme, so the host was null and the input fell through to storage with the port — but the incoming event side stripped it, guaranteeing a mismatch. Centralize the parsing in RelayUrlUtils.extractHostAndPort, which prepends "wss://" when no scheme is present and returns "host[:port]". All six previously duplicated parse sites now share it.
This commit is contained in:
@@ -13,6 +13,7 @@ import com.greenart7c3.nostrsigner.models.kindToNip
|
||||
import com.greenart7c3.nostrsigner.models.permissionTypeFromContent
|
||||
import com.greenart7c3.nostrsigner.service.AmberUtils
|
||||
import com.greenart7c3.nostrsigner.service.IntentUtils
|
||||
import com.greenart7c3.nostrsigner.service.RelayUrlUtils
|
||||
import com.greenart7c3.nostrsigner.service.model.AmberEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
@@ -170,13 +171,7 @@ class SignerProvider : ContentProvider() {
|
||||
|
||||
// For kind 22242 (NIP-42 relay auth), extract relay host once for both whitelist and permission checks
|
||||
val relayHost = if (event.kind == 22242) {
|
||||
AmberEvent.relay(event)?.let { url ->
|
||||
try {
|
||||
java.net.URI(url).host ?: url
|
||||
} catch (e: Exception) {
|
||||
url
|
||||
}
|
||||
} ?: ""
|
||||
RelayUrlUtils.extractHostAndPort(AmberEvent.relay(event))
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.greenart7c3.nostrsigner.service
|
||||
|
||||
import java.net.URI
|
||||
|
||||
object RelayUrlUtils {
|
||||
/**
|
||||
* Normalizes a relay URL/host into a comparable "host[:port]" form used by
|
||||
* the auth whitelist and per-relay permission lookups. A bare host:port
|
||||
* input (e.g. "relay.example.com:8080") would otherwise be parsed by
|
||||
* java.net.URI as a scheme, so we prepend "wss://" when no scheme is
|
||||
* present.
|
||||
*/
|
||||
fun extractHostAndPort(input: String?): String {
|
||||
if (input.isNullOrBlank()) return ""
|
||||
val trimmed = input.trim().trimEnd('/')
|
||||
return try {
|
||||
val withScheme = if (trimmed.contains("://")) trimmed else "wss://$trimmed"
|
||||
val uri = URI(withScheme)
|
||||
val host = uri.host
|
||||
when {
|
||||
host.isNullOrBlank() -> trimmed
|
||||
uri.port != -1 -> "$host:${uri.port}"
|
||||
else -> host
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
trimmed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import com.greenart7c3.nostrsigner.Amber
|
||||
import com.greenart7c3.nostrsigner.BuildFlavorChecker
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.service.RelayUrlUtils
|
||||
import com.greenart7c3.nostrsigner.service.TrustScoreService
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -72,11 +73,7 @@ fun AuthWhitelistScreen(
|
||||
|
||||
fun addEntry() {
|
||||
val input = textFieldRelay.value.text.trim()
|
||||
val url = try {
|
||||
java.net.URI(input).host ?: input
|
||||
} catch (e: Exception) {
|
||||
input
|
||||
}
|
||||
val url = RelayUrlUtils.extractHostAndPort(input)
|
||||
if (url.isNotBlank() && url !in whitelist) {
|
||||
whitelist.add(url)
|
||||
Amber.instance.settings = Amber.instance.settings.copy(authWhitelist = whitelist.toList())
|
||||
|
||||
+3
-18
@@ -53,6 +53,7 @@ import com.greenart7c3.nostrsigner.service.ApplicationNameCache
|
||||
import com.greenart7c3.nostrsigner.service.BunkerRequestUtils
|
||||
import com.greenart7c3.nostrsigner.service.EventNotificationConsumer
|
||||
import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents
|
||||
import com.greenart7c3.nostrsigner.service.RelayUrlUtils
|
||||
import com.greenart7c3.nostrsigner.service.model.AmberEvent
|
||||
import com.greenart7c3.nostrsigner.service.toShortenHex
|
||||
import com.greenart7c3.nostrsigner.ui.RememberType
|
||||
@@ -260,15 +261,7 @@ fun BunkerMultiEventHomeScreen(
|
||||
if (relayAuthScope == RelayAuthScope.ALL) {
|
||||
"*"
|
||||
} else {
|
||||
(
|
||||
AmberEvent.relay(request.request.event)?.let { url ->
|
||||
try {
|
||||
java.net.URI(url).host ?: url
|
||||
} catch (e: Exception) {
|
||||
url
|
||||
}
|
||||
} ?: ""
|
||||
)
|
||||
RelayUrlUtils.extractHostAndPort(AmberEvent.relay(request.request.event))
|
||||
}
|
||||
} else {
|
||||
""
|
||||
@@ -363,15 +356,7 @@ fun BunkerMultiEventHomeScreen(
|
||||
if (relayAuthScope == RelayAuthScope.ALL) {
|
||||
"*"
|
||||
} else {
|
||||
(
|
||||
AmberEvent.relay(localEvent)?.let { url ->
|
||||
try {
|
||||
java.net.URI(url).host ?: url
|
||||
} catch (e: Exception) {
|
||||
url
|
||||
}
|
||||
} ?: ""
|
||||
)
|
||||
RelayUrlUtils.extractHostAndPort(AmberEvent.relay(localEvent))
|
||||
}
|
||||
} else {
|
||||
""
|
||||
|
||||
+2
-7
@@ -37,6 +37,7 @@ import com.greenart7c3.nostrsigner.models.SignerType
|
||||
import com.greenart7c3.nostrsigner.models.kindToNip
|
||||
import com.greenart7c3.nostrsigner.models.toPermissionType
|
||||
import com.greenart7c3.nostrsigner.service.BunkerRequestUtils
|
||||
import com.greenart7c3.nostrsigner.service.RelayUrlUtils
|
||||
import com.greenart7c3.nostrsigner.service.isPrivateEvent
|
||||
import com.greenart7c3.nostrsigner.service.model.AmberEvent
|
||||
import com.greenart7c3.nostrsigner.service.toShortenHex
|
||||
@@ -662,13 +663,7 @@ fun BunkerSingleEventHomeScreen(
|
||||
} else if (event.kind == 22242) {
|
||||
// Kind 22242 = relay client authentication (NIP-42)
|
||||
// Permission is per-relay hostname extracted from the event's "relay" tag
|
||||
val relayUrl = AmberEvent.relay(event)?.let { url ->
|
||||
try {
|
||||
java.net.URI(url).host ?: url
|
||||
} catch (e: Exception) {
|
||||
url
|
||||
}
|
||||
} ?: ""
|
||||
val relayUrl = RelayUrlUtils.extractHostAndPort(AmberEvent.relay(event))
|
||||
|
||||
// Check for a relay-specific permission first, then wildcard "*" (all relays)
|
||||
val permission = applicationEntity?.permissions?.firstOrNull {
|
||||
|
||||
+3
-18
@@ -54,6 +54,7 @@ import com.greenart7c3.nostrsigner.models.SignerType
|
||||
import com.greenart7c3.nostrsigner.models.TagArrayEncryptedDataKind
|
||||
import com.greenart7c3.nostrsigner.service.AmberUtils
|
||||
import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents
|
||||
import com.greenart7c3.nostrsigner.service.RelayUrlUtils
|
||||
import com.greenart7c3.nostrsigner.service.model.AmberEvent
|
||||
import com.greenart7c3.nostrsigner.ui.RememberType
|
||||
import com.greenart7c3.nostrsigner.ui.theme.orange
|
||||
@@ -231,15 +232,7 @@ fun IntentMultiEventHomeScreen(
|
||||
if (relayAuthScope == RelayAuthScope.ALL) {
|
||||
"*"
|
||||
} else {
|
||||
(
|
||||
AmberEvent.relay(intentData.event)?.let { url ->
|
||||
try {
|
||||
java.net.URI(url).host ?: url
|
||||
} catch (e: Exception) {
|
||||
url
|
||||
}
|
||||
} ?: ""
|
||||
)
|
||||
RelayUrlUtils.extractHostAndPort(AmberEvent.relay(intentData.event))
|
||||
}
|
||||
} else {
|
||||
""
|
||||
@@ -341,15 +334,7 @@ fun IntentMultiEventHomeScreen(
|
||||
if (relayAuthScope == RelayAuthScope.ALL) {
|
||||
"*"
|
||||
} else {
|
||||
(
|
||||
AmberEvent.relay(localEvent)?.let { url ->
|
||||
try {
|
||||
java.net.URI(url).host ?: url
|
||||
} catch (e: Exception) {
|
||||
url
|
||||
}
|
||||
} ?: ""
|
||||
)
|
||||
RelayUrlUtils.extractHostAndPort(AmberEvent.relay(localEvent))
|
||||
}
|
||||
} else {
|
||||
""
|
||||
|
||||
+2
-7
@@ -31,6 +31,7 @@ import com.greenart7c3.nostrsigner.models.SignerType
|
||||
import com.greenart7c3.nostrsigner.models.kindToNip
|
||||
import com.greenart7c3.nostrsigner.models.toPermissionType
|
||||
import com.greenart7c3.nostrsigner.service.IntentUtils
|
||||
import com.greenart7c3.nostrsigner.service.RelayUrlUtils
|
||||
import com.greenart7c3.nostrsigner.service.isPrivateEvent
|
||||
import com.greenart7c3.nostrsigner.service.model.AmberEvent
|
||||
import com.greenart7c3.nostrsigner.service.toShortenHex
|
||||
@@ -257,13 +258,7 @@ fun IntentSingleEventHomeScreen(
|
||||
} else if (event.kind == 22242) {
|
||||
// Kind 22242 = relay client authentication (NIP-42)
|
||||
// Permission is per-relay hostname extracted from the event's "relay" tag
|
||||
val relayUrl = AmberEvent.relay(event)?.let { url ->
|
||||
try {
|
||||
java.net.URI(url).host ?: url
|
||||
} catch (e: Exception) {
|
||||
url
|
||||
}
|
||||
} ?: ""
|
||||
val relayUrl = RelayUrlUtils.extractHostAndPort(AmberEvent.relay(event))
|
||||
|
||||
// Check for relay-specific permission first, then wildcard "*" (all relays)
|
||||
val permission = applicationEntity?.permissions?.firstOrNull {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.greenart7c3.nostrsigner.service
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class RelayUrlUtilsTest {
|
||||
|
||||
// --- bare host inputs (no scheme) ---
|
||||
|
||||
@Test
|
||||
fun `bare hostname is returned unchanged`() {
|
||||
assertEquals("relay.example.com", RelayUrlUtils.extractHostAndPort("relay.example.com"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bare hostname with port is preserved`() {
|
||||
// Regression: URI("relay.example.com:8080") treats the host as a scheme,
|
||||
// so a naive URI.host call returns null and drops the port. The helper
|
||||
// prepends "wss://" so the port is preserved.
|
||||
assertEquals("relay.example.com:8080", RelayUrlUtils.extractHostAndPort("relay.example.com:8080"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bare IPv4 with port is preserved`() {
|
||||
assertEquals("127.0.0.1:7777", RelayUrlUtils.extractHostAndPort("127.0.0.1:7777"))
|
||||
}
|
||||
|
||||
// --- inputs with scheme ---
|
||||
|
||||
@Test
|
||||
fun `wss URL without port returns host only`() {
|
||||
assertEquals("relay.example.com", RelayUrlUtils.extractHostAndPort("wss://relay.example.com"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `wss URL with port returns host and port`() {
|
||||
assertEquals("relay.example.com:8080", RelayUrlUtils.extractHostAndPort("wss://relay.example.com:8080"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ws URL with port returns host and port`() {
|
||||
assertEquals("relay.example.com:8443", RelayUrlUtils.extractHostAndPort("ws://relay.example.com:8443"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `https URL with port returns host and port`() {
|
||||
assertEquals("relay.example.com:443", RelayUrlUtils.extractHostAndPort("https://relay.example.com:443"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `trailing slash is stripped`() {
|
||||
assertEquals("relay.example.com", RelayUrlUtils.extractHostAndPort("wss://relay.example.com/"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `trailing slash on host with port is stripped`() {
|
||||
assertEquals("relay.example.com:8080", RelayUrlUtils.extractHostAndPort("wss://relay.example.com:8080/"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `surrounding whitespace is trimmed`() {
|
||||
assertEquals("relay.example.com:8080", RelayUrlUtils.extractHostAndPort(" wss://relay.example.com:8080 "))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `path after host is ignored`() {
|
||||
assertEquals("relay.example.com:8080", RelayUrlUtils.extractHostAndPort("wss://relay.example.com:8080/some/path"))
|
||||
}
|
||||
|
||||
// --- equivalence: the whitelist check (uses `in`) is essentially a string compare ---
|
||||
|
||||
@Test
|
||||
fun `bare host-port and wss URL with same port normalize to same value`() {
|
||||
// This is the core whitelist regression: a user adds "relay.example.com:8080"
|
||||
// to the whitelist; the event's "relay" tag is "wss://relay.example.com:8080".
|
||||
// Both must normalize to the same string for `relayHost in authWhitelist` to pass.
|
||||
val whitelistEntry = RelayUrlUtils.extractHostAndPort("relay.example.com:8080")
|
||||
val eventTag = RelayUrlUtils.extractHostAndPort("wss://relay.example.com:8080")
|
||||
assertEquals(whitelistEntry, eventTag)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `different ports on the same host do not collide`() {
|
||||
val portA = RelayUrlUtils.extractHostAndPort("wss://relay.example.com:8080")
|
||||
val portB = RelayUrlUtils.extractHostAndPort("wss://relay.example.com:9090")
|
||||
assertEquals("relay.example.com:8080", portA)
|
||||
assertEquals("relay.example.com:9090", portB)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `host with port is distinct from same host without port`() {
|
||||
val withPort = RelayUrlUtils.extractHostAndPort("wss://relay.example.com:8080")
|
||||
val withoutPort = RelayUrlUtils.extractHostAndPort("wss://relay.example.com")
|
||||
assertEquals("relay.example.com:8080", withPort)
|
||||
assertEquals("relay.example.com", withoutPort)
|
||||
}
|
||||
|
||||
// --- edge cases ---
|
||||
|
||||
@Test
|
||||
fun `null input returns empty string`() {
|
||||
assertEquals("", RelayUrlUtils.extractHostAndPort(null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty input returns empty string`() {
|
||||
assertEquals("", RelayUrlUtils.extractHostAndPort(""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blank input returns empty string`() {
|
||||
assertEquals("", RelayUrlUtils.extractHostAndPort(" "))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unparseable input falls back to the trimmed original`() {
|
||||
// Garbage that URI cannot parse should fall through to the trimmed input
|
||||
// rather than throw.
|
||||
val weird = "not a valid uri at all <<>>"
|
||||
assertEquals(weird, RelayUrlUtils.extractHostAndPort(" $weird "))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user