mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-22 09:12:05 -07:00
fix(rate-limit.js): Refactored to use JS Class
This commit is contained in:
+4
-2
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user