Skip to content
Open
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
2 changes: 2 additions & 0 deletions .changeset/red-dancers-ask.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
33 changes: 31 additions & 2 deletions src/handlers/request-handlers/root-request-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import accepts from 'accepts'
import { NextFunction, Request, Response } from 'express'
import { path, pathEq } from 'ramda'
import { createSettings } from '../../factories/settings-factory'
Expand All @@ -8,10 +7,40 @@ import { fromBech32 } from '../../utils/transform'
import { getTemplate } from '../../utils/template-cache'
import packageJson from '../../../package.json'

export const hasExplicitNostrJsonAcceptHeader = (request: Request): boolean => {
const acceptHeader = request.headers.accept

if (!acceptHeader) {
return false
}

const acceptHeaderValue = Array.isArray(acceptHeader) ? acceptHeader.join(',') : acceptHeader

return acceptHeaderValue.split(',').some((token) => {
const [mediaType, ...params] = token
.split(';')
.map((value) => value.trim().toLowerCase())

if (mediaType !== 'application/nostr+json') {
return false
}

const quality = params.find((param) => param.startsWith('q='))

if (!quality) {
return true
}

const qValue = Number.parseFloat(quality.slice(2))

return !Number.isNaN(qValue) && qValue > 0
})
}

export const rootRequestHandler = (request: Request, response: Response, next: NextFunction) => {
const settings = createSettings()

if (accepts(request).type(['application/nostr+json'])) {
if (hasExplicitNostrJsonAcceptHeader(request)) {
const {
info: { name, description, pubkey: rawPubkey, contact, relay_url },
} = settings
Expand Down
5 changes: 2 additions & 3 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import accepts from 'accepts'
import express from 'express'

import { nodeinfo21Handler, nodeinfoHandler } from '../handlers/request-handlers/nodeinfo-handler'
Expand All @@ -9,12 +8,12 @@ import { getPrivacyRequestHandler } from '../handlers/request-handlers/get-priva
import { getTermsRequestHandler } from '../handlers/request-handlers/get-terms-request-handler'
import invoiceRouter from './invoices'
import { rateLimiterMiddleware } from '../handlers/request-handlers/rate-limiter-middleware'
import { rootRequestHandler } from '../handlers/request-handlers/root-request-handler'
import { hasExplicitNostrJsonAcceptHeader, rootRequestHandler } from '../handlers/request-handlers/root-request-handler'

const router = express.Router()

router.use((req, res, next) => {
if (req.method === 'GET' && accepts(req).type(['application/nostr+json'])) {
if (req.method === 'GET' && req.path === '/' && hasExplicitNostrJsonAcceptHeader(req)) {
return rootRequestHandler(req, res, next)
}
next()
Expand Down
5 changes: 5 additions & 0 deletions test/integration/features/nip-11/nip-11.feature
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ Feature: NIP-11
Then the response Content-Type does not include "application/nostr+json"
And the response body is not a relay information document

Scenario: Relay serves HTML for typical browser Accept header
When a browser requests the root path
Then the response Content-Type includes "text/html"
And the response body is not a relay information document

Scenario: Relay information document reports max_filters from settings
When a client requests the relay information document
Then the limitation object contains a max_filters field
Expand Down
10 changes: 10 additions & 0 deletions test/integration/features/nip-11/nip-11.feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ When('a client requests the root path with Accept header {string}', async functi
this.parameters.httpResponse = response
})

When('a browser requests the root path', async function(this: World<Record<string, any>>) {
const response: AxiosResponse = await axios.get(BASE_URL, {
headers: {
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
},
validateStatus: () => true,
})
this.parameters.httpResponse = response
})

Then('the response status is {int}', function(this: World<Record<string, any>>, status: number) {
expect(this.parameters.httpResponse.status).to.equal(status)
})
Expand Down
22 changes: 21 additions & 1 deletion test/unit/handlers/request-handlers/root-request-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ const { expect } = chai

import * as settingsFactory from '../../../../src/factories/settings-factory'
import * as templateCache from '../../../../src/utils/template-cache'
import { rootRequestHandler } from '../../../../src/handlers/request-handlers/root-request-handler'
import {
hasExplicitNostrJsonAcceptHeader,
rootRequestHandler,
} from '../../../../src/handlers/request-handlers/root-request-handler'

const baseSettings = {
info: {
Expand Down Expand Up @@ -40,6 +43,23 @@ const settingsWithFee = {
},
}

describe('hasExplicitNostrJsonAcceptHeader', () => {
it('returns true for explicit application/nostr+json', () => {
expect(hasExplicitNostrJsonAcceptHeader({ headers: { accept: 'application/nostr+json' } } as any)).to.equal(true)
})

it('returns false for typical browser Accept header', () => {
const browserAcceptHeader =
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'

expect(hasExplicitNostrJsonAcceptHeader({ headers: { accept: browserAcceptHeader } } as any)).to.equal(false)
})

it('returns false when q=0 for application/nostr+json', () => {
expect(hasExplicitNostrJsonAcceptHeader({ headers: { accept: 'application/nostr+json;q=0' } } as any)).to.equal(false)
})
})

describe('rootRequestHandler', () => {
let createSettingsStub: sinon.SinonStub
let getTemplateStub: sinon.SinonStub
Expand Down
Loading