From 07110650a0aa9fb3d0cad898f5c525db518715ac Mon Sep 17 00:00:00 2001 From: Mihajlo Medjedovic Date: Fri, 25 Aug 2023 15:22:30 +0200 Subject: [PATCH 1/7] feat: admission check --- .../get-invoice-pubkey-check-controller.ts | 72 +++++++++++++++++++ ...invoice-pubkey-check-controller-factory.ts | 16 +++++ src/routes/invoices/index.ts | 4 +- 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/controllers/invoices/get-invoice-pubkey-check-controller.ts create mode 100644 src/factories/controllers/get-invoice-pubkey-check-controller-factory.ts diff --git a/src/controllers/invoices/get-invoice-pubkey-check-controller.ts b/src/controllers/invoices/get-invoice-pubkey-check-controller.ts new file mode 100644 index 00000000..89eb8b47 --- /dev/null +++ b/src/controllers/invoices/get-invoice-pubkey-check-controller.ts @@ -0,0 +1,72 @@ +import { Request, Response } from 'express' +import { createLogger } from '../../factories/logger-factory' +import { getRemoteAddress } from '../../utils/http' +import { IController } from '../../@types/controllers' +import { IRateLimiter } from '../../@types/utils' +import { IUserRepository } from '../../@types/repositories' +import { path } from 'ramda' +import { Settings } from '../../@types/settings' + +const debug = createLogger('get-invoice-pubkey-check-controller') + +export class GetInvoicePubkeyCheckController implements IController { + public constructor( + private readonly userRepository: IUserRepository, + private readonly settings: () => Settings, + private readonly rateLimiter: () => IRateLimiter, + ){} + + public async handleRequest(request: Request, response: Response): Promise { + const currentSettings = this.settings() + + const limited = await this.isRateLimited(request, currentSettings) + if (limited) { + response + .status(429) + .setHeader('content-type', 'text/plain; charset=utf8') + .send('Too many requests') + return + } + + const pubkey = request.params.pubkey + const user = await this.userRepository.findByPubkey(pubkey) + + console.log('user', user) + + let userAdmitted = false + + const minBalance = currentSettings.limits?.event?.pubkey?.minBalance + if (user && user.isAdmitted && (!minBalance || user.balance >= minBalance)) { + userAdmitted = true + } + + response + .status(400) + .setHeader('content-type', 'application/json; charset=utf8') + .send({ userAdmitted }) + + return + } + + public async isRateLimited(request: Request, settings: Settings) { + const rateLimits = path(['limits', 'invoice', 'rateLimits'], settings) + if (!Array.isArray(rateLimits) || !rateLimits.length) { + return false + } + + const ipWhitelist = path(['limits', 'invoice', 'ipWhitelist'], settings) + const remoteAddress = getRemoteAddress(request, settings) + + let limited = false + if (Array.isArray(ipWhitelist) && !ipWhitelist.includes(remoteAddress)) { + const rateLimiter = this.rateLimiter() + for (const { rate, period } of rateLimits) { + if (await rateLimiter.hit(`${remoteAddress}:invoice:${period}`, 1, { period, rate })) { + debug('rate limited %s: %d in %d milliseconds', remoteAddress, rate, period) + limited = true + } + } + } + return limited + } +} \ No newline at end of file diff --git a/src/factories/controllers/get-invoice-pubkey-check-controller-factory.ts b/src/factories/controllers/get-invoice-pubkey-check-controller-factory.ts new file mode 100644 index 00000000..dfefec6f --- /dev/null +++ b/src/factories/controllers/get-invoice-pubkey-check-controller-factory.ts @@ -0,0 +1,16 @@ +import { createSettings } from '../settings-factory' +import { GetInvoicePubkeyCheckController } from '../../controllers/invoices/get-invoice-pubkey-check-controller' +import { getMasterDbClient } from '../../database/client' +import { slidingWindowRateLimiterFactory } from '../rate-limiter-factory' +import { UserRepository } from '../../repositories/user-repository' + +export const createGetInvoicePubkeyCheckController = () => { + const dbClient = getMasterDbClient() + const userRepository = new UserRepository(dbClient) + + return new GetInvoicePubkeyCheckController( + userRepository, + createSettings, + slidingWindowRateLimiterFactory + ) +} diff --git a/src/routes/invoices/index.ts b/src/routes/invoices/index.ts index 5592d964..0e082b9d 100644 --- a/src/routes/invoices/index.ts +++ b/src/routes/invoices/index.ts @@ -1,6 +1,7 @@ import { Router, urlencoded } from 'express' import { createGetInvoiceController } from '../../factories/controllers/get-invoice-controller-factory' +import { createGetInvoicePubkeyCheckController } from '../../factories/controllers/get-invoice-pubkey-check-controller-factory' import { createGetInvoiceStatusController } from '../../factories/controllers/get-invoice-status-controller-factory' import { createPostInvoiceController } from '../../factories/controllers/post-invoice-controller-factory' import { withController } from '../../handlers/request-handlers/with-controller-request-handler' @@ -10,6 +11,7 @@ const invoiceRouter = Router() invoiceRouter .get('/', withController(createGetInvoiceController)) .get('/:invoiceId/status', withController(createGetInvoiceStatusController)) + .get('/check/:pubkey', withController(createGetInvoicePubkeyCheckController)) .post('/', urlencoded({ extended: true }), withController(createPostInvoiceController)) -export default invoiceRouter +export default invoiceRouter \ No newline at end of file From 3f585aa1d110e3111842b4f9f534d5e8ce66e8ae Mon Sep 17 00:00:00 2001 From: Mihajlo Medjedovic Date: Fri, 25 Aug 2023 15:32:51 +0200 Subject: [PATCH 2/7] chore: test --- .../get-invoice-pubkey-check-controller.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/controllers/invoices/get-invoice-pubkey-check-controller.ts b/src/controllers/invoices/get-invoice-pubkey-check-controller.ts index 89eb8b47..7047ccd8 100644 --- a/src/controllers/invoices/get-invoice-pubkey-check-controller.ts +++ b/src/controllers/invoices/get-invoice-pubkey-check-controller.ts @@ -19,14 +19,14 @@ export class GetInvoicePubkeyCheckController implements IController { public async handleRequest(request: Request, response: Response): Promise { const currentSettings = this.settings() - const limited = await this.isRateLimited(request, currentSettings) - if (limited) { - response - .status(429) - .setHeader('content-type', 'text/plain; charset=utf8') - .send('Too many requests') - return - } + // const limited = await this.isRateLimited(request, currentSettings) + // if (limited) { + // response + // .status(429) + // .setHeader('content-type', 'text/plain; charset=utf8') + // .send('Too many requests') + // return + // } const pubkey = request.params.pubkey const user = await this.userRepository.findByPubkey(pubkey) From 0d3b18249d085c84053620143fd0856794d0ded9 Mon Sep 17 00:00:00 2001 From: Mihajlo Medjedovic Date: Fri, 25 Aug 2023 16:12:06 +0200 Subject: [PATCH 3/7] chore: status 200 --- src/controllers/invoices/get-invoice-pubkey-check-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/invoices/get-invoice-pubkey-check-controller.ts b/src/controllers/invoices/get-invoice-pubkey-check-controller.ts index 7047ccd8..d449baab 100644 --- a/src/controllers/invoices/get-invoice-pubkey-check-controller.ts +++ b/src/controllers/invoices/get-invoice-pubkey-check-controller.ts @@ -41,7 +41,7 @@ export class GetInvoicePubkeyCheckController implements IController { } response - .status(400) + .status(200) .setHeader('content-type', 'application/json; charset=utf8') .send({ userAdmitted }) From 97945f6dfe9c158b51aab85cf60d0cae1a38d15c Mon Sep 17 00:00:00 2001 From: Mihajlo Medjedovic Date: Tue, 29 Aug 2023 12:28:43 +0200 Subject: [PATCH 4/7] chore: rate limit enable --- .../get-invoice-pubkey-check-controller.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/controllers/invoices/get-invoice-pubkey-check-controller.ts b/src/controllers/invoices/get-invoice-pubkey-check-controller.ts index d449baab..dda84085 100644 --- a/src/controllers/invoices/get-invoice-pubkey-check-controller.ts +++ b/src/controllers/invoices/get-invoice-pubkey-check-controller.ts @@ -19,14 +19,14 @@ export class GetInvoicePubkeyCheckController implements IController { public async handleRequest(request: Request, response: Response): Promise { const currentSettings = this.settings() - // const limited = await this.isRateLimited(request, currentSettings) - // if (limited) { - // response - // .status(429) - // .setHeader('content-type', 'text/plain; charset=utf8') - // .send('Too many requests') - // return - // } + const limited = await this.isRateLimited(request, currentSettings) + if (limited) { + response + .status(429) + .setHeader('content-type', 'text/plain; charset=utf8') + .send('Too many requests') + return + } const pubkey = request.params.pubkey const user = await this.userRepository.findByPubkey(pubkey) From 0c2ea0e890aab33a86a29c9a7c2a4ece6785e196 Mon Sep 17 00:00:00 2001 From: Mihajlo Medjedovic Date: Wed, 30 Aug 2023 14:07:25 +0200 Subject: [PATCH 5/7] feat: added admission controller --- .../get-admission-check-controller.ts} | 6 ++---- ...ry.ts => get-admission-check-controller-factory.ts} | 6 +++--- src/routes/admissions/index.ts | 10 ++++++++++ src/routes/index.ts | 2 ++ src/routes/invoices/index.ts | 2 -- 5 files changed, 17 insertions(+), 9 deletions(-) rename src/controllers/{invoices/get-invoice-pubkey-check-controller.ts => admission/get-admission-check-controller.ts} (93%) rename src/factories/controllers/{get-invoice-pubkey-check-controller-factory.ts => get-admission-check-controller-factory.ts} (66%) create mode 100644 src/routes/admissions/index.ts diff --git a/src/controllers/invoices/get-invoice-pubkey-check-controller.ts b/src/controllers/admission/get-admission-check-controller.ts similarity index 93% rename from src/controllers/invoices/get-invoice-pubkey-check-controller.ts rename to src/controllers/admission/get-admission-check-controller.ts index dda84085..de464aa4 100644 --- a/src/controllers/invoices/get-invoice-pubkey-check-controller.ts +++ b/src/controllers/admission/get-admission-check-controller.ts @@ -7,9 +7,9 @@ import { IUserRepository } from '../../@types/repositories' import { path } from 'ramda' import { Settings } from '../../@types/settings' -const debug = createLogger('get-invoice-pubkey-check-controller') +const debug = createLogger('get-admission-check-controller') -export class GetInvoicePubkeyCheckController implements IController { +export class GetSubmissionCheckController implements IController { public constructor( private readonly userRepository: IUserRepository, private readonly settings: () => Settings, @@ -31,8 +31,6 @@ export class GetInvoicePubkeyCheckController implements IController { const pubkey = request.params.pubkey const user = await this.userRepository.findByPubkey(pubkey) - console.log('user', user) - let userAdmitted = false const minBalance = currentSettings.limits?.event?.pubkey?.minBalance diff --git a/src/factories/controllers/get-invoice-pubkey-check-controller-factory.ts b/src/factories/controllers/get-admission-check-controller-factory.ts similarity index 66% rename from src/factories/controllers/get-invoice-pubkey-check-controller-factory.ts rename to src/factories/controllers/get-admission-check-controller-factory.ts index dfefec6f..c7d2d47e 100644 --- a/src/factories/controllers/get-invoice-pubkey-check-controller-factory.ts +++ b/src/factories/controllers/get-admission-check-controller-factory.ts @@ -1,14 +1,14 @@ import { createSettings } from '../settings-factory' -import { GetInvoicePubkeyCheckController } from '../../controllers/invoices/get-invoice-pubkey-check-controller' import { getMasterDbClient } from '../../database/client' +import { GetSubmissionCheckController } from '../../controllers/admission/get-admission-check-controller' import { slidingWindowRateLimiterFactory } from '../rate-limiter-factory' import { UserRepository } from '../../repositories/user-repository' -export const createGetInvoicePubkeyCheckController = () => { +export const createGetAdmissionCheckController = () => { const dbClient = getMasterDbClient() const userRepository = new UserRepository(dbClient) - return new GetInvoicePubkeyCheckController( + return new GetSubmissionCheckController( userRepository, createSettings, slidingWindowRateLimiterFactory diff --git a/src/routes/admissions/index.ts b/src/routes/admissions/index.ts new file mode 100644 index 00000000..8e63fb89 --- /dev/null +++ b/src/routes/admissions/index.ts @@ -0,0 +1,10 @@ +import { createGetAdmissionCheckController } from '../../factories/controllers/get-admission-check-controller-factory' +import { Router } from 'express' +import { withController } from '../../handlers/request-handlers/with-controller-request-handler' + +const admissionRouter = Router() + +admissionRouter + .get('/check/:pubkey', withController(createGetAdmissionCheckController)) + +export default admissionRouter \ No newline at end of file diff --git a/src/routes/index.ts b/src/routes/index.ts index 2c1e963f..a9ff0ac0 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,5 +1,6 @@ import express from 'express' +import admissionRouter from './admissions' import callbacksRouter from './callbacks' import { getHealthRequestHandler } from '../handlers/request-handlers/get-health-request-handler' import { getTermsRequestHandler } from '../handlers/request-handlers/get-terms-request-handler' @@ -14,6 +15,7 @@ router.get('/healthz', getHealthRequestHandler) router.get('/terms', getTermsRequestHandler) router.use('/invoices', rateLimiterMiddleware, invoiceRouter) +router.use('/admissions', rateLimiterMiddleware, admissionRouter) router.use('/callbacks', rateLimiterMiddleware, callbacksRouter) export default router diff --git a/src/routes/invoices/index.ts b/src/routes/invoices/index.ts index 0e082b9d..beefa741 100644 --- a/src/routes/invoices/index.ts +++ b/src/routes/invoices/index.ts @@ -1,7 +1,6 @@ import { Router, urlencoded } from 'express' import { createGetInvoiceController } from '../../factories/controllers/get-invoice-controller-factory' -import { createGetInvoicePubkeyCheckController } from '../../factories/controllers/get-invoice-pubkey-check-controller-factory' import { createGetInvoiceStatusController } from '../../factories/controllers/get-invoice-status-controller-factory' import { createPostInvoiceController } from '../../factories/controllers/post-invoice-controller-factory' import { withController } from '../../handlers/request-handlers/with-controller-request-handler' @@ -11,7 +10,6 @@ const invoiceRouter = Router() invoiceRouter .get('/', withController(createGetInvoiceController)) .get('/:invoiceId/status', withController(createGetInvoiceStatusController)) - .get('/check/:pubkey', withController(createGetInvoicePubkeyCheckController)) .post('/', urlencoded({ extended: true }), withController(createPostInvoiceController)) export default invoiceRouter \ No newline at end of file From 428262c19e63013042f66abcb3df64e4e02585b8 Mon Sep 17 00:00:00 2001 From: Mihajlo Medjedovic Date: Tue, 12 Sep 2023 11:08:22 +0200 Subject: [PATCH 6/7] chore: admission check as a separate limits configuration --- CONFIGURATION.md | 3 +++ resources/default-settings.yaml | 9 +++++++++ src/@types/settings.ts | 6 ++++++ .../admission/get-admission-check-controller.ts | 6 +++--- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 4edec215..adcc4de6 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -112,3 +112,6 @@ Running `nostream` for the first time creates the settings file in ` Date: Fri, 12 Jan 2024 19:52:15 +0100 Subject: [PATCH 7/7] chore: style --- src/routes/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/index.ts b/src/routes/index.ts index 33541fcb..0e291f94 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,7 +1,7 @@ import express from 'express' -import admissionRouter from './admissions' import { nodeinfo21Handler, nodeinfoHandler } from '../handlers/request-handlers/nodeinfo-handler' +import admissionRouter from './admissions' import callbacksRouter from './callbacks' import { getHealthRequestHandler } from '../handlers/request-handlers/get-health-request-handler' import { getTermsRequestHandler } from '../handlers/request-handlers/get-terms-request-handler'