diff --git a/.changeset/validate-relay-url.md b/.changeset/validate-relay-url.md new file mode 100644 index 00000000..e820f851 --- /dev/null +++ b/.changeset/validate-relay-url.md @@ -0,0 +1,7 @@ +--- +"nostream": patch +--- + +fix(settings): validate `info.relay_url` is a usable absolute URL + +`validateSettings()` now rejects a `relay_url` that is not a valid absolute URL, parses with an empty host (scheme-only strings like `localhost:8008`), or uses a protocol other than `ws`/`wss`/`http`/`https`. Previously an invalid value started cleanly and then broke every HTTP request with `500 TypeError: Invalid URL`, including the Admin Console, leaving the bad setting fixable only by editing the file by hand. Found in #824. diff --git a/src/utils/settings-config.ts b/src/utils/settings-config.ts index 93fb9149..ee3f2aba 100644 --- a/src/utils/settings-config.ts +++ b/src/utils/settings-config.ts @@ -594,6 +594,21 @@ export const validateSettings = (settings: Settings): ValidationIssue[] => { if (!settings.info?.relay_url) { issues.push({ path: 'info.relay_url', message: 'relay_url is required' }) + } else if (typeof settings.info.relay_url === 'string') { + try { + const parsed = new URL(settings.info.relay_url) + + if (!parsed.host) { + issues.push({ path: 'info.relay_url', message: 'relay_url must include a valid host' }) + } else if (!['ws:', 'wss:', 'http:', 'https:'].includes(parsed.protocol)) { + issues.push({ + path: 'info.relay_url', + message: 'relay_url must use ws, wss, http, or https protocol', + }) + } + } catch { + issues.push({ path: 'info.relay_url', message: 'relay_url must be a valid absolute URL' }) + } } if (!settings.info?.name) { diff --git a/test/unit/utils/settings-config.spec.ts b/test/unit/utils/settings-config.spec.ts index d9179f35..dc0fe08b 100644 --- a/test/unit/utils/settings-config.spec.ts +++ b/test/unit/utils/settings-config.spec.ts @@ -98,6 +98,65 @@ describe('settings-config', () => { expect(issues.some((issue) => issue.path === 'network')).to.equal(true) }) + it('accepts a relay_url with a valid ws/wss/http/https absolute URL', () => { + for (const relay_url of [ + 'ws://nostream.example.com:8008', + 'wss://nostream.example.com', + 'http://localhost:8008', + 'http://nostream.example.com', + 'https://nostream.example.com', + 'https://nostream.example.com:8443', + 'ws://[::1]:8008', + 'wss://127.0.0.1', + ]) { + const issues = validateSettings({ info: { relay_url, name: 'test' } } as any) + + expect( + issues.filter((issue) => issue.path === 'info.relay_url'), + `expected ${relay_url} to pass`, + ).to.be.empty + } + }) + + it('rejects a relay_url that is not a valid absolute URL', () => { + const issues = validateSettings({ info: { relay_url: 'nostream.example.com', name: 'test' } } as any) + + expect( + issues.some( + (issue) => issue.path === 'info.relay_url' && issue.message === 'relay_url must be a valid absolute URL', + ), + ).to.equal(true) + }) + + it('rejects a scheme-only relay_url with no host', () => { + const issues = validateSettings({ info: { relay_url: 'localhost:8008', name: 'test' } } as any) + + expect( + issues.some( + (issue) => issue.path === 'info.relay_url' && issue.message === 'relay_url must include a valid host', + ), + ).to.equal(true) + }) + + it('rejects a relay_url whose protocol is not ws/wss/http/https', () => { + for (const relay_url of [ + 'ftp://nostream.example.com', + 'gopher://nostream.example.com:70', + 'whttp://nostream.example.com', + ]) { + const issues = validateSettings({ info: { relay_url, name: 'test' } } as any) + + expect( + issues.some( + (issue) => + issue.path === 'info.relay_url' && + issue.message === 'relay_url must use ws, wss, http, or https protocol', + ), + `expected ${relay_url} to be rejected for protocol`, + ).to.equal(true) + } + }) + describe('rate limit period validation', () => { const baseSettings = () => ({