diff --git a/.changeset/red-dancers-ask.md b/.changeset/red-dancers-ask.md new file mode 100644 index 00000000..a845151c --- /dev/null +++ b/.changeset/red-dancers-ask.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/src/handlers/request-handlers/root-request-handler.ts b/src/handlers/request-handlers/root-request-handler.ts index d3502fa3..5aa2b265 100644 --- a/src/handlers/request-handlers/root-request-handler.ts +++ b/src/handlers/request-handlers/root-request-handler.ts @@ -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' @@ -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 diff --git a/src/routes/index.ts b/src/routes/index.ts index c4c2460b..28f94336 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,4 +1,3 @@ -import accepts from 'accepts' import express from 'express' import { nodeinfo21Handler, nodeinfoHandler } from '../handlers/request-handlers/nodeinfo-handler' @@ -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() diff --git a/test/integration/features/nip-11/nip-11.feature b/test/integration/features/nip-11/nip-11.feature index 6e1bd4c0..bdd8e928 100644 --- a/test/integration/features/nip-11/nip-11.feature +++ b/test/integration/features/nip-11/nip-11.feature @@ -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 diff --git a/test/integration/features/nip-11/nip-11.feature.ts b/test/integration/features/nip-11/nip-11.feature.ts index 79aa1e13..d2926f55 100644 --- a/test/integration/features/nip-11/nip-11.feature.ts +++ b/test/integration/features/nip-11/nip-11.feature.ts @@ -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>) { + 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>, status: number) { expect(this.parameters.httpResponse.status).to.equal(status) }) diff --git a/test/unit/handlers/request-handlers/root-request-handler.spec.ts b/test/unit/handlers/request-handlers/root-request-handler.spec.ts index d10c2e0b..017d515f 100644 --- a/test/unit/handlers/request-handlers/root-request-handler.spec.ts +++ b/test/unit/handlers/request-handlers/root-request-handler.spec.ts @@ -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: { @@ -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