Skip to content

Commit 61e6c82

Browse files
authored
Fix rate limiter on number of requests & connections (#986)
* Fix rate limiter on number of requests. * Created new util function for checking global connections. Implemented the rate limiters checks for P2P protocol as well. * Updated imports.
1 parent 2c7e3c6 commit 61e6c82

7 files changed

Lines changed: 132 additions & 86 deletions

File tree

src/components/Auth/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@ import jwt from 'jsonwebtoken'
33
import { checkNonce, NonceResponse } from '../core/utils/nonceHandler.js'
44
import { OceanNode } from '../../OceanNode.js'
55
import { getConfiguration } from '../../utils/index.js'
6-
7-
export interface CommonValidation {
8-
valid: boolean
9-
error: string
10-
}
6+
import { CommonValidation } from '../../utils/validators.js'
117

128
export interface AuthValidation {
139
token?: string

src/components/P2P/handleProtocolCommands.ts

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,10 @@ import { GENERIC_EMOJIS, LOG_LEVELS_STR } from '../../utils/logging/Logger.js'
99
import StreamConcat from 'stream-concat'
1010
import { BaseHandler } from '../core/handler/handler.js'
1111
import { getConfiguration } from '../../utils/index.js'
12-
import { checkConnectionsRateLimit } from '../httpRoutes/requestValidator.js'
13-
import { CONNECTIONS_RATE_INTERVAL } from '../../utils/constants.js'
14-
import { RequestLimiter } from '../../OceanNode.js'
15-
16-
// hold data about last request made
17-
const connectionsData: RequestLimiter = {
18-
lastRequestTime: Date.now(),
19-
requester: '',
20-
numRequests: 0
21-
}
12+
import {
13+
checkGlobalConnectionsRateLimit,
14+
checkRequestsRateLimit
15+
} from '../../utils/validators.js'
2216

2317
export class ReadableString extends Readable {
2418
private sent = false
@@ -94,22 +88,32 @@ export async function handleProtocolCommands(otherPeerConnection: any) {
9488
}
9589
}
9690
// check connections rate limit
97-
const requestTime = Date.now()
98-
if (requestTime - connectionsData.lastRequestTime > CONNECTIONS_RATE_INTERVAL) {
99-
// last one was more than 1 minute ago? reset counter
100-
connectionsData.numRequests = 0
91+
const now = Date.now()
92+
93+
const rateLimitCheck = checkRequestsRateLimit(remoteAddr, configuration, now)
94+
if (!rateLimitCheck.valid) {
95+
P2P_LOGGER.warn(
96+
`Incoming request denied to peer: ${remotePeer} (rate limit exceeded)`
97+
)
98+
if (connectionStatus === 'open') {
99+
statusStream = new ReadableString(
100+
JSON.stringify(buildWrongCommandStatus(403, 'Rate limit exceeded'))
101+
)
102+
try {
103+
await pipe(statusStream, otherPeerConnection.stream.sink)
104+
} catch (e) {
105+
P2P_LOGGER.error(e)
106+
}
107+
}
108+
await closeStreamConnection(otherPeerConnection.connection, remotePeer)
109+
return
101110
}
102-
// always increment counter
103-
connectionsData.numRequests += 1
104-
// update time and requester information
105-
connectionsData.lastRequestTime = requestTime
106-
connectionsData.requester = remoteAddr
107111

108112
// check global rate limits (not ip related)
109-
const requestRateValidation = checkConnectionsRateLimit(configuration, connectionsData)
110-
if (!requestRateValidation.valid) {
113+
const connectionsRateValidation = checkGlobalConnectionsRateLimit(configuration, now)
114+
if (!connectionsRateValidation.valid) {
111115
P2P_LOGGER.warn(
112-
`Incoming request denied to peer: ${remotePeer} (rate limit exceeded)`
116+
`Exceeded limit of connections per minute ${configuration.maxConnections}: ${connectionsRateValidation.error}`
113117
)
114118
if (connectionStatus === 'open') {
115119
statusStream = new ReadableString(

src/components/core/admin/adminHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import {
88
} from '../../httpRoutes/validateCommands.js'
99
import { validateAdminSignature } from '../../../utils/auth.js'
1010
import { BaseHandler } from '../handler/handler.js'
11-
import { CommonValidation } from '../../httpRoutes/requestValidator.js'
1211
import { P2PCommandResponse } from '../../../@types/OceanNode.js'
1312
import { ReadableString } from '../../P2P/handleProtocolCommands.js'
13+
import { CommonValidation } from '../../../utils/validators.js'
1414

1515
export abstract class AdminCommandHandler
1616
extends BaseHandler

src/components/httpRoutes/logs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import express from 'express'
22
import { validateAdminSignature } from '../../utils/auth.js'
33
import { HTTP_LOGGER } from '../../utils/logging/common.js'
4-
import { CommonValidation } from './requestValidator.js'
4+
import { CommonValidation } from '../../utils/validators.js'
55

66
export const logRoutes = express.Router()
77

src/components/httpRoutes/requestValidator.ts

Lines changed: 24 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,74 +2,42 @@ import { Request, Response } from 'express'
22
import { getConfiguration } from '../../utils/config.js'
33
import { HTTP_LOGGER } from '../../utils/logging/common.js'
44
import { OceanNodeConfig } from '../../@types/OceanNode.js'
5-
import { RequestLimiter } from '../../OceanNode.js'
65
import {
7-
CONNECTIONS_RATE_INTERVAL,
8-
DEFAULT_MAX_CONNECTIONS_PER_MINUTE
9-
} from '../../utils/constants.js'
6+
checkGlobalConnectionsRateLimit,
7+
checkRequestsRateLimit,
8+
CommonValidation
9+
} from '../../utils/validators.js'
1010

11-
// TODO we should group common stuff,
12-
// we have multiple similar validation interfaces
13-
export interface CommonValidation {
14-
valid: boolean
15-
error?: string
16-
}
17-
18-
// hold data about last request made
19-
const connectionsData: RequestLimiter = {
20-
lastRequestTime: Date.now(),
21-
requester: '',
22-
numRequests: 0
23-
}
24-
25-
// midleware to validate client addresses against a denylist
26-
// it also checks the global rate limit
11+
// Middleware to validate IP and apply rate limiting
2712
export const requestValidator = async function (req: Request, res: Response, next: any) {
28-
// Perform the validations.
29-
const requestIP = req.headers['x-forwarded-for'] || req.socket.remoteAddress
30-
31-
// grab request time
32-
const requestTime = Date.now()
33-
if (requestTime - connectionsData.lastRequestTime > CONNECTIONS_RATE_INTERVAL) {
34-
// last one was more than 1 minute ago? reset counter
35-
connectionsData.numRequests = 0
36-
}
37-
// always increment counter
38-
connectionsData.numRequests += 1
39-
// update time and requester information
40-
connectionsData.lastRequestTime = requestTime
41-
connectionsData.requester = requestIP
13+
const now = Date.now()
14+
const requestIP = (req.headers['x-forwarded-for'] ||
15+
req.socket.remoteAddress ||
16+
'') as string
4217

4318
const configuration = await getConfiguration()
4419

45-
// check if IP is allowed or denied
4620
const ipValidation = await checkIP(requestIP, configuration)
47-
// Validation failed, or an error occurred during the external request.
4821
if (!ipValidation.valid) {
49-
res.status(403).send(ipValidation.error)
50-
return
22+
HTTP_LOGGER.logMessage(`IP denied: ${ipValidation.error}`)
23+
return res.status(403).send(ipValidation.error)
5124
}
52-
// check global rate limits (not ip related)
53-
const requestRateValidation = checkConnectionsRateLimit(configuration, connectionsData)
54-
if (!requestRateValidation.valid) {
55-
res.status(403).send(requestRateValidation.error)
56-
return
25+
26+
const rateLimitCheck = checkRequestsRateLimit(requestIP, configuration, now)
27+
if (!rateLimitCheck.valid) {
28+
HTTP_LOGGER.logMessage(
29+
`Exceeded limit of requests per minute ${configuration.rateLimit}: ${rateLimitCheck.error}`
30+
)
31+
return res.status(429).send(rateLimitCheck.error)
5732
}
58-
// Validation passed.
59-
next()
60-
}
6133

62-
export function checkConnectionsRateLimit(
63-
configuration: OceanNodeConfig,
64-
connectionsData: RequestLimiter
65-
): CommonValidation {
66-
const connectionLimits =
67-
configuration.maxConnections || DEFAULT_MAX_CONNECTIONS_PER_MINUTE
68-
const ok = connectionsData.numRequests <= connectionLimits
69-
return {
70-
valid: ok,
71-
error: ok ? '' : 'Unauthorized request. Rate limit exceeded!'
34+
const connectionsRateValidation = checkGlobalConnectionsRateLimit(configuration, now)
35+
if (!connectionsRateValidation.valid) {
36+
res.status(403).send(connectionsRateValidation.error)
37+
return
7238
}
39+
40+
next()
7341
}
7442

7543
function checkIP(

src/utils/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import AccessListContract from '@oceanprotocol/contracts/artifacts/contracts/acc
77
import { getAccountsFromAccessList } from '../utils/credentials.js'
88
import { OceanNodeConfig } from '../@types/OceanNode.js'
99
import { LOG_LEVELS_STR } from './logging/Logger.js'
10-
import { CommonValidation } from '../components/httpRoutes/requestValidator.js'
10+
import { CommonValidation } from './validators.js'
1111
export async function validateAdminSignature(
1212
expiryTimestamp: number,
1313
signature: string

src/utils/validators.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
import { ConsumerParameter } from '../@types/DDO/ConsumerParameter.js'
2+
import { OceanNodeConfig } from '../@types/OceanNode.js'
23
import { ValidateParams } from '../components/httpRoutes/validateCommands.js'
4+
import { RequestLimiter } from '../OceanNode.js'
35
import { CORE_LOGGER } from './logging/common.js'
6+
import {
7+
CONNECTIONS_RATE_INTERVAL,
8+
DEFAULT_MAX_CONNECTIONS_PER_MINUTE
9+
} from './constants.js'
10+
11+
// TODO we should group common stuff,
12+
// we have multiple similar validation interfaces
13+
export interface CommonValidation {
14+
valid: boolean
15+
error?: string
16+
}
17+
18+
// hold data about last request made
19+
const connectionsData = new Map<string, RequestLimiter>()
420

521
function checkString(value: any) {
622
return typeof value === 'string' || value instanceof String
@@ -18,6 +34,68 @@ function checkObject(value: any) {
1834
return typeof value === 'object' && value !== null && !Array.isArray(value)
1935
}
2036

37+
export function checkRequestsRateLimit(
38+
requestIP: string,
39+
configuration: OceanNodeConfig,
40+
now: number
41+
): CommonValidation {
42+
const limit = configuration.rateLimit
43+
let clientData = connectionsData.get(requestIP)
44+
45+
if (!clientData || clientData === undefined) {
46+
clientData = {
47+
lastRequestTime: now,
48+
requester: requestIP,
49+
numRequests: 1
50+
}
51+
connectionsData.set(requestIP, clientData)
52+
return { valid: true, error: '' }
53+
}
54+
55+
const timeSinceLastRequest = now - clientData.lastRequestTime
56+
const windowExpired = timeSinceLastRequest > CONNECTIONS_RATE_INTERVAL
57+
58+
if (clientData.numRequests >= limit && !windowExpired) {
59+
const waitTime = Math.ceil((CONNECTIONS_RATE_INTERVAL - timeSinceLastRequest) / 1000)
60+
return {
61+
valid: false,
62+
error: `Rate limit exceeded. Try again in ${waitTime} seconds.`
63+
}
64+
}
65+
66+
if (windowExpired) {
67+
clientData.numRequests = 1
68+
clientData.lastRequestTime = now
69+
return { valid: true, error: '' }
70+
}
71+
72+
clientData.numRequests += 1
73+
return { valid: true, error: '' }
74+
}
75+
76+
export function checkGlobalConnectionsRateLimit(
77+
configuration: OceanNodeConfig,
78+
now: number
79+
): CommonValidation {
80+
const maxConnections =
81+
configuration.maxConnections || DEFAULT_MAX_CONNECTIONS_PER_MINUTE
82+
let activeRequesters = 0
83+
84+
for (const [, clientData] of connectionsData.entries()) {
85+
if (now - clientData.lastRequestTime <= CONNECTIONS_RATE_INTERVAL) {
86+
activeRequesters += 1
87+
}
88+
}
89+
const valid = activeRequesters <= maxConnections
90+
91+
return {
92+
valid,
93+
error: valid
94+
? ''
95+
: `Too many active connections (${activeRequesters}/${maxConnections}) in the last minute.`
96+
}
97+
}
98+
2199
export function validateConsumerParameters(
22100
ddoConsumerParameters: ConsumerParameter | ConsumerParameter[],
23101
userSentObject: any | any[]

0 commit comments

Comments
 (0)