Finished implementation

This commit is contained in:
Daniel Gonzalez
2021-05-04 16:08:44 -04:00
parent 6207c6c3d5
commit 608f435bf4
7 changed files with 216 additions and 93 deletions
+122 -84
View File
@@ -13565,45 +13565,6 @@
"ms": "2.1.2" "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": { "ipfs-utils": {
"version": "6.0.4", "version": "6.0.4",
"resolved": "https://registry.npmjs.org/ipfs-utils/-/ipfs-utils-6.0.4.tgz", "resolved": "https://registry.npmjs.org/ipfs-utils/-/ipfs-utils-6.0.4.tgz",
@@ -13659,51 +13620,6 @@
"version": "3.1.22", "version": "3.1.22",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz",
"integrity": "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==" "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": { "socket.io-parser": {
"version": "3.4.1", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz",
+3
View File
@@ -9,6 +9,7 @@ const jsonrpc = require('jsonrpc-lite')
// const AuthLib = require('../../lib/auth') // const AuthLib = require('../../lib/auth')
const UserLib = require('../../lib/users') const UserLib = require('../../lib/users')
const wlogger = require('../../lib/wlogger') const wlogger = require('../../lib/wlogger')
const RateLimit = require('../rate-limit')
class AuthRPC { class AuthRPC {
constructor (localConfig) { constructor (localConfig) {
@@ -16,6 +17,7 @@ class AuthRPC {
// this.authLib = new AuthLib() // this.authLib = new AuthLib()
this.jsonrpc = jsonrpc this.jsonrpc = jsonrpc
this.userLib = new UserLib() this.userLib = new UserLib()
this.rateLimit = new RateLimit()
} }
// Top-level router for this library. All other methods in this class are for // 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. // Route the call based on the requested endpoint.
switch (endpoint) { switch (endpoint) {
case 'authUser': case 'authUser':
await this.rateLimit.limiter(rpcData.from)
return await this.authUser(rpcData) return await this.authUser(rpcData)
} }
} catch (err) { } catch (err) {
+10 -7
View File
@@ -7,12 +7,12 @@
const RateLimitLib = require('koa2-ratelimit').RateLimit const RateLimitLib = require('koa2-ratelimit').RateLimit
class RateLimit { class RateLimit {
constructor () { constructor (options) {
// Encapsulate dependencies // Encapsulate dependencies
this.RateLimitLib = RateLimitLib this.RateLimitLib = RateLimitLib
this.options = { this.defaultOptions = {
interval: { min: 1 }, interval: { min: 1 },
max: 2, max: 1,
onLimitReached: this.onLimitReached onLimitReached: this.onLimitReached
} }
@@ -27,12 +27,15 @@ class RateLimit {
user: '', user: '',
set: () => { } set: () => { }
} }
// Stasblish provided options as the default options
this.rateLimitOptions = Object.assign({}, this.defaultOptions, options)
this.rateLimit = this.RateLimitLib.middleware(this.rateLimitOptions)
} }
onLimitReached () { onLimitReached () {
try { try {
const error = new Error() const error = new Error()
error.message = 'limit per minute reached!' error.message = 'Too many requests, please try again later.'
error.status = 429 error.status = 429
throw error throw error
} catch (error) { } catch (error) {
@@ -46,16 +49,16 @@ class RateLimit {
if (!from || typeof from !== 'string') { if (!from || typeof from !== 'string') {
throw new Error('from must be a string') throw new Error('from must be a string')
} }
const rateLimit = this.RateLimitLib.middleware(this.options)
// Set context // Set context
this.context.state.user = from this.context.state.user = from
this.context.request.ip = from this.context.request.ip = from
this.context.user = from this.context.user = from
await rateLimit(this.context, () => { }) await this.rateLimit(this.context, () => { })
return true
} catch (error) { } catch (error) {
console.error('Error in limiter()', error) console.error('Error in limiter()')
throw error throw error
} }
} }
+4
View File
@@ -33,6 +33,7 @@ class UserRPC {
// Route the call based on the value of the method property. // Route the call based on the value of the method property.
switch (endpoint) { switch (endpoint) {
case 'createUser': case 'createUser':
await this.rateLimit.limiter(rpcData.from)
return await this.createUser(rpcData) return await this.createUser(rpcData)
case 'getAllUsers': case 'getAllUsers':
@@ -42,14 +43,17 @@ class UserRPC {
case 'getUser': case 'getUser':
user = await this.validators.ensureUser(rpcData) user = await this.validators.ensureUser(rpcData)
await this.rateLimit.limiter(rpcData.from)
return await this.getUser(rpcData, user) return await this.getUser(rpcData, user)
case 'updateUser': case 'updateUser':
user = await this.validators.ensureTargetUserOrAdmin(rpcData) user = await this.validators.ensureTargetUserOrAdmin(rpcData)
await this.rateLimit.limiter(rpcData.from)
return await this.updateUser(rpcData, user) return await this.updateUser(rpcData, user)
case 'deleteUser': case 'deleteUser':
user = await this.validators.ensureTargetUserOrAdmin(rpcData) user = await this.validators.ensureTargetUserOrAdmin(rpcData)
await this.rateLimit.limiter(rpcData.from)
return await this.deleteUser(rpcData, user) return await this.deleteUser(rpcData, user)
} }
} catch (err) { } catch (err) {
+4
View File
@@ -15,6 +15,7 @@ process.env.SVC_ENV = 'test'
// Local libraries // Local libraries
const config = require('../../../config') const config = require('../../../config')
const AuthRPC = require('../../../src/rpc/auth') const AuthRPC = require('../../../src/rpc/auth')
const RateLimit = require('../../../src/rpc/rate-limit')
const UserLib = require('../../../src/lib/users') const UserLib = require('../../../src/lib/users')
const userLib = new UserLib() const userLib = new UserLib()
@@ -48,6 +49,7 @@ describe('#AuthRPC', () => {
sandbox = sinon.createSandbox() sandbox = sinon.createSandbox()
uut = new AuthRPC() uut = new AuthRPC()
uut.rateLimit = new RateLimit({ max: 100 })
}) })
afterEach(() => sandbox.restore()) afterEach(() => sandbox.restore())
@@ -71,6 +73,7 @@ describe('#AuthRPC', () => {
const authCall = jsonrpc.request(id, 'auth', { endpoint: 'authUser' }) const authCall = jsonrpc.request(id, 'auth', { endpoint: 'authUser' })
const jsonStr = JSON.stringify(authCall, null, 2) const jsonStr = JSON.stringify(authCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr) const rpcData = jsonrpc.parse(jsonStr)
rpcData.from = 'Origin request'
const result = await uut.authRouter(rpcData) const result = await uut.authRouter(rpcData)
@@ -87,6 +90,7 @@ describe('#AuthRPC', () => {
const authCall = jsonrpc.request(id, 'auth', { endpoint: 'authUser' }) const authCall = jsonrpc.request(id, 'auth', { endpoint: 'authUser' })
const jsonStr = JSON.stringify(authCall, null, 2) const jsonStr = JSON.stringify(authCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr) const rpcData = jsonrpc.parse(jsonStr)
rpcData.from = 'Origin request'
const result = await uut.authRouter(rpcData) const result = await uut.authRouter(rpcData)
+9 -2
View File
@@ -15,6 +15,7 @@ process.env.SVC_ENV = 'test'
// Local libraries // Local libraries
const config = require('../../../config') const config = require('../../../config')
const UserRPC = require('../../../src/rpc/users') const UserRPC = require('../../../src/rpc/users')
const RateLimit = require('../../../src/rpc/rate-limit')
const UserModel = require('../../../src/models/users') const UserModel = require('../../../src/models/users')
describe('#UserRPC', () => { describe('#UserRPC', () => {
@@ -40,6 +41,7 @@ describe('#UserRPC', () => {
sandbox = sinon.createSandbox() sandbox = sinon.createSandbox()
uut = new UserRPC() uut = new UserRPC()
uut.rateLimit = new RateLimit({ max: 100 })
}) })
afterEach(() => sandbox.restore()) afterEach(() => sandbox.restore())
@@ -120,6 +122,7 @@ describe('#UserRPC', () => {
const userCall = jsonrpc.request(id, 'users', { endpoint: 'createUser' }) const userCall = jsonrpc.request(id, 'users', { endpoint: 'createUser' })
const jsonStr = JSON.stringify(userCall, null, 2) const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr) const rpcData = jsonrpc.parse(jsonStr)
rpcData.from = 'Origin request'
const result = await uut.userRouter(rpcData) const result = await uut.userRouter(rpcData)
@@ -139,9 +142,10 @@ describe('#UserRPC', () => {
}) })
const jsonStr = JSON.stringify(userCall, null, 2) const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr) const rpcData = jsonrpc.parse(jsonStr)
rpcData.from = 'Origin request'
const result = await uut.userRouter(rpcData) const result = await uut.userRouter(rpcData)
console.log('result', result)
assert.equal(result, true) assert.equal(result, true)
}) })
@@ -159,6 +163,7 @@ describe('#UserRPC', () => {
}) })
const jsonStr = JSON.stringify(userCall, null, 2) const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr) const rpcData = jsonrpc.parse(jsonStr)
rpcData.from = 'Origin request'
const result = await uut.userRouter(rpcData) const result = await uut.userRouter(rpcData)
// console.log('result: ', result) // console.log('result: ', result)
@@ -179,7 +184,7 @@ describe('#UserRPC', () => {
}) })
const jsonStr = JSON.stringify(userCall, null, 2) const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr) const rpcData = jsonrpc.parse(jsonStr)
rpcData.from = 'Origin request'
const result = await uut.userRouter(rpcData) const result = await uut.userRouter(rpcData)
assert.equal(result, true) assert.equal(result, true)
@@ -199,6 +204,7 @@ describe('#UserRPC', () => {
}) })
const jsonStr = JSON.stringify(userCall, null, 2) const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr) const rpcData = jsonrpc.parse(jsonStr)
rpcData.from = 'Origin request'
const result = await uut.userRouter(rpcData) const result = await uut.userRouter(rpcData)
// console.log('result: ', result) // console.log('result: ', result)
@@ -216,6 +222,7 @@ describe('#UserRPC', () => {
const userCall = jsonrpc.request(id, 'users', { endpoint: 'createUser' }) const userCall = jsonrpc.request(id, 'users', { endpoint: 'createUser' })
const jsonStr = JSON.stringify(userCall, null, 2) const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr) const rpcData = jsonrpc.parse(jsonStr)
rpcData.from = 'Origin request'
const result = await uut.userRouter(rpcData) const result = await uut.userRouter(rpcData)
// console.log('result: ', result) // console.log('result: ', result)
+64
View File
@@ -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.')
}
})
})
})