Browse Source

fix websocket normalization

imwald
Silberengel 3 weeks ago
parent
commit
3cc1921aa9
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 10
      src/components/MailboxSetting/DiscoveredRelays.tsx
  4. 4
      src/components/MailboxSetting/index.tsx
  5. 6
      src/lib/pre-publish-relay-cap.ts
  6. 16
      src/lib/relay-list-sanitize.ts
  7. 9
      src/lib/relay-url-normalize.test.ts
  8. 4
      src/lib/relay-url-priority.ts
  9. 13
      src/lib/url.ts
  10. 7
      src/services/relay-selection.service.ts

4
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "imwald", "name": "imwald",
"version": "23.15.1", "version": "23.16.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "imwald", "name": "imwald",
"version": "23.15.1", "version": "23.16.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@asciidoctor/core": "^3.0.4", "@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "imwald", "name": "imwald",
"version": "23.16.0", "version": "23.16.1",
"description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true, "private": true,
"type": "module", "type": "module",

10
src/components/MailboxSetting/DiscoveredRelays.tsx

@ -1,7 +1,7 @@
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { isLocalNetworkUrl, normalizeHttpRelayUrl } from '@/lib/url' import { isLocalNetworkUrl, isWebsocketUrl, normalizeRelayUrlByScheme } from '@/lib/url'
import { getRelaysFromNip07Extension, verifyNip05 } from '@/lib/nip05' import { getRelaysFromNip07Extension, verifyNip05 } from '@/lib/nip05'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { TMailboxRelay } from '@/types' import { TMailboxRelay } from '@/types'
@ -44,8 +44,8 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd:
const nip05Result = await verifyNip05(profile.nip05, account.pubkey) const nip05Result = await verifyNip05(profile.nip05, account.pubkey)
if (nip05Result.isVerified && nip05Result.relays) { if (nip05Result.isVerified && nip05Result.relays) {
nip05Result.relays.forEach(url => { nip05Result.relays.forEach(url => {
const normalized = normalizeHttpRelayUrl(url) const normalized = normalizeRelayUrlByScheme(url)
if (normalized && !discovered.has(normalized)) { if (normalized && isWebsocketUrl(normalized) && !discovered.has(normalized)) {
discovered.set(normalized, { discovered.set(normalized, {
url: normalized, url: normalized,
source: 'nip05', source: 'nip05',
@ -64,8 +64,8 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd:
try { try {
const extensionRelays = await getRelaysFromNip07Extension() const extensionRelays = await getRelaysFromNip07Extension()
extensionRelays.forEach(url => { extensionRelays.forEach(url => {
const normalized = normalizeHttpRelayUrl(url) const normalized = normalizeRelayUrlByScheme(url)
if (normalized && !discovered.has(normalized)) { if (normalized && isWebsocketUrl(normalized) && !discovered.has(normalized)) {
discovered.set(normalized, { discovered.set(normalized, {
url: normalized, url: normalized,
source: 'nip07', source: 'nip07',

4
src/components/MailboxSetting/index.tsx

@ -1,5 +1,5 @@
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { isLocalNetworkUrl, normalizeHttpRelayUrl } from '@/lib/url' import { isLocalNetworkUrl, normalizeAnyRelayUrl } from '@/lib/url'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { TMailboxRelay, TMailboxRelayScope } from '@/types' import { TMailboxRelay, TMailboxRelayScope } from '@/types'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -98,7 +98,7 @@ export default function MailboxSetting() {
const saveNewMailboxRelay = (url: string) => { const saveNewMailboxRelay = (url: string) => {
if (url === '') return null if (url === '') return null
const normalizedUrl = normalizeHttpRelayUrl(url) const normalizedUrl = normalizeAnyRelayUrl(url)
if (!normalizedUrl) { if (!normalizedUrl) {
return t('Invalid relay URL') return t('Invalid relay URL')
} }

6
src/lib/pre-publish-relay-cap.ts

@ -2,7 +2,7 @@ import { isSocialKindBlockedKind, MAX_PUBLISH_RELAYS, SOCIAL_KIND_BLOCKED_RELAY_
import { kinds } from 'nostr-tools' import { kinds } from 'nostr-tools'
import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter' import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter'
import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority' import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority'
import { normalizeAnyRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url' import { normalizeHttpRelayUrl, normalizeRelayUrlByScheme, normalizeUrl } from '@/lib/url'
import type { NostrEvent } from 'nostr-tools' import type { NostrEvent } from 'nostr-tools'
export type TPrePublishRelayCapPreview = { export type TPrePublishRelayCapPreview = {
@ -50,7 +50,7 @@ export function computePrePublishRelayCapPreview({
const socialBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u)) const socialBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
outbox = dedupeNormalizeRelayUrlsOrdered( outbox = dedupeNormalizeRelayUrlsOrdered(
filterRelaysForEventPublish(outbox, previewKind).filter((url) => { filterRelaysForEventPublish(outbox, previewKind).filter((url) => {
const n = normalizeAnyRelayUrl(url) || url const n = normalizeRelayUrlByScheme(url) || url
if (applySocialOutboxFilter && socialBlockedSet.has(n)) return false if (applySocialOutboxFilter && socialBlockedSet.has(n)) return false
return true return true
}) })
@ -64,7 +64,7 @@ export function computePrePublishRelayCapPreview({
const outboxNormSet = new Set(outbox) const outboxNormSet = new Set(outbox)
const outboxSlotsInPublish = const outboxSlotsInPublish =
selectedRelayUrls.length > 0 ? 0 : capped.filter((u) => outboxNormSet.has(u)).length selectedRelayUrls.length > 0 ? 0 : capped.filter((u) => outboxNormSet.has(u)).length
const selectedNorm = selectedRelayUrls.map((u) => normalizeAnyRelayUrl(u) || u) const selectedNorm = selectedRelayUrls.map((u) => normalizeRelayUrlByScheme(u) || u)
const selectedContacted = selectedNorm.filter((u) => capped.includes(u)).length const selectedContacted = selectedNorm.filter((u) => capped.includes(u)).length
const showCapHint = const showCapHint =

16
src/lib/relay-list-sanitize.ts

@ -1,4 +1,10 @@
import { isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url' import {
isLocalNetworkUrl,
normalizeAnyRelayUrl,
normalizeHttpRelayUrl,
normalizeRelayUrlByScheme,
normalizeUrl
} from '@/lib/url'
import type { TMailboxRelay, TMailboxRelayScope, TRelayList } from '@/types' import type { TMailboxRelay, TMailboxRelayScope, TRelayList } from '@/types'
/** True if this URL is not loopback / LAN (safe to open from another user's browser as a REQ target). */ /** True if this URL is not loopback / LAN (safe to open from another user's browser as a REQ target). */
@ -81,13 +87,7 @@ export function stripLocalNetworkRelaysForWssReq(urls: readonly string[]): strin
return out return out
} }
const normRelayKey = (u: string): string => { const normRelayKey = (u: string): string => normalizeRelayUrlByScheme(u) || u.trim()
const t = typeof u === 'string' ? u.trim() : ''
if (!t) return ''
if (/^wss?:\/\//i.test(t)) return normalizeUrl(t) || t
if (/^https?:\/\//i.test(t)) return normalizeHttpRelayUrl(t) || t
return normalizeUrl(t) || normalizeHttpRelayUrl(t) || t
}
/** /**
* When NIP-65 `originalRelays` is empty but `read` / `write` URL lists are filled (e.g. PROFILE_FETCH fallback), * When NIP-65 `originalRelays` is empty but `read` / `write` URL lists are filled (e.g. PROFILE_FETCH fallback),

9
src/lib/relay-url-normalize.test.ts

@ -5,6 +5,7 @@ import {
httpIndexRelayBasesInUrlBatch, httpIndexRelayBasesInUrlBatch,
normalizeAnyRelayUrl, normalizeAnyRelayUrl,
normalizeHttpRelayUrl, normalizeHttpRelayUrl,
normalizeRelayUrlByScheme,
normalizeRelayUrlForPage, normalizeRelayUrlForPage,
normalizeUrl normalizeUrl
} from '@/lib/url' } from '@/lib/url'
@ -27,6 +28,14 @@ describe('relay URL normalization', () => {
expect(normalizeUrl('wss://nostr.land/')).toMatch(/^wss:\/\/nostr\.land\/?$/) expect(normalizeUrl('wss://nostr.land/')).toMatch(/^wss:\/\/nostr\.land\/?$/)
}) })
it('normalizeRelayUrlByScheme routes by scheme', () => {
expect(normalizeRelayUrlByScheme('wss://nostr.land/')).toMatch(/^wss:\/\/nostr\.land\/?$/)
expect(normalizeRelayUrlByScheme('https://mercury-relay.imwald.eu/')).toMatch(
/^https:\/\/mercury-relay\.imwald\.eu\/?$/
)
expect(normalizeRelayUrlByScheme('mercury-relay.imwald.eu')).toBe('')
})
it('rejects bare hostnames', () => { it('rejects bare hostnames', () => {
expect(normalizeAnyRelayUrl('mercury-relay.imwald.eu')).toBe('') expect(normalizeAnyRelayUrl('mercury-relay.imwald.eu')).toBe('')
expect(normalizeAnyRelayUrl('nostr.land')).toBe('') expect(normalizeAnyRelayUrl('nostr.land')).toBe('')

4
src/lib/relay-url-priority.ts

@ -5,7 +5,7 @@ import {
MAX_REQ_RELAY_URLS MAX_REQ_RELAY_URLS
} from '@/constants' } from '@/constants'
import { feedRelayPolicyUrls, type FeedRelayLayer } from '@/features/feed/relay-policy' import { feedRelayPolicyUrls, type FeedRelayLayer } from '@/features/feed/relay-policy'
import { isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url' import { isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeRelayUrlByScheme, normalizeUrl } from '@/lib/url'
export { MAX_REQ_RELAY_URLS } export { MAX_REQ_RELAY_URLS }
@ -13,7 +13,7 @@ export function dedupeNormalizeRelayUrlsOrdered(urls: readonly string[]): string
const seen = new Set<string>() const seen = new Set<string>()
const out: string[] = [] const out: string[] = []
for (const u of urls) { for (const u of urls) {
const n = normalizeAnyRelayUrl(u) || u.trim() const n = normalizeRelayUrlByScheme(u) || u.trim()
if (!n || seen.has(n)) continue if (!n || seen.has(n)) continue
seen.add(n) seen.add(n)
out.push(n) out.push(n)

13
src/lib/url.ts

@ -118,9 +118,20 @@ export function normalizeAnyRelayUrl(url: string): string {
return normalizeUrl(url) return normalizeUrl(url)
} }
/**
* Normalize a relay URL using the route for its scheme: `http(s)` index relays (kind 10243)
* vs `ws(s)` NIP-01 relays (kind 10002). Bare hostnames are rejected by both routes.
*/
export function normalizeRelayUrlByScheme(url: string): string {
const trimmed = url.trim()
if (!trimmed) return ''
if (isHttpOrHttpsScheme(trimmed)) return normalizeHttpRelayUrl(trimmed)
return normalizeAnyRelayUrl(trimmed)
}
/** Relay explore/detail routes accept WebSocket relays or kind-10243 HTTP index bases. */ /** Relay explore/detail routes accept WebSocket relays or kind-10243 HTTP index bases. */
export function normalizeRelayUrlForPage(url: string): string { export function normalizeRelayUrlForPage(url: string): string {
return normalizeAnyRelayUrl(url) || normalizeHttpRelayUrl(url) return normalizeRelayUrlByScheme(url)
} }
/** Stable key for per-relay session stats (scheme preserved; no https→wss aliasing). */ /** Stable key for per-relay session stats (scheme preserved; no https→wss aliasing). */

7
src/services/relay-selection.service.ts

@ -11,10 +11,9 @@ import client from '@/services/client.service'
import { eventService } from '@/services/client.service' import { eventService } from '@/services/client.service'
import { import {
canonicalRelaySessionKey, canonicalRelaySessionKey,
isHttpOrHttpsScheme,
isLocalNetworkUrl, isLocalNetworkUrl,
normalizeAnyRelayUrl, normalizeAnyRelayUrl,
normalizeHttpRelayUrl normalizeRelayUrlByScheme
} from '@/lib/url' } from '@/lib/url'
import { TRelaySet, TRelayList } from '@/types' import { TRelaySet, TRelayList } from '@/types'
import logger from '@/lib/logger' import logger from '@/lib/logger'
@ -144,9 +143,7 @@ class RelaySelectionService {
const addRelay = (url: string, type: RelaySourceType) => { const addRelay = (url: string, type: RelaySourceType) => {
if (!url) return if (!url) return
const normalized = isHttpOrHttpsScheme(url) const normalized = normalizeRelayUrlByScheme(url)
? normalizeHttpRelayUrl(url)
: normalizeAnyRelayUrl(url)
const key = normalized ? canonicalRelaySessionKey(normalized) : '' const key = normalized ? canonicalRelaySessionKey(normalized) : ''
if (key && !seen.has(key)) { if (key && !seen.has(key)) {
seen.add(key) seen.add(key)

Loading…
Cancel
Save