diff --git a/src/app.js b/src/app.js index 1863839..3531fac 100644 --- a/src/app.js +++ b/src/app.js @@ -96,17 +96,26 @@ app.use('/', logReqInfo) const v4prefix = 'v4' -// Inspect the header for a JWT token. -app.use(`/${v4prefix}/`, jwtAuth.getTokenFromHeaders) - -// Instantiate the authorization middleware, used to implement pro-tier rate limiting. -// Handles Anonymous and Basic Authorization schemes used by passport.js +// START Rate Limits +// Allow users to turn off rate limits with an environment variable. +const USE_RATE_LIMITS = process.env.USE_RATE_LIMITS + ? process.env.USE_RATE_LIMITS + : true const auth = new AuthMW() -app.use(`/${v4prefix}/`, auth.mw()) -// Rate limit on all v4 routes -// Establish and enforce rate limits. -app.use(`/${v4prefix}/`, rateLimits.rateLimitByResource) +if (USE_RATE_LIMITS) { + // Inspect the header for a JWT token. + app.use(`/${v4prefix}/`, jwtAuth.getTokenFromHeaders) + + // Instantiate the authorization middleware, used to implement pro-tier rate limiting. + // Handles Anonymous and Basic Authorization schemes used by passport.js + app.use(`/${v4prefix}/`, auth.mw()) + + // Rate limit on all v4 routes + // Establish and enforce rate limits. + app.use(`/${v4prefix}/`, rateLimits.rateLimitByResource) +} +// END Rate Limits // Connect v4 routes app.use(`/${v4prefix}/` + 'health-check', healthCheckV4) diff --git a/src/middleware/auth.js b/src/middleware/auth.js index b349d4d..b99a81c 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -1,22 +1,25 @@ /* - CT 2/4/20 Note: This library handles anonymous and Basic auth. This library - can be phased out with the chage to JWT tokens and the new rate-limit library. - - Handle authorization for bypassing rate limits. + This library handles anonymous and Basic Authentication. 1) Default is 'Anonymous Authentication', which unlocks the freemimum tier by default. + 2) Hard-coded 'Basic Authentication' is a token that does not expire and is - provided to buisiness partners. + provided for clients who run their own isolated infrastructure without rate + limits, but still need a way from preventing the random public from using + their API. + 3) JWT-based 'Local Authentication' is used for normal users that pay to access the premium pro-tier services. This file uses the passport npm library to check the header of each REST API - call for the prescence of a Basic authorization header: + call for the prescence of a Basic Authentication header: https://en.wikipedia.org/wiki/Basic_access_authentication If the header is found and validated, the req.locals.proLimit Boolean value - is set and passed to the route-ratelimits.ts middleware. + is set and passed to the route-ratelimit.js middleware. route-ratelimit.js + is for fine-grain JWT-based rate limits. If req.locals.proLimit is set to + true, then those rate limits will be skipped. */ 'use strict' @@ -76,8 +79,9 @@ class AuthMW { req.locals.proLimit = false // Evaluate the username and password and set the rate limit accordingly. - // if (username === "BITBOX" && password === PRO_PASS) { if (username === 'fullstackcash') { + // Can set several different passwords in the environment variable. + // Loop through each one to see if one matches. for (let i = 0; i < PRO_PASS.length; i++) { const thisPass = PRO_PASS[i] diff --git a/src/middleware/route-ratelimit2.js b/src/middleware/route-ratelimit2.js new file mode 100644 index 0000000..aaa9127 --- /dev/null +++ b/src/middleware/route-ratelimit2.js @@ -0,0 +1,103 @@ +/* +This file will replace the original rate-limit.js file. + +Sets the rate limits for the anonymous and paid tiers. Current rate limits: +- 1000 points in 60 seconds +- 10 points per call for paid tier (100 RPM) +- 50 points per call for anonymous tier (20 RPM) + +The rate limit handling is designed for these four use cases: +- Users who want to buy a JWT token for 24 hour access. +- Users who want to buy different RPM tiers: 100, 250, 600 +- Basic Authentication which should not have any rate limits applied. + +The Basci Auth use cases is considered when determining internal rate limits. +The internal rate limits should not be applied to calls from those users. + +A lot of attention has been paid to passing rate-limit information for the user +when they trigger an endpoint that makes a lot of internal API calls. Examples +are hydrateUtxos() and getPublicKey(). +*/ + +// Public npm libraries. +const jwt = require('jsonwebtoken') +const Redis = require('ioredis') +const { RateLimiterRedis } = require('rate-limiter-flexible') + +// local libraries. +// const wlogger = require('../util/winston-logging') +const config = require('../../config') + +// let _this // Global pointer to instance of class, when 'this' context is lost. + +// Setup Redis to track rate limits for each user. +const redisOptions = { + enableOfflineQueue: false, + port: process.env.REDIS_PORT ? process.env.REDIS_PORT : 6379, + host: process.env.REDIS_HOST ? process.env.REDIS_HOST : '127.0.0.1' +} +console.log(`redisOptions: ${JSON.stringify(redisOptions, null, 2)}`) +const redisClient = new Redis(redisOptions) +const rateLimitOptions = { + storeClient: redisClient, + points: 1000, // Number of points + duration: 60 // Per minute (per 60 seconds) +} + +// Constants +// const ANON_LIMITS = config.anonRateLimit +// const WHITELIST_RATE_LIMIT = config.whitelistRateLimit +// const WHITELIST_DOMAINS = config.whitelistDomains +// const INTERNAL_RATE_LIMIT = 1 + +class RateLimits { + constructor () { + // _this = this + + this.jwt = jwt + this.rateLimiter = new RateLimiterRedis(rateLimitOptions) + this.config = config + } + + // This is the main middleware funciton of this library. All other functions + // support this function. + async applyRateLimits (req, res, next) { + try { + // let userId + // let decoded = {} + + // Create a re*Q*.locals object if not passed in. + // req.locals.proLimit will be true if the user is using Basic Authentication. + if (!req.locals) { + req.locals = { + // default values + jwtToken: '', + proLimit: false, + apiLevel: 0 + } + } + + // Create a re*S*.locals object if it does not exist. + if (!res.locals) { + res.locals = { + rateLimitTriggered: false + } + } + } catch (err) { + + } + } + + // Used to disconnect from the Redis DB. + // Called by unit tests so that node.js thread doesn't live forever. + closeRedis () { + redisClient.disconnect() + } + + // Clear the redis database. Used by unit tests. + async wipeRedis () { + await redisClient.flushdb() + } +} + +module.exports = RateLimits