mirror of
https://github.com/Cameri/nostream.git
synced 2025-03-17 13:21:45 +01:00
Admission check endpoint (#338)
This commit is contained in:
parent
fa99657b4a
commit
ed30823511
@ -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. |
|
@ -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
|
||||
|
@ -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
|
||||
|
70
src/controllers/admission/get-admission-check-controller.ts
Normal file
70
src/controllers/admission/get-admission-check-controller.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
10
src/routes/admissions/index.ts
Normal file
10
src/routes/admissions/index.ts
Normal 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
|
@ -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
|
||||
|
@ -12,4 +12,4 @@ invoiceRouter
|
||||
.get('/:invoiceId/status', withController(createGetInvoiceStatusController))
|
||||
.post('/', urlencoded({ extended: true }), withController(createPostInvoiceController))
|
||||
|
||||
export default invoiceRouter
|
||||
export default invoiceRouter
|
Loading…
x
Reference in New Issue
Block a user