Skip to content

Commit 6eb8f61

Browse files
Merge pull request #32 from JavaScriptSolidServer/issue-31-safeurl-concat
Relay cleanup: reject concat junk + sweep --real-only prune
2 parents 1d94dbd + f903b42 commit 6eb8f61

3 files changed

Lines changed: 41 additions & 8 deletions

File tree

scripts/sweep-relays.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,27 @@
55
// node scripts/sweep-relays.js # dry run (report only)
66
// node scripts/sweep-relays.js --apply # execute
77
// node scripts/sweep-relays.js --max-per-host=3 --stale=7 --apply
8+
// node scripts/sweep-relays.js --real-only --apply # keep only online-ever bare origins
89
import { connect, close } from '../src/db.js';
910
import { planRelaySweep } from '../src/hoses/relays.js';
1011
import 'dotenv/config';
1112

1213
const RELAY_DIRECTORY = process.env.MONGO_RELAY_DIRECTORY_COLLECTION || 'relays';
1314
const APPLY = process.argv.includes('--apply');
15+
const realOnly = process.argv.includes('--real-only');
1416
const numArg = (name, dflt) => { const a = process.argv.find((x) => x.startsWith(`--${name}=`)); return a ? Number(a.split('=')[1]) : dflt; };
1517
const maxPerHost = numArg('max-per-host', 3);
1618
const staleDays = numArg('stale', 0);
1719

1820
const db = await connect();
1921
const col = db.collection(RELAY_DIRECTORY);
20-
const docs = await col.find({}, { projection: { relay: 1, checksTotal: 1, lastChecked: 1 } }).toArray();
22+
const docs = await col.find({}, { projection: { relay: 1, checksTotal: 1, checksOnline: 1, lastChecked: 1 } }).toArray();
2123

22-
const plan = planRelaySweep(docs, { maxPerHost, staleDays, now: Date.now() });
23-
const delIds = [...new Set([...plan.bad, ...plan.dups, ...plan.hostSpam, ...(staleDays ? plan.stale : [])].map((d) => d._id))];
24+
const plan = planRelaySweep(docs, { maxPerHost, staleDays, realOnly, now: Date.now() });
25+
const delIds = [...new Set([...plan.bad, ...plan.dups, ...plan.hostSpam, ...plan.notReal, ...(staleDays ? plan.stale : [])].map((d) => d._id))];
2426

25-
console.log(`[sweep] ${docs.length} relays | bad ${plan.bad.length} | dup ${plan.dups.length} | host-spam ${plan.hostSpam.length} | renorm ${plan.renames.length}`
27+
console.log(`[sweep] ${docs.length} relays | bad ${plan.bad.length} | dup ${plan.dups.length} | host-spam ${plan.hostSpam.length}`
28+
+ (realOnly ? ` | not-real ${plan.notReal.length}` : '') + ` | renorm ${plan.renames.length}`
2629
+ (staleDays ? ` | stale>${staleDays}d ${plan.stale.length}` : '') + ` | keep ${plan.kept}`);
2730
console.log(`[sweep] maxPerHost=${maxPerHost} -> would delete ${delIds.length}, normalize ${plan.renames.length} (${APPLY ? 'APPLYING' : 'dry run — pass --apply'})`);
2831

src/hoses/relays.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,13 @@ function isBlockedHost(host) {
4343
* (trailing slash on bare origins), or null if it must not be stored/dialed.
4444
*/
4545
export function safeRelayUrl(url) {
46+
const raw = String(url);
47+
// Real relay URLs are short and single-scheme. Reject length blowups,
48+
// whitespace, and a second "://" (embedded scheme) — the signature of
49+
// concatenated multi-relay junk packed into one tag by broken clients.
50+
if (raw.length > 120 || /\s/.test(raw) || raw.indexOf('://') !== raw.lastIndexOf('://')) return null;
4651
let u;
47-
try { u = new URL(String(url)); } catch { return null; }
52+
try { u = new URL(raw); } catch { return null; }
4853
if (u.protocol !== 'wss:' && u.protocol !== 'ws:') return null;
4954
if (isBlockedHost(u.hostname)) return null;
5055
if (u.pathname === '' || u.pathname === '/') return `${u.protocol}//${u.host}/`;
@@ -68,8 +73,10 @@ const tms = (x) => (x ? new Date(x).getTime() || 0 : 0);
6873
* e.g. a bomb) — keep the origin + richest few, drop the rest
6974
* - renames: a surviving doc's stored `relay` ≠ its canonical form
7075
* - stale: (if staleDays) a survivor not checked within staleDays
76+
* - notReal: (if realOnly) survivors that aren't a bare origin that has been
77+
* online at least once — i.e. the unverified/dead/path entries
7178
*/
72-
export function planRelaySweep(docs, { maxPerHost = 3, staleDays = 0, now = 0 } = {}) {
79+
export function planRelaySweep(docs, { maxPerHost = 3, staleDays = 0, now = 0, realOnly = false } = {}) {
7380
const rich = (a, b) => (b.checksTotal || 0) - (a.checksTotal || 0) || tms(b.lastChecked) - tms(a.lastChecked);
7481
// 1. screen + canonicalize, group by canonical URL
7582
const byCanon = new Map();
@@ -102,15 +109,26 @@ export function planRelaySweep(docs, { maxPerHost = 3, staleDays = 0, now = 0 }
102109
}
103110
survivors = kept;
104111
}
105-
// 4. normalizations + stale among survivors
112+
// 4. realOnly: keep only bare-origin relays online at least once (the
113+
// verified/reachable set); everything else is unverified/dead/path junk.
114+
const notReal = [];
115+
if (realOnly) {
116+
const real = [];
117+
for (const s of survivors) {
118+
if (isOrigin(s._canon) && (s.checksOnline || 0) >= 1) real.push(s);
119+
else notReal.push(s);
120+
}
121+
survivors = real;
122+
}
123+
// 5. normalizations + stale among final survivors
106124
const renames = [];
107125
const stale = [];
108126
const cutoff = staleDays && now ? now - staleDays * 864e5 : null;
109127
for (const s of survivors) {
110128
if (s.relay !== s._canon) renames.push({ doc: s, canonical: s._canon });
111129
if (cutoff && tms(s.lastChecked) < cutoff) stale.push(s);
112130
}
113-
return { bad, dups, hostSpam, renames, stale, kept: survivors.length };
131+
return { bad, dups, hostSpam, notReal, renames, stale, kept: survivors.length };
114132
}
115133

116134
export async function ensureRelayDirectoryIndex(db) {

test/relay-sweep.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@ test('renorm flags survivors whose stored URL is not canonical', () => {
5858
assert.equal(p.renames[0].canonical, 'wss://nos.lol/');
5959
});
6060

61+
test('realOnly: keep only bare origins online at least once', () => {
62+
const docs = [
63+
{ _id: 1, relay: 'wss://good.example.com/', checksOnline: 3 }, // bare + online -> keep
64+
{ _id: 2, relay: 'wss://dead.example.com/', checksOnline: 0 }, // bare but never online -> notReal
65+
{ _id: 3, relay: 'wss://path.example.com/echo-zulu', checksOnline: 5 }, // online but has path -> notReal
66+
{ _id: 4, relay: 'wss://fresh.example.com/' }, // no checksOnline -> notReal
67+
];
68+
const p = planRelaySweep(docs, { realOnly: true });
69+
assert.equal(p.kept, 1);
70+
assert.deepEqual(ids(p.notReal), [2, 3, 4]);
71+
});
72+
6173
test('stale prune by age (only when staleDays set)', () => {
6274
const now = 1_000_000_000_000;
6375
const docs = [

0 commit comments

Comments
 (0)