From ed308235115b56777c4cbca7bbc612a5164cb18e Mon Sep 17 00:00:00 2001 From: Mihajlo Medjedovic Date: Fri, 12 Jan 2024 21:43:33 +0100 Subject: [PATCH] Admission check endpoint (#338) --- CONFIGURATION.md | 3 + resources/default-settings.yaml | 9 +++ src/@types/settings.ts | 6 ++ .../get-admission-check-controller.ts | 70 +++++++++++++++++++ .../get-admission-check-controller-factory.ts | 16 +++++ src/routes/admissions/index.ts | 10 +++ src/routes/index.ts | 2 + src/routes/invoices/index.ts | 2 +- 8 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 src/controllers/admission/get-admission-check-controller.ts create mode 100644 src/factories/controllers/get-admission-check-controller-factory.ts create mode 100644 src/routes/admissions/index.ts 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 ` 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) + + let userAdmitted = false + + const minBalance = currentSettings.limits?.event?.pubkey?.minBalance + if (user && user.isAdmitted && (!minBalance || user.balance >= minBalance)) { + userAdmitted = true + } + + response + .status(200) + .setHeader('content-type', 'application/json; charset=utf8') + .send({ userAdmitted }) + + return + } + + public async isRateLimited(request: Request, settings: Settings) { + const rateLimits = path(['limits', 'admissionCheck', 'rateLimits'], settings) + if (!Array.isArray(rateLimits) || !rateLimits.length) { + return false + } + + const ipWhitelist = path(['limits', 'admissionCheck', '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}:admission-check:${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-admission-check-controller-factory.ts b/src/factories/controllers/get-admission-check-controller-factory.ts new file mode 100644 index 00000000..c7d2d47e --- /dev/null +++ b/src/factories/controllers/get-admission-check-controller-factory.ts @@ -0,0 +1,16 @@ +import { createSettings } from '../settings-factory' +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 createGetAdmissionCheckController = () => { + const dbClient = getMasterDbClient() + const userRepository = new UserRepository(dbClient) + + 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 04d288e2..0e291f94 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,6 +1,7 @@ import express from 'express' 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' @@ -19,6 +20,7 @@ router.get('/nodeinfo/2.1', nodeinfo21Handler) router.get('/nodeinfo/2.0', nodeinfo21Handler) 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 5592d964..beefa741 100644 --- a/src/routes/invoices/index.ts +++ b/src/routes/invoices/index.ts @@ -12,4 +12,4 @@ invoiceRouter .get('/:invoiceId/status', withController(createGetInvoiceStatusController)) .post('/', urlencoded({ extended: true }), withController(createPostInvoiceController)) -export default invoiceRouter +export default invoiceRouter \ No newline at end of file