/* This file controls the request-per-minute (RPM) rate limits. It is assumed that this middleware is run AFTER the jwt-auth.js and auth.js middleware. Current rate limiting rules in requests-per-minute: - anonymous access: 3 - free access: 10, apiLevel = 0 - any paid tier: 100, apiLevel > 0 If a person signs up for full node access but not indexer access, then the apiLevel will be 10. If they call an endpoint that uses an indexer, the apiLevel will be downgraded to 0 on-the-fly. Indexer endpoints will effectively be downgraded to the anonymous access tier. */ "use strict" const express = require("express") const RateLimit = require("express-rate-limit") const axios = require("axios") const jwt = require("jsonwebtoken") const KeyEncoder = require("key-encoder").default const keyEncoder = new KeyEncoder("secp256k1") // Redis const Redis = require("ioredis") const redisClient = new Redis({ enableOfflineQueue: false }) // Rate limiter middleware lib. const { RateLimiterRedis } = require("rate-limiter-flexible") const rateLimitOptions = { storeClient: redisClient, points: 100, // Number of points duration: 1 // Per second } // Set max requests per minute const maxRequests = process.env.RATE_LIMIT_MAX_REQUESTS ? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) : 3 // Pro-tier rate limits are 10x the freemium limits. // const PRO_RPM = 10 * maxRequests // Unique route mapped to its rate limit const uniqueRateLimits = {} let _this class RateLimits { constructor() { _this = this this.rateLimiter = new RateLimiterRedis(rateLimitOptions) } async routeRateLimit(req, res, next) { // Disable rate limiting if 0 passed from RATE_LIMIT_MAX_REQUESTS if (maxRequests === 0) return next() // Create a res.locals object if not passed in. if (!req.locals) { req.locals = { // default values jwtToken: "", proLimit: false, apiLevel: 0 } } // 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}`) // 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 = _this.evalUserPermissioins(req, jwtInfo) // console.log( // `userPermissions: ${JSON.stringify(userPermissions, null, 2)}` // ) req.locals.proLimit = userPermissions.proLimit req.locals.apiLevel = userPermissions.apiLevel } } } // 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) } // 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 } async newRateLimit(req, res, next) { try { let userId // Create a res.locals object if not passed in. if (!req.locals) { req.locals = { // default values jwtToken: "", proLimit: false, apiLevel: 0 } } if (req.locals.jwtToken) { // Hexadecimal const publicKey = "03e6c358092a459f7da9420de770eef3e16cf3c9c54a3d3d14ac2d7f0b82af4d7d" const jwtOptions = { algorithms: ["ES256"] } const pemPublicKey = keyEncoder.encodePublic(publicKey, "raw", "pem") // Validate the JWT token. const decoded = jwt.verify( req.locals.jwtToken, pemPublicKey, jwtOptions ) console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`) userId = decoded.id } else { console.log(`No JWT token found!`) } try { // https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#authorized-and-not-authorized-users const key = userId ? userId : req.ip const pointsToConsume = userId ? 1 : 30 console.log(`User ${userId} consuming ${pointsToConsume} points.`) await this.rateLimiter.consume(key, pointToConsume) } catch (err) { // 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 ${maxRequests} requests per minute. Increase rate limits at https://account.bchjs.cash` }) } } catch (err) { console.error(`Error in route-ratelimit.js/newRateLimit(): `, err) // throw err } next() } } module.exports = RateLimits