Skip to content

Commit

Permalink
Admission check endpoint (#338)
Browse files Browse the repository at this point in the history
  • Loading branch information
medjedovicm authored Jan 12, 2024
1 parent fa99657 commit ed30823
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 1 deletion.
3 changes: 3 additions & 0 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
9 changes: 9 additions & 0 deletions resources/default-settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/@types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions src/controllers/admission/get-admission-check-controller.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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 changes: 10 additions & 0 deletions src/routes/admissions/index.ts
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
2 changes: 1 addition & 1 deletion src/routes/invoices/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ invoiceRouter
.get('/:invoiceId/status', withController(createGetInvoiceStatusController))
.post('/', urlencoded({ extended: true }), withController(createPostInvoiceController))

export default invoiceRouter
export default invoiceRouter

0 comments on commit ed30823

Please sign in to comment.