From 608f435bf4ba3f8ca5f5852a9ba9edd2acbe3601 Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Tue, 4 May 2021 16:08:44 -0400 Subject: [PATCH] Finished implementation --- package-lock.json | 206 +++++++++++++++----------- src/rpc/auth/index.js | 3 + src/rpc/rate-limit.js | 17 ++- src/rpc/users/index.js | 4 + test/unit/json-rpc/a11-auth.unit.js | 4 + test/unit/json-rpc/a13-users.unit.js | 11 +- test/unit/json-rpc/a14-rate-limits.js | 64 ++++++++ 7 files changed, 216 insertions(+), 93 deletions(-) create mode 100644 test/unit/json-rpc/a14-rate-limits.js diff --git a/package-lock.json b/package-lock.json index b25d041..a5bd379 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13565,45 +13565,6 @@ "ms": "2.1.2" } }, - "engine.io": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.1.1.tgz", - "integrity": "sha512-t2E9wLlssQjGw0nluF6aYyfX8LwYU8Jj0xct+pAhfWfv/YrBn6TSNtEYsgxHIfaMqfrLx07czcMg9bMN6di+3w==", - "requires": { - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.4.1", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~4.0.0", - "ws": "~7.4.2" - } - }, - "engine.io-client": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-4.1.3.tgz", - "integrity": "sha512-C3JYYyvna0FvSKOWGWpo0/EQ6MayO8uEKam1oedLZRpxLego9Fk6K3UH/Phieu1xPHauM7YqAygJ+6SraVc0Qg==", - "requires": { - "base64-arraybuffer": "0.1.4", - "component-emitter": "~1.3.0", - "debug": "~4.3.1", - "engine.io-parser": "~4.0.1", - "has-cors": "1.1.0", - "parseqs": "0.0.6", - "parseuri": "0.0.6", - "ws": "~7.4.2", - "xmlhttprequest-ssl": "~1.5.4", - "yeast": "0.1.2" - } - }, - "engine.io-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz", - "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==", - "requires": { - "base64-arraybuffer": "0.1.4" - } - }, "ipfs-utils": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/ipfs-utils/-/ipfs-utils-6.0.4.tgz", @@ -13659,51 +13620,6 @@ "version": "3.1.22", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz", "integrity": "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==" - }, - "socket.io-adapter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz", - "integrity": "sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==" - }, - "socket.io-client-next": { - "version": "npm:socket.io-client@3.1.3", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-3.1.3.tgz", - "integrity": "sha512-4sIGOGOmCg3AOgGi7EEr6ZkTZRkrXwub70bBB/F0JSkMOUFpA77WsL87o34DffQQ31PkbMUIadGOk+3tx1KGbw==", - "requires": { - "@types/component-emitter": "^1.2.10", - "backo2": "~1.0.2", - "component-emitter": "~1.3.0", - "debug": "~4.3.1", - "engine.io-client": "~4.1.0", - "parseuri": "0.0.6", - "socket.io-parser": "~4.0.4" - } - }, - "socket.io-next": { - "version": "npm:socket.io@3.1.2", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-3.1.2.tgz", - "integrity": "sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw==", - "requires": { - "@types/cookie": "^0.4.0", - "@types/cors": "^2.8.8", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "debug": "~4.3.1", - "engine.io": "~4.1.0", - "socket.io-adapter": "~2.1.0", - "socket.io-parser": "~4.0.3" - } - }, - "socket.io-parser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", - "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", - "requires": { - "@types/component-emitter": "^1.2.10", - "component-emitter": "~1.3.0", - "debug": "~4.3.1" - } } } }, @@ -20236,6 +20152,128 @@ } } }, + "socket.io-client-next": { + "version": "npm:socket.io-client@3.1.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-3.1.3.tgz", + "integrity": "sha512-4sIGOGOmCg3AOgGi7EEr6ZkTZRkrXwub70bBB/F0JSkMOUFpA77WsL87o34DffQQ31PkbMUIadGOk+3tx1KGbw==", + "requires": { + "@types/component-emitter": "^1.2.10", + "backo2": "~1.0.2", + "component-emitter": "~1.3.0", + "debug": "~4.3.1", + "engine.io-client": "~4.1.0", + "parseuri": "0.0.6", + "socket.io-parser": "~4.0.4" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "engine.io-client": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-4.1.3.tgz", + "integrity": "sha512-C3JYYyvna0FvSKOWGWpo0/EQ6MayO8uEKam1oedLZRpxLego9Fk6K3UH/Phieu1xPHauM7YqAygJ+6SraVc0Qg==", + "requires": { + "base64-arraybuffer": "0.1.4", + "component-emitter": "~1.3.0", + "debug": "~4.3.1", + "engine.io-parser": "~4.0.1", + "has-cors": "1.1.0", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~7.4.2", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + } + }, + "engine.io-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz", + "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==", + "requires": { + "base64-arraybuffer": "0.1.4" + } + }, + "socket.io-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "requires": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + } + } + } + }, + "socket.io-next": { + "version": "npm:socket.io@3.1.2", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-3.1.2.tgz", + "integrity": "sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw==", + "requires": { + "@types/cookie": "^0.4.0", + "@types/cors": "^2.8.8", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.1", + "engine.io": "~4.1.0", + "socket.io-adapter": "~2.1.0", + "socket.io-parser": "~4.0.3" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "engine.io": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.1.1.tgz", + "integrity": "sha512-t2E9wLlssQjGw0nluF6aYyfX8LwYU8Jj0xct+pAhfWfv/YrBn6TSNtEYsgxHIfaMqfrLx07czcMg9bMN6di+3w==", + "requires": { + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~4.0.0", + "ws": "~7.4.2" + } + }, + "engine.io-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz", + "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==", + "requires": { + "base64-arraybuffer": "0.1.4" + } + }, + "socket.io-adapter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz", + "integrity": "sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==" + }, + "socket.io-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "requires": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + } + } + } + }, "socket.io-parser": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz", diff --git a/src/rpc/auth/index.js b/src/rpc/auth/index.js index f7f7868..e9f3d67 100644 --- a/src/rpc/auth/index.js +++ b/src/rpc/auth/index.js @@ -9,6 +9,7 @@ const jsonrpc = require('jsonrpc-lite') // const AuthLib = require('../../lib/auth') const UserLib = require('../../lib/users') const wlogger = require('../../lib/wlogger') +const RateLimit = require('../rate-limit') class AuthRPC { constructor (localConfig) { @@ -16,6 +17,7 @@ class AuthRPC { // this.authLib = new AuthLib() this.jsonrpc = jsonrpc this.userLib = new UserLib() + this.rateLimit = new RateLimit() } // Top-level router for this library. All other methods in this class are for @@ -32,6 +34,7 @@ class AuthRPC { // Route the call based on the requested endpoint. switch (endpoint) { case 'authUser': + await this.rateLimit.limiter(rpcData.from) return await this.authUser(rpcData) } } catch (err) { diff --git a/src/rpc/rate-limit.js b/src/rpc/rate-limit.js index ffa5353..6d912a7 100644 --- a/src/rpc/rate-limit.js +++ b/src/rpc/rate-limit.js @@ -7,12 +7,12 @@ const RateLimitLib = require('koa2-ratelimit').RateLimit class RateLimit { - constructor () { + constructor (options) { // Encapsulate dependencies this.RateLimitLib = RateLimitLib - this.options = { + this.defaultOptions = { interval: { min: 1 }, - max: 2, + max: 1, onLimitReached: this.onLimitReached } @@ -27,12 +27,15 @@ class RateLimit { user: '', set: () => { } } + // Stasblish provided options as the default options + this.rateLimitOptions = Object.assign({}, this.defaultOptions, options) + this.rateLimit = this.RateLimitLib.middleware(this.rateLimitOptions) } onLimitReached () { try { const error = new Error() - error.message = 'limit per minute reached!' + error.message = 'Too many requests, please try again later.' error.status = 429 throw error } catch (error) { @@ -46,16 +49,16 @@ class RateLimit { if (!from || typeof from !== 'string') { throw new Error('from must be a string') } - const rateLimit = this.RateLimitLib.middleware(this.options) // Set context this.context.state.user = from this.context.request.ip = from this.context.user = from - await rateLimit(this.context, () => { }) + await this.rateLimit(this.context, () => { }) + return true } catch (error) { - console.error('Error in limiter()', error) + console.error('Error in limiter()') throw error } } diff --git a/src/rpc/users/index.js b/src/rpc/users/index.js index 7faea34..fd4bfa4 100644 --- a/src/rpc/users/index.js +++ b/src/rpc/users/index.js @@ -33,6 +33,7 @@ class UserRPC { // Route the call based on the value of the method property. switch (endpoint) { case 'createUser': + await this.rateLimit.limiter(rpcData.from) return await this.createUser(rpcData) case 'getAllUsers': @@ -42,14 +43,17 @@ class UserRPC { case 'getUser': user = await this.validators.ensureUser(rpcData) + await this.rateLimit.limiter(rpcData.from) return await this.getUser(rpcData, user) case 'updateUser': user = await this.validators.ensureTargetUserOrAdmin(rpcData) + await this.rateLimit.limiter(rpcData.from) return await this.updateUser(rpcData, user) case 'deleteUser': user = await this.validators.ensureTargetUserOrAdmin(rpcData) + await this.rateLimit.limiter(rpcData.from) return await this.deleteUser(rpcData, user) } } catch (err) { diff --git a/test/unit/json-rpc/a11-auth.unit.js b/test/unit/json-rpc/a11-auth.unit.js index 41bff49..76a09ae 100644 --- a/test/unit/json-rpc/a11-auth.unit.js +++ b/test/unit/json-rpc/a11-auth.unit.js @@ -15,6 +15,7 @@ process.env.SVC_ENV = 'test' // Local libraries const config = require('../../../config') const AuthRPC = require('../../../src/rpc/auth') +const RateLimit = require('../../../src/rpc/rate-limit') const UserLib = require('../../../src/lib/users') const userLib = new UserLib() @@ -48,6 +49,7 @@ describe('#AuthRPC', () => { sandbox = sinon.createSandbox() uut = new AuthRPC() + uut.rateLimit = new RateLimit({ max: 100 }) }) afterEach(() => sandbox.restore()) @@ -71,6 +73,7 @@ describe('#AuthRPC', () => { const authCall = jsonrpc.request(id, 'auth', { endpoint: 'authUser' }) const jsonStr = JSON.stringify(authCall, null, 2) const rpcData = jsonrpc.parse(jsonStr) + rpcData.from = 'Origin request' const result = await uut.authRouter(rpcData) @@ -87,6 +90,7 @@ describe('#AuthRPC', () => { const authCall = jsonrpc.request(id, 'auth', { endpoint: 'authUser' }) const jsonStr = JSON.stringify(authCall, null, 2) const rpcData = jsonrpc.parse(jsonStr) + rpcData.from = 'Origin request' const result = await uut.authRouter(rpcData) diff --git a/test/unit/json-rpc/a13-users.unit.js b/test/unit/json-rpc/a13-users.unit.js index 3dacaf1..aecb427 100644 --- a/test/unit/json-rpc/a13-users.unit.js +++ b/test/unit/json-rpc/a13-users.unit.js @@ -15,6 +15,7 @@ process.env.SVC_ENV = 'test' // Local libraries const config = require('../../../config') const UserRPC = require('../../../src/rpc/users') +const RateLimit = require('../../../src/rpc/rate-limit') const UserModel = require('../../../src/models/users') describe('#UserRPC', () => { @@ -40,6 +41,7 @@ describe('#UserRPC', () => { sandbox = sinon.createSandbox() uut = new UserRPC() + uut.rateLimit = new RateLimit({ max: 100 }) }) afterEach(() => sandbox.restore()) @@ -120,6 +122,7 @@ describe('#UserRPC', () => { const userCall = jsonrpc.request(id, 'users', { endpoint: 'createUser' }) const jsonStr = JSON.stringify(userCall, null, 2) const rpcData = jsonrpc.parse(jsonStr) + rpcData.from = 'Origin request' const result = await uut.userRouter(rpcData) @@ -139,9 +142,10 @@ describe('#UserRPC', () => { }) const jsonStr = JSON.stringify(userCall, null, 2) const rpcData = jsonrpc.parse(jsonStr) + rpcData.from = 'Origin request' const result = await uut.userRouter(rpcData) - + console.log('result', result) assert.equal(result, true) }) @@ -159,6 +163,7 @@ describe('#UserRPC', () => { }) const jsonStr = JSON.stringify(userCall, null, 2) const rpcData = jsonrpc.parse(jsonStr) + rpcData.from = 'Origin request' const result = await uut.userRouter(rpcData) // console.log('result: ', result) @@ -179,7 +184,7 @@ describe('#UserRPC', () => { }) const jsonStr = JSON.stringify(userCall, null, 2) const rpcData = jsonrpc.parse(jsonStr) - + rpcData.from = 'Origin request' const result = await uut.userRouter(rpcData) assert.equal(result, true) @@ -199,6 +204,7 @@ describe('#UserRPC', () => { }) const jsonStr = JSON.stringify(userCall, null, 2) const rpcData = jsonrpc.parse(jsonStr) + rpcData.from = 'Origin request' const result = await uut.userRouter(rpcData) // console.log('result: ', result) @@ -216,6 +222,7 @@ describe('#UserRPC', () => { const userCall = jsonrpc.request(id, 'users', { endpoint: 'createUser' }) const jsonStr = JSON.stringify(userCall, null, 2) const rpcData = jsonrpc.parse(jsonStr) + rpcData.from = 'Origin request' const result = await uut.userRouter(rpcData) // console.log('result: ', result) diff --git a/test/unit/json-rpc/a14-rate-limits.js b/test/unit/json-rpc/a14-rate-limits.js new file mode 100644 index 0000000..f2604d0 --- /dev/null +++ b/test/unit/json-rpc/a14-rate-limits.js @@ -0,0 +1,64 @@ +/* + Unit tests for the JSON RPC validator middleware. + + TODO: ensureTargetUserOrAdmin: it should exit quietly if user is an admin. +*/ + +// Public npm libraries +const sinon = require('sinon') +const assert = require('chai').assert + +// Set the environment variable to signal this is a test. +process.env.SVC_ENV = 'test' + +// Local libraries +const RateLimit = require('../../../src/rpc/rate-limit') + +describe('#rate-limit', () => { + let uut + let sandbox + + beforeEach(() => { + sandbox = sinon.createSandbox() + + uut = new RateLimit() + }) + + afterEach(() => sandbox.restore()) + + describe('#onLimitReached', () => { + it('should throw error', async () => { + try { + uut.onLimitReached() + assert.fail('unexpected error') + } catch (error) { + assert.equal(error.status, 429) + assert.include(error.message, 'Too many requests, please try again later.') + } + }) + }) + describe('#limiter', () => { + it('should throw error if "from" input is not provider', async () => { + try { + await uut.limiter() + assert.fail('unexpected error') + } catch (error) { + assert.include(error.message, 'from must be a string') + } + }) + it('should throw error 429', async () => { + try { + const _uut = new RateLimit({ max: 1 }) + const from = 'Origin request' + + const firtsRequest = await _uut.limiter(from) + assert.isTrue(firtsRequest) + + await _uut.limiter(from) + assert.fail('unexpected error') + } catch (error) { + assert.include(error.message, 'Too many requests, please try again later.') + } + }) + }) +})