diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 34b6a4a..df85235 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -22,9 +22,9 @@ 'use strict' const passport = require('passport') -// const BasicStrategy = require('passport-http').BasicStrategy +const BasicStrategy = require('passport-http').BasicStrategy const AnonymousStrategy = require('passport-anonymous') -// const wlogger = require('../util/winston-logging') +const wlogger = require('../util/winston-logging') // Used for debugging and iterrogating JS objects. const util = require('util') @@ -33,9 +33,9 @@ util.inspect.defaultOptions = { depth: 1 } // let _this // Set default rate limit value for testing -// const PRO_PASSES = process.env.PRO_PASS ? process.env.PRO_PASS : 'BITBOX' +const PRO_PASSES = process.env.PRO_PASS ? process.env.PRO_PASS : 'BITBOX' // Convert the pro-tier password string into an array split by ':'. -// const PRO_PASS = PRO_PASSES.split(':') +const PRO_PASS = PRO_PASSES.split(':') // wlogger.verbose(`PRO_PASS set to: ${PRO_PASS}`) @@ -48,55 +48,55 @@ class AuthMW { passport.use(new AnonymousStrategy()) // Initialize passport for 'basic' authentication. - // passport.use( - // new BasicStrategy({ passReqToCallback: true }, function ( - // req, - // username, - // password, - // done - // ) { - // // console.log(`req: ${util.inspect(req)}`) - // // console.log(`username: ${username}`) - // // console.log(`password: ${password}`) - // - // // Create the req.locals property if it does not yet exist. - // if (!req.locals) { - // req.locals = { - // // default values - // proLimit: false, - // apiLevel: 0 - // } - // } - // - // // Set pro-tier rate limit to flag to false by default. - // req.locals.proLimit = false - // - // // Evaluate the username and password and set the rate limit accordingly. - // // if (username === "BITBOX" && password === PRO_PASS) { - // if (username === 'BITBOX') { - // for (let i = 0; i < PRO_PASS.length; i++) { - // const thisPass = PRO_PASS[i] - // - // if (password === thisPass) { - // wlogger.verbose(`${req.url} called by ${password.slice(0, 6)}`) - // - // // Success - // req.locals.proLimit = true - // break - // } - // } - // } - // - // // console.log(`req.locals: ${util.inspect(req.locals)}`) - // - // return done(null, true) - // }) - // ) + passport.use( + new BasicStrategy({ passReqToCallback: true }, function ( + req, + username, + password, + done + ) { + // console.log(`req: ${util.inspect(req)}`) + // console.log(`username: ${username}`) + // console.log(`password: ${password}`) + + // Create the req.locals property if it does not yet exist. + if (!req.locals) { + req.locals = { + // default values + proLimit: false, + apiLevel: 0 + } + } + + // Set pro-tier rate limit to flag to false by default. + req.locals.proLimit = false + + // Evaluate the username and password and set the rate limit accordingly. + // if (username === "BITBOX" && password === PRO_PASS) { + if (username === 'fullstackcash') { + for (let i = 0; i < PRO_PASS.length; i++) { + const thisPass = PRO_PASS[i] + + if (password === thisPass) { + wlogger.verbose(`${req.url} called by ${password.slice(0, 6)}`) + + // Success + req.locals.proLimit = true + break + } + } + } + + // console.log(`req.locals: ${util.inspect(req.locals)}`) + + return done(null, true) + }) + ) } // Middleware called by the route. mw () { - return passport.authenticate(['anonymous'], { + return passport.authenticate(['anonymous', 'basic'], { session: false }) } diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index 4467f9c..83c2c6c 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -113,74 +113,77 @@ class RateLimits { // Default value is 50 points per request = 20 RPM let rateLimit = 50 - // 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 { - // The resource being consumed: full node, indexer, SLPDB, etc. - const resource = _this.getResource(req.url) - wlogger.debug(`resource: ${resource}`) + // Only evaluate the JWT token if the user is not using Basic Authentication. + if (!req.locals.proLimit) { + // 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 { + // The resource being consumed: full node, indexer, SLPDB, etc. + const resource = _this.getResource(req.url) + wlogger.debug(`resource: ${resource}`) - let key = userId || req.ip - res.locals.key = key // Feedback for tests. + let key = userId || req.ip + res.locals.key = key // Feedback for tests. - // const pointsToConsume = userId ? 1 : 30 - decoded.resource = resource - let pointsToConsume = _this.calcPoints(decoded) - res.locals.pointsToConsume = pointsToConsume // Feedback for tests. - - // Retrieve the origin. - let origin = req.get('origin') - - // Handle calls coming from the intranet. - if (origin === undefined && key.indexOf('10.0.0.5') > -1) { - origin = 'slp-api' - } - - wlogger.info(`origin: ${origin}`) - - // If the request originates from one of the approved wallet apps, then - // apply paid-access rate limits. - if ( - origin && - (origin.toString().indexOf('wallet.fullstack.cash') > -1 || - origin.toString().indexOf('sandbox.fullstack.cash') > -1 || - origin === 'slp-api') - ) { - pointsToConsume = 10 + // const pointsToConsume = userId ? 1 : 30 + decoded.resource = resource + let pointsToConsume = _this.calcPoints(decoded) res.locals.pointsToConsume = pointsToConsume // Feedback for tests. + + // Retrieve the origin. + let origin = req.get('origin') + + // Handle calls coming from the intranet. + if (origin === undefined && key.indexOf('10.0.0.5') > -1) { + origin = 'slp-api' + } + + wlogger.info(`origin: ${origin}`) + + // If the request originates from one of the approved wallet apps, then + // apply paid-access rate limits. + if ( + origin && + (origin.toString().indexOf('wallet.fullstack.cash') > -1 || + origin.toString().indexOf('sandbox.fullstack.cash') > -1 || + origin === 'slp-api') + ) { + pointsToConsume = 10 + res.locals.pointsToConsume = pointsToConsume // Feedback for tests. + } + + // For internal calls, increase rate limits to as fast as possible. + if ( + key.toString().indexOf('172.17.') > -1 || + key.toString().indexOf('::ffff:127.0.0.1') > -1 + ) { + pointsToConsume = 1 + res.locals.pointsToConsume = pointsToConsume // Feedback for tests. + } + + wlogger.info( + `User ${key} consuming ${pointsToConsume} point for resource ${resource}.` + ) + + rateLimit = Math.floor(1000 / pointsToConsume) + + // Update the key so that rate limits track both the user and the resource. + key = `${key}-${resource}` + + await _this.rateLimiter.consume(key, pointsToConsume) + } catch (err) { + // console.log('err: ', err) + + // Used for returning data for tests. + res.locals.rateLimitTriggered = true + // console.log('res.locals: ', res.locals) + + // 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 ${rateLimit} requests per minute. Increase rate limits at https://fullstack.cash` + }) } - - // For internal calls, increase rate limits to as fast as possible. - if ( - key.toString().indexOf('172.17.') > -1 || - key.toString().indexOf('::ffff:127.0.0.1') > -1 - ) { - pointsToConsume = 1 - res.locals.pointsToConsume = pointsToConsume // Feedback for tests. - } - - wlogger.info( - `User ${key} consuming ${pointsToConsume} point for resource ${resource}.` - ) - - rateLimit = Math.floor(1000 / pointsToConsume) - - // Update the key so that rate limits track both the user and the resource. - key = `${key}-${resource}` - - await _this.rateLimiter.consume(key, pointsToConsume) - } catch (err) { - // console.log('err: ', err) - - // Used for returning data for tests. - res.locals.rateLimitTriggered = true - // console.log('res.locals: ', res.locals) - - // 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 ${rateLimit} requests per minute. Increase rate limits at https://fullstack.cash` - }) } } catch (err) { wlogger.error('Error in route-ratelimit.js/newRateLimit(): ', err) diff --git a/start-dev-example.sh b/start-dev-example.sh index 3168208..713c56b 100755 --- a/start-dev-example.sh +++ b/start-dev-example.sh @@ -26,5 +26,7 @@ export LOCAL_RESTURL=http://127.0.0.1:3000/v3/ # slp-api alternative SLP validator. export SLP_API_URL=http://10.0.0.5:5001/ +# Basic Authentication password +export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword npm start diff --git a/test/v3/integration/rate-limits.js b/test/v3/integration/rate-limits.js index cb4c65f..e741dba 100644 --- a/test/v3/integration/rate-limits.js +++ b/test/v3/integration/rate-limits.js @@ -21,7 +21,7 @@ const SERVER = 'http://localhost:3000/v3/' const TEST_JWT = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlODhhY2JmMDIyMWMxMDAxMmFkOTNmZiIsImVtYWlsIjoiY2hyaXMudHJvdXRuZXJAZ21haWwuY29tIiwiYXBpTGV2ZWwiOjQwLCJyYXRlTGltaXQiOjMsImlhdCI6MTYwMDYyODk1MSwiZXhwIjoxNjAzMjIwOTUxfQ.JPXDJQsxJFtCGZjHOd-hRfJuY41Ef_FQ4ET06CtYdNk' -describe('#rate limits', () => { +describe('#JWT rate limits', () => { it('should get control/getNetworkInfo() with no auth', async () => { const options = { method: 'GET', @@ -36,25 +36,24 @@ describe('#rate limits', () => { assert.hasAnyKeys(result.data, ['version']) }) - it('should trigger rate-limit handler if rate limits exceeds 5 request per minute', async () => { + it('should trigger rate-limit handler if rate limits exceeds 20 request per minute', async () => { try { - // Actual rate limit is 60 per minute X 4 nodes = 240 rpm. const options = { method: 'GET', url: `${SERVER}control/getNetworkInfo` } const promises = [] - for (let i = 0; i < 5; i++) { + for (let i = 0; i < 30; i++) { const promise = axios(options) promises.push(promise) } await Promise.all(promises) - assert.equal(true, false, 'Unexpected result!') + assert.fail('Unexpected result!') } catch (err) { - // console.log(`err.response: ${util.inspect(err.response)}`) + console.log('err: ', err) assert.equal(err.response.status, 429) assert.include(err.response.data.error, 'Too many requests')