Merge pull request #94 from christroutner/redis-rate-limits

Refactored rate limits
This commit is contained in:
Chris Troutner
2020-02-07 17:17:05 -08:00
committed by GitHub
9 changed files with 855 additions and 261 deletions
+4
View File
@@ -9,6 +9,10 @@ sudo: required
services:
- docker
- redis-server
#before_install:
# - ./install-redis.sh
script: "npm run test"
+185 -4
View File
@@ -731,11 +731,27 @@
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
"dev": true
},
"@types/bn.js": {
"version": "4.11.6",
"resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz",
"integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==",
"requires": {
"@types/node": "*"
}
},
"@types/color-name": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
},
"@types/elliptic": {
"version": "6.4.12",
"resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.12.tgz",
"integrity": "sha512-gP1KsqoouLJGH6IJa28x7PXb3cRqh83X8HCLezd2dF+XcAIMKYv53KV+9Zn6QA561E120uOqZBQ+Jy/cl+fviw==",
"requires": {
"@types/bn.js": "*"
}
},
"@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
@@ -1158,6 +1174,17 @@
"safer-buffer": "~2.1.0"
}
},
"asn1.js": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.3.0.tgz",
"integrity": "sha512-WHnQJFcOrIWT1RLOkFFBQkFVvyt9BPOOrH+Dp152Zk4R993rSzXUGPmkybIcUFhHE2d/iHH+nCaOWVCDbO8fgA==",
"requires": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
}
},
"assert": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz",
@@ -1870,6 +1897,11 @@
"ieee754": "^1.1.4"
}
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
},
"buffer-equals": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/buffer-equals/-/buffer-equals-1.0.4.tgz",
@@ -2256,6 +2288,11 @@
"wrap-ansi": "^5.1.0"
}
},
"cluster-key-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
"integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw=="
},
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -3041,6 +3078,11 @@
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
},
"denque": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz",
"integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ=="
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -3231,6 +3273,14 @@
"safer-buffer": "^2.1.0"
}
},
"ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"requires": {
"safe-buffer": "^5.0.1"
}
},
"ecurve": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/ecurve/-/ecurve-1.0.6.tgz",
@@ -5690,6 +5740,22 @@
"p-is-promise": "^3.0.0"
}
},
"ioredis": {
"version": "4.14.1",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.14.1.tgz",
"integrity": "sha512-94W+X//GHM+1GJvDk6JPc+8qlM7Dul+9K+lg3/aHixPN7ZGkW6qlvX0DG6At9hWtH2v3B32myfZqWoANUJYGJA==",
"requires": {
"cluster-key-slot": "^1.1.0",
"debug": "^4.1.1",
"denque": "^1.1.0",
"lodash.defaults": "^4.2.0",
"lodash.flatten": "^4.4.0",
"redis-commands": "1.5.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.0.1"
}
},
"ipaddr.js": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
@@ -6244,6 +6310,30 @@
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
"dev": true
},
"jsonwebtoken": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
"requires": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^5.6.0"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
}
}
},
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@@ -6271,6 +6361,25 @@
"integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==",
"dev": true
},
"jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"requires": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"requires": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"keccak": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/keccak/-/keccak-1.4.0.tgz",
@@ -6282,6 +6391,17 @@
"safe-buffer": "^5.1.0"
}
},
"key-encoder": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/key-encoder/-/key-encoder-2.0.3.tgz",
"integrity": "sha512-fgBtpAGIr/Fy5/+ZLQZIPPhsZEcbSlYu/Wu96tNDFNSjSACw5lEIOFeaVdQ/iwrb8oxjlWi6wmWdH76hV6GZjg==",
"requires": {
"@types/elliptic": "^6.4.9",
"asn1.js": "^5.0.1",
"bn.js": "^4.11.8",
"elliptic": "^6.4.1"
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -6473,12 +6593,22 @@
"integrity": "sha1-+CbJtOKoUR2E46yinbBeGk87cqk=",
"dev": true
},
"lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
},
"lodash.escaperegexp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
"integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=",
"dev": true
},
"lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
},
"lodash.flattendeep": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
@@ -6490,23 +6620,46 @@
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
"dev": true
},
"lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
},
"lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
},
"lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
},
"lodash.ismatch": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz",
"integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=",
"dev": true
},
"lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
},
"lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=",
"dev": true
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
},
"lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=",
"dev": true
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
},
"lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
},
"lodash.set": {
"version": "4.3.2",
@@ -12165,6 +12318,11 @@
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"rate-limiter-flexible": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-1.3.2.tgz",
"integrity": "sha512-f+xNvGn+52G4nZVok9VB3LTE1kfDmqbnWKRayX5n2k/LEQ7doWrYvzmzFVh7ltmLgwEdEzoYwxGaXlfCjFg4Ag=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
@@ -12332,6 +12490,24 @@
"esprima": "~4.0.0"
}
},
"redis-commands": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz",
"integrity": "sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg=="
},
"redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60="
},
"redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
"requires": {
"redis-errors": "^1.0.0"
}
},
"referrer-policy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz",
@@ -13628,6 +13804,11 @@
}
}
},
"standard-as-callback": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.0.1.tgz",
"integrity": "sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg=="
},
"standard-engine": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/standard-engine/-/standard-engine-8.0.1.tgz",
+6 -2
View File
@@ -15,15 +15,15 @@
"test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/blockchain.js",
"test:integration": "mocha test/v3/integration",
"coverage": "nyc report --reporter=text-lcov | coveralls",
"coverage:report": "export NETWORK=testnet && nyc --reporter=html mocha test/v2/",
"coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v3/",
"docs": "./node_modules/.bin/apidoc -i src/routes/v3 -o docs"
},
"engines": {
"node": ">=10.15.1"
},
"dependencies": {
"apidoc": "^0.20.0",
"@chris.troutner/bch-js": "^2.0.0",
"apidoc": "^0.20.0",
"axios": "^0.19.0",
"body-parser": "^1.18.3",
"cookie-parser": "~1.4.3",
@@ -34,6 +34,9 @@
"express-basic-auth": "^1.1.3",
"express-rate-limit": "^5.0.0",
"helmet": "^3.21.2",
"ioredis": "^4.14.1",
"jsonwebtoken": "^8.5.1",
"key-encoder": "^2.0.3",
"level": "^6.0.0",
"mkdirp": "^1.0.0",
"mocha": "^7.0.1",
@@ -44,6 +47,7 @@
"passport-http": "^0.3.0",
"pg": "^7.11.0",
"pg-hstore": "^2.3.2",
"rate-limiter-flexible": "^1.3.2",
"strftime": "^0.10.0",
"winston": "^3.2.1",
"winston-daily-rotate-file": "^4.0.0"
+8 -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")
@@ -78,11 +80,15 @@ const v3prefix = "v3"
app.use(`/${v3prefix}/`, jwtAuth.getTokenFromHeaders)
// Instantiate the authorization middleware, used to implement pro-tier rate limiting.
// Handles Anonymous and Basic Authorization schemes used by passport.js
const auth = new AuthMW()
app.use(`/${v3prefix}/`, auth.mw())
// Rate limit on all v3 routes
app.use(`/${v3prefix}/`, routeRateLimit) // Establish and enforce rate limits.
// Establish and enforce rate limits.
// app.use(`/${v3prefix}/`, rateLimits.routeRateLimit)
app.use(`/${v3prefix}/`, rateLimits.rateLimitByResource)
app.use(`/${v3prefix}/` + `health-check`, healthCheckV3)
app.use(`/${v3prefix}/` + `blockchain`, blockchainV3.router)
app.use(`/${v3prefix}/` + `control`, controlV3.router)
+3
View File
@@ -1,4 +1,7 @@
/*
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.
1) Default is 'Anonymous Authentication', which unlocks the freemimum tier by
+2
View File
@@ -1,4 +1,6 @@
/*
This is a middleware library for handling and processing JWT tokens.
This middleware inspects the request header for a JWT token.
If found, will populate req.locals.jwtToken with the JWT token.
*/
+353 -165
View File
@@ -1,26 +1,27 @@
/*
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 wlogger = require("../util/winston-logging")
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)
@@ -32,169 +33,356 @@ 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
this.jwt = jwt
this.rateLimiter = new RateLimiterRedis(rateLimitOptions)
}
// 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}`)
// Used to disconnect from the Redis DB.
// Called by unit tests so that node.js thread doesn't live forever.
closeRedis() {
redisClient.disconnect()
}
// URL for the auth server.
const path = `${authServer}apitoken/isvalid/${req.locals.jwtToken}`
/*
This function controls the tierd request-per-minute (RPM) rate limits.
This is an older implementation that is currently not used.
// 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
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.
*/
// CT 2/7/20: Older rate-limiting code that does not scale well.
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.
// CT 2/7/20: I believe this is older code that is only used by routeRateLimit.
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
}
/*
This is the new rate limit function that uses the rate-limiter-flexible npm
library.
*/
async rateLimitByResource(req, res, next) {
try {
let userId
let decoded = {}
// 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.
decoded = _this.jwt.verify(
req.locals.jwtToken,
pemPublicKey,
jwtOptions
)
// console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`)
userId = decoded.id
} else {
wlogger.debug(`No JWT token found!`)
}
// Code here for the rate limiter is adapted from this example:
// https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#authorized-and-not-authorized-users
try {
// The resource being consumed: full node, indexer, SLPDB, etc.
const resource = _this.getResource(req.url)
wlogger.debug(`resource: ${resource}`)
let key = userId ? userId : req.ip
// const pointsToConsume = userId ? 1 : 30
decoded.resource = resource
const pointsToConsume = _this.calcPoints(decoded)
wlogger.info(
`User ${key} consuming ${pointsToConsume} point for resource ${resource}.`
)
// Update the key so that rate limits track both the user and the resource.
key = `${key}-${resource}`
await _this.rateLimiter.consume(key, pointsToConsume)
} catch (err) {
// console.log(`err: `, 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) {
wlogger.error(`Error in route-ratelimit.js/newRateLimit(): `, err)
// throw err
}
next()
}
// Calculates the points consumed, based on the jwt information and the route
// requested.
calcPoints(jwtInfo) {
let retVal = 30 // By default, use anonymous tier.
try {
// 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)}`
// )
const apiLevel = jwtInfo.apiLevel
const resource = jwtInfo.resource
req.locals.proLimit = userPermissions.proLimit
req.locals.apiLevel = userPermissions.apiLevel
const level30Routes = ["insight", "bitcore", "blockbook"]
const level40Routes = ["slp"]
wlogger.debug(`apiLevel: ${apiLevel}`)
// Only evaluate if user is using a JWT token.
if (jwtInfo.id) {
// SLP indexer routes
if (level40Routes.includes(resource)) {
if (apiLevel >= 40) retVal = 1
// else if (apiLevel >= 10) retVal = 10
else retVal = 10
}
// Normal indexer routes
else if (level30Routes.includes(resource)) {
if (apiLevel >= 30) retVal = 1
else retVal = 10
}
// Full node tier
else if (apiLevel >= 20) {
retVal = 1
}
// Free tier, full node only.
else {
retVal = 10
}
}
return retVal
} catch (err) {
wlogger.error(`Error in route-ratelimit.js/calcPoints()`)
// throw err
retVal = 30
}
return retVal
}
// Current route
const rateLimitTier = req.locals.proLimit ? "PRO" : "BASIC"
const path = req.baseUrl + req.path
// This function parses the req.url property to identify what resource
// the user is requesting.
// This was created as a function so that it can be unit tested. Not sure
// what kind of variations will be seen in production.
getResource(url) {
try {
wlogger.debug(`url: ${JSON.stringify(url, null, 2)}`)
// 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)}`)
const splitUrl = url.split("/")
const resource = splitUrl[1]
// 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`
})
}
})
return resource
} catch (err) {
wlogger.error(`Error in getResource().`)
throw err
}
}
//console.log(`calling uniqueRateLimits() on this route: ${route}`)
// Call rate limit for this route
uniqueRateLimits[route](req, res, next)
}
// This function returns the 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
}
}
}
return retObj
}
module.exports = { routeRateLimit }
module.exports = RateLimits
+1 -1
View File
@@ -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 () => {
+293 -87
View File
@@ -3,6 +3,7 @@
const chai = require("chai")
const assert = chai.assert
const nock = require("nock") // HTTP mocking
const sinon = require("sinon")
// Used for debugging.
const util = require("util")
@@ -12,7 +13,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")
let rateLimits = new RateLimits()
let rateLimitMiddleware = rateLimits.routeRateLimit
const controlRoute = require("../../src/routes/v3/full-node/control")
const jwtAuth = require("../../src/middleware/jwt-auth")
@@ -23,6 +27,8 @@ let originalEnvVars // Used during transition from integration to unit tests.
const jwt = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYWRlM2Y1NzM5ZTZjMGZmMDM0YjlhMSIsImlhdCI6MTU3MTY3NzQ1MCwiZXhwIjoxNTc0MjY5NDUwfQ.SSz7F7ETyBB3eoNG2VKCzPOhddtB-vrtmEoj7PxicrQ`
describe("#route-ratelimits & jwt-auth", () => {
let sandbox
before(() => {
// Save existing environment variables.
originalEnvVars = {
@@ -47,6 +53,16 @@ describe("#route-ratelimits & jwt-auth", () => {
req.params = {}
req.body = {}
req.query = {}
sandbox = sinon.createSandbox()
})
afterEach(() => {
sandbox.restore()
})
after(() => {
rateLimits.closeRedis()
})
describe("#jwt-auth.js", () => {
@@ -71,83 +87,10 @@ describe("#route-ratelimits & jwt-auth", () => {
assert.equal(req.locals.jwtToken, jwt)
})
})
// TODO: This code has been refactored and these unit tests no longer apply.
// I think I forgot to create new unit tests to reflect the change in code though.
/*
describe("#routeAccess", () => {
it("should do nothing if req.locals.jwtToken is undefined", () => {
// Initialize req.locals
req.locals = {
proLimit: false,
apiLevel: 0
}
req.url = "/insight/address/details"
// Reset the history of the stub and assert it has not been called.
res.status.resetHistory()
assert.equal(res.status.called, false, "stub history reset")
jwtAuth.routeAccess(req, res, next)
// console.log(`req.locals: ${JSON.stringify(req.locals, null, 2)}`)
assert.equal(
res.status.called,
false,
"stub should NOT have been called."
)
})
it("should throw error if full-node tier tries to access indexer", () => {
// Initialize req.locals
req.locals = {
proLimit: true,
apiLevel: 10,
jwtToken: jwt
}
req.url = "/insight/address/details"
try {
res.status.resetHistory()
assert.equal(res.status.called, false, "stub history reset")
jwtAuth.routeAccess(req, res, next)
assert.equal(res.status.called, true, "stub should have been called.")
} catch (err) {
console.log(`caught error: `, err)
}
})
it("should allow indexer tier tries access indexer endpoints", () => {
// Initialize req.locals
req.locals = {
proLimit: true,
apiLevel: 20,
jwtToken: jwt
}
req.url = "/insight/address/details"
try {
res.status.resetHistory()
assert.equal(res.status.called, false, "stub history reset")
jwtAuth.routeAccess(req, res, next)
assert.equal(
res.status.called,
false,
"stub should NOT have been called."
)
} catch (err) {
console.log(`caught error: `, err)
}
})
})
*/
})
describe("#routeRateLimit", () => {
rateLimitMiddleware = new RateLimits()
let routeRateLimit = rateLimitMiddleware.routeRateLimit
const getInfo = controlRoute.testableComponents.getInfo
@@ -186,10 +129,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"
@@ -241,10 +185,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"
@@ -273,10 +218,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"
@@ -303,6 +249,266 @@ describe("#route-ratelimits & jwt-auth", () => {
)
})
})
describe("#getResource", () => {
it("should decode a blockchain request", () => {
const url =
"/blockchain/getTxOut/62a3ea958a463a372bc0caf2c374a7f60be9c624be63a0db8db78f05809df6d8/0?include_mempool=true"
const result = rateLimits.getResource(url)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(result, "blockchain")
})
})
describe("#calcPoints", () => {
it("should return 30 points for anonymous user", () => {
const result = rateLimits.calcPoints()
// console.log(`result: ${result}`)
assert.equal(result, 30)
})
it("should return 10 points for free tier requesting full node access", () => {
const jwtInfo = {
apiLevel: 10,
resource: "blockchain",
id: "5e3a0415eb29a962da2708b4"
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
it("should return 10 points for free tier requesting indexer access", () => {
const jwtInfo = {
apiLevel: 10,
resource: "blockbook",
id: "5e3a0415eb29a962da2708b4"
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
it("should return 10 points for free tier requesting SLPDB access", () => {
const jwtInfo = {
apiLevel: 10,
resource: "slp",
id: "5e3a0415eb29a962da2708b4"
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
it("should return 1 point for full node tier requesting full node access", () => {
const jwtInfo = {
apiLevel: 20,
resource: "blockchain",
id: "5e3a0415eb29a962da2708b4"
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 1)
})
it("should return 10 points for full-node tier requesting indexer access", () => {
const jwtInfo = {
apiLevel: 20,
resource: "blockbook",
id: "5e3a0415eb29a962da2708b4"
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
it("should return 10 points for full node tier requesting SLPDB access", () => {
const jwtInfo = {
apiLevel: 20,
resource: "slp",
id: "5e3a0415eb29a962da2708b4"
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
it("should return 1 point for indexer tier requesting full node access", () => {
const jwtInfo = {
apiLevel: 30,
resource: "blockchain",
id: "5e3a0415eb29a962da2708b4"
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 1)
})
it("should return 1 points for indexer tier requesting indexer access", () => {
const jwtInfo = {
apiLevel: 30,
resource: "blockbook",
id: "5e3a0415eb29a962da2708b4"
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 1)
})
it("should return 10 points for indexer tier requesting SLPDB access", () => {
const jwtInfo = {
apiLevel: 30,
resource: "slp",
id: "5e3a0415eb29a962da2708b4"
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 10)
})
it("should return 1 point for SLP tier requesting full node access", () => {
const jwtInfo = {
apiLevel: 40,
resource: "blockchain",
id: "5e3a0415eb29a962da2708b4"
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 1)
})
it("should return 1 points for SLP tier requesting indexer access", () => {
const jwtInfo = {
apiLevel: 40,
resource: "blockbook",
id: "5e3a0415eb29a962da2708b4"
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 1)
})
it("should return 1 points for SLP tier requesting SLPDB access", () => {
const jwtInfo = {
apiLevel: 40,
resource: "slp",
id: "5e3a0415eb29a962da2708b4"
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 1)
})
})
describe("#rateLimitByResource", () => {
it("should pass through rate-limit middleware", async () => {
req.baseUrl = "/v3"
req.path = "/control/getNetworkInfo"
req.url = req.path
req.method = "GET"
// Call the route twice to trigger the rate handling.
await rateLimits.rateLimitByResource(req, res, next)
await rateLimits.rateLimitByResource(req, res, next)
// next() will be called if rate-limit is not triggered
assert.equal(next.called, true)
})
it("should trigger rate-limit handler if rate limits exceeds 5 request per minute", async () => {
req.baseUrl = "/v3"
req.path = "/control/getNetworkInfo"
req.url = req.path
req.method = "GET"
for (let i = 0; i < 5; i++) {
next.reset() // reset the stubbed next() function.
await rateLimits.rateLimitByResource(req, res, next)
//console.log(`next() called: ${next.called}`)
}
// Note: next() will be called unless the rate-limit kicks in.
assert.equal(
next.called,
false,
`next should not be called if rate limit was triggered.`
)
})
it("should NOT trigger rate-limit for free-tier at 5 RPM", async () => {
// Create a new instance of the rate limit so we start with zeroed tracking.
rateLimits = new RateLimits()
req.baseUrl = "/v3"
req.path = "/control/getNetworkInfo"
req.url = req.path
req.method = "GET"
req.locals.jwtToken = "some-token"
const jwtInfo = {
apiLevel: 10,
id: "5e3a0415eb29a962da2708b4"
}
// Mock the call to the jwt library.
sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo)
for (let i = 0; i < 5; i++) {
next.reset() // reset the stubbed next() function.
await rateLimits.rateLimitByResource(req, res, next)
//console.log(`next() called: ${next.called}`)
}
//console.log(`req.locals after test: ${util.inspect(req.locals)}`)
// Note: next() will be called unless the rate-limit kicks in.
assert.equal(
next.called,
true,
`next should be called if rate limit was not triggered.`
)
})
it("should trigger rate-limit for free tier after 10 RPM", async () => {
// Create a new instance of the rate limit so we start with zeroed tracking.
rateLimits = new RateLimits()
req.baseUrl = "/v3"
req.path = "/control/getNetworkInfo"
req.url = req.path
req.method = "GET"
req.locals.jwtToken = "some-token"
const jwtInfo = {
apiLevel: 10,
id: "5e3a0415eb29a962da2708b4"
}
// Mock the call to the jwt library.
sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo)
for (let i = 0; i < 12; i++) {
next.reset() // reset the stubbed next() function.
await rateLimits.rateLimitByResource(req, res, next)
//console.log(`next() called: ${next.called}`)
}
// Note: next() will be called unless the rate-limit kicks in.
assert.equal(
next.called,
false,
`next should not be called if rate limit was triggered.`
)
})
})
})
// Generates a Basic authorization header.