diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..e26ad1a --- /dev/null +++ b/config/index.js @@ -0,0 +1,7 @@ +/* + Common configuration settings. +*/ + +module.exports = { + apiTokenSecret: process.env.TOKENSECRET ? process.env.TOKENSECRET : 'secret-jwt-token' +} diff --git a/package.json b/package.json index 744fc89..67e1283 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "helmet": "^3.21.2", "ioredis": "^4.14.1", "jsonwebtoken": "^8.5.1", - "key-encoder": "^2.0.3", "level": "^6.0.0", "mkdirp": "^1.0.0", "mocha": "^7.1.1", diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index 08e3623..909bd56 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -5,10 +5,9 @@ const RateLimit = require('express-rate-limit') const axios = require('axios') const wlogger = require('../util/winston-logging') +const config = require('../../config') const jwt = require('jsonwebtoken') -const KeyEncoder = require('key-encoder').default -const keyEncoder = new KeyEncoder('secp256k1') // Redis const redisOptions = { @@ -31,8 +30,8 @@ const rateLimitOptions = { // This hard-coded value is temporary. It will be swapped out with an environment // variable when moved to production. -const publicKey = - '03e6c358092a459f7da9420de770eef3e16cf3c9c54a3d3d14ac2d7f0b82af4d7d' +// const publicKey = +// '03e6c358092a459f7da9420de770eef3e16cf3c9c54a3d3d14ac2d7f0b82af4d7d' // Set max requests per minute const maxRequests = process.env.RATE_LIMIT_MAX_REQUESTS @@ -270,18 +269,19 @@ class RateLimits { // Decode the JWT token if one exists. if (req.locals.jwtToken) { - const jwtOptions = { - algorithms: ['ES256'] - } + // const jwtOptions = { + // algorithms: ['ES256'] + // } - const pemPublicKey = keyEncoder.encodePublic(publicKey, 'raw', 'pem') + // const pemPublicKey = keyEncoder.encodePublic(publicKey, 'raw', 'pem') // Validate the JWT token. - decoded = _this.jwt.verify( - req.locals.jwtToken, - pemPublicKey, - jwtOptions - ) + // decoded = _this.jwt.verify( + // req.locals.jwtToken, + // pemPublicKey, + // jwtOptions + // ) + decoded = _this.jwt.verify(req.locals.jwtToken, config.apiTokenSecret) // console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`) userId = decoded.id @@ -398,69 +398,69 @@ class RateLimits { // potentially be used by Bitcoin.com. // Rather than using apiLevel, the rateLimit is explicitly recorded in the // JWT token. - async rateLimitSimple (req, res, next) { - try { - let userId - let decoded = {} - - // Create a res.locals object if not passed in. - if (!req.locals) { - req.locals = { - // default values - jwtToken: '', - proLimit: false, - rateLimit: 3 - } - } - - // Decode the JWT token if one exists. - if (req.locals.jwtToken) { - const jwtOptions = { - algorithms: ['ES256'] - } - - const pemPublicKey = keyEncoder.encodePublic(publicKey, 'raw', 'pem') - - // Validate the JWT token. - decoded = _this.jwt.verify( - req.locals.jwtToken, - pemPublicKey, - jwtOptions - ) - // console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`) - - userId = decoded.id - } else { - wlogger.debug('No JWT token found!') - } - - // Code here for the rate limiter is adapted from this example: - // https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#authorized-and-not-authorized-users - try { - // Key for Redis key/value pair. - const key = userId || req.ip - - const pointsToConsume = _this.calcPoints2(decoded) - - wlogger.debug(`User ${key} consuming ${pointsToConsume}.`) - - await _this.rateLimiter.consume(key, pointsToConsume) - } catch (err) { - // console.log(`err: `, err) - - // Rate limited was triggered - res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 - return res.json({ - error: `Too many requests. Your limits are currently ${maxRequests} requests per minute. Increase rate limits at https://fullstack.cash` - }) - } - } catch (err) { - wlogger.error('Error in route-ratelimit.js/rateLimitSimple(): ', err) - // throw err - } - - next() - } + // async rateLimitSimple (req, res, next) { + // try { + // let userId + // let decoded = {} + // + // // Create a res.locals object if not passed in. + // if (!req.locals) { + // req.locals = { + // // default values + // jwtToken: '', + // proLimit: false, + // rateLimit: 3 + // } + // } + // + // // Decode the JWT token if one exists. + // if (req.locals.jwtToken) { + // const jwtOptions = { + // algorithms: ['ES256'] + // } + // + // const pemPublicKey = keyEncoder.encodePublic(publicKey, 'raw', 'pem') + // + // // Validate the JWT token. + // decoded = _this.jwt.verify( + // req.locals.jwtToken, + // pemPublicKey, + // jwtOptions + // ) + // // console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`) + // + // userId = decoded.id + // } else { + // wlogger.debug('No JWT token found!') + // } + // + // // Code here for the rate limiter is adapted from this example: + // // https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#authorized-and-not-authorized-users + // try { + // // Key for Redis key/value pair. + // const key = userId || req.ip + // + // const pointsToConsume = _this.calcPoints2(decoded) + // + // wlogger.debug(`User ${key} consuming ${pointsToConsume}.`) + // + // await _this.rateLimiter.consume(key, pointsToConsume) + // } catch (err) { + // // console.log(`err: `, err) + // + // // Rate limited was triggered + // res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + // return res.json({ + // error: `Too many requests. Your limits are currently ${maxRequests} requests per minute. Increase rate limits at https://fullstack.cash` + // }) + // } + // } catch (err) { + // wlogger.error('Error in route-ratelimit.js/rateLimitSimple(): ', err) + // // throw err + // } + // + // next() + // } // Calculates the points consumed, based on the explicit rateLimit defined // in the JWT token. diff --git a/test/v3/rate-limits.js b/test/v3/rate-limits.js index 54319e9..f94d625 100644 --- a/test/v3/rate-limits.js +++ b/test/v3/rate-limits.js @@ -618,185 +618,6 @@ describe('#route-ratelimits & jwt-auth', () => { ) }) }) - - describe('#rateLimitSimple', () => { - it('should pass through rate-limit middleware', async () => { - req.baseUrl = '/v3' - req.path = '/control/getNetworkInfo' - req.url = req.path - req.method = 'GET' - - // Call the route twice to trigger the rate handling. - await rateLimits.rateLimitSimple(req, res, next) - await rateLimits.rateLimitSimple(req, res, next) - - // next() will be called if rate-limit is not triggered - assert.equal(next.called, true) - }) - - it('should trigger rate-limit handler if rate limits exceeds 5 request per minute', async () => { - req.baseUrl = '/v3' - req.path = '/control/getNetworkInfo' - req.url = req.path - req.method = 'GET' - - for (let i = 0; i < 5; i++) { - next.reset() // reset the stubbed next() function. - - await rateLimits.rateLimitSimple(req, res, next) - // console.log(`next() called: ${next.called}`) - } - - // Note: next() will be called unless the rate-limit kicks in. - assert.equal( - next.called, - false, - 'next should not be called if rate limit was triggered.' - ) - }) - - it('should NOT trigger rate-limit for free-tier at 5 RPM', async () => { - // Create a new instance of the rate limit so we start with zeroed tracking. - rateLimits = new RateLimits() - - req.baseUrl = '/v3' - req.path = '/control/getNetworkInfo' - req.url = req.path - req.method = 'GET' - - req.locals.jwtToken = 'some-token' - - const jwtInfo = { - rateLimit: 10, - id: '5e3a0415eb29a962da2708c1' - } - - // Mock the call to the jwt library. - sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo) - - for (let i = 0; i < 5; i++) { - next.reset() // reset the stubbed next() function. - - await rateLimits.rateLimitSimple(req, res, next) - // console.log(`next() called: ${next.called}`) - } - - // console.log(`req.locals after test: ${util.inspect(req.locals)}`) - - // Note: next() will be called unless the rate-limit kicks in. - assert.equal( - next.called, - true, - 'next should be called if rate limit was not triggered.' - ) - }) - - it('should trigger rate-limit for free tier after 10 RPM', async () => { - // Create a new instance of the rate limit so we start with zeroed tracking. - rateLimits = new RateLimits() - - req.baseUrl = '/v3' - req.path = '/control/getNetworkInfo' - req.url = req.path - req.method = 'GET' - - req.locals.jwtToken = 'some-token' - - const jwtInfo = { - rateLimit: 10, - id: '5e3a0415eb29a962da2708c2' - } - - // Mock the call to the jwt library. - sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo) - - for (let i = 0; i < 12; i++) { - next.reset() // reset the stubbed next() function. - - await rateLimits.rateLimitSimple(req, res, next) - // console.log(`next() called: ${next.called}`) - } - - // Note: next() will be called unless the rate-limit kicks in. - assert.equal( - next.called, - false, - 'next should not be called if rate limit was triggered.' - ) - }) - - it('should NOT trigger rate-limit handler for pro-tier at 25 RPM', async () => { - // Create a new instance of the rate limit so we start with zeroed tracking. - rateLimits = new RateLimits() - - req.baseUrl = '/v3' - req.path = '/control/getNetworkInfo' - req.url = req.path - req.method = 'GET' - - req.locals.jwtToken = 'some-token' - - const jwtInfo = { - rateLimit: 100, - id: '5e3a0415eb29a962da2708c3' - } - - // Mock the call to the jwt library. - sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo) - - for (let i = 0; i < 25; i++) { - next.reset() // reset the stubbed next() function. - - await rateLimits.rateLimitSimple(req, res, next) - // console.log(`next() called: ${next.called}`) - } - - // console.log(`req.locals after test: ${util.inspect(req.locals)}`) - - // Note: next() will be called unless the rate-limit kicks in. - assert.equal( - next.called, - true, - 'next should be called if rate limit was not triggered.' - ) - }) - - it('should still rate-limit at a higher RPM for pro-tier', async () => { - // Create a new instance of the rate limit so we start with zeroed tracking. - rateLimits = new RateLimits() - - req.baseUrl = '/v3' - req.path = '/control/getNetworkInfo' - req.url = req.path - req.method = 'GET' - - req.locals.jwtToken = 'some-token' - - const jwtInfo = { - rateLimit: 100, - id: '5e3a0415eb29a962da2708c4' - } - - // Mock the call to the jwt library. - sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo) - - for (let i = 0; i < 150; i++) { - next.reset() // reset the stubbed next() function. - - await rateLimits.rateLimitSimple(req, res, next) - // console.log(`next() called: ${next.called}`) - } - - // console.log(`req.locals after test: ${util.inspect(req.locals)}`) - - // Note: next() will be called unless the rate-limit kicks in. - assert.equal( - next.called, - false, - 'next should NOT be called if rate limit was triggered.' - ) - }) - }) }) // Generates a Basic authorization header.