diff --git a/.env.example b/.env.example index 264e0dfdc..67a9312fe 100644 --- a/.env.example +++ b/.env.example @@ -27,7 +27,7 @@ export INDEXER_INTERVAL= export ALLOWED_ADMINS= export DASHBOARD=true export RATE_DENY_LIST= -export MAX_REQ_PER_SECOND= +export MAX_REQ_PER_MINUTE= export MAX_CHECKSUM_LENGTH= export LOG_LEVEL= export HTTP_API_PORT= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b71cd3e0b..14f83117f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -278,6 +278,8 @@ jobs: P2P_ENABLE_AUTONAT: 'false' ALLOWED_ADMINS: '["0xe2DD09d719Da89e5a3D0F2549c7E24566e947260"]' DB_TYPE: 'elasticsearch' + MAX_REQ_PER_MINUTE: 320 + MAX_CONNECTIONS_PER_MINUTE: 320 - name: Check Ocean Node is running run: | for i in $(seq 1 90); do diff --git a/docs/dockerDeployment.md b/docs/dockerDeployment.md index ef1d675d9..dfa3d3393 100644 --- a/docs/dockerDeployment.md +++ b/docs/dockerDeployment.md @@ -99,7 +99,7 @@ services: # INDEXER_INTERVAL: '' DASHBOARD: 'true' # RATE_DENY_LIST: '' - # MAX_REQ_PER_SECOND: '' + # MAX_REQ_PER_MINUTE: '' # MAX_CHECKSUM_LENGTH: '' # LOG_LEVEL: '' HTTP_API_PORT: '8000' diff --git a/docs/env.md b/docs/env.md index 9c718177c..d23b46512 100644 --- a/docs/env.md +++ b/docs/env.md @@ -23,7 +23,8 @@ Environmental variables are also tracked in `ENVIRONMENT_VARIABLES` within `src/ - `ALLOWED_ADMINS`: Sets the public address of accounts which have access to admin endpoints e.g. shutting down the node. Example: `"[\"0x967da4048cD07aB37855c090aAF366e4ce1b9F48\",\"0x388C818CA8B9251b393131C08a736A67ccB19297\"]"` - `DASHBOARD`: If `false` the dashboard will not run. If not set or `true` the dashboard will start with the node. Example: `false` - `RATE_DENY_LIST`: Blocked list of IPs and peer IDs. Example: `"{ \"peers\": [\"16Uiu2HAkuYfgjXoGcSSLSpRPD6XtUgV71t5RqmTmcqdbmrWY9MJo\"], \"ips\": [\"127.0.0.1\"] }"` -- `MAX_REQ_PER_SECOND`: Number of requests per second allowed by the same client. Example: `3` +- `MAX_REQ_PER_MINUTE`: Number of requests per minute allowed by the same client (IP or Peer id). Example: `30` +- `MAX_CONNECTIONS_PER_MINUTE`: Max number of requests allowed per minute (all clients). Example: `120` - `MAX_CHECKSUM_LENGTH`: Define the maximum length for a file if checksum is required (Mb). Example: `10` - `IS_BOOTSTRAP`: Is this node to be used as bootstrap node or not. Default is `false`. diff --git a/scripts/ocean-node-quickstart.sh b/scripts/ocean-node-quickstart.sh index 4c3085338..7eb5145a5 100755 --- a/scripts/ocean-node-quickstart.sh +++ b/scripts/ocean-node-quickstart.sh @@ -155,7 +155,8 @@ services: # INDEXER_INTERVAL: '' DASHBOARD: 'true' # RATE_DENY_LIST: '' -# MAX_REQ_PER_SECOND: '' +# MAX_REQ_PER_MINUTE: '' +# MAX_CONNECTIONS_PER_MINUTE: '' # MAX_CHECKSUM_LENGTH: '' # LOG_LEVEL: '' HTTP_API_PORT: '$HTTP_API_PORT' diff --git a/src/@types/OceanNode.ts b/src/@types/OceanNode.ts index f221a30d8..d213939ff 100644 --- a/src/@types/OceanNode.ts +++ b/src/@types/OceanNode.ts @@ -91,7 +91,8 @@ export interface OceanNodeConfig { assetPurgatoryUrl: string allowedAdmins?: string[] codeHash?: string - rateLimit?: number + rateLimit?: number // per request ip or peer + maxConnections?: number // global, regardless of client address(es) denyList?: DenyList unsafeURLs?: string[] isBootstrap?: boolean diff --git a/src/OceanNode.ts b/src/OceanNode.ts index ba40feac9..e3dff43d0 100644 --- a/src/OceanNode.ts +++ b/src/OceanNode.ts @@ -11,6 +11,17 @@ import { pipe } from 'it-pipe' import { GENERIC_EMOJIS, LOG_LEVELS_STR } from './utils/logging/Logger.js' import { Handler } from './components/core/handler/handler.js' import { C2DEngines } from './components/c2d/compute_engines.js' + +export interface RequestLimiter { + requester: string | string[] // IP address or peer ID + lastRequestTime: number // time of the last request done (in miliseconds) + numRequests: number // number of requests done in the specific time period +} + +export interface RequestDataCheck { + valid: boolean + updatedRequestData: RequestLimiter +} export class OceanNode { // eslint-disable-next-line no-use-before-define private static instance: OceanNode @@ -20,6 +31,7 @@ export class OceanNode { private c2dEngines: C2DEngines // requester private remoteCaller: string | string[] + private requestMap: Map // eslint-disable-next-line no-useless-constructor private constructor( private db?: Database, @@ -28,6 +40,7 @@ export class OceanNode { private indexer?: OceanIndexer ) { this.coreHandlers = CoreHandlersRegistry.getInstance(this) + this.requestMap = new Map() if (node) { node.setCoreHandlers(this.coreHandlers) } @@ -95,6 +108,14 @@ export class OceanNode { return this.remoteCaller } + public getRequestMapSize(): number { + return this.requestMap.size + } + + public getRequestMap(): Map { + return this.requestMap + } + /** * Use this method to direct calls to the node as node cannot dial into itself * @param message command message diff --git a/src/components/P2P/handleProtocolCommands.ts b/src/components/P2P/handleProtocolCommands.ts index 924b47f05..11e290c0e 100644 --- a/src/components/P2P/handleProtocolCommands.ts +++ b/src/components/P2P/handleProtocolCommands.ts @@ -9,6 +9,16 @@ import { GENERIC_EMOJIS, LOG_LEVELS_STR } from '../../utils/logging/Logger.js' import StreamConcat from 'stream-concat' import { Handler } from '../core/handler/handler.js' import { getConfiguration } from '../../utils/index.js' +import { checkConnectionsRateLimit } from '../httpRoutes/requestValidator.js' +import { CONNECTIONS_RATE_INTERVAL } from '../../utils/constants.js' +import { RequestLimiter } from '../../OceanNode.js' + +// hold data about last request made +const connectionsData: RequestLimiter = { + lastRequestTime: Date.now(), + requester: '', + numRequests: 0 +} export class ReadableString extends Readable { private sent = false @@ -60,10 +70,14 @@ export async function handleProtocolCommands(otherPeerConnection: any) { return status } - const denyList = await (await getConfiguration()).denyList + const configuration = await getConfiguration() + // check deny list configs + const { denyList } = configuration if (denyList.peers.length > 0) { if (denyList.peers.includes(remotePeer.toString())) { - P2P_LOGGER.error(`Incoming request denied to peer: ${remotePeer}`) + P2P_LOGGER.warn( + `Incoming request denied to peer: ${remotePeer} (peer its on deny list)` + ) if (connectionStatus === 'open') { statusStream = new ReadableString( @@ -79,6 +93,37 @@ export async function handleProtocolCommands(otherPeerConnection: any) { return } } + // check connections rate limit + const requestTime = Date.now() + if (requestTime - connectionsData.lastRequestTime > CONNECTIONS_RATE_INTERVAL) { + // last one was more than 1 minute ago? reset counter + connectionsData.numRequests = 0 + } + // always increment counter + connectionsData.numRequests += 1 + // update time and requester information + connectionsData.lastRequestTime = requestTime + connectionsData.requester = remoteAddr + + // check global rate limits (not ip related) + const requestRateValidation = checkConnectionsRateLimit(configuration, connectionsData) + if (!requestRateValidation.valid) { + P2P_LOGGER.warn( + `Incoming request denied to peer: ${remotePeer} (rate limit exceeded)` + ) + if (connectionStatus === 'open') { + statusStream = new ReadableString( + JSON.stringify(buildWrongCommandStatus(403, 'Rate limit exceeded')) + ) + try { + await pipe(statusStream, otherPeerConnection.stream.sink) + } catch (e) { + P2P_LOGGER.error(e) + } + } + await closeStreamConnection(otherPeerConnection.connection, remotePeer) + return + } try { // eslint-disable-next-line no-unreachable-loop diff --git a/src/components/core/handler/handler.ts b/src/components/core/handler/handler.ts index 93aafdc94..49b3b3c75 100644 --- a/src/components/core/handler/handler.ts +++ b/src/components/core/handler/handler.ts @@ -1,5 +1,5 @@ import { P2PCommandResponse } from '../../../@types/OceanNode.js' -import { OceanNode } from '../../../OceanNode.js' +import { OceanNode, RequestDataCheck, RequestLimiter } from '../../../OceanNode.js' import { Command, ICommandHandler } from '../../../@types/commands.js' import { ValidateParams, @@ -9,23 +9,12 @@ import { import { getConfiguration } from '../../../utils/index.js' import { CORE_LOGGER } from '../../../utils/logging/common.js' import { ReadableString } from '../../P2P/handlers.js' +import { CONNECTION_HISTORY_DELETE_THRESHOLD } from '../../../utils/constants.js' -export interface RequestLimiter { - requester: string | string[] // IP address or peer ID - lastRequestTime: number // time of the last request done (in miliseconds) - numRequests: number // number of requests done in the specific time period -} - -export interface RequestDataCheck { - valid: boolean - updatedRequestData: RequestLimiter -} export abstract class Handler implements ICommandHandler { - private nodeInstance?: OceanNode - private requestMap: Map + private nodeInstance: OceanNode public constructor(oceanNode: OceanNode) { this.nodeInstance = oceanNode - this.requestMap = new Map() } abstract validate(command: Command): ValidateParams @@ -38,33 +27,40 @@ export abstract class Handler implements ICommandHandler { // TODO LOG, implement all handlers async checkRateLimit(): Promise { - const ratePerSecond = (await getConfiguration()).rateLimit + const requestMap = this.getOceanNode().getRequestMap() + const ratePerMinute = (await getConfiguration()).rateLimit const caller: string | string[] = this.getOceanNode().getRemoteCaller() const requestTime = new Date().getTime() let isOK = true + // we have to clear this from time to time, so it does not grow forever + if (requestMap.size > CONNECTION_HISTORY_DELETE_THRESHOLD) { + CORE_LOGGER.info('Request history reached threeshold, cleaning cache...') + requestMap.clear() + } + const self = this // common stuff const updateRequestData = function (remoteCaller: string) { const updatedRequestData = self.checkRequestData( remoteCaller, requestTime, - ratePerSecond + ratePerMinute ) isOK = updatedRequestData.valid - self.requestMap.set(remoteCaller, updatedRequestData.updatedRequestData) + requestMap.set(remoteCaller, updatedRequestData.updatedRequestData) } let data: RequestLimiter = null if (Array.isArray(caller)) { for (const remote of caller) { - if (!this.requestMap.has(remote)) { + if (!requestMap.has(remote)) { data = { requester: remote, lastRequestTime: requestTime, numRequests: 1 } - this.requestMap.set(remote, data) + requestMap.set(remote, data) } else { updateRequestData(remote) } @@ -72,20 +68,20 @@ export abstract class Handler implements ICommandHandler { if (!isOK) { CORE_LOGGER.warn( `Request denied (rate limit exceeded) for remote caller ${remote}. Current request map: ${JSON.stringify( - this.requestMap.get(remote) + requestMap.get(remote) )}` ) return false } } } else { - if (!this.requestMap.has(caller)) { + if (!requestMap.has(caller)) { data = { requester: caller, lastRequestTime: requestTime, numRequests: 1 } - this.requestMap.set(caller, data) + requestMap.set(caller, data) return true } else { updateRequestData(caller) @@ -93,7 +89,7 @@ export abstract class Handler implements ICommandHandler { if (!isOK) { CORE_LOGGER.warn( `Request denied (rate limit exceeded) for remote caller ${caller}. Current request map: ${JSON.stringify( - this.requestMap.get(caller) + requestMap.get(caller) )}` ) } @@ -105,18 +101,19 @@ export abstract class Handler implements ICommandHandler { /** * Checks if the request is within the rate limit defined * @param remote remote endpoint (ip or peer identifier) - * @param ratePerSecond number of calls per second allowed + * @param ratePerMinute number of calls per minute allowed (per ip or peer identifier) * @returns updated request data */ checkRequestData( remote: string, currentTime: number, - ratePerSecond: number + ratePerMinute: number ): RequestDataCheck { - const requestData: RequestLimiter = this.requestMap.get(remote) - const diffSeconds = (currentTime - requestData.lastRequestTime) / 1000 - // more than 1 sec difference means no problem - if (diffSeconds >= 1) { + const requestMap = this.getOceanNode().getRequestMap() + const requestData: RequestLimiter = requestMap.get(remote) + const diffMinutes = ((currentTime - requestData.lastRequestTime) / 1000) * 60 + // more than 1 minute difference means no problem + if (diffMinutes >= 1) { // its fine requestData.lastRequestTime = currentTime requestData.numRequests = 1 @@ -128,7 +125,7 @@ export abstract class Handler implements ICommandHandler { // requests in the same interval of 1 second requestData.numRequests++ return { - valid: requestData.numRequests <= ratePerSecond, + valid: requestData.numRequests <= ratePerMinute, updatedRequestData: requestData } } diff --git a/src/components/httpRoutes/requestValidator.ts b/src/components/httpRoutes/requestValidator.ts index 6fb6e553b..9b24ddbb0 100644 --- a/src/components/httpRoutes/requestValidator.ts +++ b/src/components/httpRoutes/requestValidator.ts @@ -1,6 +1,12 @@ import { Request, Response } from 'express' -import { getConfiguration } from '../../utils/index.js' +import { getConfiguration } from '../../utils/config.js' import { HTTP_LOGGER } from '../../utils/logging/common.js' +import { OceanNodeConfig } from '../../@types/OceanNode.js' +import { RequestLimiter } from '../../OceanNode.js' +import { + CONNECTIONS_RATE_INTERVAL, + DEFAULT_MAX_CONNECTIONS_PER_MINUTE +} from '../../utils/constants.js' // TODO we should group common stuff, // we have multiple similar validation interfaces @@ -9,23 +15,68 @@ export interface CommonValidation { error?: string } -// midleware to valid client addresses against a denylist +// hold data about last request made +const connectionsData: RequestLimiter = { + lastRequestTime: Date.now(), + requester: '', + numRequests: 0 +} + +// midleware to validate client addresses against a denylist +// it also checks the global rate limit export const requestValidator = async function (req: Request, res: Response, next: any) { // Perform the validations. const requestIP = req.headers['x-forwarded-for'] || req.socket.remoteAddress - const validation = await checkIP(requestIP) + + // grab request time + const requestTime = Date.now() + if (requestTime - connectionsData.lastRequestTime > CONNECTIONS_RATE_INTERVAL) { + // last one was more than 1 minute ago? reset counter + connectionsData.numRequests = 0 + } + // always increment counter + connectionsData.numRequests += 1 + // update time and requester information + connectionsData.lastRequestTime = requestTime + connectionsData.requester = requestIP + + const configuration = await getConfiguration() + + // check if IP is allowed or denied + const ipValidation = await checkIP(requestIP, configuration) // Validation failed, or an error occurred during the external request. - if (!validation.valid) { - res.status(403).send(validation.error) + if (!ipValidation.valid) { + res.status(403).send(ipValidation.error) + return + } + // check global rate limits (not ip related) + const requestRateValidation = checkConnectionsRateLimit(configuration, connectionsData) + if (!requestRateValidation.valid) { + res.status(403).send(requestRateValidation.error) return } // Validation passed. next() } -async function checkIP(requestIP: string | string[]): Promise { +export function checkConnectionsRateLimit( + configuration: OceanNodeConfig, + connectionsData: RequestLimiter +): CommonValidation { + const connectionLimits = + configuration.maxConnections || DEFAULT_MAX_CONNECTIONS_PER_MINUTE + const ok = connectionsData.numRequests <= connectionLimits + return { + valid: ok, + error: ok ? '' : 'Unauthorized request. Rate limit exceeded!' + } +} + +function checkIP( + requestIP: string | string[], + configuration: OceanNodeConfig +): CommonValidation { let onDenyList = false - const configuration = await getConfiguration() if (!Array.isArray(requestIP)) { onDenyList = configuration.denyList?.ips.includes(requestIP) } else { diff --git a/src/test/unit/networking.test.ts b/src/test/unit/networking.test.ts index 36e4a2a19..c030d1b58 100644 --- a/src/test/unit/networking.test.ts +++ b/src/test/unit/networking.test.ts @@ -1,11 +1,14 @@ import { - DEFAULT_RATE_LIMIT_PER_SECOND, + DEFAULT_RATE_LIMIT_PER_MINUTE, + // DEFAULT_MAX_CONNECTIONS_PER_MINUTE, ENVIRONMENT_VARIABLES, PROTOCOL_COMMANDS, - getConfiguration + getConfiguration, + CONNECTION_HISTORY_DELETE_THRESHOLD } from '../../utils/index.js' import { expect } from 'chai' import { + DEFAULT_TEST_TIMEOUT, OverrideEnvConfig, buildEnvOverrideConfig, setupEnvironment, @@ -105,7 +108,7 @@ describe('Test rate limitations and deny list defaults', () => { // const node: OceanNode = OceanNode.getInstance() before(async () => { envOverrides = buildEnvOverrideConfig( - [ENVIRONMENT_VARIABLES.RATE_DENY_LIST, ENVIRONMENT_VARIABLES.MAX_REQ_PER_SECOND], + [ENVIRONMENT_VARIABLES.RATE_DENY_LIST, ENVIRONMENT_VARIABLES.MAX_REQ_PER_MINUTE], [undefined, undefined] ) await setupEnvironment(null, envOverrides) @@ -115,7 +118,7 @@ describe('Test rate limitations and deny list defaults', () => { const config = await getConfiguration(true) expect(config.denyList.ips).to.be.length(0) expect(config.denyList.peers).to.be.length(0) - expect(config.rateLimit).to.be.equal(DEFAULT_RATE_LIMIT_PER_SECOND) + expect(config.rateLimit).to.be.equal(DEFAULT_RATE_LIMIT_PER_MINUTE) }) // put it back @@ -132,7 +135,7 @@ describe('Test rate limitations and deny list settings', () => { [ ENVIRONMENT_VARIABLES.PRIVATE_KEY, ENVIRONMENT_VARIABLES.RATE_DENY_LIST, - ENVIRONMENT_VARIABLES.MAX_REQ_PER_SECOND + ENVIRONMENT_VARIABLES.MAX_REQ_PER_MINUTE ], [ '0xcb345bd2b11264d523ddaf383094e2675c420a17511c3102a53817f13474a7ff', @@ -183,13 +186,12 @@ describe('Test rate limitations and deny list settings', () => { const ips = ['127.0.0.2', '127.0.0.3', '127.0.0.4', '127.0.0.5'] const rateLimitResponses = [] + const statusHandler: StatusHandler = CoreHandlersRegistry.getInstance( + node + ).getHandler(PROTOCOL_COMMANDS.STATUS) for (let i = 0; i < ips.length; i++) { node.setRemoteCaller(ips[i]) - const statusHandler: StatusHandler = CoreHandlersRegistry.getInstance( - node - ).getHandler(PROTOCOL_COMMANDS.STATUS) - const rateResp = await statusHandler.checkRateLimit() rateLimitResponses.push(rateResp) } @@ -197,6 +199,37 @@ describe('Test rate limitations and deny list settings', () => { // should have 4 valid responses expect(filtered.length).to.be.equal(ips.length) }) + + it('Test global rate limit, clear map after MAX (handler level) ', async function () { + // after more than CONNECTION_HISTORY_DELETE_THRESHOLD connections the map will be cleared + this.timeout(DEFAULT_TEST_TIMEOUT * 3) + let originalIPPiece = '127.0.' + + const rateLimitResponses = [] + + const statusHandler: StatusHandler = CoreHandlersRegistry.getInstance( + node + ).getHandler(PROTOCOL_COMMANDS.STATUS) + + const aboveLimit = 20 + for (let i = 0, x = 0; i < CONNECTION_HISTORY_DELETE_THRESHOLD + aboveLimit; i++) { + const ip = originalIPPiece + x // start at 127.0.0.2 + node.setRemoteCaller(ip) + const rateResp = await statusHandler.checkRateLimit() + rateLimitResponses.push(rateResp) + x++ + // start back + if (x > 254) { + x = 0 + originalIPPiece = '127.0.0.' // next piece + } + } + // it should clear the history after CONNECTION_HISTORY_DELETE_THRESHOLD + expect(statusHandler.getOceanNode().getRequestMapSize()).to.be.lessThanOrEqual( + aboveLimit + ) + }) + after(async () => { await tearDownEnvironment(envOverrides) }) diff --git a/src/utils/config.ts b/src/utils/config.ts index b15d63df7..c7e56b0ca 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -10,12 +10,16 @@ import { createFromPrivKey } from '@libp2p/peer-id-factory' import { keys } from '@libp2p/crypto' import { computeCodebaseHash, - DEFAULT_RATE_LIMIT_PER_SECOND, + DEFAULT_RATE_LIMIT_PER_MINUTE, ENVIRONMENT_VARIABLES, EnvVariable, hexStringToByteArray } from '../utils/index.js' -import { defaultBootstrapAddresses, knownUnsafeURLs } from '../utils/constants.js' +import { + DEFAULT_MAX_CONNECTIONS_PER_MINUTE, + defaultBootstrapAddresses, + knownUnsafeURLs +} from '../utils/constants.js' import { LOG_LEVELS_STR, GENERIC_EMOJIS, getLoggerLevelEmoji } from './logging/Logger.js' import { RPCS } from '../@types/blockchain' @@ -429,21 +433,43 @@ function logMissingVariableWithDefault(envVariable: EnvVariable) { true ) } -// have a rate limit for handler calls +// have a rate limit for handler calls (per IP address or peer id) function getRateLimit(isStartup: boolean = false) { - if (!existsEnvironmentVariable(ENVIRONMENT_VARIABLES.MAX_REQ_PER_SECOND)) { + if (!existsEnvironmentVariable(ENVIRONMENT_VARIABLES.MAX_REQ_PER_MINUTE)) { if (isStartup) { - logMissingVariableWithDefault(ENVIRONMENT_VARIABLES.MAX_REQ_PER_SECOND) + logMissingVariableWithDefault(ENVIRONMENT_VARIABLES.MAX_REQ_PER_MINUTE) } - return DEFAULT_RATE_LIMIT_PER_SECOND + return DEFAULT_RATE_LIMIT_PER_MINUTE } else { try { - return getIntEnvValue(process.env.MAX_REQ_PER_SECOND, DEFAULT_RATE_LIMIT_PER_SECOND) + return getIntEnvValue(process.env.MAX_REQ_PER_MINUTE, DEFAULT_RATE_LIMIT_PER_MINUTE) + } catch (err) { + CONFIG_LOGGER.error( + `Invalid "${ENVIRONMENT_VARIABLES.MAX_REQ_PER_MINUTE.name}" env variable...` + ) + return DEFAULT_RATE_LIMIT_PER_MINUTE + } + } +} + +// Global requests limit +function getConnectionsLimit(isStartup: boolean = false) { + if (!existsEnvironmentVariable(ENVIRONMENT_VARIABLES.MAX_CONNECTIONS_PER_MINUTE)) { + if (isStartup) { + logMissingVariableWithDefault(ENVIRONMENT_VARIABLES.MAX_CONNECTIONS_PER_MINUTE) + } + return DEFAULT_RATE_LIMIT_PER_MINUTE + } else { + try { + return getIntEnvValue( + process.env.MAX_CONNECTIONS_PER_MINUTE, + DEFAULT_MAX_CONNECTIONS_PER_MINUTE + ) } catch (err) { CONFIG_LOGGER.error( - `Invalid "${ENVIRONMENT_VARIABLES.MAX_REQ_PER_SECOND.name}" env variable...` + `Invalid "${ENVIRONMENT_VARIABLES.MAX_CONNECTIONS_PER_MINUTE.name}" env variable...` ) - return DEFAULT_RATE_LIMIT_PER_SECOND + return DEFAULT_MAX_CONNECTIONS_PER_MINUTE } } } @@ -620,6 +646,7 @@ async function getEnvConfig(isStartup?: boolean): Promise { assetPurgatoryUrl: getEnvValue(process.env.ASSET_PURGATORY_URL, ''), allowedAdmins: getAllowedAdmins(isStartup), rateLimit: getRateLimit(isStartup), + maxConnections: getConnectionsLimit(isStartup), denyList: getDenyList(isStartup), unsafeURLs: readListFromEnvVariable( ENVIRONMENT_VARIABLES.UNSAFE_URLS, diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 2bf91f761..be06225ee 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -266,10 +266,16 @@ export const ENVIRONMENT_VARIABLES: Record = { value: process.env.DASHBOARD, required: false }, - MAX_REQ_PER_SECOND: { - // rate limit per second - name: 'MAX_REQ_PER_SECOND', - value: process.env.MAX_REQ_PER_SECOND, + MAX_REQ_PER_MINUTE: { + // rate limit per minute (MAX requests per minute for a given IP or peer ID) + name: 'MAX_REQ_PER_MINUTE', + value: process.env.MAX_REQ_PER_MINUTE, + required: false + }, + MAX_CONNECTIONS_PER_MINUTE: { + // rate connections limit per minute (MAX requests per minute that the node will process) + name: 'MAX_CONNECTIONS_PER_MINUTE', + value: process.env.MAX_CONNECTIONS_PER_MINUTE, required: false }, RATE_DENY_LIST: { @@ -324,9 +330,13 @@ export const ENVIRONMENT_VARIABLES: Record = { required: false } } - -// default to 3 requests per second (configurable) -export const DEFAULT_RATE_LIMIT_PER_SECOND = 3 +export const CONNECTION_HISTORY_DELETE_THRESHOLD = 300 +// default to 30 requests per minute (configurable), per ip/peer +export const DEFAULT_RATE_LIMIT_PER_MINUTE = 30 +// max connections per minute (configurable), all connections +export const DEFAULT_MAX_CONNECTIONS_PER_MINUTE = 60 * 2 // 120 requests per minute +// 1 minute +export const CONNECTIONS_RATE_INTERVAL = 60 * 1000 // Typesense's maximum limit to send 250 hits at a time export const TYPESENSE_HITS_CAP = 250 export const DDO_IDENTIFIER_PREFIX = 'did:op:'