├── .gitignore ├── .jshintignore ├── .jshintrc ├── LICENSE ├── Makefile ├── README.md ├── index.js ├── package-lock.json ├── package.json └── tests └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | public 2 | node_modules 3 | django_app 4 | client/vendor 5 | venv 6 | client/ender 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "require" 4 | , "provide" 5 | , "module" 6 | , "exports" 7 | , "process" 8 | , "describe" 9 | , "context" 10 | , "it" 11 | , "beforeEach" 12 | , "afterEach" 13 | ] 14 | , "indent": 2 15 | , "maxdepth": 6 16 | , "maxlen": 120 17 | , "bitwise": false 18 | , "curly": false 19 | , "eqeqeq": false 20 | , "forin": false 21 | , "immed": false 22 | , "latedef": false 23 | , "newcap": true 24 | , "noarg": false 25 | , "noempty": true 26 | , "nonew": false 27 | , "plusplus": false 28 | , "quotmark": "single" 29 | , "regexp": false 30 | , "undef": true 31 | , "unused": "vars" 32 | , "strict": false 33 | , "trailing": true 34 | , "asi": true 35 | , "boss": true 36 | , "eqnull": true 37 | , "es5": false 38 | , "esnext": true 39 | , "evil": true 40 | , "expr": true 41 | , "funcscope": false 42 | , "globalstrict": false 43 | , "iterator": false 44 | , "lastsemic": true 45 | , "laxbreak": true 46 | , "laxcomma": true 47 | , "loopfunc": true 48 | , "multistr": false 49 | , "onecase": false 50 | , "proto": false 51 | , "regexdash": false 52 | , "scripturl": true 53 | , "smarttabs": true 54 | , "shadow": false 55 | , "sub": true 56 | , "supernew": false 57 | , "validthis": true 58 | , "browser": true 59 | , "nonstandard": true 60 | , "nomen": false 61 | , "onevar": false 62 | , "passfail": false 63 | , "devel": false 64 | } 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dustin Diaz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | test: 4 | ./node_modules/.bin/mocha --ui bdd --reporter spec tests 5 | 6 | lint: 7 | ./node_modules/.bin/jshint ./ 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Express rate-limiter 2 | Rate limiting middleware for Express applications built on redis 3 | 4 | ``` sh 5 | npm install express-limiter --save 6 | ``` 7 | 8 | ``` js 9 | var express = require('express') 10 | var app = express() 11 | var client = require('redis').createClient() 12 | 13 | var limiter = require('express-limiter')(app, client) 14 | 15 | /** 16 | * you may also pass it an Express 4.0 `Router` 17 | * 18 | * router = express.Router() 19 | * limiter = require('express-limiter')(router, client) 20 | */ 21 | 22 | limiter({ 23 | path: '/api/action', 24 | method: 'get', 25 | lookup: ['connection.remoteAddress'], 26 | // 150 requests per hour 27 | total: 150, 28 | expire: 1000 * 60 * 60 29 | }) 30 | 31 | app.get('/api/action', function (req, res) { 32 | res.send(200, 'ok') 33 | }) 34 | ``` 35 | 36 | ### API options 37 | 38 | ``` js 39 | limiter(options) 40 | ``` 41 | 42 | - `path`: `String` *optional* route path to the request 43 | - `method`: `String` *optional* http method. accepts `get`, `post`, `put`, `delete`, and of course Express' `all` 44 | - `lookup`: `Function|String|Array.` value lookup on the request object. Can be a single value, array or function. See [examples](#examples) for common usages 45 | - `total`: `Number` allowed number of requests before getting rate limited 46 | - `expire`: `Number` amount of time in `ms` before the rate-limited is reset 47 | - `whitelist`: `function(req)` optional param allowing the ability to whitelist. return `boolean`, `true` to whitelist, `false` to passthru to limiter. 48 | - `skipHeaders`: `Boolean` whether to skip sending HTTP headers for rate limits () 49 | - `ignoreErrors`: `Boolean` whether errors generated from redis should allow the middleware to call next(). Defaults to false. 50 | - `onRateLimited`: `Function` called when a request exceeds the configured rate limit. 51 | 52 | ### Examples 53 | 54 | ``` js 55 | // limit by IP address 56 | limiter({ 57 | ... 58 | lookup: 'connection.remoteAddress' 59 | ... 60 | }) 61 | 62 | // or if you are behind a trusted proxy (like nginx) 63 | limiter({ 64 | lookup: 'headers.x-forwarded-for' 65 | }) 66 | 67 | // by user (assuming a user is logged in with a valid id) 68 | limiter({ 69 | lookup: 'user.id' 70 | }) 71 | 72 | // limit your entire app 73 | limiter({ 74 | path: '*', 75 | method: 'all', 76 | lookup: 'connection.remoteAddress' 77 | }) 78 | 79 | // limit users on same IP 80 | limiter({ 81 | path: '*', 82 | method: 'all', 83 | lookup: ['user.id', 'connection.remoteAddress'] 84 | }) 85 | 86 | // whitelist user admins 87 | limiter({ 88 | path: '/delete/thing', 89 | method: 'post', 90 | lookup: 'user.id', 91 | whitelist: function (req) { 92 | return !!req.user.is_admin 93 | } 94 | }) 95 | 96 | // skip sending HTTP limit headers 97 | limiter({ 98 | path: '/delete/thing', 99 | method: 'post', 100 | lookup: 'user.id', 101 | whitelist: function (req) { 102 | return !!req.user.is_admin 103 | }, 104 | skipHeaders: true 105 | }) 106 | 107 | // call a custom limit handler 108 | limiter({ 109 | path: '*', 110 | method: 'all', 111 | lookup: 'connection.remoteAddress', 112 | onRateLimited: function (req, res, next) { 113 | next({ message: 'Rate limit exceeded', status: 429 }) 114 | } 115 | }) 116 | 117 | // with a function for dynamic-ness 118 | limiter({ 119 | lookup: function(req, res, opts, next) { 120 | if (validApiKey(req.query.api_key)) { 121 | opts.lookup = 'query.api_key' 122 | opts.total = 100 123 | } else { 124 | opts.lookup = 'connection.remoteAddress' 125 | opts.total = 10 126 | } 127 | return next() 128 | } 129 | }) 130 | 131 | ``` 132 | 133 | ### as direct middleware 134 | 135 | ``` js 136 | app.post('/user/update', limiter({ lookup: 'user.id' }), function (req, res) { 137 | User.find(req.user.id).update(function (err) { 138 | if (err) next(err) 139 | else res.send('ok') 140 | }) 141 | }) 142 | ``` 143 | 144 | ## License MIT 145 | 146 | Happy Rate Limiting! 147 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (app, db) { 2 | return function (opts) { 3 | var middleware = function (req, res, next) { 4 | if (opts.whitelist && opts.whitelist(req)) return next() 5 | opts.lookup = Array.isArray(opts.lookup) ? opts.lookup : [opts.lookup] 6 | opts.onRateLimited = typeof opts.onRateLimited === 'function' ? opts.onRateLimited : function (req, res, next) { 7 | res.status(429).send('Rate limit exceeded') 8 | } 9 | var lookups = opts.lookup.map(function (item) { 10 | return item + ':' + item.split('.').reduce(function (prev, cur) { 11 | return prev[cur] 12 | }, req) 13 | }).join(':') 14 | var path = opts.path || req.path 15 | var method = (opts.method || req.method).toLowerCase() 16 | var key = 'ratelimit:' + path + ':' + method + ':' + lookups 17 | db.get(key, function (err, limit) { 18 | if (err && opts.ignoreErrors) return next() 19 | var now = Date.now() 20 | limit = limit ? JSON.parse(limit) : { 21 | total: opts.total, 22 | remaining: opts.total, 23 | reset: now + opts.expire 24 | } 25 | 26 | if (now > limit.reset) { 27 | limit.reset = now + opts.expire 28 | limit.remaining = opts.total 29 | } 30 | 31 | // do not allow negative remaining 32 | limit.remaining = Math.max(Number(limit.remaining) - 1, -1) 33 | db.set(key, JSON.stringify(limit), 'PX', opts.expire, function (e) { 34 | if (!opts.skipHeaders) { 35 | res.set('X-RateLimit-Limit', limit.total) 36 | res.set('X-RateLimit-Reset', Math.ceil(limit.reset / 1000)) // UTC epoch seconds 37 | res.set('X-RateLimit-Remaining', Math.max(limit.remaining,0)) 38 | } 39 | 40 | if (limit.remaining >= 0) return next() 41 | 42 | var after = (limit.reset - Date.now()) / 1000 43 | 44 | if (!opts.skipHeaders) res.set('Retry-After', after) 45 | 46 | opts.onRateLimited(req, res, next) 47 | }) 48 | 49 | }) 50 | } 51 | if (typeof(opts.lookup) === 'function') { 52 | var callableLookup = opts.lookup; 53 | middleware = function (middleware, req, res, next) { 54 | return callableLookup(req, res, opts, function () { 55 | return middleware(req, res, next) 56 | }) 57 | }.bind(this, middleware) 58 | } 59 | if (opts.method && opts.path) app[opts.method](opts.path, middleware) 60 | return middleware 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-limiter", 3 | "version": "1.6.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.0.0.tgz", 10 | "integrity": "sha1-NgTHZVhsO5z3h3tpN829RYf5R9w=", 11 | "dev": true, 12 | "requires": { 13 | "mime": "1.2.11", 14 | "negotiator": "0.3.0" 15 | } 16 | }, 17 | "assertion-error": { 18 | "version": "1.0.0", 19 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", 20 | "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=", 21 | "dev": true 22 | }, 23 | "buffer-crc32": { 24 | "version": "0.2.1", 25 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.1.tgz", 26 | "integrity": "sha1-vj5TgvwCttYySVasGvmKqYsIU0w=", 27 | "dev": true 28 | }, 29 | "chai": { 30 | "version": "1.9.2", 31 | "resolved": "https://registry.npmjs.org/chai/-/chai-1.9.2.tgz", 32 | "integrity": "sha1-Pxog+CsLnXQ3V30k1vErGmnTtZA=", 33 | "dev": true, 34 | "requires": { 35 | "assertion-error": "1.0.0", 36 | "deep-eql": "0.1.3" 37 | } 38 | }, 39 | "cli": { 40 | "version": "0.6.6", 41 | "resolved": "https://registry.npmjs.org/cli/-/cli-0.6.6.tgz", 42 | "integrity": "sha1-Aq1Eo4Cr8nraxebwzdewQ9dMU+M=", 43 | "dev": true, 44 | "requires": { 45 | "exit": "0.1.2", 46 | "glob": "3.2.11" 47 | } 48 | }, 49 | "commander": { 50 | "version": "2.0.0", 51 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", 52 | "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=", 53 | "dev": true 54 | }, 55 | "console-browserify": { 56 | "version": "1.1.0", 57 | "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", 58 | "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", 59 | "dev": true, 60 | "requires": { 61 | "date-now": "0.1.4" 62 | } 63 | }, 64 | "cookie": { 65 | "version": "0.1.0", 66 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.0.tgz", 67 | "integrity": "sha1-kOtGndzpBchm3mh+/EMTHYgB+dA=", 68 | "dev": true 69 | }, 70 | "cookie-signature": { 71 | "version": "1.0.3", 72 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.3.tgz", 73 | "integrity": "sha1-kc2ZfMUftkFZVzjGnNoCAyj1D/k=", 74 | "dev": true 75 | }, 76 | "cookiejar": { 77 | "version": "1.3.0", 78 | "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-1.3.0.tgz", 79 | "integrity": "sha1-3QCzVnkCHpnL1OhVua0EGRNHR2U=", 80 | "dev": true 81 | }, 82 | "core-util-is": { 83 | "version": "1.0.2", 84 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 85 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", 86 | "dev": true 87 | }, 88 | "date-now": { 89 | "version": "0.1.4", 90 | "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", 91 | "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", 92 | "dev": true 93 | }, 94 | "debug": { 95 | "version": "0.8.1", 96 | "resolved": "https://registry.npmjs.org/debug/-/debug-0.8.1.tgz", 97 | "integrity": "sha1-IP9NJvXkIstoobrLu2EDmtjBwTA=", 98 | "dev": true 99 | }, 100 | "deep-eql": { 101 | "version": "0.1.3", 102 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", 103 | "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", 104 | "dev": true, 105 | "requires": { 106 | "type-detect": "0.1.1" 107 | } 108 | }, 109 | "diff": { 110 | "version": "1.0.7", 111 | "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", 112 | "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=", 113 | "dev": true 114 | }, 115 | "dom-serializer": { 116 | "version": "0.1.0", 117 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", 118 | "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", 119 | "dev": true, 120 | "requires": { 121 | "domelementtype": "1.1.3", 122 | "entities": "1.1.1" 123 | }, 124 | "dependencies": { 125 | "domelementtype": { 126 | "version": "1.1.3", 127 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", 128 | "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", 129 | "dev": true 130 | }, 131 | "entities": { 132 | "version": "1.1.1", 133 | "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", 134 | "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", 135 | "dev": true 136 | } 137 | } 138 | }, 139 | "domelementtype": { 140 | "version": "1.3.0", 141 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", 142 | "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", 143 | "dev": true 144 | }, 145 | "domhandler": { 146 | "version": "2.3.0", 147 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", 148 | "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", 149 | "dev": true, 150 | "requires": { 151 | "domelementtype": "1.3.0" 152 | } 153 | }, 154 | "domutils": { 155 | "version": "1.5.1", 156 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", 157 | "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", 158 | "dev": true, 159 | "requires": { 160 | "dom-serializer": "0.1.0", 161 | "domelementtype": "1.3.0" 162 | } 163 | }, 164 | "emitter-component": { 165 | "version": "1.0.0", 166 | "resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.0.0.tgz", 167 | "integrity": "sha1-8E3Rj8PcPpp0y8DzELCIZm5MAW8=", 168 | "dev": true 169 | }, 170 | "entities": { 171 | "version": "1.0.0", 172 | "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", 173 | "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=", 174 | "dev": true 175 | }, 176 | "escape-html": { 177 | "version": "1.0.1", 178 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz", 179 | "integrity": "sha1-GBoobq05ejmpKFfPsdQwUuNWv/A=", 180 | "dev": true 181 | }, 182 | "exit": { 183 | "version": "0.1.2", 184 | "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", 185 | "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", 186 | "dev": true 187 | }, 188 | "express": { 189 | "version": "4.0.0", 190 | "resolved": "https://registry.npmjs.org/express/-/express-4.0.0.tgz", 191 | "integrity": "sha1-J03IKTPJ9XTMOKDOXqgXK+nGsJQ=", 192 | "dev": true, 193 | "requires": { 194 | "accepts": "1.0.0", 195 | "buffer-crc32": "0.2.1", 196 | "cookie": "0.1.0", 197 | "cookie-signature": "1.0.3", 198 | "debug": "0.8.1", 199 | "escape-html": "1.0.1", 200 | "fresh": "0.2.2", 201 | "merge-descriptors": "0.0.2", 202 | "methods": "0.1.0", 203 | "parseurl": "1.0.1", 204 | "path-to-regexp": "0.1.2", 205 | "qs": "0.6.6", 206 | "range-parser": "1.0.0", 207 | "send": "0.2.0", 208 | "serve-static": "1.0.1", 209 | "type-is": "1.0.0", 210 | "utils-merge": "1.0.0" 211 | } 212 | }, 213 | "extend": { 214 | "version": "1.2.1", 215 | "resolved": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz", 216 | "integrity": "sha1-oPX9bPyDpf5J72mNYOyKYk3UV2w=", 217 | "dev": true 218 | }, 219 | "formatio": { 220 | "version": "1.0.2", 221 | "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.0.2.tgz", 222 | "integrity": "sha1-55kcoUT/fYz/B7uayGqbeca6R+8=", 223 | "dev": true, 224 | "requires": { 225 | "samsam": "1.1.3" 226 | } 227 | }, 228 | "formidable": { 229 | "version": "1.0.14", 230 | "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.14.tgz", 231 | "integrity": "sha1-Kz9MQRy7X91pXESEPiojUUpDIxo=", 232 | "dev": true 233 | }, 234 | "fresh": { 235 | "version": "0.2.2", 236 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.2.2.tgz", 237 | "integrity": "sha1-lzHc9WeMf660T7kDxPct9VGH+nc=", 238 | "dev": true 239 | }, 240 | "glob": { 241 | "version": "3.2.11", 242 | "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", 243 | "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", 244 | "dev": true, 245 | "requires": { 246 | "inherits": "2.0.3", 247 | "minimatch": "0.3.0" 248 | }, 249 | "dependencies": { 250 | "minimatch": { 251 | "version": "0.3.0", 252 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", 253 | "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", 254 | "dev": true, 255 | "requires": { 256 | "lru-cache": "2.7.3", 257 | "sigmund": "1.0.1" 258 | } 259 | } 260 | } 261 | }, 262 | "graceful-fs": { 263 | "version": "2.0.3", 264 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", 265 | "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=", 266 | "dev": true 267 | }, 268 | "growl": { 269 | "version": "1.7.0", 270 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", 271 | "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=", 272 | "dev": true 273 | }, 274 | "htmlparser2": { 275 | "version": "3.8.3", 276 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", 277 | "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", 278 | "dev": true, 279 | "requires": { 280 | "domelementtype": "1.3.0", 281 | "domhandler": "2.3.0", 282 | "domutils": "1.5.1", 283 | "entities": "1.0.0", 284 | "readable-stream": "1.1.14" 285 | } 286 | }, 287 | "inherits": { 288 | "version": "2.0.3", 289 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 290 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 291 | "dev": true 292 | }, 293 | "isarray": { 294 | "version": "0.0.1", 295 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 296 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", 297 | "dev": true 298 | }, 299 | "jade": { 300 | "version": "0.26.3", 301 | "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", 302 | "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", 303 | "dev": true, 304 | "requires": { 305 | "commander": "0.6.1", 306 | "mkdirp": "0.3.0" 307 | }, 308 | "dependencies": { 309 | "commander": { 310 | "version": "0.6.1", 311 | "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", 312 | "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", 313 | "dev": true 314 | }, 315 | "mkdirp": { 316 | "version": "0.3.0", 317 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", 318 | "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", 319 | "dev": true 320 | } 321 | } 322 | }, 323 | "jshint": { 324 | "version": "2.5.11", 325 | "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.5.11.tgz", 326 | "integrity": "sha1-4tlYWLuxqngwAQii6BCZ+wlWIuA=", 327 | "dev": true, 328 | "requires": { 329 | "cli": "0.6.6", 330 | "console-browserify": "1.1.0", 331 | "exit": "0.1.2", 332 | "htmlparser2": "3.8.3", 333 | "minimatch": "1.0.0", 334 | "shelljs": "0.3.0", 335 | "strip-json-comments": "1.0.4", 336 | "underscore": "1.6.0" 337 | } 338 | }, 339 | "lru-cache": { 340 | "version": "2.7.3", 341 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", 342 | "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", 343 | "dev": true 344 | }, 345 | "merge-descriptors": { 346 | "version": "0.0.2", 347 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-0.0.2.tgz", 348 | "integrity": "sha1-w2pSp4FDdRPFcnXzndnTF1FKyMc=", 349 | "dev": true 350 | }, 351 | "methods": { 352 | "version": "0.1.0", 353 | "resolved": "https://registry.npmjs.org/methods/-/methods-0.1.0.tgz", 354 | "integrity": "sha1-M11Cnu/SG3us8unJIqjSvRSjDk8=", 355 | "dev": true 356 | }, 357 | "mime": { 358 | "version": "1.2.11", 359 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", 360 | "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=", 361 | "dev": true 362 | }, 363 | "minimatch": { 364 | "version": "1.0.0", 365 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-1.0.0.tgz", 366 | "integrity": "sha1-4N0hILSeG3JM6NcUxSCCKpQ4V20=", 367 | "dev": true, 368 | "requires": { 369 | "lru-cache": "2.7.3", 370 | "sigmund": "1.0.1" 371 | } 372 | }, 373 | "mkdirp": { 374 | "version": "0.3.5", 375 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", 376 | "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", 377 | "dev": true 378 | }, 379 | "mocha": { 380 | "version": "1.18.2", 381 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.18.2.tgz", 382 | "integrity": "sha1-gAhI+PeITGHu/PoqJzBLqeVEbQs=", 383 | "dev": true, 384 | "requires": { 385 | "commander": "2.0.0", 386 | "debug": "0.8.1", 387 | "diff": "1.0.7", 388 | "glob": "3.2.3", 389 | "growl": "1.7.0", 390 | "jade": "0.26.3", 391 | "mkdirp": "0.3.5" 392 | }, 393 | "dependencies": { 394 | "glob": { 395 | "version": "3.2.3", 396 | "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", 397 | "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", 398 | "dev": true, 399 | "requires": { 400 | "graceful-fs": "2.0.3", 401 | "inherits": "2.0.3", 402 | "minimatch": "0.2.14" 403 | } 404 | }, 405 | "minimatch": { 406 | "version": "0.2.14", 407 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", 408 | "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", 409 | "dev": true, 410 | "requires": { 411 | "lru-cache": "2.7.3", 412 | "sigmund": "1.0.1" 413 | } 414 | } 415 | } 416 | }, 417 | "negotiator": { 418 | "version": "0.3.0", 419 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.3.0.tgz", 420 | "integrity": "sha1-cG1pLv7d9XTVfqn7GriaT6fuj2A=", 421 | "dev": true 422 | }, 423 | "parseurl": { 424 | "version": "1.0.1", 425 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.0.1.tgz", 426 | "integrity": "sha1-Llfc5u/dN8NRhwEDCUTCK/OIt7Q=", 427 | "dev": true 428 | }, 429 | "path-to-regexp": { 430 | "version": "0.1.2", 431 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.2.tgz", 432 | "integrity": "sha1-mysVH5zDAYye6lDKlXKeBXgXErQ=", 433 | "dev": true 434 | }, 435 | "qs": { 436 | "version": "0.6.6", 437 | "resolved": "https://registry.npmjs.org/qs/-/qs-0.6.6.tgz", 438 | "integrity": "sha1-bgFQmP9RlouKPIGQAdXyyJvEsQc=", 439 | "dev": true 440 | }, 441 | "range-parser": { 442 | "version": "1.0.0", 443 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.0.tgz", 444 | "integrity": "sha1-pLJkz+C+XONqvjdlrJwqJIdG28A=", 445 | "dev": true 446 | }, 447 | "readable-stream": { 448 | "version": "1.1.14", 449 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", 450 | "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", 451 | "dev": true, 452 | "requires": { 453 | "core-util-is": "1.0.2", 454 | "inherits": "2.0.3", 455 | "isarray": "0.0.1", 456 | "string_decoder": "0.10.31" 457 | } 458 | }, 459 | "redis": { 460 | "version": "0.10.3", 461 | "resolved": "https://registry.npmjs.org/redis/-/redis-0.10.3.tgz", 462 | "integrity": "sha1-iSf+IRDuOWF7zz/Te4nY4SORG7Y=", 463 | "dev": true 464 | }, 465 | "reduce-component": { 466 | "version": "1.0.1", 467 | "resolved": "https://registry.npmjs.org/reduce-component/-/reduce-component-1.0.1.tgz", 468 | "integrity": "sha1-4Mk1QsV0UhvqE98PlIjtgqt3xdo=", 469 | "dev": true 470 | }, 471 | "samsam": { 472 | "version": "1.1.3", 473 | "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.3.tgz", 474 | "integrity": "sha1-n1CHQZtNCR8jJXHn+lLpCw9VJiE=", 475 | "dev": true 476 | }, 477 | "send": { 478 | "version": "0.2.0", 479 | "resolved": "https://registry.npmjs.org/send/-/send-0.2.0.tgz", 480 | "integrity": "sha1-Bnq/Rc/4v/spy9t0OXJbMjiKLFg=", 481 | "dev": true, 482 | "requires": { 483 | "debug": "0.8.1", 484 | "fresh": "0.2.2", 485 | "mime": "1.2.11", 486 | "range-parser": "1.0.0" 487 | } 488 | }, 489 | "serve-static": { 490 | "version": "1.0.1", 491 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.0.1.tgz", 492 | "integrity": "sha1-ENy/1Es+ApGhMfyatKslqfWnikI=", 493 | "dev": true, 494 | "requires": { 495 | "send": "0.1.4" 496 | }, 497 | "dependencies": { 498 | "fresh": { 499 | "version": "0.2.0", 500 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.2.0.tgz", 501 | "integrity": "sha1-v9lALPPfEsSkwxDHn5mj3eE9NKc=", 502 | "dev": true 503 | }, 504 | "range-parser": { 505 | "version": "0.0.4", 506 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-0.0.4.tgz", 507 | "integrity": "sha1-wEJ//vUcEKy6B4KkbJYC50T/Ygs=", 508 | "dev": true 509 | }, 510 | "send": { 511 | "version": "0.1.4", 512 | "resolved": "https://registry.npmjs.org/send/-/send-0.1.4.tgz", 513 | "integrity": "sha1-vnDY0b4B3mGCGvE3gLUDRaT3Gr0=", 514 | "dev": true, 515 | "requires": { 516 | "debug": "0.8.1", 517 | "fresh": "0.2.0", 518 | "mime": "1.2.11", 519 | "range-parser": "0.0.4" 520 | } 521 | } 522 | } 523 | }, 524 | "shelljs": { 525 | "version": "0.3.0", 526 | "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", 527 | "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=", 528 | "dev": true 529 | }, 530 | "sigmund": { 531 | "version": "1.0.1", 532 | "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", 533 | "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", 534 | "dev": true 535 | }, 536 | "sinon": { 537 | "version": "1.9.1", 538 | "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.9.1.tgz", 539 | "integrity": "sha1-DaxiK9Pw5vlmKnQxuvZfWMNFnWk=", 540 | "dev": true, 541 | "requires": { 542 | "formatio": "1.0.2", 543 | "util": "0.10.3" 544 | } 545 | }, 546 | "sinon-chai": { 547 | "version": "2.5.0", 548 | "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-2.5.0.tgz", 549 | "integrity": "sha1-VijmhQtwPoQS6w2UpcHFvHkjYBg=", 550 | "dev": true 551 | }, 552 | "string_decoder": { 553 | "version": "0.10.31", 554 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 555 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", 556 | "dev": true 557 | }, 558 | "strip-json-comments": { 559 | "version": "1.0.4", 560 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", 561 | "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", 562 | "dev": true 563 | }, 564 | "superagent": { 565 | "version": "0.17.0", 566 | "resolved": "https://registry.npmjs.org/superagent/-/superagent-0.17.0.tgz", 567 | "integrity": "sha1-qtzVD75ak+cZkRGNeb8HFNYlu6g=", 568 | "dev": true, 569 | "requires": { 570 | "cookiejar": "1.3.0", 571 | "debug": "0.7.4", 572 | "emitter-component": "1.0.0", 573 | "extend": "1.2.1", 574 | "formidable": "1.0.14", 575 | "methods": "0.0.1", 576 | "mime": "1.2.5", 577 | "qs": "0.6.5", 578 | "reduce-component": "1.0.1" 579 | }, 580 | "dependencies": { 581 | "debug": { 582 | "version": "0.7.4", 583 | "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", 584 | "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=", 585 | "dev": true 586 | }, 587 | "methods": { 588 | "version": "0.0.1", 589 | "resolved": "https://registry.npmjs.org/methods/-/methods-0.0.1.tgz", 590 | "integrity": "sha1-J3yQ+L7zlwlkWoNxxRw7bGSOBow=", 591 | "dev": true 592 | }, 593 | "mime": { 594 | "version": "1.2.5", 595 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.5.tgz", 596 | "integrity": "sha1-nu0HMCKov14WyFZsaGe4gyv7+hM=", 597 | "dev": true 598 | }, 599 | "qs": { 600 | "version": "0.6.5", 601 | "resolved": "https://registry.npmjs.org/qs/-/qs-0.6.5.tgz", 602 | "integrity": "sha1-KUsmjksNQlD23eGbO4s0k13/FO8=", 603 | "dev": true 604 | } 605 | } 606 | }, 607 | "supertest": { 608 | "version": "0.10.0", 609 | "resolved": "https://registry.npmjs.org/supertest/-/supertest-0.10.0.tgz", 610 | "integrity": "sha1-W6ghtfTp5kMpL8+HJo39Joi9u1g=", 611 | "dev": true, 612 | "requires": { 613 | "methods": "0.1.0", 614 | "superagent": "0.17.0" 615 | } 616 | }, 617 | "type-detect": { 618 | "version": "0.1.1", 619 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", 620 | "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", 621 | "dev": true 622 | }, 623 | "type-is": { 624 | "version": "1.0.0", 625 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.0.0.tgz", 626 | "integrity": "sha1-T/Qk6XNJoe4ZELS/xIhZXs3EQ/w=", 627 | "dev": true, 628 | "requires": { 629 | "mime": "1.2.11" 630 | } 631 | }, 632 | "underscore": { 633 | "version": "1.6.0", 634 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", 635 | "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", 636 | "dev": true 637 | }, 638 | "util": { 639 | "version": "0.10.3", 640 | "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", 641 | "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", 642 | "dev": true, 643 | "requires": { 644 | "inherits": "2.0.1" 645 | }, 646 | "dependencies": { 647 | "inherits": { 648 | "version": "2.0.1", 649 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", 650 | "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", 651 | "dev": true 652 | } 653 | } 654 | }, 655 | "utils-merge": { 656 | "version": "1.0.0", 657 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", 658 | "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=", 659 | "dev": true 660 | }, 661 | "valentine": { 662 | "version": "2.0.2", 663 | "resolved": "https://registry.npmjs.org/valentine/-/valentine-2.0.2.tgz", 664 | "integrity": "sha1-zT5dfpfMYMSwJ0OXj4pAk6mt4KA=", 665 | "dev": true 666 | } 667 | } 668 | } 669 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-limiter", 3 | "version": "1.6.1", 4 | "description": "rate limiter middleware for express applications", 5 | "main": "index.js", 6 | "author": "Dustin Diaz", 7 | "license": "MIT", 8 | "scripts": { 9 | "test": "make test" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/ded/express-limiter.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/ded/express-limiter/issues" 17 | }, 18 | "homepage": "https://github.com/ded/express-limiter", 19 | "devDependencies": { 20 | "express": "4.0.0", 21 | "redis": "~0.10.1", 22 | "mocha": "~1.18.2", 23 | "chai": "~1.9.1", 24 | "sinon": "~1.9.0", 25 | "sinon-chai": "~2.5.0", 26 | "supertest": "~0.10.0", 27 | "valentine": "~2.0.2", 28 | "jshint": "~2.5.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai') 2 | , request = require('supertest') 3 | , sinon = require('sinon') 4 | , redis = require('redis').createClient() 5 | , v = require('valentine') 6 | , subject = require('../') 7 | 8 | chai.use(require('sinon-chai')) 9 | 10 | describe('rate-limiter', function () { 11 | var express, app, limiter 12 | 13 | beforeEach(function () { 14 | express = require('express') 15 | app = express() 16 | limiter = subject(app, redis) 17 | }) 18 | 19 | afterEach(function (done) { 20 | redis.flushdb(done) 21 | }) 22 | 23 | it('should work', function (done) { 24 | var map = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] 25 | var clock = sinon.useFakeTimers() 26 | 27 | limiter({ 28 | path: '/route', 29 | method: 'get', 30 | lookup: ['connection.remoteAddress'], 31 | total: 10, 32 | expire: 1000 * 60 * 60 33 | }) 34 | 35 | app.get('/route', function (req, res) { 36 | res.send(200, 'hello') 37 | }) 38 | 39 | var out = (map).map(function (item) { 40 | return function (f) { 41 | process.nextTick(function () { 42 | request(app) 43 | .get('/route') 44 | .expect('X-RateLimit-Limit', 10) 45 | .expect('X-RateLimit-Remaining', item - 1) 46 | .expect('X-RateLimit-Reset', 3600) 47 | .expect(200, function (e) {f(e)}) 48 | }) 49 | } 50 | }) 51 | out.push(function (f) { 52 | request(app) 53 | .get('/route') 54 | .expect('X-RateLimit-Limit', 10) 55 | .expect('X-RateLimit-Remaining', 0) 56 | .expect('X-RateLimit-Reset', 3600) 57 | .expect('Retry-After', /\d+/) 58 | .expect(429, function (e) {f(e)}) 59 | }) 60 | out.push(function (f) { 61 | // expire the time 62 | clock.tick(1000 * 60 * 60 + 1) 63 | request(app) 64 | .get('/route') 65 | .expect('X-RateLimit-Limit', 10) 66 | .expect('X-RateLimit-Remaining', 9) 67 | .expect('X-RateLimit-Reset', 7201) 68 | .expect(200, function (e) { 69 | clock.restore() 70 | f(e) 71 | }) 72 | }) 73 | v.waterfall(out, done) 74 | }) 75 | 76 | context('options', function() { 77 | it('should process options.skipHeaders', function (done) { 78 | limiter({ 79 | path: '/route', 80 | method: 'get', 81 | lookup: ['connection.remoteAddress'], 82 | total: 0, 83 | expire: 1000 * 60 * 60, 84 | skipHeaders: true 85 | }) 86 | 87 | app.get('/route', function (req, res) { 88 | res.send(200, 'hello') 89 | }) 90 | 91 | request(app) 92 | .get('/route') 93 | .expect(function(res) { 94 | if ('X-RateLimit-Limit' in res.headers) return 'X-RateLimit-Limit Header not to be set' 95 | }) 96 | .expect(function(res) { 97 | if ('X-RateLimit-Remaining' in res.headers) return 'X-RateLimit-Remaining Header not to be set' 98 | }) 99 | .expect(function(res) { 100 | if ('Retry-After' in res.headers) return 'Retry-After not to be set' 101 | }) 102 | .expect(429, done) 103 | }) 104 | 105 | it('should process ignoreErrors', function (done) { 106 | limiter({ 107 | path: '/route', 108 | method: 'get', 109 | lookup: ['connection.remoteAddress'], 110 | total: 10, 111 | expire: 1000 * 60 * 60, 112 | ignoreErrors: true 113 | }) 114 | 115 | app.get('/route', function (req, res) { 116 | res.send(200, 'hello') 117 | }) 118 | 119 | var stub = sinon.stub(redis, 'get', function(key, callback) { 120 | callback({err: true}) 121 | }) 122 | 123 | request(app) 124 | .get('/route') 125 | .expect(200, function (e) { 126 | done(e) 127 | stub.restore() 128 | }) 129 | }) 130 | 131 | it('should process lookup as a function', function (done) { 132 | limiter({ 133 | path: '*', 134 | method: 'all', 135 | lookup: function (req, res, opts, next) { 136 | opts.lookup = 'query.api_key'; 137 | opts.total = 20 138 | return next() 139 | }, 140 | total: 3, 141 | expire: 1000 * 60 * 60 142 | }) 143 | 144 | app.get('/route', function (req, res) { 145 | res.send(200, 'hello') 146 | }) 147 | 148 | request(app) 149 | .get('/route?api_key=foobar') 150 | .expect('X-RateLimit-Limit', 20) 151 | .expect('X-RateLimit-Remaining', 19) 152 | .expect(200, function (e) { 153 | done(e) 154 | }) 155 | }) 156 | }) 157 | 158 | context('direct middleware', function () { 159 | 160 | it('is able to mount without `path` and `method`', function (done) { 161 | var clock = sinon.useFakeTimers() 162 | var middleware = limiter({ 163 | lookup: 'connection.remoteAddress', 164 | total: 3, 165 | expire: 1000 * 60 * 60 166 | }) 167 | app.get('/direct', middleware, function (req, res, next) { 168 | res.send(200, 'is direct') 169 | }) 170 | v.waterfall( 171 | function (f) { 172 | process.nextTick(function () { 173 | request(app) 174 | .get('/direct') 175 | .expect('X-RateLimit-Limit', 3) 176 | .expect('X-RateLimit-Remaining', 2) 177 | .expect(200, function (e) {f(e)}) 178 | }) 179 | }, 180 | function (f) { 181 | process.nextTick(function () { 182 | request(app) 183 | .get('/direct') 184 | .expect('X-RateLimit-Limit', 3) 185 | .expect('X-RateLimit-Remaining', 1) 186 | .expect(200, function (e) {f(e)}) 187 | }) 188 | }, 189 | function (f) { 190 | process.nextTick(function () { 191 | request(app) 192 | .get('/direct') 193 | .expect('X-RateLimit-Limit', 3) 194 | .expect('X-RateLimit-Remaining', 0) 195 | .expect('Retry-After', /\d+/) 196 | .expect(429, function (e) { f(null) }) 197 | }) 198 | }, 199 | function (e) { 200 | done(e) 201 | } 202 | ) 203 | }) 204 | }) 205 | }) 206 | --------------------------------------------------------------------------------