Admission check endpoint (#338)

This commit is contained in:
Mihajlo Medjedovic 2024-01-12 21:43:33 +01:00 committed by GitHub
parent fa99657b4a
commit ed30823511
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 117 additions and 1 deletions

View File

@ -112,3 +112,6 @@ Running `nostream` for the first time creates the settings file in `<project_roo
| limits.message.rateLimits[].period | Rate limit period in milliseconds. |
| limits.message.rateLimits[].rate | Maximum number of messages during period. |
| limits.message.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
| limits.admissionCheck.rateLimits[].period | Rate limit period in milliseconds. |
| limits.admissionCheck.rateLimits[].rate | Maximum number of admission checks during period. |
| limits.admissionCheck.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |

View File

@ -56,6 +56,15 @@ limits:
- "::1"
- "10.10.10.1"
- "::ffff:10.10.10.1"
admissionCheck:
rateLimits:
- description: 30 admission checks/min or 1 check every 2 seconds
period: 60000
rate: 30
ipWhitelist:
- "::1"
- "10.10.10.1"
- "::ffff:10.10.10.1"
connection:
rateLimits:
- period: 1000

View File

@ -112,8 +112,14 @@ export interface InvoiceLimits {
ipWhitelist?: string[]
}
export interface AdmissionCheckLimits {
rateLimits: RateLimit[]
ipWhitelist?: string[]
}
export interface Limits {
invoice?: InvoiceLimits
admissionCheck?: AdmissionCheckLimits
connection?: ConnectionLimits
client?: ClientLimits
event?: EventLimits

View File

@ -0,0 +1,70 @@
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-admission-check-controller')
export class GetSubmissionCheckController implements IController {
public constructor(
private readonly userRepository: IUserRepository,
private readonly settings: () => Settings,
private readonly rateLimiter: () => IRateLimiter,
){}
public async handleRequest(request: Request, response: Response): Promise<void> {
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
}
}

View File

@ -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
)
}

View File

@ -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

View File

@ -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

View File

@ -12,4 +12,4 @@ invoiceRouter
.get('/:invoiceId/status', withController(createGetInvoiceStatusController))
.post('/', urlencoded({ extended: true }), withController(createPostInvoiceController))
export default invoiceRouter
export default invoiceRouter