Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/validate-relay-url.md
Original file line number Diff line number Diff line change
@@ -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.
15 changes: 15 additions & 0 deletions src/utils/settings-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
59 changes: 59 additions & 0 deletions test/unit/utils/settings-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () =>
({
Expand Down
Loading