Skip to content

Commit ed30823

Browse files
authored
Admission check endpoint (#338)
1 parent fa99657 commit ed30823

File tree

8 files changed

+117
-1
lines changed

8 files changed

+117
-1
lines changed

CONFIGURATION.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,6 @@ Running `nostream` for the first time creates the settings file in `<project_roo
112112
| limits.message.rateLimits[].period | Rate limit period in milliseconds. |
113113
| limits.message.rateLimits[].rate | Maximum number of messages during period. |
114114
| limits.message.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
115+
| limits.admissionCheck.rateLimits[].period | Rate limit period in milliseconds. |
116+
| limits.admissionCheck.rateLimits[].rate | Maximum number of admission checks during period. |
117+
| limits.admissionCheck.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |

resources/default-settings.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ limits:
5656
- "::1"
5757
- "10.10.10.1"
5858
- "::ffff:10.10.10.1"
59+
admissionCheck:
60+
rateLimits:
61+
- description: 30 admission checks/min or 1 check every 2 seconds
62+
period: 60000
63+
rate: 30
64+
ipWhitelist:
65+
- "::1"
66+
- "10.10.10.1"
67+
- "::ffff:10.10.10.1"
5968
connection:
6069
rateLimits:
6170
- period: 1000

src/@types/settings.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,14 @@ export interface InvoiceLimits {
112112
ipWhitelist?: string[]
113113
}
114114

115+
export interface AdmissionCheckLimits {
116+
rateLimits: RateLimit[]
117+
ipWhitelist?: string[]
118+
}
119+
115120
export interface Limits {
116121
invoice?: InvoiceLimits
122+
admissionCheck?: AdmissionCheckLimits
117123
connection?: ConnectionLimits
118124
client?: ClientLimits
119125
event?: EventLimits
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Request, Response } from 'express'
2+
import { createLogger } from '../../factories/logger-factory'
3+
import { getRemoteAddress } from '../../utils/http'
4+
import { IController } from '../../@types/controllers'
5+
import { IRateLimiter } from '../../@types/utils'
6+
import { IUserRepository } from '../../@types/repositories'
7+
import { path } from 'ramda'
8+
import { Settings } from '../../@types/settings'
9+
10+
const debug = createLogger('get-admission-check-controller')
11+
12+
export class GetSubmissionCheckController implements IController {
13+
public constructor(
14+
private readonly userRepository: IUserRepository,
15+
private readonly settings: () => Settings,
16+
private readonly rateLimiter: () => IRateLimiter,
17+
){}
18+
19+
public async handleRequest(request: Request, response: Response): Promise<void> {
20+
const currentSettings = this.settings()
21+
22+
const limited = await this.isRateLimited(request, currentSettings)
23+
if (limited) {
24+
response
25+
.status(429)
26+
.setHeader('content-type', 'text/plain; charset=utf8')
27+
.send('Too many requests')
28+
return
29+
}
30+
31+
const pubkey = request.params.pubkey
32+
const user = await this.userRepository.findByPubkey(pubkey)
33+
34+
let userAdmitted = false
35+
36+
const minBalance = currentSettings.limits?.event?.pubkey?.minBalance
37+
if (user && user.isAdmitted && (!minBalance || user.balance >= minBalance)) {
38+
userAdmitted = true
39+
}
40+
41+
response
42+
.status(200)
43+
.setHeader('content-type', 'application/json; charset=utf8')
44+
.send({ userAdmitted })
45+
46+
return
47+
}
48+
49+
public async isRateLimited(request: Request, settings: Settings) {
50+
const rateLimits = path(['limits', 'admissionCheck', 'rateLimits'], settings)
51+
if (!Array.isArray(rateLimits) || !rateLimits.length) {
52+
return false
53+
}
54+
55+
const ipWhitelist = path(['limits', 'admissionCheck', 'ipWhitelist'], settings)
56+
const remoteAddress = getRemoteAddress(request, settings)
57+
58+
let limited = false
59+
if (Array.isArray(ipWhitelist) && !ipWhitelist.includes(remoteAddress)) {
60+
const rateLimiter = this.rateLimiter()
61+
for (const { rate, period } of rateLimits) {
62+
if (await rateLimiter.hit(`${remoteAddress}:admission-check:${period}`, 1, { period, rate })) {
63+
debug('rate limited %s: %d in %d milliseconds', remoteAddress, rate, period)
64+
limited = true
65+
}
66+
}
67+
}
68+
return limited
69+
}
70+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { createSettings } from '../settings-factory'
2+
import { getMasterDbClient } from '../../database/client'
3+
import { GetSubmissionCheckController } from '../../controllers/admission/get-admission-check-controller'
4+
import { slidingWindowRateLimiterFactory } from '../rate-limiter-factory'
5+
import { UserRepository } from '../../repositories/user-repository'
6+
7+
export const createGetAdmissionCheckController = () => {
8+
const dbClient = getMasterDbClient()
9+
const userRepository = new UserRepository(dbClient)
10+
11+
return new GetSubmissionCheckController(
12+
userRepository,
13+
createSettings,
14+
slidingWindowRateLimiterFactory
15+
)
16+
}

src/routes/admissions/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { createGetAdmissionCheckController } from '../../factories/controllers/get-admission-check-controller-factory'
2+
import { Router } from 'express'
3+
import { withController } from '../../handlers/request-handlers/with-controller-request-handler'
4+
5+
const admissionRouter = Router()
6+
7+
admissionRouter
8+
.get('/check/:pubkey', withController(createGetAdmissionCheckController))
9+
10+
export default admissionRouter

src/routes/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import express from 'express'
22

33
import { nodeinfo21Handler, nodeinfoHandler } from '../handlers/request-handlers/nodeinfo-handler'
4+
import admissionRouter from './admissions'
45
import callbacksRouter from './callbacks'
56
import { getHealthRequestHandler } from '../handlers/request-handlers/get-health-request-handler'
67
import { getTermsRequestHandler } from '../handlers/request-handlers/get-terms-request-handler'
@@ -19,6 +20,7 @@ router.get('/nodeinfo/2.1', nodeinfo21Handler)
1920
router.get('/nodeinfo/2.0', nodeinfo21Handler)
2021

2122
router.use('/invoices', rateLimiterMiddleware, invoiceRouter)
23+
router.use('/admissions', rateLimiterMiddleware, admissionRouter)
2224
router.use('/callbacks', rateLimiterMiddleware, callbacksRouter)
2325

2426
export default router

src/routes/invoices/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ invoiceRouter
1212
.get('/:invoiceId/status', withController(createGetInvoiceStatusController))
1313
.post('/', urlencoded({ extended: true }), withController(createPostInvoiceController))
1414

15-
export default invoiceRouter
15+
export default invoiceRouter

0 commit comments

Comments
 (0)