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
5 changes: 5 additions & 0 deletions .changeset/callback-routes-conditional-registration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostream": patch
---

refactor: only register OpenNode, LNbits, and Zebedee callback routes when their processor is active
7 changes: 0 additions & 7 deletions src/controllers/callbacks/lnbits-callback-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,6 @@ export class LNbitsCallbackController implements IController {

const settings = createSettings()
const remoteAddress = getRemoteAddress(request, settings)
const paymentProcessor = settings.payments?.processor ?? 'null'

if (paymentProcessor !== 'lnbits') {
logger('denied request from %s to /callbacks/lnbits which is not the current payment processor', remoteAddress)
response.status(403).send('Forbidden')
return
}

const queryValidation = validateSchema(lnbitsCallbackQuerySchema)(request.query)
if (queryValidation.error) {
Expand Down
9 changes: 0 additions & 9 deletions src/controllers/callbacks/opennode-callback-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,6 @@ export class OpenNodeCallbackController implements IController {

const settings = createSettings()
const remoteAddress = getRemoteAddress(request, settings)
const paymentProcessor = settings.payments?.processor

if (paymentProcessor !== 'opennode') {
logger('denied request from %s to /callbacks/opennode which is not the current payment processor', remoteAddress)
response
.status(403)
.send('Forbidden')
return
}

const bodyValidation = validateSchema(opennodeWebhookCallbackBodySchema)(request.body)
if (bodyValidation.error) {
Expand Down
7 changes: 0 additions & 7 deletions src/controllers/callbacks/zebedee-callback-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,13 @@ export class ZebedeeCallbackController implements IController {

const { ipWhitelist = [] } = settings.paymentsProcessors?.zebedee ?? {}
const remoteAddress = getRemoteAddress(request, settings)
const paymentProcessor = settings.payments?.processor

if (ipWhitelist.length && !ipWhitelist.includes(remoteAddress)) {
logger('unauthorized request from %s to /callbacks/zebedee', remoteAddress)
response.status(403).send('Forbidden')
return
}

if (paymentProcessor !== 'zebedee') {
logger('denied request from %s to /callbacks/zebedee which is not the current payment processor', remoteAddress)
response.status(403).send('Forbidden')
return
}

const invoice = fromZebedeeInvoice(request.body)

logger('invoice', invoice)
Expand Down
26 changes: 16 additions & 10 deletions src/routes/callbacks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { json, Router, urlencoded } from 'express'
import { json, NextFunction, Request, Response, Router, urlencoded } from 'express'

import { createLNbitsCallbackController } from '../../factories/controllers/lnbits-callback-controller-factory'
import { createNodelessCallbackController } from '../../factories/controllers/nodeless-callback-controller-factory'
Expand All @@ -9,24 +9,30 @@ import { withController } from '../../handlers/request-handlers/with-controller-

const router: Router = Router()

const settings = createSettings()
const processor = settings.payments?.processor
const requireProcessor = (name: string) =>
(_req: Request, res: Response, next: NextFunction) => {
const settings = createSettings()
if (settings.payments?.processor !== name) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also check here if payments are enabled, not just which processor is configured?

res.status(403).send('Forbidden')
return
}
next()
}

router
.post('/zebedee', json(), withController(createZebedeeCallbackController))
.post('/lnbits', json(), withController(createLNbitsCallbackController))
.post('/opennode', urlencoded({ extended: false }), json(), withController(createOpenNodeCallbackController))

if (processor === 'nodeless') {
router.post(
.post('/zebedee', requireProcessor('zebedee'), json(), withController(createZebedeeCallbackController))
.post('/lnbits', requireProcessor('lnbits'), json(), withController(createLNbitsCallbackController))
.post('/opennode', requireProcessor('opennode'), urlencoded({ extended: false }), json(), withController(createOpenNodeCallbackController))
.post(
'/nodeless',
requireProcessor('nodeless'),
json({
verify(req, _res, buf) {
;(req as any).rawBody = buf
},
}),
withController(createNodelessCallbackController),
)
}

export default router

29 changes: 0 additions & 29 deletions test/unit/controllers/callbacks/lnbits-callback-controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,35 +108,6 @@ describe('LNbitsCallbackController', () => {
})

describe('authorization and validation', () => {
it('returns 403 when payment processor settings are missing', async () => {
createSettingsStub.returns({
network: { remoteIpHeader: 'x-forwarded-for' },
})
const { controller, paymentsService } = makeController()
const res = makeRes()

await controller.handleRequest(makeReq(), res)

expect(res.status).to.have.been.calledWith(403)
expect(res.send).to.have.been.calledWith('Forbidden')
expect(paymentsService.getInvoiceFromPaymentsProcessor).to.not.have.been.called
})

it('returns 403 when lnbits is not the configured processor', async () => {
createSettingsStub.returns({
...baseSettings,
payments: { processor: 'opennode' },
})
const { controller, paymentsService } = makeController()
const res = makeRes()

await controller.handleRequest(makeReq(), res)

expect(res.status).to.have.been.calledWith(403)
expect(res.send).to.have.been.calledWith('Forbidden')
expect(paymentsService.getInvoiceFromPaymentsProcessor).to.not.have.been.called
})

it('returns 403 for invalid query parameters', async () => {
const { controller } = makeController()
const res = makeRes()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,20 +99,6 @@ describe('OpenNodeCallbackController', () => {
})

describe('authorization and validation', () => {
it('returns 403 when opennode is not the configured processor', async () => {
createSettingsStub.returns({
payments: { processor: 'lnbits' },
})
const { controller, paymentsService } = makeController()
const res = makeRes()

await controller.handleRequest(makeReq(), res)

expect(res.status).to.have.been.calledWith(403)
expect(res.send).to.have.been.calledWith('Forbidden')
expect(paymentsService.updateInvoiceStatus).to.not.have.been.called
})

it('returns 400 for malformed request body', async () => {
const { controller, paymentsService } = makeController()
const res = makeRes()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,21 +136,6 @@ describe('ZebedeeCallbackController', () => {
expect(res.send).to.have.been.calledWith('Forbidden')
expect(paymentsService.updateInvoiceStatus).to.not.have.been.called
})

it('returns 403 when zebedee is not the configured processor', async () => {
createSettingsStub.returns({
...baseSettings,
payments: { processor: 'lnbits' },
})
const { controller, paymentsService } = makeController()
const res = makeRes()

await controller.handleRequest(makeReq(), res)

expect(res.status).to.have.been.calledWith(403)
expect(res.send).to.have.been.calledWith('Forbidden')
expect(paymentsService.updateInvoiceStatus).to.not.have.been.called
})
})

describe('invoice state handling', () => {
Expand Down
7 changes: 7 additions & 0 deletions test/unit/routes/callbacks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@ import express from 'express'
import Sinon from 'sinon'

import * as openNodeControllerFactory from '../../../src/factories/controllers/opennode-callback-controller-factory'
import * as settingsFactory from '../../../src/factories/settings-factory'

describe('callbacks router', () => {
let createOpenNodeCallbackControllerStub: Sinon.SinonStub
let createSettingsStub: Sinon.SinonStub
let receivedBody: unknown
let server: any

beforeEach(async () => {
receivedBody = undefined

createSettingsStub = Sinon.stub(settingsFactory, 'createSettings').returns({
payments: { processor: 'opennode' },
} as any)

createOpenNodeCallbackControllerStub = Sinon.stub(openNodeControllerFactory, 'createOpenNodeCallbackController').returns({
handleRequest: async (request: any, response: any) => {
receivedBody = request.body
Expand All @@ -35,6 +41,7 @@ describe('callbacks router', () => {

afterEach(async () => {
createOpenNodeCallbackControllerStub.restore()
createSettingsStub.restore()
delete require.cache[require.resolve('../../../src/routes/callbacks')]

if (server) {
Expand Down
Loading