├── .checkbuild ├── .github └── workflows │ ├── build.yml │ └── test.yml ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .npmignore ├── LICENSE ├── README.md ├── lib └── fast_ratelimit.js ├── package-lock.json ├── package.json └── test └── fast_ratelimit-test.js /.checkbuild: -------------------------------------------------------------------------------- 1 | { 2 | "checkbuild": { 3 | "enable": ["jshint", "jscs"], 4 | "continueOnError": true, 5 | "allowFailures": false 6 | }, 7 | "jshint": { 8 | "args": ["lib/**/*.js"] 9 | }, 10 | "jscs": { 11 | "args": ["lib/**/*.js"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "v*.*.*" 5 | 6 | permissions: 7 | id-token: write 8 | 9 | name: Build and Release 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Install NodeJS 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: 20.x 23 | registry-url: https://registry.npmjs.org 24 | 25 | - name: Verify versions 26 | run: node --version && npm --version && node -p process.versions.v8 27 | 28 | - name: Release package 29 | run: npm publish --ignore-scripts --provenance 30 | env: 31 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Test and Build 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest] 10 | node-version: [6.x, 8.x, 10.x, 12.x, 14.x, 16.x, 18.x, 20.x] 11 | fail-fast: false 12 | 13 | runs-on: ${{ matrix.os }} 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Install NodeJS 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | 24 | - name: Verify versions 25 | run: node --version && npm --version && node -p process.versions.v8 && gcc -v 26 | 27 | - name: Cache build artifacts 28 | id: cache-node 29 | uses: actions/cache@v4 30 | with: 31 | path: | 32 | ~/.npm 33 | node_modules 34 | key: test-${{ runner.os }}-node-${{ matrix.node-version }} 35 | 36 | - name: Install dependencies 37 | run: npm install 38 | 39 | - name: Run tests 40 | run: npm test 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | coverage/ 5 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "validateIndentation": 2, 3 | "disallowMixedSpacesAndTabs": true, 4 | "maximumLineLength": 80, 5 | "disallowTrailingWhitespace": true, 6 | "requireCurlyBraces": null 7 | } 8 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "camelcase": false, 3 | "node": true, 4 | "esnext" : true, 5 | "predef": [ 6 | "require", 7 | "define", 8 | "escape", 9 | "Buffer", 10 | "console", 11 | "module" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | coverage/ 3 | .github/ 4 | .jshintrc 5 | .jscsrc 6 | .checkbuild 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Valerian Saliou 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-fast-ratelimit 2 | 3 | [![Test and Build](https://github.com/valeriansaliou/node-fast-ratelimit/workflows/Test%20and%20Build/badge.svg?branch=master)](https://github.com/valeriansaliou/node-fast-ratelimit/actions?query=workflow%3A%22Test+and+Build%22) [![Build and Release](https://github.com/valeriansaliou/node-fast-ratelimit/workflows/Build%20and%20Release/badge.svg)](https://github.com/valeriansaliou/node-fast-ratelimit/actions?query=workflow%3A%22Build+and+Release%22) [![NPM](https://img.shields.io/npm/v/fast-ratelimit.svg)](https://www.npmjs.com/package/fast-ratelimit) [![Downloads](https://img.shields.io/npm/dt/fast-ratelimit.svg)](https://www.npmjs.com/package/fast-ratelimit) [![Gitter](https://img.shields.io/gitter/room/valeriansaliou/node-fast-ratelimit.svg)](https://gitter.im/valeriansaliou/node-fast-ratelimit) [![Buy Me A Coffee](https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg)](https://www.buymeacoffee.com/valeriansaliou) 4 | 5 | Fast and efficient in-memory rate-limit, used to alleviate most common DOS attacks. 6 | 7 | This rate-limiter was designed to be as generic as possible, usable in any NodeJS project environment, regardless of whether you're using a framework or just vanilla code. It does not require any dependencies, making it lightweight to install and use. 8 | 9 | **🇫🇷 Crafted in Lannion, France.** 10 | 11 | ## Who uses it? 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
CrispDoctrineAnchor.ChatWeStudents
27 | 28 | _👋 You use fast-ratelimit and you want to be listed there? [Contact me](https://valeriansaliou.name/)._ 29 | 30 | ## How to install? 31 | 32 | Include `fast-ratelimit` in your `package.json` dependencies. 33 | 34 | Alternatively, you can run `npm install fast-ratelimit --save`. 35 | 36 | TypeScript users can install type definitions by running `npm install --save-dev @types/fast-ratelimit`. _Note that this is a third-party package, ie. not maintained by myself._ 37 | 38 | ## How to use? 39 | 40 | The `fast-ratelimit` API is pretty simple, here are some keywords used in the docs: 41 | 42 | * `ratelimiter`: ratelimiter instance, which plays the role of limits storage 43 | * `namespace`: the master ratelimit storage namespace (eg: set `namespace` to the user client IP, or user username) 44 | 45 | You can create as many `ratelimiter` instances as you need in your application. This is great if you need to rate-limit IPs on specific zones (eg: for a chat application, you don't want the message send rate limit to affect the message composing notification rate limit). 46 | 47 | Here's how to proceed (we take the example of rate-limiting messages sending in a chat app): 48 | 49 | ### 1. Create the rate-limiter 50 | 51 | The rate-limiter can be instanciated as such: 52 | 53 | ```javascript 54 | var FastRateLimit = require("fast-ratelimit").FastRateLimit; 55 | 56 | var messageLimiter = new FastRateLimit({ 57 | threshold : 20, // available tokens over timespan 58 | ttl : 60 // time-to-live value of token bucket (in seconds) 59 | }); 60 | ``` 61 | 62 | This limiter will allow 20 messages to be sent every minute per namespace. 63 | An user can send a maximum number of 20 messages in a 1 minute timespan, with a token counter reset every minute for a given namespace. 64 | 65 | The reset scheduling is done per-namespace; eg: if namespace `user_1` sends 1 message at 11:00:32am, he will have 19 messages remaining from 11:00:32am to 11:01:32am. Hence, his limiter will reset at 11:01:32am, and won't scheduler any more reset until he consumes another token. 66 | 67 | ### 2. Check by consuming a token 68 | 69 | On the message send portion of our application code, we would add a call to the ratelimiter instance. 70 | 71 | #### 2.1. Consume token with asynchronous API (Promise catch/reject) 72 | 73 | ```javascript 74 | // This would be dynamic in your application, based on user session data, or user IP 75 | namespace = "user_1"; 76 | 77 | // Check if user is allowed to send message 78 | messageLimiter.consume(namespace) 79 | .then(() => { 80 | // Consumed a token 81 | // Send message 82 | message.send(); 83 | }) 84 | .catch(() => { 85 | // No more token for namespace in current timespan 86 | // Silently discard message 87 | }); 88 | ``` 89 | 90 | #### 2.2. Consume token with synchronous API (boolean test) 91 | 92 | ```javascript 93 | // This would be dynamic in your application, based on user session data, or user IP 94 | namespace = "user_1"; 95 | 96 | // Check if user is allowed to send message 97 | if (messageLimiter.consumeSync(namespace) === true) { 98 | // Consumed a token 99 | // Send message 100 | message.send(); 101 | } else { 102 | // consumeSync returned false since there is no more tokens available 103 | // Silently discard message 104 | } 105 | ``` 106 | 107 | ### 3. Check without consuming a token 108 | 109 | In some instances, like password brute forcing prevention, you may want to check without consuming a token and consume only when password validation fails. 110 | 111 | #### 3.1. Check whether there are remaining tokens with asynchronous API (Promise catch/reject) 112 | 113 | ```javascript 114 | limiter.hasToken(request.ip).then(() => { 115 | return authenticate(request.login, request.password) 116 | }) 117 | .then( 118 | () => { 119 | // User is authenticated 120 | }, 121 | 122 | () => { 123 | // User is not authenticated 124 | // Consume a token and reject promise 125 | return limiter.consume(request.ip) 126 | .then(() => Promise.reject()) 127 | } 128 | ) 129 | .catch(() => { 130 | // Either invalid authentication or too many invalid login 131 | return response.unauthorized(); 132 | }) 133 | ``` 134 | 135 | #### 3.2. Check whether there are remaining tokens with synchronous API (boolean test) 136 | 137 | ```javascript 138 | if (!limiter.hasTokenSync(request.ip)) { 139 | throw new Error("Too many invalid login"); 140 | } 141 | 142 | const is_authenticated = authenticateSync(request.login, request.password); 143 | 144 | if (!is_authenticated) { 145 | limiter.consumeSync(request.ip); 146 | 147 | throw new Error("Invalid login/password"); 148 | } 149 | ``` 150 | 151 | ## Notes on performance 152 | 153 | This module is used extensively on edge WebSocket servers, handling thousands of connections every second with multiple rate limit lists on the top of each other. Everything works smoothly, I/O doesn't block and RAM didn't move that much with the rate-limiting module enabled. 154 | 155 | On one core of a 2,3 GHz 8-Core Intel Core i9, the parallel asynchronous processing of 100,000 namespaces in the same limiter take an average of 160 ms, which is fine (1.6 microseconds per operation). 156 | 157 | ## Why not using existing similar modules? 158 | 159 | I was looking for an efficient, yet simple, DOS-prevention technique that wouldn't hurt performance and consume tons of memory. All proper modules I found were relying on Redis as the keystore for limits, which is definitely not great if you want to keep away from DOS attacks: using such a module under DOS conditions would subsequently DOS Redis since 1 (or more) Redis queries are made per limit check (1 attacker request = 1 limit check). Attacks should definitely not be allieviated this way, although a Redis-based solution would be perfect to limit abusing users. 160 | 161 | This module keeps all limits in-memory, which is much better for our attack-prevention concern. The only downside: since the limits database isn't shared, limits are per-process. This means that you should only use this module to prevent hard-attacks at any level of your infrastructure. This works pretty well for micro-service infrastructures, which is what we're using it in. 162 | -------------------------------------------------------------------------------- /lib/fast_ratelimit.js: -------------------------------------------------------------------------------- 1 | /* 2 | * node-fast-ratelimit 3 | * 4 | * Copyright 2016, Valerian Saliou 5 | * Author: Valerian Saliou 6 | */ 7 | 8 | 9 | "use strict"; 10 | 11 | 12 | /** 13 | * FastRateLimit 14 | * @class 15 | * @classdesc Instanciates a new rate-limiter 16 | * @param {object} options 17 | */ 18 | var FastRateLimit = function(options) { 19 | // Sanitize options 20 | if (typeof options !== "object") { 21 | throw new Error("Invalid or missing options"); 22 | } 23 | if (typeof options.threshold !== "number" || options.threshold < 0) { 24 | throw new Error("Invalid or missing options.threshold"); 25 | } 26 | if (typeof options.ttl !== "number" || options.ttl < 0) { 27 | throw new Error("Invalid or missing options.ttl"); 28 | } 29 | 30 | // Environment 31 | var secondInMilliseconds = 1000; 32 | 33 | // Storage space 34 | this.__options = { 35 | threshold : options.threshold, 36 | ttl_millisec : (options.ttl * secondInMilliseconds) 37 | }; 38 | 39 | this.__tokens = new Map(); 40 | }; 41 | 42 | 43 | /** 44 | * tokenCheck 45 | * @private 46 | * @param {boolean} consumeToken Whether to consume token or not 47 | * @returns {function} A configured token checking function 48 | */ 49 | var tokenCheck = function(consumeToken) { 50 | return function(namespace) { 51 | // No namespace provided? 52 | if (!namespace) { 53 | // Do not rate-limit (1 token remaining each hop) 54 | return true; 55 | } 56 | 57 | let _tokens_count; 58 | 59 | // Token bucket empty for namespace? 60 | if (this.__tokens.has(namespace) === false) { 61 | _tokens_count = this.__options.threshold; 62 | 63 | this.__scheduleExpireToken(namespace); 64 | } else { 65 | _tokens_count = this.__tokens.get(namespace); 66 | } 67 | 68 | // Check remaining tokens in bucket 69 | if (_tokens_count > 0) { 70 | if (consumeToken) { 71 | this.__tokens.set( 72 | namespace, (_tokens_count - 1) 73 | ); 74 | } 75 | 76 | return true; 77 | } 78 | 79 | return false; 80 | }; 81 | }; 82 | 83 | 84 | /** 85 | * FastRateLimit.prototype.consumeSync 86 | * @public 87 | * @param {string} namespace 88 | * @return {boolean} Whether tokens remain in current timespan or not 89 | */ 90 | FastRateLimit.prototype.consumeSync = tokenCheck(true); 91 | 92 | 93 | /** 94 | * FastRateLimit.prototype.hasTokenSync 95 | * @public 96 | * @param {string} namespace 97 | * @return {boolean} Whether tokens remain in current timespan or not 98 | */ 99 | 100 | FastRateLimit.prototype.hasTokenSync = tokenCheck(false); 101 | 102 | 103 | /** 104 | * FastRateLimit.prototype.consume 105 | * @public 106 | * @param {string} namespace 107 | * @return {object} Promise object 108 | */ 109 | FastRateLimit.prototype.consume = function(namespace) { 110 | if (this.consumeSync(namespace) === true) { 111 | return Promise.resolve(); 112 | } 113 | 114 | return Promise.reject(); 115 | }; 116 | 117 | 118 | /** 119 | * FastRateLimit.prototype.hasToken 120 | * @public 121 | * @param {string} namespace 122 | * @return {object} Promise object 123 | */ 124 | FastRateLimit.prototype.hasToken = function(namespace) { 125 | if (this.hasTokenSync(namespace) === true) { 126 | return Promise.resolve(); 127 | } 128 | 129 | return Promise.reject(); 130 | }; 131 | 132 | 133 | /** 134 | * FastRateLimit.prototype.__scheduleExpireToken 135 | * @private 136 | * @param {string} namespace 137 | * @return {undefined} 138 | */ 139 | FastRateLimit.prototype.__scheduleExpireToken = function(namespace) { 140 | var self = this; 141 | 142 | setTimeout(function() { 143 | // Expire token storage for namespace 144 | self.__tokens.delete(namespace); 145 | }, this.__options.ttl_millisec); 146 | }; 147 | 148 | 149 | exports.FastRateLimit = FastRateLimit; 150 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-ratelimit", 3 | "description": "Fast and efficient in-memory rate-limit for Node, used to alleviate severe DOS attacks.", 4 | "version": "3.0.1", 5 | "homepage": "https://github.com/valeriansaliou/node-fast-ratelimit", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Valerian Saliou", 9 | "email": "valerian@valeriansaliou.name", 10 | "url": "https://valeriansaliou.name/" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/valeriansaliou/node-fast-ratelimit.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/valeriansaliou/node-fast-ratelimit/issues" 18 | }, 19 | "licenses": [ 20 | { 21 | "type": "MIT", 22 | "url": "https://github.com/valeriansaliou/node-fast-ratelimit/blob/master/LICENSE" 23 | } 24 | ], 25 | "main": "lib/fast_ratelimit", 26 | "engines": { 27 | "node": ">= 6.0.0" 28 | }, 29 | "scripts": { 30 | "test": "check-build && istanbul cover _mocha" 31 | }, 32 | "devDependencies": { 33 | "check-build": "2.8.2", 34 | "mocha": "3.4.2", 35 | "mocha-lcov-reporter": "1.3.0", 36 | "istanbul": "0.4.5" 37 | }, 38 | "keywords": [ 39 | "ratelimit", 40 | "rate-limit", 41 | "rate", 42 | "limit", 43 | "attack", 44 | "flood", 45 | "security", 46 | "dos", 47 | "ddos" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /test/fast_ratelimit-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * node-fast-ratelimit 3 | * 4 | * Copyright 2016, Valerian Saliou 5 | * Author: Valerian Saliou 6 | */ 7 | 8 | 9 | "use strict"; 10 | 11 | 12 | var FastRateLimit = require("../").FastRateLimit; 13 | var assert = require("assert"); 14 | 15 | 16 | var __Promise = ( 17 | (typeof Promise !== "undefined") ? 18 | Promise : require("es6-promise-polyfill").Promise 19 | ); 20 | 21 | 22 | describe("node-fast-ratelimit", function() { 23 | describe("constructor", function() { 24 | it("should succeed creating a limiter with valid options", function() { 25 | assert.doesNotThrow( 26 | function() { 27 | new FastRateLimit({ 28 | threshold : 5, 29 | ttl : 10 30 | }); 31 | }, 32 | 33 | "FastRateLimit should not throw on valid options" 34 | ); 35 | }); 36 | 37 | it("should fail creating a limiter with missing threshold", function() { 38 | assert.throws( 39 | function() { 40 | new FastRateLimit({ 41 | ttl : 10 42 | }); 43 | }, 44 | 45 | "FastRateLimit should throw on missing threshold" 46 | ); 47 | }); 48 | 49 | it("should fail creating a limiter with invalid threshold", function() { 50 | assert.throws( 51 | function() { 52 | new FastRateLimit({ 53 | threshold : -1, 54 | ttl : 10 55 | }); 56 | }, 57 | 58 | "FastRateLimit should throw on invalid threshold" 59 | ); 60 | }); 61 | 62 | it("should fail creating a limiter with missing ttl", function() { 63 | assert.throws( 64 | function() { 65 | new FastRateLimit({ 66 | threshold : 2 67 | }); 68 | }, 69 | 70 | "FastRateLimit should throw on missing ttl" 71 | ); 72 | }); 73 | 74 | it("should fail creating a limiter with invalid ttl", function() { 75 | assert.throws( 76 | function() { 77 | new FastRateLimit({ 78 | ttl : "120" 79 | }); 80 | }, 81 | 82 | "FastRateLimit should throw on invalid ttl" 83 | ); 84 | }); 85 | }); 86 | 87 | describe("consumeSync method", function() { 88 | it("should not rate limit an empty namespace", function() { 89 | var limiter = new FastRateLimit({ 90 | threshold : 100, 91 | ttl : 10 92 | }); 93 | 94 | assert.ok( 95 | limiter.consumeSync(null), 96 | "Limiter consume should succeed for `null` (null) namespace (resolve)" 97 | ); 98 | 99 | assert.ok( 100 | limiter.consumeSync(""), 101 | "Limiter consume should succeed for `` (blank) namespace (resolve)" 102 | ); 103 | 104 | assert.ok( 105 | limiter.consumeSync(0), 106 | "Limiter consume should succeed for `0` (number) namespace (resolve)" 107 | ); 108 | }); 109 | 110 | it("should not rate limit a single namespace", function() { 111 | var options = { 112 | threshold : 100, 113 | ttl : 10 114 | }; 115 | 116 | var namespace = "127.0.0.1"; 117 | var limiter = new FastRateLimit(options); 118 | 119 | for (var i = 1; i <= options.threshold; i++) { 120 | assert.ok( 121 | limiter.consumeSync(namespace), 122 | "Limiter consume should succeed" 123 | ); 124 | } 125 | }); 126 | 127 | it("should rate limit a single namespace", function() { 128 | var namespace = "127.0.0.1"; 129 | 130 | var limiter = new FastRateLimit({ 131 | threshold : 3, 132 | ttl : 10 133 | }); 134 | 135 | assert.ok( 136 | limiter.consumeSync(namespace), 137 | "Limiter consume succeed at consume #1 (resolve)" 138 | ); 139 | 140 | assert.ok( 141 | limiter.consumeSync(namespace), 142 | "Limiter consume succeed at consume #2 (resolve)" 143 | ); 144 | 145 | assert.ok( 146 | limiter.consumeSync(namespace), 147 | "Limiter consume succeed at consume #3 (resolve)" 148 | ); 149 | 150 | assert.ok( 151 | !(limiter.consumeSync(namespace)), 152 | "Limiter consume fail at consume #4 (reject)" 153 | ); 154 | }); 155 | 156 | it("should not rate limit multiple namespaces", function() { 157 | var limiter = new FastRateLimit({ 158 | threshold : 2, 159 | ttl : 10 160 | }); 161 | 162 | assert.ok( 163 | limiter.consumeSync("user_1"), 164 | "Limiter consume should succeed at consume #1 of user_1 (resolve)" 165 | ); 166 | 167 | assert.ok( 168 | limiter.consumeSync("user_2"), 169 | "Limiter consume should succeed at consume #1 of user_2 (resolve)" 170 | ); 171 | }); 172 | 173 | it("should rate limit multiple namespaces", function() { 174 | var limiter = new FastRateLimit({ 175 | threshold : 2, 176 | ttl : 10 177 | }); 178 | 179 | assert.ok( 180 | limiter.consumeSync("user_1"), 181 | "Limiter consume should succeed at consume #1 of user_1 (resolve)" 182 | ); 183 | 184 | assert.ok( 185 | limiter.consumeSync("user_2"), 186 | "Limiter consume should succeed at consume #1 of user_2 (resolve)" 187 | ); 188 | 189 | assert.ok( 190 | limiter.consumeSync("user_1"), 191 | "Limiter consume should succeed at consume #2 of user_1 (resolve)" 192 | ); 193 | 194 | assert.ok( 195 | limiter.consumeSync("user_2"), 196 | "Limiter consume should succeed at consume #2 of user_2 (resolve)" 197 | ); 198 | 199 | assert.ok( 200 | !(limiter.consumeSync("user_1")), 201 | "Limiter consume should fail at consume #3 of user_1 (reject)" 202 | ); 203 | 204 | assert.ok( 205 | !(limiter.consumeSync("user_2")), 206 | "Limiter consume should fail at consume #3 of user_2 (reject)" 207 | ); 208 | }); 209 | 210 | it("should expire token according to TTL", function(done) { 211 | // Do not consider timeout as slow 212 | this.timeout(2000); 213 | this.slow(5000); 214 | 215 | var options = { 216 | threshold : 2, 217 | ttl : 1 218 | }; 219 | 220 | var namespace = "127.0.0.1"; 221 | var limiter = new FastRateLimit(options); 222 | 223 | assert.ok( 224 | limiter.consumeSync(namespace), 225 | "Limiter consume should succeed at consume #1 (resolve)" 226 | ); 227 | 228 | assert.ok( 229 | limiter.consumeSync(namespace), 230 | "Limiter consume should succeed at consume #2 (resolve)" 231 | ); 232 | 233 | assert.ok( 234 | !(limiter.consumeSync(namespace)), 235 | "Limiter consume should fail at consume #3 (reject)" 236 | ); 237 | 238 | // Wait for TTL reset. 239 | setTimeout(function() { 240 | assert.ok( 241 | limiter.consumeSync(namespace), 242 | "Limiter consume should succeed at consume #4 (resolve)" 243 | ); 244 | 245 | done(); 246 | }, ((options.ttl * 1000) + 100)); 247 | }); 248 | 249 | it("should not block writing random namespaces", function(done) { 250 | // Timeout if longer than 1 second (check for blocking writes) 251 | this.timeout(1000); 252 | this.slow(250); 253 | 254 | var limiter = new FastRateLimit({ 255 | threshold : 100, 256 | ttl : 60 257 | }); 258 | 259 | var asyncFlowSteps = 10000, 260 | asyncFlowTotal = 10, 261 | asyncFlowCountDone = 0; 262 | 263 | var launchAsyncFlow = function(id) { 264 | setTimeout(function() { 265 | for (var i = 0; i < asyncFlowSteps; i++) { 266 | assert.ok( 267 | limiter.consumeSync("flow-" + id + "-" + i), 268 | "Limiter consume should succeed at flow #" + id + " (resolve)" 269 | ); 270 | } 271 | 272 | if (++asyncFlowCountDone === asyncFlowTotal) { 273 | done(); 274 | } 275 | }); 276 | } 277 | 278 | // Launch asynchronous flows 279 | for (var i = 1; i <= asyncFlowTotal; i++) { 280 | launchAsyncFlow(i); 281 | } 282 | }); 283 | 284 | 285 | }); 286 | 287 | describe("hasTokenSync method", function() { 288 | it("should not consume token", function() { 289 | var limiter = new FastRateLimit({ 290 | threshold : 1, 291 | ttl : 10 292 | }); 293 | var namespace = "127.0.0.1"; 294 | 295 | assert.ok(limiter.hasTokenSync(namespace), "Limiter hasTokenSync should succeed at hasTokenSync #1"); 296 | assert.ok(limiter.hasTokenSync(namespace), "Limiter hasTokenSync should succeed at hasTokenSync #2"); 297 | }); 298 | 299 | it("should rate limit", function() { 300 | var limiter = new FastRateLimit({ 301 | threshold : 1, 302 | ttl : 10 303 | }); 304 | var namespace = "127.0.0.1"; 305 | 306 | assert.ok(limiter.hasTokenSync(namespace), "Limiter hasTokenSync should succeed at hasTokenSync #1"); 307 | assert.ok(limiter.consumeSync(namespace), "Limiter consumeSync should succeed at consumeSync #1"); 308 | assert.ok(!limiter.hasTokenSync(namespace), "Limiter hasTokenSync should fail at hasTokenSync #2"); 309 | }); 310 | }); 311 | 312 | describe("hasToken method", function() { 313 | it("should not consume token", function(done) { 314 | var limiter = new FastRateLimit({ 315 | threshold : 1, 316 | ttl : 10 317 | }); 318 | var namespace = "127.0.0.1"; 319 | var promises_all = []; 320 | 321 | promises_all.push(limiter.hasToken(namespace)); 322 | promises_all.push(limiter.hasToken(namespace)); 323 | 324 | __Promise.all(promises_all) 325 | .then(function() { 326 | done(); 327 | }) 328 | .catch(function(error) { 329 | if (error) { 330 | done(error); 331 | } else { 332 | done( 333 | new Error("Limiter hasToken should not fail at the end (reject)") 334 | ); 335 | } 336 | }); 337 | }); 338 | 339 | it("should rate limit", function(done) { 340 | var limiter = new FastRateLimit({ 341 | threshold : 1, 342 | ttl : 10 343 | }); 344 | var namespace = "127.0.0.1"; 345 | var promises_all = []; 346 | 347 | promises_all.push(limiter.hasToken(namespace)); 348 | promises_all.push(limiter.consume(namespace)); 349 | promises_all.push(limiter.hasToken(namespace)); 350 | 351 | __Promise.all(promises_all) 352 | .then(function() { 353 | done(new Error("Limiter hasToken should not succeed at the end (reject)")); 354 | }) 355 | .catch(function(error) { 356 | if (error) { 357 | done(error); 358 | } 359 | 360 | done(); 361 | }); 362 | }); 363 | }); 364 | 365 | describe("consume method", function() { 366 | it("should not rate limit", function(done) { 367 | var options = { 368 | threshold : 100, 369 | ttl : 10 370 | }; 371 | 372 | var namespace = "127.0.0.1"; 373 | var limiter = new FastRateLimit(options); 374 | 375 | var promises_all = []; 376 | 377 | for (var i = 1; i <= options.threshold; i++) { 378 | promises_all.push( 379 | limiter.consume(namespace) 380 | ); 381 | } 382 | 383 | __Promise.all(promises_all) 384 | .then(function() { 385 | done(); 386 | }) 387 | .catch(function(error) { 388 | if (error) { 389 | done(error); 390 | } else { 391 | done( 392 | new Error("Limiter consume should not fail at the end (reject)") 393 | ); 394 | } 395 | }); 396 | }); 397 | 398 | it("should rate limit", function(done) { 399 | var options = { 400 | threshold : 100, 401 | ttl : 10 402 | }; 403 | 404 | var namespace = "127.0.0.1"; 405 | var limiter = new FastRateLimit(options); 406 | 407 | var promises_all = []; 408 | 409 | for (var i = 1; i <= (options.threshold + 5); i++) { 410 | promises_all.push( 411 | limiter.consume(namespace) 412 | ); 413 | } 414 | 415 | __Promise.all(promises_all) 416 | .then(function(remaining_tokens_list) { 417 | done( 418 | new Error("Limiter consume should not succeed at the end (reject)") 419 | ); 420 | }) 421 | .catch(function(error) { 422 | if (error) { 423 | done(error); 424 | } else { 425 | done(); 426 | } 427 | }); 428 | }); 429 | }); 430 | }); 431 | --------------------------------------------------------------------------------