diff --git a/src/app.js b/src/app.js index 95597a8..1a972f0 100644 --- a/src/app.js +++ b/src/app.js @@ -3,7 +3,9 @@ const express = require("express") // Middleware -const { routeRateLimit } = require("./middleware/route-ratelimit") +// const { routeRateLimit } = require("./middleware/route-ratelimit") +const RateLimits = require("./middleware/route-ratelimit") +const rateLimits = new RateLimits() const path = require("path") const logger = require("morgan") @@ -84,7 +86,7 @@ app.use(`/${v3prefix}/`, auth.mw()) // Rate limit on all v3 routes // Establish and enforce rate limits. -app.use(`/${v3prefix}/`, routeRateLimit) +app.use(`/${v3prefix}/`, rateLimits.routeRateLimit) app.use(`/${v3prefix}/` + `health-check`, healthCheckV3) app.use(`/${v3prefix}/` + `blockchain`, blockchainV3.router) diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index 3dd8b6a..f8f3935 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -32,169 +32,177 @@ const maxRequests = process.env.RATE_LIMIT_MAX_REQUESTS // Unique route mapped to its rate limit const uniqueRateLimits = {} -const routeRateLimit = async function(req, res, next) { - // Disable rate limiting if 0 passed from RATE_LIMIT_MAX_REQUESTS - if (maxRequests === 0) return next() +let _this - // Create a res.locals object if not passed in. - if (!req.locals) { - req.locals = { - // default values - jwtToken: "", - proLimit: false, - apiLevel: 0 - } +class RateLimits { + constructor() { + _this = this } - // Warn if JWT_AUTH_SERVER env var is not set. - const authServer = process.env.JWT_AUTH_SERVER - if (!authServer || authServer === "") { - console.warn( - "JWT_AUTH_SERVER env var is not set. JWT tokens not being evaluated." - ) - } else { - // If a JWT token is passed in, validate it and enable pro-tier rate limits - // if it's valid. - if (req.locals.jwtToken) { - // console.log(`req.locals.jwtToken: ${req.locals.jwtToken}`) + async routeRateLimit(req, res, next) { + // Disable rate limiting if 0 passed from RATE_LIMIT_MAX_REQUESTS + if (maxRequests === 0) return next() - // URL for the auth server. - const path = `${authServer}apitoken/isvalid/${req.locals.jwtToken}` - - // Ask Auth server if the JWT token is valid. - // Get the API level for this user. - let jwtInfo = await axios.get(path) - jwtInfo = jwtInfo.data - // console.log(`jwtInfo: ${JSON.stringify(jwtInfo, null, 2)}`) - - // If JWT if valid, evaluate the API level for the user. - if (jwtInfo.isValid) { - // Set fine-grain permissions for each user based on the JWT token. - const userPermissions = evalUserPermissioins(req, jwtInfo) - // console.log( - // `userPermissions: ${JSON.stringify(userPermissions, null, 2)}` - // ) - - req.locals.proLimit = userPermissions.proLimit - req.locals.apiLevel = userPermissions.apiLevel + // Create a res.locals object if not passed in. + if (!req.locals) { + req.locals = { + // default values + jwtToken: "", + proLimit: false, + apiLevel: 0 } } - } - // Current route - const rateLimitTier = req.locals.proLimit ? "PRO" : "BASIC" - const path = req.baseUrl + req.path + // Warn if JWT_AUTH_SERVER env var is not set. + const authServer = process.env.JWT_AUTH_SERVER + if (!authServer || authServer === "") { + console.warn( + "JWT_AUTH_SERVER env var is not set. JWT tokens not being evaluated." + ) + } else { + // If a JWT token is passed in, validate it and enable pro-tier rate limits + // if it's valid. + if (req.locals.jwtToken) { + // console.log(`req.locals.jwtToken: ${req.locals.jwtToken}`) - // Create a unique string as a route identifier. - const route = - rateLimitTier + - req.method + - req.locals.apiLevel + // Generates new rate limit when user upgrades JWT token. - path - .split("/") - .slice(0, 4) - .join("/") - //console.log(`route identifier: ${JSON.stringify(route, null, 2)}`) + // URL for the auth server. + const path = `${authServer}apitoken/isvalid/${req.locals.jwtToken}` - // console.log(`req.locals: ${JSON.stringify(req.locals, null, 2)}`) + // Ask Auth server if the JWT token is valid. + // Get the API level for this user. + let jwtInfo = await axios.get(path) + jwtInfo = jwtInfo.data + // console.log(`jwtInfo: ${JSON.stringify(jwtInfo, null, 2)}`) - // This boolean value is passed from the auth.js middleware. - const proRateLimits = req.locals.proLimit + // If JWT if valid, evaluate the API level for the user. + if (jwtInfo.isValid) { + // Set fine-grain permissions for each user based on the JWT token. + const userPermissions = _this.evalUserPermissioins(req, jwtInfo) + // console.log( + // `userPermissions: ${JSON.stringify(userPermissions, null, 2)}` + // ) - // console.log(`proRateLimits: ${proRateLimits}`) - - // Pro level rate limits - if (proRateLimits || proRateLimits === 0) { - // TODO: replace the console.logs with calls to our logging system. - // console.log(`applying pro-rate limits`) - - let PRO_RPM = 10 // Default value for free tier - if (req.locals.apiLevel > 0) PRO_RPM = 100 // RPM for paid tiers. - - // console.log(`PRO_RPM: ${PRO_RPM}, apiLevel: ${req.locals.apiLevel}`) - - // Create new RateLimit if none exists for this route - if (!uniqueRateLimits[route]) { - uniqueRateLimits[route] = new RateLimit({ - windowMs: 60 * 1000, // 1 minute window - delayMs: 0, // disable delaying - full speed until the max limit is reached - max: PRO_RPM, // start blocking after this many requests per minute - handler: function(req, res) { - //console.log(`pro-tier rate-handler triggered.`) - - res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 - return res.json({ - error: `Too many requests. Limits are ${PRO_RPM} requests per minute. Increase rate limits at https://account.bchjs.cash` - }) + req.locals.proLimit = userPermissions.proLimit + req.locals.apiLevel = userPermissions.apiLevel } - }) - } - - // Freemium level rate limits - } else { - // TODO: replace the console.logs with calls to our logging system. - // console.log(`applying freemium limits`) - - // Create new RateLimit if none exists for this route - if (!uniqueRateLimits[route]) { - uniqueRateLimits[route] = new RateLimit({ - windowMs: 60 * 1000, // 1 minute window - delayMs: 0, // disable delaying - full speed until the max limit is reached - max: maxRequests, // start blocking after maxRequests - handler: function(req, res) { - //console.log(`freemium rate-handler 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://account.bchjs.cash` - }) - } - }) - } - } - - //console.log(`calling uniqueRateLimits() on this route: ${route}`) - - // Call rate limit for this route - uniqueRateLimits[route](req, res, next) -} - -// This function returns an object with proLimit and apiLevel properties. -// It does fine-grane analysis on the data coming from the auth servers and -// uses its output to adjust rate limits on-the-fly based on the users -// permission level. -function evalUserPermissioins(req, authData) { - // console.log(`authData: ${JSON.stringify(authData, null, 2)}`) - - // Return object with default values - const retObj = { - proLimit: authData.isValid, - apiLevel: authData.apiLevel - } - - // if apiLevel = 0 (free tier), then return the default values. - if (retObj.apiLevel === 0) return retObj - - const level20Routes = ["insight", "bitcore", "blockbook"] - - const locals = req.locals - // console.log(`locals: ${JSON.stringify(locals, null, 2)}`) - const url = req.url - // console.log(`url: ${JSON.stringify(url, null, 2)}`) - - if (authData.apiLevel < 20) { - // Loop through the routes that are not accessible to this tier. - for (let i = 0; i < level20Routes.length; i++) { - // If the requested route is for a higher tier, - // revert to anonymous level permissions. - if (url.indexOf(level20Routes[i]) > -1) { - retObj.proLimit = false - retObj.apiLevel = 0 } } + + // Current route + const rateLimitTier = req.locals.proLimit ? "PRO" : "BASIC" + const path = req.baseUrl + req.path + + // Create a unique string as a route identifier. + const route = + rateLimitTier + + req.method + + req.locals.apiLevel + // Generates new rate limit when user upgrades JWT token. + path + .split("/") + .slice(0, 4) + .join("/") + //console.log(`route identifier: ${JSON.stringify(route, null, 2)}`) + + // console.log(`req.locals: ${JSON.stringify(req.locals, null, 2)}`) + + // This boolean value is passed from the auth.js middleware. + const proRateLimits = req.locals.proLimit + + // console.log(`proRateLimits: ${proRateLimits}`) + + // Pro level rate limits + if (proRateLimits || proRateLimits === 0) { + // TODO: replace the console.logs with calls to our logging system. + // console.log(`applying pro-rate limits`) + + let PRO_RPM = 10 // Default value for free tier + if (req.locals.apiLevel > 0) PRO_RPM = 100 // RPM for paid tiers. + + // console.log(`PRO_RPM: ${PRO_RPM}, apiLevel: ${req.locals.apiLevel}`) + + // Create new RateLimit if none exists for this route + if (!uniqueRateLimits[route]) { + uniqueRateLimits[route] = new RateLimit({ + windowMs: 60 * 1000, // 1 minute window + delayMs: 0, // disable delaying - full speed until the max limit is reached + max: PRO_RPM, // start blocking after this many requests per minute + handler: function(req, res) { + //console.log(`pro-tier rate-handler triggered.`) + + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Too many requests. Limits are ${PRO_RPM} requests per minute. Increase rate limits at https://account.bchjs.cash` + }) + } + }) + } + + // Freemium level rate limits + } else { + // TODO: replace the console.logs with calls to our logging system. + // console.log(`applying freemium limits`) + + // Create new RateLimit if none exists for this route + if (!uniqueRateLimits[route]) { + uniqueRateLimits[route] = new RateLimit({ + windowMs: 60 * 1000, // 1 minute window + delayMs: 0, // disable delaying - full speed until the max limit is reached + max: maxRequests, // start blocking after maxRequests + handler: function(req, res) { + //console.log(`freemium rate-handler 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://account.bchjs.cash` + }) + } + }) + } + } + + //console.log(`calling uniqueRateLimits() on this route: ${route}`) + + // Call rate limit for this route + uniqueRateLimits[route](req, res, next) } - return retObj + // This function returns an object with proLimit and apiLevel properties. + // It does fine-grane analysis on the data coming from the auth servers and + // uses its output to adjust rate limits on-the-fly based on the users + // permission level. + evalUserPermissioins(req, authData) { + // console.log(`authData: ${JSON.stringify(authData, null, 2)}`) + + // Return object with default values + const retObj = { + proLimit: authData.isValid, + apiLevel: authData.apiLevel + } + + // if apiLevel = 0 (free tier), then return the default values. + if (retObj.apiLevel === 0) return retObj + + const level20Routes = ["insight", "bitcore", "blockbook"] + + const locals = req.locals + // console.log(`locals: ${JSON.stringify(locals, null, 2)}`) + const url = req.url + // console.log(`url: ${JSON.stringify(url, null, 2)}`) + + if (authData.apiLevel < 20) { + // Loop through the routes that are not accessible to this tier. + for (let i = 0; i < level20Routes.length; i++) { + // If the requested route is for a higher tier, + // revert to anonymous level permissions. + if (url.indexOf(level20Routes[i]) > -1) { + retObj.proLimit = false + retObj.apiLevel = 0 + } + } + } + + return retObj + } } -module.exports = { routeRateLimit } +module.exports = RateLimits diff --git a/test/v3/integration/rate-limits.js b/test/v3/integration/rate-limits.js index 99ba139..4142369 100644 --- a/test/v3/integration/rate-limits.js +++ b/test/v3/integration/rate-limits.js @@ -18,7 +18,7 @@ util.inspect.defaultOptions = { depth: 1 } const SERVER = `http://localhost:3000/v3/` const TEST_JWT = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYTc5ZDk4OTYyMjRjNjM2MmQwYzkwMiIsImlhdCI6MTU3MTUzOTU1MSwiZXhwIjoxNTc0MTMxNTUxfQ.PfPW_Z2NYT1O2zUHXopcz2aLGHSGudaKOIGnt7SuAi4" + "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlM2EwNDE1ZWIyOWE5NjJkYTI3MDhiNCIsImFwaUxldmVsIjowLCJyYXRlTGltaXQiOjEwLCJpYXQiOjE1ODA4NjA0NjcsImV4cCI6MTU4MzQ1MjQ2N30.fuY5S-YrF0J11h5uyMjPe7wiVkYRnIyXi4dL9-V-C6pLJm33p0dSq_pSheVVWw78n5kAvL_9kFHngbnmQiOJYQ" describe("#rate limits", () => { it("should get control/getNetworkInfo() with no auth", async () => { diff --git a/test/v3/rate-limits.js b/test/v3/rate-limits.js index 3753ccc..3ded94a 100644 --- a/test/v3/rate-limits.js +++ b/test/v3/rate-limits.js @@ -12,7 +12,10 @@ util.inspect.defaultOptions = { depth: 1 } const { mockReq, mockRes, mockNext } = require("./mocks/express-mocks") // Libraries under test -let rateLimitMiddleware = require("../../src/middleware/route-ratelimit") +const RateLimits = require("../../src/middleware/route-ratelimit") +const rateLimits = new RateLimits() +let rateLimitMiddleware = rateLimits.routeRateLimit + const controlRoute = require("../../src/routes/v3/full-node/control") const jwtAuth = require("../../src/middleware/jwt-auth") @@ -74,6 +77,7 @@ describe("#route-ratelimits & jwt-auth", () => { }) describe("#routeRateLimit", () => { + rateLimitMiddleware = new RateLimits() let routeRateLimit = rateLimitMiddleware.routeRateLimit const getInfo = controlRoute.testableComponents.getInfo @@ -112,10 +116,11 @@ describe("#route-ratelimits & jwt-auth", () => { it("should NOT trigger rate-limit for free-tier at 5 RPM", async () => { // Clear the require cache before running this test. - delete require.cache[ - require.resolve("../../src/middleware/route-ratelimit") - ] - rateLimitMiddleware = require("../../src/middleware/route-ratelimit") + // delete require.cache[ + // require.resolve("../../src/middleware/route-ratelimit") + // ] + // rateLimitMiddleware = require("../../src/middleware/route-ratelimit") + rateLimitMiddleware = new RateLimits() routeRateLimit = rateLimitMiddleware.routeRateLimit req.baseUrl = "/v3" @@ -167,10 +172,11 @@ describe("#route-ratelimits & jwt-auth", () => { it("should NOT trigger rate-limit handler for pro-tier at 25 RPM", async () => { // Clear the require cache before running this test. - delete require.cache[ - require.resolve("../../src/middleware/route-ratelimit") - ] - rateLimitMiddleware = require("../../src/middleware/route-ratelimit") + // delete require.cache[ + // require.resolve("../../src/middleware/route-ratelimit") + // ] + // rateLimitMiddleware = require("../../src/middleware/route-ratelimit") + rateLimitMiddleware = new RateLimits() routeRateLimit = rateLimitMiddleware.routeRateLimit req.baseUrl = "/v3" @@ -199,10 +205,11 @@ describe("#route-ratelimits & jwt-auth", () => { it("rate-limiting should still kick in at a higher RPM for pro-tier", async () => { // Clear the require cache before running this test. - delete require.cache[ - require.resolve("../../src/middleware/route-ratelimit") - ] - rateLimitMiddleware = require("../../src/middleware/route-ratelimit") + // delete require.cache[ + // require.resolve("../../src/middleware/route-ratelimit") + // ] + // rateLimitMiddleware = require("../../src/middleware/route-ratelimit") + rateLimitMiddleware = new RateLimits() routeRateLimit = rateLimitMiddleware.routeRateLimit req.baseUrl = "/v3"