diff --git a/external/libp2p-service.js b/external/libp2p-service.js index ef2bf3806d..375c263128 100644 --- a/external/libp2p-service.js +++ b/external/libp2p-service.js @@ -71,10 +71,7 @@ class Libp2pService { initializationObject.peerId = this.config.peerId; this.workerPool = this.config.workerPool; - this.limiter = new InMemoryRateLimiter({ - interval: constants.NETWORK_API_RATE_LIMIT_TIME_WINDOW_MILLS, - maxInInterval: constants.NETWORK_API_RATE_LIMIT_MAX_NUMBER, - }); + this._initializeRateLimiters(); Libp2p.create(initializationObject).then((node) => { this.node = node; @@ -94,6 +91,25 @@ class Libp2pService { }); } + _initializeRateLimiters() { + const basicRateLimiter = new InMemoryRateLimiter({ + interval: constants.NETWORK_API_RATE_LIMIT.TIME_WINDOW_MILLS, + maxInInterval: constants.NETWORK_API_RATE_LIMIT.MAX_NUMBER, + }); + + const spamDetection = new InMemoryRateLimiter({ + interval: constants.NETWORK_API_SPAM_DETECTION.TIME_WINDOW_MILLS, + maxInInterval: constants.NETWORK_API_SPAM_DETECTION.MAX_NUMBER, + }); + + this.rateLimiter = { + basicRateLimiter, + spamDetection, + } + + this.blackList = {}; + } + _initializeNodeListeners() { this.node.on('peer:discovery', (peer) => { this._onPeerDiscovery(peer); @@ -167,14 +183,13 @@ class Libp2pService { this.node.handle(eventName, async (handlerProps) => { const {stream} = handlerProps; let timestamp = Date.now(); - const blocked = await this.limiter.limit(handlerProps.connection.remotePeer.toB58String()); - if(blocked) { + const remotePeerId = handlerProps.connection.remotePeer._idB58String; + if(await this.limitRequest(remotePeerId)) { const preparedBlockedResponse = await this.prepareForSending(constants.NETWORK_RESPONSES.BLOCKED); await pipe( [preparedBlockedResponse], stream ); - this.logger.info(`Blocking request from ${handlerProps.connection.remotePeer._idB58String}. Max number of requests exceeded.`); return; } let data = await pipe( @@ -190,10 +205,10 @@ class Libp2pService { ) try { data = await this.workerPool.exec('JSONParse', [data.toString()]); - this.logger.info(`Receiving message from ${handlerProps.connection.remotePeer._idB58String} to ${this.config.id}: event=${eventName};`); + this.logger.info(`Receiving message from ${remotePeerId} to ${this.config.id}: event=${eventName};`); if (!async) { const result = await handler(data); - this.logger.info(`Sending response from ${this.config.id} to ${handlerProps.connection.remotePeer._idB58String}: event=${eventName};`); + this.logger.info(`Sending response from ${this.config.id} to ${remotePeerId}: event=${eventName};`); const preparedData = await this.prepareForSending(result); await pipe( [Buffer.from(preparedData)], @@ -206,12 +221,12 @@ class Libp2pService { stream ) - this.logger.info(`Sending response from ${this.config.id} to ${handlerProps.connection.remotePeer._idB58String}: event=${eventName};`); + this.logger.info(`Sending response from ${this.config.id} to ${remotePeerId}: event=${eventName};`); const result = await handler(data); if (Date.now() <= timestamp + timeout) { await this.sendMessage(`${eventName}/result`, result, handlerProps.connection.remotePeer); } else { - this.logger.warn(`Too late to send response from ${this.config.id} to ${handlerProps.connection.remotePeer._idB58String}: event=${eventName};`); + this.logger.warn(`Too late to send response from ${this.config.id} to ${remotePeerId}: event=${eventName};`); } } } catch (e) { @@ -267,6 +282,38 @@ class Libp2pService { return false; } + async limitRequest(remotePeerId) { + if(this.blackList[remotePeerId]){ + const remainingMinutes = Math.floor( + constants.NETWORK_API_BLACK_LIST_TIME_WINDOW_MINUTES - + (Date.now() - this.blackList[remotePeerId]) / (1000 * 60) + ); + + if(remainingMinutes > 0) { + this.logger.info(`Blocking request from ${remotePeerId}. Node is blacklisted for ${remainingMinutes} minutes.`); + + return true; + } else { + delete this.blackList[remotePeerId] + } + } + + if(await this.rateLimiter.spamDetection.limit(remotePeerId)) { + this.blackList[remotePeerId] = Date.now(); + this.logger.info( + `Blocking request from ${remotePeerId}. Spammer detected and blacklisted for ${constants.NETWORK_API_BLACK_LIST_TIME_WINDOW_MINUTES} minutes.` + ); + + return true; + } else if (await this.rateLimiter.basicRateLimiter.limit(remotePeerId)) { + this.logger.info(`Blocking request from ${remotePeerId}. Max number of requests exceeded.`); + + return true; + } + + return false; + } + async restartService() { this.logger.info('Restrating libp2p service...'); // TODO: reinitialize service diff --git a/modules/constants.js b/modules/constants.js index 08a5b1975b..31fbd0bffa 100644 --- a/modules/constants.js +++ b/modules/constants.js @@ -12,46 +12,47 @@ exports.DID = 'DID'; exports.MAX_FILE_SIZE = 2621440; /** - * @constant {number} SERVICE_API_RATE_LIMIT_TIME_WINDOW_MILLS - * - Express rate limit time window in milliseconds + * @constant {object} SERVICE_API_RATE_LIMIT + * - Express rate limit configuration constants */ -exports.SERVICE_API_RATE_LIMIT_TIME_WINDOW_MILLS = 1 * 60 * 1000; - -/** - * @constant {number} SERVICE_API_RATE_LIMIT_MAX_NUMBER - * - Express rate limit max number of requests allowed in the specified time window - */ -exports.SERVICE_API_RATE_LIMIT_MAX_NUMBER = 10; - -/** - * @constant {number} SERVICE_API_SLOW_DOWN_TIME_WINDOW_MILLS - * - Express slow down time window in milliseconds - */ -exports.SERVICE_API_SLOW_DOWN_TIME_WINDOW_MILLS = 1 * 60 * 1000; +exports.SERVICE_API_RATE_LIMIT = { + TIME_WINDOW_MILLS: 1 * 60 * 1000, + MAX_NUMBER: 10, +}; /** - * @constant {number} SERVICE_API_SLOW_DOWN_DELAY_AFTER - * - Express slow down number of seconds after which it starts delaying requests + * @constant {object} SERVICE_API_SLOW_DOWN + * - Express slow down configuration constants */ -exports.SERVICE_API_SLOW_DOWN_DELAY_AFTER = 5; +exports.SERVICE_API_SLOW_DOWN = { + TIME_WINDOW_MILLS: 1 * 60 * 1000, + DELAY_AFTER_SECONDS: 5, + DELAY_MILLS: 3 * 1000, +}; /** - * @constant {number} SERVICE_API_SLOW_DOWN_DELAY_MILLS - * - Express slow down delay between requests in milliseconds + * @constant {object} NETWORK_API_RATE_LIMIT + * - Network (Libp2p) rate limiter configuration constants */ -exports.SERVICE_API_SLOW_DOWN_DELAY_MILLS = 3 * 1000; +exports.NETWORK_API_RATE_LIMIT = { + TIME_WINDOW_MILLS: 1 * 60 * 1000, + MAX_NUMBER: this.SERVICE_API_RATE_LIMIT.MAX_NUMBER, +}; /** - * @constant {number} NETWORK_API_RATE_LIMIT_TIME_WINDOW_MILLS - * - Network (Libp2p) rate limit time window in milliseconds + * @constant {object} NETWORK_API_SPAM_DETECTION + * - Network (Libp2p) spam detection rate limiter configuration constants */ -exports.NETWORK_API_RATE_LIMIT_TIME_WINDOW_MILLS = 1 * 60 * 1000; +exports.NETWORK_API_SPAM_DETECTION = { + TIME_WINDOW_MILLS: 1 * 60 * 1000, + MAX_NUMBER: 20, +}; /** - * @constant {number} NETWORK_API_RATE_LIMIT_MAX_NUMBER - * - Network (Libp2p) rate limit max number of requests allowed in the specified time window + * @constant {object} NETWORK_API_BLACK_LIST_TIME_WINDOW_MINUTES + * - Network (Libp2p) black list time window in minutes */ -exports.NETWORK_API_RATE_LIMIT_MAX_NUMBER = 10; +exports.NETWORK_API_BLACK_LIST_TIME_WINDOW_MINUTES = 60; /** * @constant {number} DID_PREFIX diff --git a/modules/controller/rpc-controller.js b/modules/controller/rpc-controller.js index e659788b28..a8f4895fdc 100644 --- a/modules/controller/rpc-controller.js +++ b/modules/controller/rpc-controller.js @@ -126,9 +126,9 @@ class RpcController { initializeRateLimitMiddleware() { this.rateLimitMiddleware = rateLimit({ - windowMs: constants.SERVICE_API_RATE_LIMIT_TIME_WINDOW_MILLS, - max: constants.SERVICE_API_RATE_LIMIT_MAX_NUMBER, - message: `Too many requests sent, maximum number of requests per minute is ${constants.SERVICE_API_RATE_LIMIT_MAX_NUMBER}`, + windowMs: constants.SERVICE_API_RATE_LIMIT.TIME_WINDOW_MILLS, + max: constants.SERVICE_API_RATE_LIMIT.MAX_NUMBER, + message: `Too many requests sent, maximum number of requests per minute is ${constants.SERVICE_API_RATE_LIMIT.MAX_NUMBER}`, standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); @@ -136,9 +136,9 @@ class RpcController { initializeSlowDownMiddleWare() { this.slowDownMiddleware = slowDown({ - windowMs: constants.SERVICE_API_SLOW_DOWN_TIME_WINDOW_MILLS, - delayAfter: constants.SERVICE_API_SLOW_DOWN_DELAY_AFTER, - delayMs: constants.SERVICE_API_SLOW_DOWN_DELAY_MILLS, + windowMs: constants.SERVICE_API_SLOW_DOWN.TIME_WINDOW_MILLS, + delayAfter: constants.SERVICE_API_SLOW_DOWN.DELAY_AFTER_SECONDS, + delayMs: constants.SERVICE_API_SLOW_DOWN.DELAY_MILLS, }); } @@ -191,11 +191,11 @@ class RpcController { this.logger.info(`Service API module enabled, server running on port ${this.config.rpcPort}`); this.app.post('/publish', this.rateLimitMiddleware, this.slowDownMiddleware, async (req, res, next) => { - await this.publish(req, res, next, {isAsset: false}); + await this.publish(req, res, next, { isAsset: false }); }); this.app.post('/provision', this.rateLimitMiddleware, this.slowDownMiddleware, async (req, res, next) => { - await this.publish(req, res, next, {isAsset: true, ual: null}); + await this.publish(req, res, next, { isAsset: true, ual: null }); }); this.app.post('/update', this.rateLimitMiddleware, this.slowDownMiddleware, async (req, res, next) => {