├── .npmrc ├── index.js ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── LICENSE.md ├── package.json ├── lib ├── index.js └── internals.js ├── README.md └── test ├── test-routes.js └── index.js /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('./lib') 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.html 2 | node_modules 3 | *.log 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "13:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [20.x, 22.x, 24.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm i 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Michael Garvin 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a 5 | copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 19 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-rate-limit", 3 | "version": "8.0.0", 4 | "description": "Rate limiting plugin for hapi", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "lab -a @hapi/code -t 100 -v -e test", 8 | "lint": "standard", 9 | "lint:fix": "standard --fix" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/wraithgar/hapi-rate-limit.git" 14 | }, 15 | "keywords": [ 16 | "hapi", 17 | "rate", 18 | "limit" 19 | ], 20 | "author": "Gar ", 21 | "license": "MIT", 22 | "engines": { 23 | "node": ">=20.0.0" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/wraithgar/hapi-rate-limit/issues" 27 | }, 28 | "pre-commit": [ 29 | "test" 30 | ], 31 | "homepage": "https://github.com/wraithgar/hapi-rate-limit#readme", 32 | "peerDependencies": { 33 | "@hapi/hapi": ">=18.0.0" 34 | }, 35 | "dependencies": { 36 | "@hapi/boom": "^10.0.1", 37 | "@hapi/catbox-memory": "^6.0.2", 38 | "@hapi/hoek": "^11.0.7", 39 | "joi": "^18.0.0" 40 | }, 41 | "files": [ 42 | "index.js", 43 | "lib" 44 | ], 45 | "devDependencies": { 46 | "@hapi/code": "^9.0.3", 47 | "@hapi/hapi": "^21.4.2", 48 | "@hapi/lab": "^26.0.0", 49 | "git-validate": "^2.2.4", 50 | "standard": "^17.1.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Joi = require('joi') 4 | const Pkg = require('../package.json') 5 | 6 | const internals = require('./internals') 7 | 8 | const register = function (plugin, options) { 9 | const pluginSettings = Joi.attempt(Object.assign({}, options), internals.schema) 10 | 11 | // we call toString on the user attribute in getUser, so we have to do it here too. 12 | pluginSettings.userWhitelist = pluginSettings.userWhitelist.map(user => user.toString()) 13 | 14 | const userCache = plugin.cache(pluginSettings.userCache) 15 | const pathCache = plugin.cache(pluginSettings.pathCache) 16 | const userPathCache = plugin.cache(pluginSettings.userPathCache) 17 | const authCache = plugin.cache(pluginSettings.authCache) 18 | 19 | // called regardless if authentication is performed, before authentication is performed 20 | plugin.ext('onPreAuth', async (request, h) => { 21 | const routeSettings = request.route.settings.plugins[internals.pluginName] || {} 22 | 23 | delete routeSettings.userCache 24 | 25 | if (routeSettings.userLimit !== false) { 26 | delete routeSettings.userLimit 27 | } 28 | 29 | const settings = { ...pluginSettings, ...routeSettings } 30 | 31 | request.plugins[internals.pluginName] = { settings } 32 | 33 | if (settings.enabled === false) { 34 | return h.continue 35 | } 36 | 37 | const remaining = await internals.authCheck(authCache, request) 38 | 39 | if (remaining < 0) { 40 | return settings.limitExceededResponse(request, h) 41 | } 42 | 43 | return h.continue 44 | }) 45 | 46 | // called regardless if authentication is performed, but not if authentication fails 47 | plugin.ext('onPostAuth', async (request, h) => { 48 | const { settings } = request.plugins[internals.pluginName] 49 | 50 | if (settings.enabled === false) { 51 | return h.continue 52 | } 53 | 54 | const remaining = await Promise.all([ 55 | internals.userCheck(userCache, request), 56 | internals.userPathCheck(userPathCache, request), 57 | internals.pathCheck(pathCache, request) 58 | ]) 59 | 60 | if (remaining.some(r => r < 0)) { 61 | return settings.limitExceededResponse(request, h) 62 | } 63 | 64 | return h.continue 65 | }) 66 | 67 | // always called, unless the request is aborted 68 | plugin.ext('onPreResponse', async (request, h) => { 69 | const requestPlugin = request.plugins[internals.pluginName] 70 | 71 | if (!requestPlugin) { 72 | // We never even made it to onPreAuth 73 | return h.continue 74 | } 75 | 76 | const { response } = request 77 | 78 | // Non 401s can include authToken in their data and if it's there it counts 79 | if (response.isBoom) { 80 | await internals.authFailure(authCache, request) 81 | } 82 | 83 | const { settings } = requestPlugin 84 | 85 | if (settings.headers !== false) { 86 | let headers = response.headers 87 | 88 | if (response.isBoom) { 89 | headers = response.output.headers 90 | } 91 | 92 | if (settings.pathLimit !== false) { 93 | headers['X-RateLimit-PathLimit'] = requestPlugin.pathLimit 94 | headers['X-RateLimit-PathRemaining'] = requestPlugin.pathRemaining 95 | headers['X-RateLimit-PathReset'] = requestPlugin.pathReset 96 | } 97 | 98 | if (settings.userPathLimit !== false) { 99 | headers['X-RateLimit-UserPathLimit'] = requestPlugin.userPathLimit 100 | headers['X-RateLimit-UserPathRemaining'] = requestPlugin.userPathRemaining 101 | headers['X-RateLimit-UserPathReset'] = requestPlugin.userPathReset 102 | } 103 | 104 | if (settings.userLimit !== false) { 105 | headers['X-RateLimit-UserLimit'] = requestPlugin.userLimit 106 | headers['X-RateLimit-UserRemaining'] = requestPlugin.userRemaining 107 | headers['X-RateLimit-UserReset'] = requestPlugin.userReset 108 | } 109 | } 110 | 111 | return h.continue 112 | }) 113 | } 114 | 115 | module.exports = { 116 | register, 117 | pkg: Pkg 118 | } 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hapi-rate-limit 2 | 3 | Lead Maintainer: [Gar](https://github.com/wraithgar) 4 | 5 | ## Introduction 6 | 7 | **hapi-rate-limit** is a plugin for [hapi](http://hapijs.com) that enables rate limiting. 8 | 9 | ## Use 10 | 11 | ```javascript 12 | const Hapi = require('hapi'); 13 | 14 | const server = Hapi.server({}); 15 | server.register({ 16 | plugin: require('hapi-rate-limit'), 17 | options: {} 18 | }); 19 | ``` 20 | 21 | ## Options 22 | 23 | Defaults are given here 24 | 25 | - `enabled`: `true` whether or not rate limiting is enabled at all. Set this to `false` in a route's config to bypass all rate limiting for that route 26 | - `userLimit`: `300` number of total requests a user can make per period. Set to `false` to disable limiting requests per user. 27 | - `userCache`: Object with the following properties: 28 | - `segment`: `hapi-rate-limit-user` Name of the cache segment to use for storing user rate limit info 29 | - `expiresIn`: `600000` Time (in milliseconds) of period for `userLimit` 30 | - `cache`: Optional cache name configured in server.cache. Defaults to the default cache. 31 | - `userAttribute`: `id` credentials attribute to use when determining distinct authenticated users 32 | - `userWhitelist`: `[]` array of users (as defined by `userAttribute` for whom to bypass rate limiting. This is only applied to authenticated users, for ip whitelisting use `ipWhitelist`. 33 | - `addressOnly`: `false` if true, only consider user address when determining distinct authenticated users 34 | - `pathLimit`: `50` number of total requests that can be made on a given path per period. Set to `false` to disable limiting requests per path. 35 | - `ignorePathParams`: `false` if true, the limit will be applied to the route (`/route/{param}`: single cache entry) rather than to the path (`/route/1` or `/route/2`: 2 distinct cache entries). 36 | - `pathCache`: Object with the following properties: 37 | - `segment`: `hapi-rate-limit-path` Name of the cache segment to use for storing path rate limit info 38 | - `expiresIn`: `60000` Time (in milliseconds) of period for `pathLimit` 39 | - `cache`: Optional cache name configured in server.cache. Defaults to the default cache. 40 | - `userPathLimit`: `false` number of total requests that can be made on a given path per user per period. Set to `false` to disable limiting requests per path per user. 41 | - `userPathCache`: Object with the following properties: 42 | - `segment`: `hapi-rate-limit-userPath` Name of the cache segment to use for storing userPath rate limit info 43 | - `expiresIn`: `60000` Time (in milliseconds) of period for `userPathLimit` 44 | - `cache`: Optional cache name configured in server.cache. Defaults to the default cache. 45 | - `headers`: `true` Whether or not to include headers in responses 46 | - `ipWhitelist`: `[]` array of IPs for whom to bypass rate limiting. Note that a whitelisted IP would also bypass restrictions an authenticated user would otherwise have. 47 | - `trustProxy`: `false` If true, honor the `X-Forwarded-For` header. See note below. 48 | - `getIpFromProxyHeader`: `undefined` a function which will extract the remote address from the `X-Forwarded-For` header. The default implementation takes the first entry. 49 | - `proxyHeaderName`: `X-Forwarded-For` name of the header to use for remote address lookup. 50 | - `limitExceededResponse`: `() => Boom.tooManyRequests('Rate limit exceeded');` a `function(request, h)` that returns a custom response to be used when the rate limit is hit. If the function returns a Boom error, it will be used. If it returns an object, the response will be 200 and the payload whatever the function returns. 51 | - `authLimit`: 5 number of total separate invalid auth attempts that can be made from any given IP. Once that limit has been reached the offending IP will be blocked before hapi's auth layer runs. Set to `false` to disable this feature. 52 | - `authToken`: `authToken` this is the attribute that will be looked for either in auth artifacts, or in boom data for thrown errors to rate limit invalid auth attempts. For instance you would set `artifacts.authToken` to the value of `headers.authorization` to rate limit invalid authorization headers. 53 | 54 | ## Users 55 | 56 | A user is considered a single `remoteAddress` for routes that are unauthenticated. On authenticated routes it is the `userAtribute` (default `id`) of the authenticated user. 57 | 58 | If `trustProxy` is true, the address from the `X-Forwarded-For` header will be use instead of `remoteAddress`, if present. 59 | 60 | If `trustProxy` is true and `getIpFromProxyHeader` is not defined, the address will be determined using the first entry in the `X-Forwarded-For` header. 61 | 62 | 63 | ## Auth 64 | 65 | ## Proxies 66 | 67 | If you set `trustProxy` to true, make sure that your proxy server is the only thing that can access the server, and be sure to configure your proxy to strip all incoming `X-Forwarded-For` headers. 68 | 69 | For example if you were using [haproxy](http://www.haproxy.org) you would add `reqidel ^X-Forwarded-For` to your config. 70 | 71 | Failure to do this would allow anyone to spoof that header to bypass your rate limiting. 72 | 73 | ## Response Headers 74 | 75 | The following headers will be included in server responses if their respective limits are enabled 76 | 77 | - `x-ratelimit-pathlimit`: Will equal `pathLimit` 78 | - `x-ratelimit-pathremaining`: Remaining number of requests path has this - period 79 | - `x-ratelimit-pathreset`: Time (in milliseconds) until reset of `pathLimit` period 80 | - `x-ratelimit-userlimit`: Will equal `userLimit` 81 | - `x-ratelimit-userremaining`: Remaining number of requests user has this period 82 | - `x-ratelimit-userreset`: Time (in milliseconds) until reset of `userLimit` period 83 | - `x-ratelimit-userpathlimit`: Will equal `userPathLimit` 84 | - `x-ratelimit-userpathremaining`: Remaining number of requests user has this period for this path 85 | - `x-ratelimit-userpathreset`: Time (in milliseconds) until reset of `userPathLimit` period 86 | 87 | Note that authLimit does not generate any headers. It is not in your best interest to let bad actors know what their limits are when brute forcing your auth systems. 88 | 89 | ## Per-route settings 90 | 91 | All of the settings (except for `userLimit` and `userCache`) can be overridden in your route's config. 92 | 93 | For instance, to disable `pathLimit` for a route you would add this to its `config` attribute 94 | 95 | ```javascript 96 | plugins: { 97 | 'hapi-rate-limit': { 98 | pathLimit: false 99 | } 100 | } 101 | ``` 102 | 103 | To disable all rate limiting for a route you woul add this to its `config` attribute 104 | 105 | ```javascript 106 | plugins: { 107 | 'hapi-rate-limit': { 108 | enabled: false 109 | } 110 | } 111 | ``` 112 | 113 | ## 114 | 115 | License: MIT 116 | -------------------------------------------------------------------------------- /test/test-routes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Boom = require('@hapi/boom') 4 | 5 | module.exports = [ 6 | { 7 | method: 'GET', 8 | path: '/defaults', 9 | config: { 10 | description: 'Route with no special config, letting defaults take over', 11 | handler: request => { 12 | return request.path 13 | } 14 | } 15 | }, 16 | { 17 | method: 'GET', 18 | path: '/auth', 19 | config: { 20 | description: 'Authenticated route', 21 | handler: function (request) { 22 | return request.path 23 | }, 24 | auth: { 25 | strategy: 'trusty' 26 | } 27 | } 28 | }, 29 | { 30 | method: 'GET', 31 | path: '/addressOnly', 32 | config: { 33 | description: 'Authenticated route with addressOnly set', 34 | handler: function (request) { 35 | return request.path 36 | }, 37 | plugins: { 38 | 'hapi-rate-limit': { 39 | addressOnly: true 40 | } 41 | }, 42 | auth: { 43 | strategy: 'trusty' 44 | } 45 | } 46 | }, 47 | { 48 | method: 'GET', 49 | path: '/addressOnlyUserPathLimit', 50 | config: { 51 | description: 'Authenticated route with addressOnly set with userPathLimit and no user limit', 52 | handler: function (request) { 53 | return request.path 54 | }, 55 | plugins: { 56 | 'hapi-rate-limit': { 57 | addressOnly: true, 58 | userLimit: false, 59 | userPathLimit: 50 60 | } 61 | }, 62 | auth: { 63 | strategy: 'trusty' 64 | } 65 | } 66 | }, 67 | { 68 | method: 'GET', 69 | path: '/authName', 70 | config: { 71 | description: 'Authenticated route with name set as the userAttribute', 72 | handler: function (request) { 73 | return request.path 74 | }, 75 | plugins: { 76 | 'hapi-rate-limit': { 77 | userAttribute: 'name' 78 | } 79 | }, 80 | auth: { 81 | strategy: 'trusty' 82 | } 83 | } 84 | }, 85 | { 86 | method: 'GET', 87 | path: '/notfound', 88 | config: { 89 | description: 'Route that returns a 404', 90 | handler: function () { 91 | return Boom.notFound() 92 | } 93 | } 94 | }, 95 | { 96 | method: 'GET', 97 | path: '/noUserLimit', 98 | config: { 99 | description: 'Route with userLimit disabled', 100 | handler: function (request) { 101 | return request.path 102 | }, 103 | plugins: { 104 | 'hapi-rate-limit': { 105 | userLimit: false 106 | } 107 | } 108 | } 109 | }, 110 | { 111 | method: 'GET', 112 | path: '/noHeaders', 113 | config: { 114 | description: 'Route with rate limit headers disabled', 115 | handler: function (request) { 116 | return request.path 117 | }, 118 | plugins: { 119 | 'hapi-rate-limit': { 120 | userPathLimit: true, 121 | headers: false 122 | } 123 | } 124 | } 125 | }, 126 | { 127 | method: 'GET', 128 | path: '/noPathLimit', 129 | config: { 130 | description: 'Route with pathLimit disabled', 131 | handler: function (request) { 132 | return request.path 133 | }, 134 | plugins: { 135 | 'hapi-rate-limit': { 136 | pathLimit: false 137 | } 138 | } 139 | } 140 | }, 141 | { 142 | method: 'GET', 143 | path: '/noUserPathLimit', 144 | config: { 145 | description: 'Route with userPathLimit disabled', 146 | handler: function (request) { 147 | return request.path 148 | }, 149 | plugins: { 150 | 'hapi-rate-limit': { 151 | userPathLimit: false 152 | } 153 | } 154 | } 155 | }, 156 | { 157 | method: 'GET', 158 | path: '/setPathLimit', 159 | config: { 160 | description: 'Route with set pathLimit', 161 | handler: function (request) { 162 | return request.path 163 | }, 164 | plugins: { 165 | 'hapi-rate-limit': { 166 | pathLimit: 50 167 | } 168 | } 169 | } 170 | }, 171 | { 172 | method: 'GET', 173 | path: '/setUserPathLimit', 174 | config: { 175 | description: 'Route with set userPathLimit', 176 | handler: function (request) { 177 | return request.path 178 | }, 179 | plugins: { 180 | 'hapi-rate-limit': { 181 | userPathLimit: 50 182 | } 183 | }, 184 | auth: { 185 | strategy: 'trusty' 186 | } 187 | } 188 | }, 189 | { 190 | method: 'GET', 191 | path: '/setUserPathLimit2', 192 | config: { 193 | description: 'Route with set userPathLimit', 194 | handler: function (request) { 195 | return request.path 196 | }, 197 | plugins: { 198 | 'hapi-rate-limit': { 199 | userPathLimit: 50 200 | } 201 | } 202 | } 203 | }, 204 | { 205 | method: 'GET', 206 | path: '/setUserPathLimitOnly', 207 | config: { 208 | description: 'Route with set userPathLimit', 209 | handler: function (request) { 210 | return request.path 211 | }, 212 | plugins: { 213 | 'hapi-rate-limit': { 214 | userPathLimit: 50, 215 | userLimit: false, 216 | pathLimit: false 217 | } 218 | }, 219 | auth: { 220 | strategy: 'trusty' 221 | } 222 | } 223 | }, 224 | { 225 | method: 'GET', 226 | path: '/lowPathLimit', 227 | config: { 228 | description: 'Route with very low pathLimit', 229 | handler: function (request) { 230 | return request.path 231 | }, 232 | plugins: { 233 | 'hapi-rate-limit': { 234 | pathLimit: 2 235 | } 236 | } 237 | } 238 | }, 239 | { 240 | method: 'GET', 241 | path: '/lowUserPathLimit', 242 | config: { 243 | description: 'Route with very low userPathLimit', 244 | handler: function (request) { 245 | return request.path 246 | }, 247 | plugins: { 248 | 'hapi-rate-limit': { 249 | userPathLimit: 2 250 | } 251 | } 252 | } 253 | }, 254 | { 255 | method: 'GET', 256 | path: '/trustProxy', 257 | config: { 258 | description: 'Route with trustProxy set', 259 | handler: function (request) { 260 | return request.path 261 | }, 262 | plugins: { 263 | 'hapi-rate-limit': { 264 | trustProxy: true 265 | } 266 | } 267 | } 268 | }, 269 | { 270 | method: 'GET', 271 | path: '/trustProxyCustomHeader', 272 | config: { 273 | description: 'Route with trustProxy set and custom header name', 274 | handler: function (request) { 275 | return request.path 276 | }, 277 | plugins: { 278 | 'hapi-rate-limit': { 279 | trustProxy: true, 280 | proxyHeaderName: 'x-something-else' 281 | } 282 | } 283 | } 284 | }, 285 | { 286 | method: 'GET', 287 | path: '/ipWhitelist', 288 | config: { 289 | description: 'Route with an ipWhitelist', 290 | handler: function (request) { 291 | return request.path 292 | }, 293 | plugins: { 294 | 'hapi-rate-limit': { 295 | ipWhitelist: ['127.0.0.1'] 296 | } 297 | } 298 | } 299 | }, 300 | { 301 | method: 'GET', 302 | path: '/userWhitelist', 303 | config: { 304 | description: 'Route with a userWhitelist', 305 | handler: function (request) { 306 | return request.path 307 | }, 308 | plugins: { 309 | 'hapi-rate-limit': { 310 | userWhitelist: ['1'] 311 | } 312 | }, 313 | auth: { 314 | strategy: 'trusty' 315 | } 316 | } 317 | }, 318 | { 319 | method: 'GET', 320 | path: '/pathDisabled', 321 | config: { 322 | description: 'Route that has disabled rate limiting', 323 | handler: function (request) { 324 | return request.path 325 | }, 326 | plugins: { 327 | 'hapi-rate-limit': { 328 | enabled: false 329 | } 330 | } 331 | } 332 | }, 333 | { 334 | method: 'GET', 335 | path: '/managePathParams/{param}', 336 | config: { 337 | description: 'Route with params used by cache', 338 | handler: function (request) { 339 | return request.path 340 | }, 341 | plugins: { 342 | 'hapi-rate-limit': { 343 | pathLimit: 2 344 | } 345 | } 346 | } 347 | }, 348 | { 349 | method: 'GET', 350 | path: '/ignorePathParams/{param}', 351 | config: { 352 | description: 'Route with params ignored by cache', 353 | handler: function (request) { 354 | return request.path 355 | }, 356 | plugins: { 357 | 'hapi-rate-limit': { 358 | pathLimit: 2, 359 | ignorePathParams: true 360 | } 361 | } 362 | } 363 | } 364 | ] 365 | -------------------------------------------------------------------------------- /lib/internals.js: -------------------------------------------------------------------------------- 1 | const Pkg = require('../package.json') 2 | 3 | const Crypto = require('crypto') 4 | const Boom = require('@hapi/boom') 5 | const Hoek = require('@hapi/hoek') 6 | const Joi = require('joi') 7 | 8 | const pluginName = Pkg.name 9 | 10 | const schema = Joi.object({ 11 | enabled: Joi.boolean().default(true), 12 | addressOnly: Joi.boolean().default(false), 13 | headers: Joi.boolean().default(true), 14 | ipWhitelist: Joi.array().default([]), 15 | authCache: Joi.object({ 16 | getDecoratedValue: Joi.boolean().default(true), 17 | cache: Joi.string().optional(), 18 | segment: Joi.string().default(`${pluginName}-auth`), 19 | expiresIn: Joi.number().default(1 * 60 * 1000) // 1 minute 20 | }).default(), 21 | authToken: Joi.string().default('authToken'), 22 | authLimit: Joi.alternatives() 23 | .try(Joi.boolean(), Joi.number()) 24 | .default(5), 25 | pathCache: Joi.object({ 26 | getDecoratedValue: Joi.boolean().default(true), 27 | cache: Joi.string().optional(), 28 | segment: Joi.string().default(`${pluginName}-path`), 29 | expiresIn: Joi.number().default(1 * 60 * 1000) // 1 minute 30 | }).default(), 31 | pathLimit: Joi.alternatives() 32 | .try(Joi.boolean(), Joi.number()) 33 | .default(50), 34 | ignorePathParams: Joi.boolean().default(false), 35 | trustProxy: Joi.boolean().default(false), 36 | getIpFromProxyHeader: Joi.func().default(null), 37 | proxyHeaderName: Joi.string().default('x-forwarded-for'), 38 | userAttribute: Joi.string().default('id'), 39 | userCache: Joi.object({ 40 | getDecoratedValue: Joi.boolean().default(true), 41 | cache: Joi.string().optional(), 42 | segment: Joi.string().default(`${pluginName}-user`), 43 | expiresIn: Joi.number().default(10 * 60 * 1000) // 10 minutes 44 | }).default(), 45 | userLimit: Joi.alternatives() 46 | .try(Joi.boolean(), Joi.number()) 47 | .default(300), 48 | userWhitelist: Joi.array().default([]), 49 | userPathCache: Joi.object({ 50 | getDecoratedValue: Joi.boolean().default(true), 51 | cache: Joi.string().optional(), 52 | segment: Joi.string().default(`${pluginName}-userPath`), 53 | expiresIn: Joi.number().default(1 * 60 * 1000) // 1 minute 54 | }).default(), 55 | userPathLimit: Joi.alternatives() 56 | .try(Joi.boolean(), Joi.number()) 57 | .default(false), 58 | limitExceededResponse: Joi.func().default(() => { 59 | return limitExceededResponse 60 | }) 61 | }) 62 | 63 | function getUser (request, settings) { 64 | if (request.auth.isAuthenticated) { 65 | const user = Hoek.reach(request.auth.credentials, settings.userAttribute) 66 | if (user !== undefined) { 67 | return user.toString() 68 | } 69 | } 70 | } 71 | 72 | function getIP (request, settings) { 73 | let ip 74 | 75 | if (settings.trustProxy && request.headers[settings.proxyHeaderName]) { 76 | if (settings.getIpFromProxyHeader) { 77 | ip = settings.getIpFromProxyHeader(request.headers[settings.proxyHeaderName]) 78 | } else { 79 | const ips = request.headers[settings.proxyHeaderName].split(',') 80 | ip = ips[0] 81 | } 82 | } 83 | 84 | if (ip === undefined) { 85 | ip = request.info.remoteAddress 86 | } 87 | 88 | return ip 89 | } 90 | 91 | async function authFailure (authCache, request) { 92 | const requestPlugin = request.plugins[pluginName] 93 | const settings = requestPlugin.settings 94 | if (settings.authLimit === false) { 95 | return 96 | } 97 | 98 | const ip = getIP(request, settings) 99 | 100 | const { value, cached } = await authCache.get(ip) 101 | 102 | let token 103 | 104 | token = Hoek.reach(request, `auth.artifacts.${settings.authToken}`) 105 | 106 | if (!token) { 107 | token = Hoek.reach(request, `auth.error.data.${settings.authToken}`) 108 | } 109 | 110 | if (!token) { 111 | return 112 | } 113 | 114 | const tokenHash = Crypto.createHash('sha1') 115 | .update(token) 116 | .digest('hex') 117 | .slice(0, 6) 118 | 119 | let tokens 120 | let ttl = settings.userPathCache.expiresIn 121 | 122 | /* $lab:coverage:off$ */ 123 | if (value === null || cached.isStale) { 124 | /* $lab:coverage:on$ */ 125 | tokens = new Set([tokenHash]) 126 | } else { 127 | ttl = cached.ttl 128 | tokens = new Set([...value, tokenHash]) 129 | } 130 | 131 | // Sets don't stringify so we cast to an array before storing in the cache 132 | await authCache.set(ip, Array.from(tokens), ttl) 133 | } 134 | 135 | async function authCheck (authCache, request) { 136 | const requestPlugin = request.plugins[pluginName] 137 | const settings = requestPlugin.settings 138 | if (settings.authLimit === false) { 139 | requestPlugin.authLimit = false 140 | return 141 | } 142 | 143 | const ip = getIP(request, settings) 144 | const { value, cached } = await authCache.get(ip) 145 | 146 | /* $lab:coverage:off$ */ 147 | if (value === null || cached.isStale) { 148 | /* $lab:coverage:on$ */ 149 | return 150 | } 151 | 152 | const badIps = new Set(value) 153 | const remaining = settings.authLimit - badIps.size 154 | 155 | return remaining 156 | } 157 | 158 | async function pathCheck (pathCache, request) { 159 | const requestPlugin = request.plugins[pluginName] 160 | const settings = requestPlugin.settings 161 | const path = settings.ignorePathParams ? request.route.path : request.path 162 | 163 | if (settings.pathLimit === false) { 164 | requestPlugin.pathLimit = false 165 | return 166 | } 167 | 168 | const { value, cached } = await pathCache.get(path) 169 | let count 170 | let ttl = settings.pathCache.expiresIn 171 | 172 | /* $lab:coverage:off$ */ 173 | if (value === null || cached.isStale) { 174 | /* $lab:coverage:on$ */ 175 | count = 1 176 | } else { 177 | count = value + 1 178 | ttl = cached.ttl 179 | } 180 | 181 | let remaining = settings.pathLimit - count 182 | if (remaining < 0) { 183 | remaining = -1 184 | } 185 | 186 | await pathCache.set(path, count, ttl) 187 | 188 | requestPlugin.pathLimit = settings.pathLimit 189 | requestPlugin.pathRemaining = remaining 190 | requestPlugin.pathReset = Date.now() + ttl 191 | 192 | return remaining 193 | } 194 | 195 | async function userCheck (userCache, request) { 196 | const requestPlugin = request.plugins[pluginName] 197 | const settings = requestPlugin.settings 198 | const ip = getIP(request, settings) 199 | let user = getUser(request, settings) 200 | if ( 201 | settings.ipWhitelist.indexOf(ip) > -1 || 202 | (user && settings.userWhitelist.indexOf(user) > -1) || 203 | settings.userLimit === false 204 | ) { 205 | requestPlugin.userLimit = false 206 | return 207 | } 208 | 209 | if (settings.addressOnly || user === undefined) { 210 | user = ip 211 | } 212 | 213 | const { value, cached } = await userCache.get(user) 214 | 215 | let count 216 | let ttl = settings.userCache.expiresIn 217 | 218 | /* $lab:coverage:off$ */ 219 | if (value === null || cached.isStale) { 220 | /* $lab:coverage:on$ */ 221 | count = 1 222 | } else { 223 | count = value + 1 224 | ttl = cached.ttl 225 | } 226 | 227 | let remaining = settings.userLimit - count 228 | if (remaining < 0) { 229 | remaining = -1 230 | } 231 | 232 | await userCache.set(user, count, ttl) 233 | 234 | requestPlugin.userLimit = settings.userLimit 235 | requestPlugin.userRemaining = remaining 236 | requestPlugin.userReset = Date.now() + ttl 237 | 238 | return remaining 239 | } 240 | 241 | async function userPathCheck (userPathCache, request) { 242 | const requestPlugin = request.plugins[pluginName] 243 | const settings = requestPlugin.settings 244 | const ip = getIP(request, settings) 245 | let user = getUser(request, settings) 246 | const path = settings.ignorePathParams ? request.route.path : request.path 247 | 248 | if ( 249 | settings.ipWhitelist.indexOf(ip) > -1 || 250 | (user && settings.userWhitelist.indexOf(user) > -1) || 251 | settings.userPathLimit === false 252 | ) { 253 | requestPlugin.userPathLimit = false 254 | return 255 | } 256 | 257 | if (settings.addressOnly || user === undefined) { 258 | user = ip 259 | } 260 | 261 | const userPath = `${user}:${path}` 262 | 263 | const { value, cached } = await userPathCache.get(userPath) 264 | 265 | let count 266 | let ttl = settings.userPathCache.expiresIn 267 | 268 | /* $lab:coverage:off$ */ 269 | if (value === null || cached.isStale) { 270 | /* $lab:coverage:on$ */ 271 | count = 1 272 | } else { 273 | count = value + 1 274 | ttl = cached.ttl 275 | } 276 | 277 | let remaining = settings.userPathLimit - count 278 | if (remaining < 0) { 279 | remaining = -1 280 | } 281 | 282 | await userPathCache.set(userPath, count, ttl) 283 | 284 | requestPlugin.userPathLimit = settings.userPathLimit 285 | requestPlugin.userPathRemaining = remaining 286 | requestPlugin.userPathReset = Date.now() + ttl 287 | 288 | return remaining 289 | } 290 | 291 | function limitExceededResponse () { 292 | return Boom.tooManyRequests('Rate limit exceeded') 293 | } 294 | 295 | module.exports = { 296 | authCheck, 297 | authFailure, 298 | getIP, 299 | getUser, 300 | limitExceededResponse, 301 | pathCheck, 302 | pluginName, 303 | schema, 304 | userCheck, 305 | userPathCheck 306 | } 307 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const lab = (exports.lab = require('@hapi/lab').script()) 4 | const expect = require('@hapi/code').expect 5 | 6 | const beforeEach = lab.beforeEach 7 | const describe = lab.describe 8 | const it = lab.it 9 | const { promisify } = require('util') 10 | 11 | const timeout = promisify(setTimeout) 12 | 13 | const Hapi = require('@hapi/hapi') 14 | const Boom = require('@hapi/boom') 15 | const HapiRateLimit = require('../') 16 | 17 | describe('hapi-rate-limit', () => { 18 | describe('defaults', () => { 19 | let server 20 | 21 | beforeEach(async () => { 22 | server = Hapi.server({ 23 | autoListen: false 24 | }) 25 | 26 | server.auth.scheme('trusty', () => { 27 | return { 28 | authenticate: function (request, h) { 29 | if (request.query.fail) { 30 | return h.unauthenticated(Boom.unauthorized(), { credentials: {}, artifacts: request.query }) 31 | } 32 | if (request.query.error) { 33 | throw Boom.notAcceptable(null, request.query) 34 | } 35 | return h.authenticated({ 36 | credentials: { ...request.query } 37 | }) 38 | } 39 | } 40 | }) 41 | server.auth.strategy('trusty', 'trusty') 42 | 43 | await server.register(HapiRateLimit) 44 | 45 | server.route(require('./test-routes')) 46 | await server.initialize() 47 | }) 48 | 49 | it('no route settings', async () => { 50 | let res 51 | res = await server.inject({ method: 'GET', url: '/defaults' }) 52 | 53 | expect(res.statusCode).to.equal(200) 54 | const pathReset = res.headers['x-ratelimit-pathreset'] 55 | const userReset = res.headers['x-ratelimit-userreset'] 56 | 57 | expect(res.headers).to.include([ 58 | 'x-ratelimit-pathlimit', 59 | 'x-ratelimit-pathremaining', 60 | 'x-ratelimit-pathreset', 61 | 'x-ratelimit-userlimit', 62 | 'x-ratelimit-userremaining', 63 | 'x-ratelimit-userreset' 64 | ]) 65 | expect(res.headers).to.not.include([ 66 | 'x-ratelimit-userpathlimit', 67 | 'x-ratelimit-userpathremaining', 68 | 'x-ratelimit-userpathreset' 69 | ]) 70 | expect(res.headers['x-ratelimit-pathlimit']).to.equal(50) 71 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(49) 72 | expect(res.headers['x-ratelimit-pathreset']).to.be.a.number() 73 | expect(res.headers['x-ratelimit-pathreset'] - Date.now()).to.be.within(59900, 60100) 74 | expect(res.headers['x-ratelimit-userlimit']).to.equal(300) 75 | expect(res.headers['x-ratelimit-userremaining']).to.equal(299) 76 | expect(res.headers['x-ratelimit-userreset']).to.be.a.number() 77 | expect(res.headers['x-ratelimit-userreset'] - Date.now()).to.be.within(599900, 600100) 78 | 79 | res = await server.inject({ method: 'GET', url: '/defaults' }) 80 | expect(res.headers).to.include([ 81 | 'x-ratelimit-pathlimit', 82 | 'x-ratelimit-pathremaining', 83 | 'x-ratelimit-pathreset', 84 | 'x-ratelimit-userlimit', 85 | 'x-ratelimit-userremaining', 86 | 'x-ratelimit-userreset' 87 | ]) 88 | expect(res.headers).to.not.include([ 89 | 'x-ratelimit-userpathlimit', 90 | 'x-ratelimit-userpathremaining', 91 | 'x-ratelimit-userpathreset' 92 | ]) 93 | expect(res.headers['x-ratelimit-pathlimit']).to.equal(50) 94 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(48) 95 | expect(res.headers['x-ratelimit-pathreset'] - pathReset).to.be.within(-100, 100) 96 | expect(res.headers['x-ratelimit-userlimit']).to.equal(300) 97 | expect(res.headers['x-ratelimit-userremaining']).to.equal(298) 98 | expect(res.headers['x-ratelimit-userreset'] - userReset).to.be.within(-100, 100) 99 | }) 100 | 101 | it('authenticated request', async () => { 102 | let res 103 | 104 | res = await server.inject({ method: 'GET', url: '/auth?id=1' }) 105 | expect(res.headers['x-ratelimit-userremaining']).to.equal(299) 106 | 107 | res = await server.inject({ method: 'GET', url: '/auth?id=1' }) 108 | expect(res.headers['x-ratelimit-userremaining']).to.equal(298) 109 | 110 | res = await server.inject({ method: 'GET', url: '/auth?id=2' }) 111 | expect(res.headers['x-ratelimit-userremaining']).to.equal(299) 112 | }) 113 | 114 | it('bad auth tokens', async () => { 115 | let res 116 | await server.inject({ method: 'GET', url: '/auth?authToken=one&id=3&fail=true' }) 117 | await server.inject({ method: 'GET', url: '/auth?authToken=two&id=3&fail=true' }) 118 | await server.inject({ method: 'GET', url: '/auth?authToken=three&id=3&fail=true' }) 119 | await server.inject({ method: 'GET', url: '/auth?authToken=four&id=3&fail=true' }) 120 | await server.inject({ method: 'GET', url: '/auth?authToken=five&id=3&fail=true' }) 121 | await server.inject({ method: 'GET', url: '/auth?authToken=six&id=3&fail=true' }) 122 | // token list is now over the default limit of 5 123 | 124 | res = await server.inject({ method: 'GET', url: '/auth?authToken=seven&id=3&fail=true' }) 125 | expect(res.statusCode).to.equal(429) 126 | 127 | await server.inject({ method: 'GET', url: '/auth?authToken=one&id=4&error=true', remoteAddress: '127.0.0.2' }) 128 | await server.inject({ method: 'GET', url: '/auth?authToken=two&id=4&error=true', remoteAddress: '127.0.0.2' }) 129 | await server.inject({ method: 'GET', url: '/auth?authToken=three&id=4&error=true', remoteAddress: '127.0.0.2' }) 130 | await server.inject({ method: 'GET', url: '/auth?authToken=four&id=4&error=true', remoteAddress: '127.0.0.2' }) 131 | await server.inject({ method: 'GET', url: '/auth?authToken=five&id=4&error=true', remoteAddress: '127.0.0.2' }) 132 | await server.inject({ method: 'GET', url: '/auth?authToken=six&id=4&error=true', remoteAddress: '127.0.0.2' }) 133 | 134 | // token list is now over the default limit of 5 135 | res = await server.inject({ method: 'GET', url: '/auth?authToken=seven&id=4&error=true', remoteAddress: '127.0.0.2' }) 136 | }) 137 | 138 | it('user with missing userAttribute', async () => { 139 | let res 140 | 141 | res = await server.inject({ method: 'GET', url: '/auth?uuid=1' }) 142 | expect(res.headers['x-ratelimit-userremaining']).to.equal(299) 143 | 144 | res = await server.inject({ method: 'GET', url: '/auth?uuid=1' }) 145 | expect(res.headers['x-ratelimit-userremaining']).to.equal(298) 146 | 147 | res = await server.inject({ method: 'GET', url: '/auth?uuid=2' }) 148 | expect(res.headers['x-ratelimit-userremaining']).to.equal(297) 149 | }) 150 | 151 | it('route configured user attribute', async () => { 152 | let res 153 | 154 | res = await server.inject({ 155 | method: 'GET', 156 | url: '/authName?id=1&name=foo' 157 | }) 158 | expect(res.headers['x-ratelimit-userremaining']).to.equal(299) 159 | 160 | res = await server.inject({ 161 | method: 'GET', 162 | url: '/authName?id=1&name=foo' 163 | }) 164 | expect(res.headers['x-ratelimit-userremaining']).to.equal(298) 165 | 166 | res = await server.inject({ 167 | method: 'GET', 168 | url: '/authName?id=1&name=bar' 169 | }) 170 | expect(res.headers['x-ratelimit-userremaining']).to.equal(299) 171 | }) 172 | 173 | it('route configured addressOnly', async () => { 174 | let res 175 | res = await server.inject({ 176 | method: 'GET', 177 | url: '/addressOnly?id=3' 178 | }) 179 | expect(res.headers['x-ratelimit-userremaining']).to.equal(299) 180 | 181 | res = await server.inject({ 182 | method: 'GET', 183 | url: '/addressOnly?id=3' 184 | }) 185 | expect(res.headers['x-ratelimit-userremaining']).to.equal(298) 186 | res = await server.inject({ 187 | method: 'GET', 188 | url: '/addressOnly?id=4' 189 | }) 190 | expect(res.headers['x-ratelimit-userremaining']).to.equal(297) 191 | }) 192 | 193 | it('route configured addressOnly for userPathLimit', async () => { 194 | let res 195 | res = await server.inject({ 196 | method: 'GET', 197 | url: '/addressOnlyUserPathLimit?id=3' 198 | }) 199 | expect(res.headers['x-ratelimit-userpathremaining']).to.equal(49) 200 | 201 | res = await server.inject({ 202 | method: 'GET', 203 | url: '/addressOnlyUserPathLimit?id=3' 204 | }) 205 | expect(res.headers['x-ratelimit-userpathremaining']).to.equal(48) 206 | 207 | res = await server.inject({ 208 | method: 'GET', 209 | url: '/addressOnlyUserPathLimit?id=4' 210 | }) 211 | expect(res.headers['x-ratelimit-userpathremaining']).to.equal(47) 212 | }) 213 | 214 | it('route disabled pathLimit', async () => { 215 | const res = await server.inject({ 216 | method: 'GET', 217 | url: '/noPathLimit' 218 | }) 219 | expect(res.headers).to.include([ 220 | 'x-ratelimit-userlimit', 221 | 'x-ratelimit-userremaining', 222 | 'x-ratelimit-userreset' 223 | ]) 224 | expect(res.headers).to.not.include([ 225 | 'x-ratelimit-pathlimit', 226 | 'x-ratelimit-pathremaining', 227 | 'x-ratelimit-pathreset' 228 | ]) 229 | }) 230 | 231 | it('route disabled userLimit', async () => { 232 | const res = await server.inject({ 233 | method: 'GET', 234 | url: '/noUserLimit' 235 | }) 236 | expect(res.headers).to.include([ 237 | 'x-ratelimit-pathlimit', 238 | 'x-ratelimit-pathremaining', 239 | 'x-ratelimit-pathreset' 240 | ]) 241 | expect(res.headers).to.not.include([ 242 | 'x-ratelimit-userlimit', 243 | 'x-ratelimit-userremaining', 244 | 'x-ratelimit-userreset' 245 | ]) 246 | }) 247 | 248 | it('route disabled userPathLimit', async () => { 249 | const res = await server.inject({ 250 | method: 'GET', 251 | url: '/noUserPathLimit' 252 | }) 253 | expect(res.headers).to.include([ 254 | 'x-ratelimit-pathlimit', 255 | 'x-ratelimit-pathremaining', 256 | 'x-ratelimit-pathreset' 257 | ]) 258 | expect(res.headers).to.include([ 259 | 'x-ratelimit-userlimit', 260 | 'x-ratelimit-userremaining', 261 | 'x-ratelimit-userreset' 262 | ]) 263 | expect(res.headers).to.not.include([ 264 | 'x-ratelimit-userpathlimit', 265 | 'x-ratelimit-userpathremaining', 266 | 'x-ratelimit-userpathreset' 267 | ]) 268 | }) 269 | 270 | it('route configured pathLimit', async () => { 271 | let res 272 | res = await server.inject({ method: 'GET', url: '/setPathLimit' }) 273 | const pathReset = res.headers['x-ratelimit-pathreset'] 274 | expect(res.headers).to.include([ 275 | 'x-ratelimit-pathlimit', 276 | 'x-ratelimit-pathremaining', 277 | 'x-ratelimit-pathreset', 278 | 'x-ratelimit-userlimit', 279 | 'x-ratelimit-userremaining', 280 | 'x-ratelimit-userreset' 281 | ]) 282 | expect(res.headers['x-ratelimit-pathlimit']).to.equal(50) 283 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(49) 284 | expect(res.headers['x-ratelimit-pathreset']).to.be.a.number() 285 | expect(res.headers['x-ratelimit-pathreset'] - Date.now()).to.be.within(59900, 60100) 286 | 287 | res = await server.inject({ method: 'GET', url: '/setPathLimit' }) 288 | expect(res.headers).to.include([ 289 | 'x-ratelimit-pathlimit', 290 | 'x-ratelimit-pathremaining', 291 | 'x-ratelimit-pathreset', 292 | 'x-ratelimit-userlimit', 293 | 'x-ratelimit-userremaining', 294 | 'x-ratelimit-userreset' 295 | ]) 296 | expect(res.headers['x-ratelimit-pathlimit']).to.equal(50) 297 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(48) 298 | expect(res.headers['x-ratelimit-pathreset'] - pathReset).to.be.within(-100, 100) 299 | }) 300 | 301 | it('runs out of pathLimit', async () => { 302 | let res 303 | res = await server.inject({ method: 'GET', url: '/lowPathLimit' }) 304 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(1) 305 | 306 | res = await server.inject({ method: 'GET', url: '/lowPathLimit' }) 307 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(0) 308 | 309 | res = await server.inject({ method: 'GET', url: '/lowPathLimit' }) 310 | expect(res.statusCode).to.equal(429) 311 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(-1) 312 | 313 | res = await server.inject({ method: 'GET', url: '/lowPathLimit' }) 314 | expect(res.statusCode).to.equal(429) 315 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(-1) 316 | }) 317 | 318 | it('route configured userPathLimit', async () => { 319 | let res 320 | res = await server.inject({ 321 | method: 'GET', 322 | url: '/setUserPathLimit?id=1' 323 | }) 324 | const userPathReset = res.headers['x-ratelimit-userpathreset'] 325 | expect(res.headers).to.include([ 326 | 'x-ratelimit-pathlimit', 327 | 'x-ratelimit-pathremaining', 328 | 'x-ratelimit-pathreset', 329 | 'x-ratelimit-userlimit', 330 | 'x-ratelimit-userremaining', 331 | 'x-ratelimit-userreset', 332 | 'x-ratelimit-userpathlimit', 333 | 'x-ratelimit-userpathremaining', 334 | 'x-ratelimit-userpathreset' 335 | ]) 336 | expect(res.headers['x-ratelimit-userpathlimit']).to.equal(50) 337 | expect(res.headers['x-ratelimit-userpathremaining']).to.equal(49) 338 | expect(res.headers['x-ratelimit-userpathreset']).to.be.a.number() 339 | expect(res.headers['x-ratelimit-userpathreset'] - Date.now()).to.be.within(59900, 60100) 340 | 341 | res = await server.inject({ 342 | method: 'GET', 343 | url: '/setUserPathLimit2?id=1' 344 | }) 345 | expect(res.headers).to.include([ 346 | 'x-ratelimit-pathlimit', 347 | 'x-ratelimit-pathremaining', 348 | 'x-ratelimit-pathreset', 349 | 'x-ratelimit-userlimit', 350 | 'x-ratelimit-userremaining', 351 | 'x-ratelimit-userreset', 352 | 'x-ratelimit-userpathlimit', 353 | 'x-ratelimit-userpathremaining', 354 | 'x-ratelimit-userpathreset' 355 | ]) 356 | expect(res.headers['x-ratelimit-userpathlimit']).to.equal(50) 357 | expect(res.headers['x-ratelimit-userpathremaining']).to.equal(49) 358 | expect(res.headers['x-ratelimit-userpathreset']).to.be.a.number() 359 | expect(res.headers['x-ratelimit-userpathreset'] - Date.now()).to.be.within(59900, 60100) 360 | 361 | res = await server.inject({ 362 | method: 'GET', 363 | url: '/setUserPathLimit?id=1' 364 | }) 365 | expect(res.headers).to.include([ 366 | 'x-ratelimit-pathlimit', 367 | 'x-ratelimit-pathremaining', 368 | 'x-ratelimit-pathreset', 369 | 'x-ratelimit-userlimit', 370 | 'x-ratelimit-userremaining', 371 | 'x-ratelimit-userreset', 372 | 'x-ratelimit-userpathlimit', 373 | 'x-ratelimit-userpathremaining', 374 | 'x-ratelimit-userpathreset' 375 | ]) 376 | expect(res.headers['x-ratelimit-userpathlimit']).to.equal(50) 377 | expect(res.headers['x-ratelimit-userpathremaining']).to.equal(48) 378 | expect(res.headers['x-ratelimit-userpathreset'] - userPathReset).to.be.within(-100, 100) 379 | }) 380 | 381 | it('runs out of userPathLimit', async () => { 382 | let res 383 | res = await server.inject({ 384 | method: 'GET', 385 | url: '/lowUserPathLimit' 386 | }) 387 | expect(res.headers['x-ratelimit-userpathremaining']).to.equal(1) 388 | 389 | res = await server.inject({ 390 | method: 'GET', 391 | url: '/lowUserPathLimit' 392 | }) 393 | expect(res.headers['x-ratelimit-userpathremaining']).to.equal(0) 394 | 395 | res = await server.inject({ 396 | method: 'GET', 397 | url: '/lowUserPathLimit' 398 | }) 399 | expect(res.statusCode).to.equal(429) 400 | expect(res.headers['x-ratelimit-userpathremaining']).to.equal(-1) 401 | 402 | res = await server.inject({ 403 | method: 'GET', 404 | url: '/lowUserPathLimit' 405 | }) 406 | expect(res.statusCode).to.equal(429) 407 | expect(res.headers['x-ratelimit-userpathremaining']).to.equal(-1) 408 | }) 409 | 410 | it('route configured no headers', async () => { 411 | let res = await server.inject({ method: 'GET', url: '/noHeaders' }) 412 | expect(res.headers).to.not.include([ 413 | 'x-ratelimit-pathlimit', 414 | 'x-ratelimit-pathremaining', 415 | 'x-ratelimit-pathreset', 416 | 'x-ratelimit-userlimit', 417 | 'x-ratelimit-userremaining', 418 | 'x-ratelimit-userreset', 419 | 'x-ratelimit-userpathlimit', 420 | 'x-ratelimit-userpathremaining', 421 | 'x-ratelimit-userpathreset' 422 | ]) 423 | res = await server.inject({ method: 'GET', url: '/noHeaders' }) 424 | expect(res.headers).to.not.include([ 425 | 'x-ratelimit-pathlimit', 426 | 'x-ratelimit-pathremaining', 427 | 'x-ratelimit-pathreset', 428 | 'x-ratelimit-userlimit', 429 | 'x-ratelimit-userremaining', 430 | 'x-ratelimit-userreset', 431 | 'x-ratelimit-userpathlimit', 432 | 'x-ratelimit-userpathremaining', 433 | 'x-ratelimit-userpathreset' 434 | ]) 435 | }) 436 | 437 | it('404 reply from handler', async () => { 438 | let res 439 | res = await server.inject({ method: 'GET', url: '/notfound' }) 440 | expect(res.statusCode).to.equal(404) 441 | expect(res.headers).to.include([ 442 | 'x-ratelimit-userlimit', 443 | 'x-ratelimit-userremaining', 444 | 'x-ratelimit-userreset' 445 | ]) 446 | expect(res.headers).to.not.include([ 447 | 'x-ratelimit-pathlimit', 448 | 'x-ratelimit-pathremaining', 449 | 'x-ratelimit-pathreset', 450 | 'x-ratelimit-userpathlimit', 451 | 'x-ratelimit-userpathremaining', 452 | 'x-ratelimit-userpathreset' 453 | ]) 454 | 455 | const userCount = res.headers['x-ratelimit-userremaining'] 456 | 457 | res = await server.inject({ method: 'GET', url: '/notfound' }) 458 | expect(userCount - res.headers['x-ratelimit-userremaining']).to.equal(1) 459 | }) 460 | 461 | it('404 reply from internal hapi catchall', async () => { 462 | const res = await server.inject({ 463 | method: 'GET', 464 | url: '/notinroutingtable' 465 | }) 466 | expect(res.statusCode).to.equal(404) 467 | expect(res.headers).to.not.include([ 468 | 'x-ratelimit-userlimit', 469 | 'x-ratelimit-userremaining', 470 | 'x-ratelimit-userreset' 471 | ]) 472 | expect(res.headers).to.not.include([ 473 | 'x-ratelimit-pathlimit', 474 | 'x-ratelimit-pathremaining', 475 | 'x-ratelimit-pathreset', 476 | 'x-ratelimit-userpathlimit', 477 | 'x-ratelimit-userpathremaining', 478 | 'x-ratelimit-userpathreset' 479 | ]) 480 | }) 481 | 482 | it('route configured trustProxy', async () => { 483 | let res 484 | res = await server.inject({ 485 | method: 'GET', 486 | url: '/trustProxy', 487 | headers: { 'x-forwarded-for': '127.0.0.2, 127.0.0.1' } 488 | }) 489 | expect(res.headers['x-ratelimit-userremaining']).to.equal(299) 490 | 491 | res = await server.inject({ 492 | method: 'GET', 493 | url: '/trustProxy', 494 | headers: { 'x-forwarded-for': '127.0.0.2, 127.0.0.1' } 495 | }) 496 | expect(res.headers['x-ratelimit-userremaining']).to.equal(298) 497 | 498 | res = await server.inject({ method: 'GET', url: '/trustProxy' }) 499 | expect(res.headers['x-ratelimit-userremaining']).to.equal(299) 500 | }) 501 | 502 | it('route configured trustProxy with custom header', async () => { 503 | let res 504 | res = await server.inject({ 505 | method: 'GET', 506 | url: '/trustProxyCustomHeader', 507 | headers: { 'x-something-else': '127.0.0.2, 127.0.0.1' } 508 | }) 509 | expect(res.headers['x-ratelimit-userremaining']).to.equal(299) 510 | 511 | res = await server.inject({ 512 | method: 'GET', 513 | url: '/trustProxyCustomHeader', 514 | headers: { 'x-something-else': '127.0.0.2, 127.0.0.1' } 515 | }) 516 | expect(res.headers['x-ratelimit-userremaining']).to.equal(298) 517 | 518 | res = await server.inject({ method: 'GET', url: '/trustProxy' }) 519 | expect(res.headers['x-ratelimit-userremaining']).to.equal(299) 520 | }) 521 | 522 | it('route configured ipWhitelist', async () => { 523 | const res = await server.inject({ 524 | method: 'GET', 525 | url: '/ipWhitelist', 526 | headers: { 'x-forwarded-for': '127.0.0.2, 127.0.0.1' } 527 | }) 528 | expect(res.headers).to.include([ 529 | 'x-ratelimit-pathlimit', 530 | 'x-ratelimit-pathremaining', 531 | 'x-ratelimit-pathreset' 532 | ]) 533 | expect(res.headers).to.not.include([ 534 | 'x-ratelimit-userlimit', 535 | 'x-ratelimit-userremaining', 536 | 'x-ratelimit-userreset' 537 | ]) 538 | }) 539 | 540 | it('route configured userWhitelist', async () => { 541 | let res 542 | res = await server.inject({ 543 | method: 'GET', 544 | url: '/userWhitelist?id=1' 545 | }) 546 | expect(res.headers).to.include([ 547 | 'x-ratelimit-pathlimit', 548 | 'x-ratelimit-pathremaining', 549 | 'x-ratelimit-pathreset' 550 | ]) 551 | expect(res.headers).to.not.include([ 552 | 'x-ratelimit-userlimit', 553 | 'x-ratelimit-userremaining', 554 | 'x-ratelimit-userreset' 555 | ]) 556 | 557 | res = await server.inject({ 558 | method: 'GET', 559 | url: '/userWhitelist?id=2' 560 | }) 561 | expect(res.headers).to.include([ 562 | 'x-ratelimit-pathlimit', 563 | 'x-ratelimit-pathremaining', 564 | 'x-ratelimit-pathreset', 565 | 'x-ratelimit-userlimit', 566 | 'x-ratelimit-userremaining', 567 | 'x-ratelimit-userreset' 568 | ]) 569 | expect(res.headers['x-ratelimit-userremaining']).to.equal(299) 570 | }) 571 | 572 | it('multiple plugin registrations do not share cache instances', async () => { 573 | const secondServer = Hapi.server({ 574 | autoListen: false 575 | }) 576 | 577 | secondServer.auth.scheme('trusty', () => { 578 | return { 579 | authenticate: function (request, h) { 580 | return h.authenticated({ 581 | credentials: { ...request.query } 582 | }) 583 | } 584 | } 585 | }) 586 | secondServer.auth.strategy('trusty', 'trusty') 587 | 588 | await secondServer.register(HapiRateLimit) 589 | 590 | secondServer.route(require('./test-routes')) 591 | await secondServer.initialize() 592 | 593 | let res 594 | res = await server.inject({ method: 'GET', url: '/defaults' }) 595 | 596 | res = await secondServer.inject({ 597 | method: 'GET', 598 | url: '/defaults' 599 | }) 600 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(49) 601 | expect(res.headers['x-ratelimit-userremaining']).to.equal(299) 602 | }) 603 | 604 | it('runs out of pathLimit using params', async () => { 605 | let res 606 | res = await server.inject({ 607 | method: 'GET', 608 | url: '/managePathParams/1' 609 | }) 610 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(1) 611 | 612 | res = await server.inject({ 613 | method: 'GET', 614 | url: '/managePathParams/1' 615 | }) 616 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(0) 617 | 618 | res = await server.inject({ 619 | method: 'GET', 620 | url: '/managePathParams/1' 621 | }) 622 | expect(res.statusCode).to.equal(429) 623 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(-1) 624 | 625 | res = await server.inject({ 626 | method: 'GET', 627 | url: '/managePathParams/2' 628 | }) 629 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(1) 630 | 631 | res = await server.inject({ 632 | method: 'GET', 633 | url: '/managePathParams/2' 634 | }) 635 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(0) 636 | 637 | res = await server.inject({ 638 | method: 'GET', 639 | url: '/managePathParams/2' 640 | }) 641 | expect(res.statusCode).to.equal(429) 642 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(-1) 643 | 644 | res = await server.inject({ 645 | method: 'GET', 646 | url: '/ignorePathParams/1' 647 | }) 648 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(1) 649 | 650 | res = await server.inject({ 651 | method: 'GET', 652 | url: '/ignorePathParams/2' 653 | }) 654 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(0) 655 | 656 | res = await server.inject({ 657 | method: 'GET', 658 | url: '/ignorePathParams/3' 659 | }) 660 | expect(res.statusCode).to.equal(429) 661 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(-1) 662 | }) 663 | }) 664 | 665 | describe('configured user limit', () => { 666 | let server 667 | 668 | beforeEach(async () => { 669 | server = Hapi.server({ 670 | autoListen: false 671 | }) 672 | 673 | server.events.on({ name: 'request', channels: ['error'] }, (request, event) => { 674 | console.log(event.error) 675 | }) 676 | 677 | server.auth.scheme('trusty', () => { 678 | return { 679 | authenticate: function (request, h) { 680 | return h.authenticated({ 681 | credentials: { 682 | id: request.query.id, 683 | name: request.query.name 684 | } 685 | }) 686 | } 687 | } 688 | }) 689 | 690 | server.auth.strategy('trusty', 'trusty') 691 | 692 | await server.register([ 693 | { 694 | plugin: HapiRateLimit, 695 | options: { 696 | userLimit: 2, 697 | userCache: { 698 | expiresIn: 500 699 | } 700 | } 701 | } 702 | ]) 703 | server.route(require('./test-routes')) 704 | await server.initialize() 705 | }) 706 | 707 | it('runs out of configured userLimit', async () => { 708 | let res 709 | res = await server.inject({ method: 'GET', url: '/defaults' }) 710 | expect(res.headers['x-ratelimit-userremaining']).to.equal(1) 711 | expect(res.headers['x-ratelimit-userlimit']).to.equal(2) 712 | 713 | res = await server.inject({ method: 'GET', url: '/defaults' }) 714 | expect(res.headers['x-ratelimit-userremaining']).to.equal(0) 715 | 716 | res = await server.inject({ method: 'GET', url: '/defaults' }) 717 | expect(res.statusCode).to.equal(429) 718 | await timeout(1000) 719 | res = await server.inject({ method: 'GET', url: '/defaults' }) 720 | expect(res.headers['x-ratelimit-userremaining']).to.equal(1) 721 | expect(res.headers['x-ratelimit-userlimit']).to.equal(2) 722 | }) 723 | 724 | it('disabled path limit runs out of userLimit', async () => { 725 | await server.inject({ method: 'GET', url: '/noPathLimit' }) 726 | await server.inject({ method: 'GET', url: '/noPathLimit' }) 727 | const res = await server.inject({ 728 | method: 'GET', 729 | url: '/noPathLimit' 730 | }) 731 | expect(res.statusCode).to.equal(429) 732 | expect(res.headers).to.not.include([ 733 | 'x-ratelimit-pathlimit', 734 | 'x-ratelimit-pathremaining', 735 | 'x-ratelimit-pathreset' 736 | ]) 737 | }) 738 | }) 739 | 740 | describe('disable authLimit', () => { 741 | let server 742 | 743 | beforeEach(async () => { 744 | server = Hapi.server({ 745 | autoListen: false 746 | }) 747 | 748 | server.auth.scheme('trusty', () => { 749 | return { 750 | authenticate: function (request, h) { 751 | return h.unauthenticated(Boom.unauthorized(), { credentials: {}, artifacts: request.query }) 752 | } 753 | } 754 | }) 755 | 756 | server.auth.strategy('trusty', 'trusty') 757 | 758 | await server.register([ 759 | { 760 | plugin: HapiRateLimit, 761 | options: { 762 | authLimit: false 763 | } 764 | } 765 | ]) 766 | server.route(require('./test-routes')) 767 | await server.initialize() 768 | }) 769 | 770 | it('bad auth tokens', async () => { 771 | await server.inject({ method: 'GET', url: '/auth?authToken=one&id=3' }) 772 | await server.inject({ method: 'GET', url: '/auth?authToken=two&id=3' }) 773 | await server.inject({ method: 'GET', url: '/auth?authToken=three&id=3' }) 774 | await server.inject({ method: 'GET', url: '/auth?authToken=four&id=3' }) 775 | await server.inject({ method: 'GET', url: '/auth?authToken=five&id=3' }) 776 | await server.inject({ method: 'GET', url: '/auth?authToken=six&id=3' }) 777 | 778 | const res = await server.inject({ method: 'GET', url: '/auth?authToken=seven&id=3' }) 779 | expect(res.statusCode).to.equal(401) 780 | }) 781 | }) 782 | 783 | describe('disabled routes', () => { 784 | let server 785 | 786 | beforeEach(async () => { 787 | server = Hapi.server({ 788 | autoListen: false 789 | }) 790 | 791 | server.auth.scheme('trusty', () => { 792 | return { 793 | authenticate: function (request, h) { 794 | return h.authenticated({ 795 | credentials: { 796 | id: request.query.id, 797 | name: request.query.name 798 | } 799 | }) 800 | } 801 | } 802 | }) 803 | 804 | server.auth.strategy('trusty', 'trusty') 805 | 806 | await server.register([ 807 | { 808 | plugin: HapiRateLimit, 809 | options: { 810 | userLimit: 1, 811 | pathLimit: 1, 812 | userCache: { 813 | expiresIn: 500 814 | } 815 | } 816 | } 817 | ]) 818 | server.route(require('./test-routes')) 819 | await server.initialize() 820 | }) 821 | 822 | it('route disabled', async () => { 823 | const res = await server.inject({ 824 | method: 'GET', 825 | url: '/pathDisabled' 826 | }) 827 | 828 | expect(res.headers).to.not.include([ 829 | 'x-ratelimit-pathlimit', 830 | 'x-ratelimit-pathremaining', 831 | 'x-ratelimit-pathreset' 832 | ]) 833 | expect(res.headers).to.not.include([ 834 | 'x-ratelimit-userlimit', 835 | 'x-ratelimit-userremaining', 836 | 'x-ratelimit-userreset' 837 | ]) 838 | }) 839 | }) 840 | 841 | describe('configured user limit with numeric id', () => { 842 | let server 843 | let id = 10 844 | 845 | beforeEach(async () => { 846 | server = Hapi.server({ 847 | autoListen: false 848 | }) 849 | 850 | server.auth.scheme('trusty', () => { 851 | return { 852 | authenticate: function (request, h) { 853 | return h.authenticated({ credentials: { id } }) 854 | } 855 | } 856 | }) 857 | 858 | server.auth.strategy('trusty', 'trusty') 859 | 860 | await server.register([ 861 | { 862 | plugin: HapiRateLimit, 863 | options: { 864 | userLimit: 2, 865 | userCache: { 866 | expiresIn: 500 867 | }, 868 | userWhitelist: [12] 869 | } 870 | } 871 | ]) 872 | server.route(require('./test-routes')) 873 | await server.initialize() 874 | }) 875 | 876 | it('runs out of configured userLimit', async () => { 877 | let res 878 | res = await server.inject({ method: 'GET', url: '/auth' }) 879 | expect(res.headers['x-ratelimit-userremaining']).to.equal(1) 880 | expect(res.headers['x-ratelimit-userlimit']).to.equal(2) 881 | 882 | res = await server.inject({ method: 'GET', url: '/auth' }) 883 | expect(res.headers['x-ratelimit-userremaining']).to.equal(0) 884 | 885 | res = await server.inject({ method: 'GET', url: '/auth' }) 886 | expect(res.statusCode).to.equal(429) 887 | await timeout(1000) 888 | res = await server.inject({ method: 'GET', url: '/auth' }) 889 | 890 | expect(res.headers['x-ratelimit-userremaining']).to.equal(1) 891 | expect(res.headers['x-ratelimit-userlimit']).to.equal(2) 892 | }) 893 | 894 | it('disabled path limit runs out of userLimit', async () => { 895 | await server.inject({ method: 'GET', url: '/noPathLimit' }) 896 | await server.inject({ method: 'GET', url: '/noPathLimit' }) 897 | const res = await server.inject({ 898 | method: 'GET', 899 | url: '/noPathLimit' 900 | }) 901 | 902 | expect(res.statusCode).to.equal(429) 903 | expect(res.headers).to.not.include([ 904 | 'x-ratelimit-pathlimit', 905 | 'x-ratelimit-pathremaining', 906 | 'x-ratelimit-pathreset' 907 | ]) 908 | 909 | id = 12 910 | }) 911 | 912 | it('disabled user limit with userWhitelist', async () => { 913 | const res = await server.inject({ method: 'GET', url: '/auth' }) 914 | expect(res.headers['x-ratelimit-userlimit']).to.equal(false) 915 | expect(res.headers).to.not.include(['x-ratelimit-userremaining', 'x-ratelimit-userreset']) 916 | }) 917 | }) 918 | 919 | describe('custom get ip from proxy header which returns the last one', () => { 920 | let server 921 | 922 | beforeEach(async () => { 923 | server = Hapi.server({ 924 | autoListen: false 925 | }) 926 | 927 | server.auth.scheme('trusty', () => { 928 | return { 929 | authenticate: function (request, h) { 930 | return h.authenticated({ 931 | credentials: { 932 | id: request.query.id, 933 | name: request.query.name 934 | } 935 | }) 936 | } 937 | } 938 | }) 939 | server.auth.strategy('trusty', 'trusty') 940 | 941 | await server.register([ 942 | { 943 | plugin: HapiRateLimit, 944 | options: { 945 | getIpFromProxyHeader: xForwardedFor => xForwardedFor.split(',')[1] // Take always the second one 946 | } 947 | } 948 | ]) 949 | server.route(require('./test-routes')) 950 | await server.initialize() 951 | }) 952 | 953 | it('increases the user remaining only when the last ip in the x-forwarded-for header changes', async () => { 954 | let res 955 | res = await server.inject({ 956 | method: 'GET', 957 | url: '/trustProxy', 958 | headers: { 'x-forwarded-for': '127.0.0.2, 127.0.0.1' } 959 | }) 960 | expect(res.headers['x-ratelimit-userremaining']).to.equal(299) 961 | 962 | res = await server.inject({ 963 | method: 'GET', 964 | url: '/trustProxy', 965 | headers: { 'x-forwarded-for': '127.0.0.2, 127.0.0.3' } 966 | }) 967 | expect(res.headers['x-ratelimit-userremaining']).to.equal(299) 968 | }) 969 | }) 970 | 971 | describe('configured user limit with strings joi converts to primatives', () => { 972 | let server 973 | 974 | beforeEach(async () => { 975 | server = Hapi.server({ 976 | autoListen: false 977 | }) 978 | 979 | server.events.on({ name: 'request', channels: ['error'] }, (request, event) => { 980 | console.log(event.error) 981 | }) 982 | 983 | server.auth.scheme('trusty', () => { 984 | return { 985 | authenticate: function (request, h) { 986 | return h.authenticated({ 987 | credentials: { 988 | id: request.query.id, 989 | name: request.query.name 990 | } 991 | }) 992 | } 993 | } 994 | }) 995 | 996 | server.auth.strategy('trusty', 'trusty') 997 | 998 | await server.register([ 999 | { 1000 | plugin: HapiRateLimit, 1001 | options: { 1002 | userLimit: '2', 1003 | userCache: { 1004 | expiresIn: '500' 1005 | }, 1006 | userPathLimit: 'false', 1007 | pathLimit: 'false' 1008 | } 1009 | } 1010 | ]) 1011 | server.route(require('./test-routes')) 1012 | await server.initialize() 1013 | }) 1014 | 1015 | it('runs out of configured userLimit using strings', async () => { 1016 | let res 1017 | res = await server.inject({ method: 'GET', url: '/defaults' }) 1018 | expect(res.headers['x-ratelimit-userremaining']).to.equal(1) 1019 | expect(res.headers['x-ratelimit-userlimit']).to.equal(2) 1020 | 1021 | res = await server.inject({ method: 'GET', url: '/defaults' }) 1022 | expect(res.headers['x-ratelimit-userremaining']).to.equal(0) 1023 | 1024 | res = await server.inject({ method: 'GET', url: '/defaults' }) 1025 | expect(res.statusCode).to.equal(429) 1026 | await timeout(1000) 1027 | res = await server.inject({ method: 'GET', url: '/defaults' }) 1028 | expect(res.headers['x-ratelimit-userremaining']).to.equal(1) 1029 | expect(res.headers['x-ratelimit-userlimit']).to.equal(2) 1030 | }) 1031 | }) 1032 | 1033 | describe('configured limit exceeded response', () => { 1034 | let server 1035 | 1036 | beforeEach(async () => { 1037 | server = Hapi.server({ 1038 | autoListen: false 1039 | }) 1040 | 1041 | server.auth.scheme('trusty', () => { 1042 | return { 1043 | authenticate: function (request, h) { 1044 | return h.authenticated({ 1045 | credentials: { ...request.query } 1046 | }) 1047 | } 1048 | } 1049 | }) 1050 | server.auth.strategy('trusty', 'trusty') 1051 | 1052 | await server.register([ 1053 | { 1054 | plugin: HapiRateLimit, 1055 | options: { 1056 | limitExceededResponse: function (request, h) { 1057 | return h 1058 | .response('custom response') 1059 | .code(477) 1060 | .takeover() 1061 | } 1062 | } 1063 | } 1064 | ]) 1065 | 1066 | server.route(require('./test-routes')) 1067 | await server.initialize() 1068 | }) 1069 | 1070 | it('gets correct responses with rate limiting headers', async () => { 1071 | let res 1072 | res = await server.inject({ method: 'GET', url: '/lowPathLimit' }) 1073 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(1) 1074 | 1075 | res = await server.inject({ method: 'GET', url: '/lowPathLimit' }) 1076 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(0) 1077 | 1078 | res = await server.inject({ method: 'GET', url: '/lowPathLimit' }) 1079 | expect(res.result).to.equal('custom response') 1080 | expect(res.statusCode).to.equal(477) 1081 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(-1) 1082 | 1083 | res = await server.inject({ method: 'GET', url: '/lowPathLimit' }) 1084 | expect(res.result).to.equal('custom response') 1085 | expect(res.statusCode).to.equal(477) 1086 | expect(res.headers['x-ratelimit-pathremaining']).to.equal(-1) 1087 | }) 1088 | }) 1089 | 1090 | describe('configured cache', () => { 1091 | let server 1092 | const localIp = '127.0.0.1' 1093 | 1094 | const configureWithCacheName = async cacheName => { 1095 | server = Hapi.server({ autoListen: false }) 1096 | 1097 | server.cache.provision({ 1098 | name: 'a-custom-cache-name', 1099 | provider: require('@hapi/catbox-memory').Engine 1100 | }) 1101 | 1102 | server.events.on({ name: 'request', channels: ['error'] }, (request, event) => { 1103 | console.log(event.error) 1104 | }) 1105 | 1106 | server.auth.scheme('trusty', () => { 1107 | return { 1108 | authenticate: function (request, h) { 1109 | return h.authenticated({ 1110 | credentials: { 1111 | id: request.query.id, 1112 | name: request.query.name 1113 | } 1114 | }) 1115 | } 1116 | } 1117 | }) 1118 | 1119 | server.auth.strategy('trusty', 'trusty') 1120 | server.route(require('./test-routes')) 1121 | 1122 | await server.register([ 1123 | { 1124 | plugin: HapiRateLimit, 1125 | options: { 1126 | addressOnly: true, 1127 | userCache: { segment: 'a-custom-cache-name', cache: 'a-custom-cache-name' }, 1128 | userPathLimit: false, 1129 | pathLimit: false 1130 | } 1131 | } 1132 | ]) 1133 | await server.initialize() 1134 | } 1135 | 1136 | it('uses non-default hapi cache', async () => { 1137 | await configureWithCacheName() 1138 | 1139 | const rateLimitCache = server.cache({ 1140 | segment: 'a-custom-cache-name', 1141 | cache: 'a-custom-cache-name', 1142 | shared: true 1143 | }) 1144 | const defaultCache = server.cache({ 1145 | segment: 'default', 1146 | shared: true 1147 | }) 1148 | 1149 | await server.inject({ method: 'GET', url: '/addressOnly?id=1' }) 1150 | expect(await rateLimitCache.get(localIp)).to.equal(1) 1151 | expect(await defaultCache.get(localIp)).to.be.null() 1152 | 1153 | await server.inject({ method: 'GET', url: '/addressOnly?id=1' }) 1154 | expect(await rateLimitCache.get(localIp)).to.equal(2) 1155 | expect(await defaultCache.get(localIp)).to.be.null() 1156 | }) 1157 | }) 1158 | }) 1159 | --------------------------------------------------------------------------------