diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt index 401f6bf0e..6af430c1d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt @@ -20,8 +20,12 @@ */ package com.vitorpamplona.quartz.nip01Core.relay.normalizer +import android.util.Log +import android.util.Log.e import androidx.collection.LruCache +import kotlinx.coroutines.CancellationException import org.czeal.rfc3986.URIReference +import kotlin.contracts.ExperimentalContracts val normalizedUrls = LruCache(5000) @@ -37,6 +41,27 @@ class RelayUrlNormalizer { fun isOnion(url: String) = url.endsWith(".onion") || url.contains(".onion/") + fun isRelaySchemePrefix(url: String) = url.length > 6 && url[0] == 'w' && url[1] == 's' + + fun isRelaySchemePrefixSecure(url: String) = url[2] == 's' && url[3] == ':' && url[4] == '/' && url[5] == '/' && url[6] != '/' + + fun isRelaySchemePrefixInsecure(url: String) = url[2] == ':' && url[3] == '/' && url[4] == '/' && url[5] != '/' + + fun isRelayUrl(url: String): Boolean { + val trimmed = url.trim().ifEmpty { return false } + + // fast + if (isRelaySchemePrefix(trimmed)) { + if (isRelaySchemePrefixSecure(trimmed)) { + return true + } else if (isRelaySchemePrefixInsecure(trimmed)) { + return true + } + } + + return false + } + private fun norm(url: String) = NormalizedRelayUrl( URIReference @@ -46,19 +71,20 @@ class RelayUrlNormalizer { .intern(), ) - fun fix(url: String): String { - val trimmed = url.trim() + @OptIn(ExperimentalContracts::class) + fun fix(url: String): String? { + val trimmed = url.trim().ifEmpty { return null } - // fast - if (trimmed.length > 4 && trimmed[0] == 'w' && trimmed[1] == 's') { - if (trimmed[2] == 's' && trimmed[3] == ':' && trimmed[4] == '/' && trimmed[5] == '/') { - return trimmed - } else if (trimmed[2] == ':' && trimmed[3] == '/' && trimmed[4] == '/') { + if (trimmed.isEmpty()) return null + + // fast for good wss:// urls + if (isRelaySchemePrefix(trimmed)) { + if (isRelaySchemePrefixSecure(trimmed) || isRelaySchemePrefixInsecure(trimmed)) { return trimmed } } - // fast + // fast for good https:// urls if (trimmed.length > 8 && trimmed[0] == 'h' && trimmed[1] == 't' && trimmed[2] == 't' && trimmed[3] == 'p') { if (trimmed[4] == 's' && trimmed[5] == ':' && trimmed[6] == '/' && trimmed[7] == '/') { // https:// @@ -69,6 +95,12 @@ class RelayUrlNormalizer { } } + if (trimmed.contains("://")) { + // some other scheme we cannot connect to. + Log.w("RelayUrlNormalizer", "Rejected relay URL: $url") + return null + } + return if (isOnion(trimmed) || isLocalHost(trimmed)) { "ws://$trimmed" } else { @@ -80,7 +112,8 @@ class RelayUrlNormalizer { normalizedUrls.get(url)?.let { return it } return try { - val normalized = norm(fix(url)) + val fixed = fix(url) ?: return NormalizedRelayUrl(url) + val normalized = norm(fixed) normalizedUrls.put(url, normalized) normalized } catch (e: Exception) { @@ -89,13 +122,22 @@ class RelayUrlNormalizer { } fun normalizeOrNull(url: String): NormalizedRelayUrl? { - normalizedUrls.get(url)?.let { return it } + if (url.isEmpty()) return null + normalizedUrls[url]?.let { return it } return try { - val normalized = norm(fix(url)) - normalizedUrls.put(url, normalized) - normalized + val fixed = fix(url) + if (fixed != null) { + val normalized = norm(fixed) + normalizedUrls.put(url, normalized) + return normalized + } else { + Log.w("NormalizedRelayUrl", "Rejected Error $url") + null + } } catch (e: Exception) { + if (e is CancellationException) throw e + Log.w("NormalizedRelayUrl", "Rejected Error $url") null } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt index f9901163f..181aa88ac 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.events +import android.R.attr.tag import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has @@ -72,8 +73,7 @@ data class ETag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } - val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } - return ETag(tag[1], hint, tag.getOrNull(3)) + return ETag(tag[1], pickRelayHint(tag), pickAuthor(tag)) } @JvmStatic @@ -84,6 +84,32 @@ data class ETag( return tag[1] } + // simple case ["e", "id", "relay"] + // empty tags ["e", "id", "relay", ""] + // current root ["e", "id", "relay", "marker"] + // current root ["e", "id", "relay", "marker", "pubkey"] + // empty tags ["e", "id", "relay", "", "pubkey"] + // pubkey marker ["e", "id", "relay", "pubkey"] + // pubkey marker ["e", "id", "relay", "pubkey", "marker"] + // pubkey marker ["e", "id", "pubkey"] // incorrect + // current root ["e", "id", "marker"] // incorrect + + @JvmStatic + private fun pickRelayHint(tag: Array): NormalizedRelayUrl? { + if (tag.has(2) && tag[2].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[2])) return RelayUrlNormalizer.normalizeOrNull(tag[2]) + if (tag.has(3) && tag[3].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[3])) return RelayUrlNormalizer.normalizeOrNull(tag[3]) + if (tag.has(4) && tag[4].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[4])) return RelayUrlNormalizer.normalizeOrNull(tag[4]) + return null + } + + @JvmStatic + private fun pickAuthor(tag: Array): HexKey? { + if (tag.has(2) && tag[2].length == 64) return tag[2] + if (tag.has(3) && tag[3].length == 64) return tag[3] + if (tag.has(4) && tag[4].length == 64) return tag[4] + return null + } + @JvmStatic fun parseAsHint(tag: Array): EventIdHint? { ensure(tag.has(2)) { return null } @@ -91,7 +117,8 @@ data class ETag( ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } - val hint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + val hint = pickRelayHint(tag) + ensure(hint != null) { return null } return EventIdHint(tag[1], hint) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt index 4695d5b1f..2f8cda9fd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt @@ -66,11 +66,16 @@ data class PTag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } - val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + val hint = pickRelayHint(tag) return PTag(tag[1], hint) } + private fun pickRelayHint(tag: Array): NormalizedRelayUrl? { + if (tag.has(2) && tag[2].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[2])) return RelayUrlNormalizer.normalizeOrNull(tag[2]) + return null + } + @JvmStatic fun parseKey(tag: Array): HexKey? { ensure(tag.has(1)) { return null } @@ -89,10 +94,11 @@ data class PTag( ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } - val normalized = RelayUrlNormalizer.normalizeOrNull(tag[2]) - ensure(normalized != null) { return null } + val hint = pickRelayHint(tag) - return PubKeyHint(tag[1], normalized) + ensure(hint != null) { return null } + + return PubKeyHint(tag[1], hint) } @JvmStatic diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt index ece41e8b2..576b04b17 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt @@ -20,12 +20,14 @@ */ package com.vitorpamplona.quartz.nip10Notes.tags +import android.R.attr.tag import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer.Companion.isRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull @@ -87,67 +89,71 @@ data class MarkedETag( @JvmStatic fun parse(tag: Array): MarkedETag? { - if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + ensure(tag.has(2)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + ensure(tag[2].isNotEmpty()) { return null } return MarkedETag( - tag[ORDER_EVT_ID], - RelayUrlNormalizer.normalizeOrNull(tag[ORDER_RELAY]), - MARKER.parse(tag[ORDER_MARKER]), - tag.getOrNull(ORDER_PUBKEY), + eventId = tag[1], + relayHint = pickRelayHint(tag), + marker = pickMarker(tag), + authorPubKeyHex = pickAuthor(tag), ) } @JvmStatic fun parseId(tag: Array): HexKey? { - if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + ensure(tag.has(2)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } - return tag[ORDER_EVT_ID] + return tag[1] + } + + // simple case ["e", "id", "relay"] + // empty tags ["e", "id", "relay", ""] + // current root ["e", "id", "relay", "marker"] + // current root ["e", "id", "relay", "marker", "pubkey"] + // empty tags ["e", "id", "relay", "", "pubkey"] + // pubkey marker ["e", "id", "relay", "pubkey"] + // pubkey marker ["e", "id", "relay", "pubkey", "marker"] + // pubkey marker ["e", "id", "pubkey"] // incorrect + // current root ["e", "id", "marker"] // incorrect + + @JvmStatic + private fun pickRelayHint(tag: Array): NormalizedRelayUrl? { + if (tag.has(2) && tag[2].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[2])) return RelayUrlNormalizer.normalizeOrNull(tag[2]) + if (tag.has(3) && tag[3].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[3])) return RelayUrlNormalizer.normalizeOrNull(tag[3]) + if (tag.has(4) && tag[4].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[4])) return RelayUrlNormalizer.normalizeOrNull(tag[4]) + return null + } + + @JvmStatic + private fun pickAuthor(tag: Array): HexKey? { + if (tag.has(3) && tag[3].length == 64) return tag[3] + if (tag.has(4) && tag[4].length == 64) return tag[4] + if (tag.has(2) && tag[2].length == 64) return tag[2] + return null + } + + @JvmStatic + private fun pickMarker(tag: Array): MARKER? { + if (tag.has(3)) MARKER.parse(tag[3])?.let { return it } + if (tag.has(4)) MARKER.parse(tag[4])?.let { return it } + if (tag.has(2)) MARKER.parse(tag[2])?.let { return it } + return null } @JvmStatic fun parseAllThreadTags(tag: Array): MarkedETag? = if (tag.size >= 2 && tag[0] == TAG_NAME) { - if (tag.size <= 3) { - // simple case ["e", "id", "relay"] - MarkedETag(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }, null, null) - } else if (tag.size == 4) { - val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) - if (tag[3].isEmpty()) { - // empty tags ["e", "id", "relay", ""] - MarkedETag(tag[1], relayHint, null, null) - } else if (tag[3].length == 64) { - // updated case with pubkey instead of marker ["e", "id", "relay", "pubkey"] - MarkedETag(tag[1], relayHint, null, tag[3]) - } else if (tag[3] == MARKER.ROOT.code) { - // corrent root ["e", "id", "relay", "root"] - MarkedETag(tag[1], relayHint, MARKER.ROOT) - } else if (tag[3] == MARKER.REPLY.code) { - // correct reply ["e", "id", "relay", "reply"] - MarkedETag(tag[1], relayHint, MARKER.REPLY) - } else { - // ignore "mention" and "fork" markers - null - } - } else { - val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) - // tag.size >= 5 - if (tag[3].isEmpty()) { - // empty tags ["e", "id", "relay", "", "pubkey"] - MarkedETag(tag[1], relayHint, null, tag[4]) - } else if (tag[3].length == 64) { - // updated case with pubkey instead of marker ["e", "id", "relay", "pubkey"] - MarkedETag(tag[1], relayHint, null, tag[3]) - } else if (tag[3] == MARKER.ROOT.code) { - // corrent root ["e", "id", "relay", "root"] - MarkedETag(tag[1], relayHint, MARKER.ROOT, tag[4]) - } else if (tag[3] == MARKER.REPLY.code) { - // correct reply ["e", "id", "relay", "reply"] - MarkedETag(tag[1], relayHint, MARKER.REPLY, tag[4]) - } else { - // ignore "mention" and "fork" markers - null - } - } + MarkedETag( + eventId = tag[1], + relayHint = pickRelayHint(tag), + marker = pickMarker(tag), + authorPubKeyHex = pickAuthor(tag), + ) } else { null } @@ -187,9 +193,8 @@ data class MarkedETag( ensure(tag.has(2)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } - ensure(tag[2].isNotEmpty()) { return null } - val hint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + val hint = pickRelayHint(tag) ensure(hint != null) { return null } return EventIdHint(tag[1], hint) @@ -197,15 +202,18 @@ data class MarkedETag( @JvmStatic fun parseRoot(tag: Array): MarkedETag? { - if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null - if (tag[ORDER_MARKER] != MARKER.ROOT.code) return null + ensure(tag.has(3)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + + val marker = pickMarker(tag) + ensure(marker == MARKER.ROOT) { return null } - // ["e", id hex, relay hint, marker, pubkey] return MarkedETag( - eventId = tag[ORDER_EVT_ID], - relayHint = RelayUrlNormalizer.normalizeOrNull(tag[ORDER_RELAY]), - marker = MARKER.ROOT, - authorPubKeyHex = tag.getOrNull(ORDER_PUBKEY), + eventId = tag[1], + relayHint = pickRelayHint(tag), + marker = marker, + authorPubKeyHex = pickAuthor(tag), ) } @@ -215,23 +223,25 @@ data class MarkedETag( @JvmStatic fun parseUnmarkedRoot(tag: Array): MarkedETag? = if (tag.size in 2..3 && tag[0] == TAG_NAME) { - MarkedETag(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }, MARKER.ROOT) + MarkedETag(tag[1], pickRelayHint(tag), MARKER.ROOT) } else { null } @JvmStatic fun parseReply(tag: Array): MarkedETag? { - if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null - if (tag[ORDER_MARKER] != MARKER.REPLY.code) return null - // ["e", id hex, relay hint, marker, pubkey] + ensure(tag.has(3)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + + val marker = pickMarker(tag) + ensure(marker == MARKER.REPLY) { return null } + return MarkedETag( - tag[ORDER_EVT_ID], - RelayUrlNormalizer.normalizeOrNull(tag[ORDER_RELAY]), - MARKER.REPLY, - tag.getOrNull( - ORDER_PUBKEY, - ), + eventId = tag[1], + relayHint = pickRelayHint(tag), + marker = marker, + authorPubKeyHex = pickAuthor(tag), ) } @@ -241,17 +251,21 @@ data class MarkedETag( @JvmStatic fun parseUnmarkedReply(tag: Array): MarkedETag? = if (tag.size in 2..3 && tag[0] == TAG_NAME) { - MarkedETag(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }, MARKER.REPLY) + MarkedETag(tag[1], pickRelayHint(tag), MARKER.REPLY) } else { null } @JvmStatic fun parseRootId(tag: Array): HexKey? { - if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null - if (tag[ORDER_MARKER] != MARKER.ROOT.code) return null - // ["e", id hex, relay hint, marker, pubkey] - return tag[ORDER_EVT_ID] + ensure(tag.has(3)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + + val marker = pickMarker(tag) + ensure(marker == MARKER.ROOT) { return null } + + return tag[1] } @JvmStatic diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt index f059e9a90..ae275a8dc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt @@ -20,9 +20,11 @@ */ package com.vitorpamplona.quartz.nip18Reposts.quotes +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.utils.ensure @@ -38,16 +40,28 @@ interface QTag { ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } - val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + val relayHint = pickRelayHint(tag) return if (tag[1].length == 64) { - QEventTag(tag[1], relayHint, tag.getOrNull(3)) + QEventTag(tag[1], relayHint, pickAuthor(tag)) } else { val address = Address.parse(tag[1]) ?: return null QAddressableTag(address, relayHint) } } + private fun pickRelayHint(tag: Array): NormalizedRelayUrl? { + if (tag.has(2) && tag[2].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[2])) return RelayUrlNormalizer.normalizeOrNull(tag[2]) + if (tag.has(3) && tag[3].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[3])) return RelayUrlNormalizer.normalizeOrNull(tag[3]) + return null + } + + private fun pickAuthor(tag: Array): HexKey? { + if (tag.has(2) && tag[2].length == 64) return tag[2] + if (tag.has(3) && tag[3].length == 64) return tag[3] + return null + } + @JvmStatic fun parseKey(tag: Array): String? { ensure(tag.has(1)) { return null } @@ -62,7 +76,7 @@ interface QTag { ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } - val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + val relayHint = pickRelayHint(tag) ensure(relayHint != null) { return null } return EventIdHint(tag[1], relayHint) @@ -76,7 +90,7 @@ interface QTag { ensure(tag[2].isNotEmpty()) { return null } ensure(!tag[1].contains(':')) { return null } - val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + val relayHint = pickRelayHint(tag) ensure(relayHint != null) { return null } return AddressHint(tag[1], relayHint)