fix(rate-limit.js): Refactored to use JS Class

This commit is contained in:
Chris Troutner
2020-02-04 16:35:31 -08:00
parent 13b41a50c6
commit 5ecc321b3e
4 changed files with 181 additions and 164 deletions
+4 -2
View File
@@ -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)
+156 -148
View File
@@ -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