mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-15 09:08:43 +02:00
fix: filter invalid relay URLs from event tags
Add validation to prevent invalid URLs from being used as relay hints. The issue occurred when "r" tags containing non-relay URLs (like https://(strangelove@basspistol.org/) were being extracted and used as relay connection targets. Changes: - Add isValidRelayURL() helper to validate relay URLs (must have ws:// or wss:// protocol and valid URL structure) - Update extractRelayContext() in loaders.ts to filter r-tags, e-tag relay hints, and a-tag relay hints using the new validator - Add comprehensive tests for isValidRelayURL() https://claude.ai/code/session_01Ca2fKD2r4wHKRD8rcRohj9
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { normalizeRelayURL } from "./relay-url";
|
||||
import { normalizeRelayURL, isValidRelayURL } from "./relay-url";
|
||||
|
||||
describe("normalizeRelayURL", () => {
|
||||
it("should add trailing slash to URL without one", () => {
|
||||
@@ -127,3 +127,98 @@ describe("normalizeRelayURL", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidRelayURL", () => {
|
||||
describe("Valid relay URLs", () => {
|
||||
it("should return true for wss:// URLs", () => {
|
||||
expect(isValidRelayURL("wss://relay.example.com")).toBe(true);
|
||||
expect(isValidRelayURL("wss://relay.example.com/")).toBe(true);
|
||||
expect(isValidRelayURL("wss://nos.lol")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for ws:// URLs", () => {
|
||||
expect(isValidRelayURL("ws://localhost")).toBe(true);
|
||||
expect(isValidRelayURL("ws://localhost:8080")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for URLs with paths", () => {
|
||||
expect(isValidRelayURL("wss://relay.example.com/inbox")).toBe(true);
|
||||
expect(isValidRelayURL("wss://relay.example.com/path/to/relay")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return true for URLs with ports", () => {
|
||||
expect(isValidRelayURL("wss://relay.example.com:8080")).toBe(true);
|
||||
expect(isValidRelayURL("ws://localhost:3000/")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Invalid relay URLs - wrong protocol", () => {
|
||||
it("should return false for http:// URLs", () => {
|
||||
expect(isValidRelayURL("http://example.com")).toBe(false);
|
||||
expect(isValidRelayURL("http://basspistol.org/inbox")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for https:// URLs", () => {
|
||||
expect(isValidRelayURL("https://example.com")).toBe(false);
|
||||
expect(isValidRelayURL("https://basspistol.org/")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for URLs without protocol", () => {
|
||||
expect(isValidRelayURL("relay.example.com")).toBe(false);
|
||||
expect(isValidRelayURL("nos.lol")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for other protocols", () => {
|
||||
expect(isValidRelayURL("ftp://example.com")).toBe(false);
|
||||
expect(isValidRelayURL("file:///path/to/file")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Invalid relay URLs - malformed", () => {
|
||||
it("should return false for URLs with invalid characters", () => {
|
||||
// Real-world case: NIP-05 identifier incorrectly parsed as URL
|
||||
expect(isValidRelayURL("https://(strangelove@basspistol.org/")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return false for empty or whitespace strings", () => {
|
||||
expect(isValidRelayURL("")).toBe(false);
|
||||
expect(isValidRelayURL(" ")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for non-string types", () => {
|
||||
expect(isValidRelayURL(null)).toBe(false);
|
||||
expect(isValidRelayURL(undefined)).toBe(false);
|
||||
expect(isValidRelayURL(123)).toBe(false);
|
||||
expect(isValidRelayURL({})).toBe(false);
|
||||
expect(isValidRelayURL([])).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for incomplete URLs", () => {
|
||||
expect(isValidRelayURL("wss://")).toBe(false);
|
||||
expect(isValidRelayURL("ws://")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge cases", () => {
|
||||
it("should handle URLs with whitespace (trimmed)", () => {
|
||||
expect(isValidRelayURL(" wss://relay.example.com ")).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle URLs with query parameters", () => {
|
||||
expect(isValidRelayURL("wss://relay.example.com?token=abc")).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle localhost URLs", () => {
|
||||
expect(isValidRelayURL("ws://127.0.0.1")).toBe(true);
|
||||
expect(isValidRelayURL("ws://localhost:8080")).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle IP addresses", () => {
|
||||
expect(isValidRelayURL("wss://192.168.1.1:8080")).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,47 @@
|
||||
import { normalizeURL as applesauceNormalizeURL } from "applesauce-core/helpers";
|
||||
|
||||
/**
|
||||
* Check if a string is a valid relay URL
|
||||
* - Must have ws:// or wss:// protocol
|
||||
* - Must be a valid URL structure
|
||||
* - Must not contain invalid characters
|
||||
*
|
||||
* @returns true if the URL is a valid relay URL, false otherwise
|
||||
*/
|
||||
export function isValidRelayURL(url: unknown): url is string {
|
||||
// Must be a non-empty string
|
||||
if (typeof url !== "string" || !url.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const trimmed = url.trim();
|
||||
|
||||
// Must start with ws:// or wss://
|
||||
if (!trimmed.startsWith("ws://") && !trimmed.startsWith("wss://")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Must be a valid URL structure
|
||||
const parsed = new URL(trimmed);
|
||||
|
||||
// Protocol must be ws: or wss:
|
||||
if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have a valid hostname
|
||||
if (!parsed.hostname || parsed.hostname.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
// URL parsing failed
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a relay URL to ensure consistent comparison
|
||||
* - Validates input is a non-empty string
|
||||
|
||||
@@ -16,6 +16,7 @@ import pool from "./relay-pool";
|
||||
import eventStore from "./event-store";
|
||||
import { relayListCache } from "./relay-list-cache";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
import { isValidRelayURL } from "@/lib/relay-url";
|
||||
|
||||
/**
|
||||
* Extract relay context from a Nostr event for comprehensive relay selection
|
||||
@@ -31,13 +32,15 @@ function extractRelayContext(event: NostrEvent): {
|
||||
// Get relays where this event was seen (tracked by applesauce)
|
||||
const seenRelays = getSeenRelays(event);
|
||||
|
||||
// Extract all "r" tags (URL references per NIP-01)
|
||||
// Extract relay URLs from "r" tags (URL references per NIP-01)
|
||||
// Only include valid relay URLs (ws:// or wss://) - filter out http/https links
|
||||
const rTags = event.tags
|
||||
.filter((t) => t[0] === "r")
|
||||
.map((t) => t[1])
|
||||
.filter(Boolean);
|
||||
.filter(isValidRelayURL);
|
||||
|
||||
// Extract relay hints from all "e" tags using applesauce helper
|
||||
// Filter to only valid relay URLs
|
||||
const eTagRelays = event.tags
|
||||
.filter((t) => t[0] === "e")
|
||||
.map((tag) => {
|
||||
@@ -45,17 +48,18 @@ function extractRelayContext(event: NostrEvent): {
|
||||
// v5: returns null for invalid tags instead of throwing
|
||||
return pointer?.relays?.[0]; // First relay hint from the pointer
|
||||
})
|
||||
.filter((relay): relay is string => relay !== undefined);
|
||||
.filter(isValidRelayURL);
|
||||
|
||||
// Extract relay hints from all "a" tags (addressable event references)
|
||||
// This includes both lowercase "a" (reply) and uppercase "A" (root) tags
|
||||
// Filter to only valid relay URLs
|
||||
const aTagRelays = event.tags
|
||||
.filter((t) => t[0] === "a" || t[0] === "A")
|
||||
.map((tag) => {
|
||||
const pointer = getAddressPointerFromATag(tag);
|
||||
return pointer?.relays?.[0]; // First relay hint from the pointer
|
||||
})
|
||||
.filter((relay): relay is string => relay !== undefined);
|
||||
.filter(isValidRelayURL);
|
||||
|
||||
// Extract first "p" tag as author hint using applesauce helper
|
||||
const authorHint = getTagValue(event, "p");
|
||||
|
||||
Reference in New Issue
Block a user