mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-21 16:52:04 -07:00
Finished initial tests, ready for live testing
This commit is contained in:
@@ -87,7 +87,7 @@ class RateLimits {
|
||||
|
||||
// Exit if the user has already authenticated with Basic Authentication.
|
||||
if (req.locals.proLimit) {
|
||||
console.log(
|
||||
wlogger.debug(
|
||||
'req.locals.proLimit = true; Using Basic Authentication instead of rate limits'
|
||||
)
|
||||
return next()
|
||||
@@ -106,34 +106,10 @@ class RateLimits {
|
||||
// Internal API calls should pass the authentication data in through the
|
||||
// the usrObj in the body.
|
||||
if (req.body && req.body.usrObj) {
|
||||
//
|
||||
if (req.body.usrObj.proLimit) {
|
||||
// If this is an internal call that originated from a user using
|
||||
// Basic Authentication, then skip rate-limits.
|
||||
return next()
|
||||
|
||||
//
|
||||
// } else if (req.body.usrObj.jwtToken) {
|
||||
// // Internal call originated from a user using a JWT token.
|
||||
// console.log(
|
||||
// 'Internal call originated from a user using a JWT token'
|
||||
// )
|
||||
//
|
||||
// const hasExceededRateLimit = await _this.trackRateLimits(
|
||||
// req,
|
||||
// res,
|
||||
// req.body.usrObj.jwtToken
|
||||
// )
|
||||
// if (!hasExceededRateLimit) {
|
||||
// return next()
|
||||
// } else {
|
||||
// return hasExceededRateLimit
|
||||
// }
|
||||
// //
|
||||
// } else {
|
||||
// // Internal call originates from an anonymous user.
|
||||
// console.log('Internal call originates from an anonymous user')
|
||||
// }
|
||||
} else {
|
||||
// Determine if user has exceeded their rate limits. Pass in the
|
||||
// JWT token if one exists.
|
||||
@@ -142,13 +118,38 @@ class RateLimits {
|
||||
res,
|
||||
req.body.usrObj.jwtToken
|
||||
)
|
||||
|
||||
if (!hasExceededRateLimit) {
|
||||
// Rate limits have not been exceeded. Processing can continue.
|
||||
return next()
|
||||
} else {
|
||||
// trackRateLimits() returns the 'res' object with an error message
|
||||
// and status code.
|
||||
return hasExceededRateLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
//
|
||||
} else {
|
||||
// Handle the normal use-case of external requests
|
||||
|
||||
// Track the rate limit for this user. Pass in the JWT token, if one
|
||||
// is available.
|
||||
const hasExceededRateLimit = await _this.trackRateLimits(
|
||||
req,
|
||||
res,
|
||||
req.locals.jwtToken
|
||||
)
|
||||
|
||||
if (!hasExceededRateLimit) {
|
||||
// Rate limits have not been exceeded. Processing can continue.
|
||||
return next()
|
||||
} else {
|
||||
// trackRateLimits() returns the 'res' object with an error message
|
||||
// and status code.
|
||||
return hasExceededRateLimit
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in route-ratelimit2.js/applyRateLimits(): ', err)
|
||||
@@ -158,14 +159,14 @@ class RateLimits {
|
||||
next()
|
||||
}
|
||||
|
||||
// A wrapper for Redis-based rate limiter. This function is called by
|
||||
// applyRateLimits(), after it's determined the key to use.
|
||||
// A wrapper for Redis-based rate limiter.
|
||||
// Will return false if the user has not exceeded the rate limit. Otherwise
|
||||
// it will return the res object that should be returned by the middleware.
|
||||
// it will return the 'res' object with an error status and message, which
|
||||
// should be returned by the middleware.
|
||||
async trackRateLimits (req, res, jwtToken) {
|
||||
// Anonymous rate limits are used by default.
|
||||
let pointsToConsume = 50
|
||||
let key = req.ip
|
||||
let key = req.ip // Use the IP address as the key, by default.
|
||||
|
||||
try {
|
||||
// Decode the JWT token if it exists
|
||||
@@ -173,7 +174,9 @@ class RateLimits {
|
||||
const decoded = _this.decodeJwtToken(jwtToken)
|
||||
// console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`)
|
||||
|
||||
// Preferentially use the decoded ID in the JWT payload, as the key.
|
||||
key = decoded.id
|
||||
|
||||
pointsToConsume = decoded.pointsToConsume
|
||||
}
|
||||
console.log(`rate limit key: ${key}`)
|
||||
@@ -204,25 +207,42 @@ class RateLimits {
|
||||
|
||||
// Attempts to decode a JWT token. Returns default values if it fails.
|
||||
decodeJwtToken (jwtToken) {
|
||||
// Default values, in case there is an error.
|
||||
let decoded = {
|
||||
const defaultPayload = {
|
||||
id: '123.456.789.10',
|
||||
email: 'test@bchtest.net',
|
||||
apiLevel: 10,
|
||||
rateLimit: 3,
|
||||
pointsToConsume: 50,
|
||||
duration: 30,
|
||||
iat: 1615157087,
|
||||
exp: 1617749087
|
||||
duration: 30
|
||||
}
|
||||
|
||||
try {
|
||||
decoded = _this.jwt.verify(jwtToken, _this.config.apiTokenSecret)
|
||||
} catch (err) {
|
||||
console.error('Error in route-ratelimit2.js/decodeJwtTokens()')
|
||||
}
|
||||
// Default values, in case there is an error.
|
||||
const defaultJwt = _this.generateJwtToken(defaultPayload)
|
||||
|
||||
return decoded
|
||||
// Generate a default payload to use, if the decoding of the user-provided
|
||||
// jwt fails.
|
||||
let decoded = _this.jwt.verify(defaultJwt, _this.config.apiTokenSecret)
|
||||
|
||||
try {
|
||||
decoded = _this.jwt.verify(jwtToken, _this.config.apiTokenSecret)
|
||||
} catch (err) {
|
||||
wlogger.error('Error in route-ratelimit2.js/decodeJwtTokens(): ', err)
|
||||
}
|
||||
|
||||
return decoded
|
||||
} catch (err) {
|
||||
wlogger.error(
|
||||
'Unhandled error in route-ratelimit2.js/deocdeJwtToken: ',
|
||||
err
|
||||
)
|
||||
|
||||
// Making sure there is an exp property. Not sure if this will cause an
|
||||
// issue, using a hard-coded value.
|
||||
defaultPayload.exp = 1574269450
|
||||
|
||||
return defaultPayload
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a boolean if the origin of the request matches a domain in the
|
||||
@@ -301,13 +321,8 @@ class RateLimits {
|
||||
expiresIn: '30 days'
|
||||
}
|
||||
|
||||
const jwtPayload = {
|
||||
id: payload.id,
|
||||
pointsToConsume: payload.pointsToConsume
|
||||
}
|
||||
|
||||
const token = _this.jwt.sign(
|
||||
jwtPayload,
|
||||
payload,
|
||||
_this.config.apiTokenSecret,
|
||||
jwtOptions
|
||||
)
|
||||
|
||||
@@ -121,6 +121,85 @@ describe('#rate-routelimit2', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('#decodeJwtToken', () => {
|
||||
it('should return the default JWT payload if decoding fails', () => {
|
||||
const jwt =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlODhhY2JmMDIyMWMxMDAxMmFkOTNmZiIsImVtYWlsIjoiY2hyaXMudHJvdXRuZXJAZ21haWwuY29tIiwiYXBpTGV2ZWwiOjQwLCJyYXRlTGltaXQiOjMsImlhdCI6MTYxNTE1NzA4NywiZXhwIjoxNjE3NzQ5MDg3fQ.RLNGuYAa-CcLdhTGD27tDeaxT6-GIdeR8T4JWZZLDZA'
|
||||
|
||||
const result = uut.decodeJwtToken(jwt)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'id')
|
||||
assert.equal(result.id, '123.456.789.10')
|
||||
assert.property(result, 'email')
|
||||
assert.equal(result.email, 'test@bchtest.net')
|
||||
assert.property(result, 'pointsToConsume')
|
||||
assert.equal(result.pointsToConsume, 50)
|
||||
assert.property(result, 'duration')
|
||||
assert.equal(result.duration, 30)
|
||||
assert.property(result, 'exp')
|
||||
})
|
||||
|
||||
it('should return the default JWT payload if no input is given', () => {
|
||||
const result = uut.decodeJwtToken()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'id')
|
||||
assert.equal(result.id, '123.456.789.10')
|
||||
assert.property(result, 'email')
|
||||
assert.equal(result.email, 'test@bchtest.net')
|
||||
assert.property(result, 'pointsToConsume')
|
||||
assert.equal(result.pointsToConsume, 50)
|
||||
assert.property(result, 'duration')
|
||||
assert.equal(result.duration, 30)
|
||||
assert.property(result, 'exp')
|
||||
})
|
||||
|
||||
it('should correctly decode a JWT token', () => {
|
||||
// Generate a new JWT token for the test.
|
||||
const jwtPayload = {
|
||||
id: '5dade3f5739e6c0ff034b9a1',
|
||||
pointsToConsume: 10,
|
||||
email: 'gooduser@test.com',
|
||||
apiLevel: 40,
|
||||
rateLimit: 100,
|
||||
duration: 30
|
||||
}
|
||||
const jwtToken = uut.generateJwtToken(jwtPayload)
|
||||
|
||||
const result = uut.decodeJwtToken(jwtToken)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'id')
|
||||
assert.equal(result.id, jwtPayload.id)
|
||||
assert.property(result, 'email')
|
||||
assert.equal(result.email, jwtPayload.email)
|
||||
assert.property(result, 'pointsToConsume')
|
||||
assert.equal(result.pointsToConsume, jwtPayload.pointsToConsume)
|
||||
assert.property(result, 'duration')
|
||||
assert.equal(result.duration, jwtPayload.duration)
|
||||
assert.property(result, 'exp')
|
||||
})
|
||||
|
||||
it('should return the default payload if there is an unhandled error', () => {
|
||||
// Force an error.
|
||||
sandbox.stub(uut, 'generateJwtToken').throws(new Error('test error'))
|
||||
|
||||
const result = uut.decodeJwtToken()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'id')
|
||||
assert.equal(result.id, '123.456.789.10')
|
||||
assert.property(result, 'email')
|
||||
assert.equal(result.email, 'test@bchtest.net')
|
||||
assert.property(result, 'pointsToConsume')
|
||||
assert.equal(result.pointsToConsume, 50)
|
||||
assert.property(result, 'duration')
|
||||
assert.equal(result.duration, 30)
|
||||
assert.property(result, 'exp')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#trackRateLimits', () => {
|
||||
it('should apply anonymous rate limits if no JWT token is provided', async () => {
|
||||
req.ip = '127.0.0.1'
|
||||
@@ -195,5 +274,61 @@ describe('#rate-routelimit2', () => {
|
||||
'Expecting next to be called'
|
||||
)
|
||||
})
|
||||
|
||||
it('should apply rate limits to anonymous users', async () => {
|
||||
req.ip = '123.456.7.8'
|
||||
|
||||
// console.log('next.callCount: ', next.callCount)
|
||||
const startCallCount = next.callCount
|
||||
|
||||
await uut.applyRateLimits(req, res, next)
|
||||
|
||||
// console.log('next.callCount: ', next.callCount)
|
||||
const endCallCount = next.callCount
|
||||
|
||||
assert.isAbove(
|
||||
endCallCount,
|
||||
startCallCount,
|
||||
'Expecting next to be called'
|
||||
)
|
||||
|
||||
assert.equal(
|
||||
res.locals.pointsToConsume,
|
||||
50,
|
||||
'Anonymous rate limits applied'
|
||||
)
|
||||
})
|
||||
|
||||
it('should apply rate limits when JWT token is provided', async () => {
|
||||
// Generate a new JWT token for the test.
|
||||
const jwtPayload = {
|
||||
id: '5dade3f5739e6c0ff034b9a1',
|
||||
pointsToConsume: 10
|
||||
}
|
||||
const jwtToken = uut.generateJwtToken(jwtPayload)
|
||||
|
||||
req.ip = '123.456.7.8'
|
||||
req.locals.jwtToken = jwtToken
|
||||
|
||||
// console.log('next.callCount: ', next.callCount)
|
||||
const startCallCount = next.callCount
|
||||
|
||||
await uut.applyRateLimits(req, res, next)
|
||||
|
||||
// console.log('next.callCount: ', next.callCount)
|
||||
const endCallCount = next.callCount
|
||||
|
||||
assert.isAbove(
|
||||
endCallCount,
|
||||
startCallCount,
|
||||
'Expecting next to be called'
|
||||
)
|
||||
|
||||
assert.equal(
|
||||
res.locals.pointsToConsume,
|
||||
10,
|
||||
'Anonymous rate limits applied'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user