diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index a1b4aa0..0000000 --- a/.eslintrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": [ - "eslint-config-airbnb-base", - "eslint-config-airbnb-base/rules/strict" - ], - "rules": { - "no-param-reassign": 0, - "object-shorthand": 0, - "strict": 0, - "consistent-return": 0 - } -} diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..ad5b831 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,22 @@ +# Support ES2016 features +parser: babel-eslint + +extends: standard + +plugins: [ + "babel" +] + +rules: + arrow-parens: 0 + babel/arrow-parens: [2, "as-needed"] + comma-dangle: [2, "always-multiline"] + eqeqeq: 0 + no-return-assign: 0 # fails for arrow functions + no-var: 2 + semi: [2, always] + space-before-function-paren: [2, never] + yoda: 0 + arrow-spacing: 2 + dot-location: [2, "property"] + prefer-arrow-callback: 2 diff --git a/index.js b/index.js index 986d93c..30831f9 100644 --- a/index.js +++ b/index.js @@ -7,16 +7,22 @@ const debug = require('debug')('koa-simple-ratelimit'); const ms = require('ms'); function find(db, p) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { db.get(p, (err, reply) => { + if (err) { + reject(err); + } resolve(reply); }); }); } function pttl(db, p) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { db.pttl(p, (err, reply) => { + if (err) { + reject(err); + } resolve(reply); }); }); @@ -46,7 +52,7 @@ function ratelimit(opts) { opts.headers.reset = opts.headers.reset || 'X-RateLimit-Reset'; opts.headers.total = opts.headers.total || 'X-RateLimit-Limit'; - return function ratelimiter(ctx, next) { + return async function ratelimiter(ctx, next) { const id = opts.id ? opts.id(ctx) : ctx.ip; if (id === false) { @@ -63,53 +69,50 @@ function ratelimit(opts) { } const name = `limit:${id}:count`; - return find(opts.db, name).then((cur) => { - const n = Math.floor(cur); - let t = Date.now(); - t += opts.duration || 3600000; - t = new Date(t).getTime() / 1000 || 0; + const cur = await find(opts.db, name); + const n = Math.floor(cur); + let t = Date.now(); + t += opts.duration || 3600000; + t = new Date(t).getTime() / 1000 || 0; - const headers = {}; - headers[opts.headers.remaining] = opts.max - 1; - headers[opts.headers.reset] = t; - headers[opts.headers.total] = opts.max; - ctx.set(headers); + const headers = {}; + headers[opts.headers.remaining] = opts.max - 1; + headers[opts.headers.reset] = t; + headers[opts.headers.total] = opts.max; + ctx.set(headers); - // not existing in redis - if (cur === null) { - debug('remaining %s/%s %s', opts.max - 1, opts.max, id); - opts.db.set(name, opts.max - 1, 'PX', opts.duration || 3600000, 'NX'); - return next(); - } + // not existing in redis + if (cur === null) { + debug('remaining %s/%s %s', opts.max - 1, opts.max, id); + opts.db.set(name, opts.max - 1, 'PX', opts.duration || 3600000, 'NX'); + return next(); + } - return pttl(opts.db, name).then((expires) => { - if (n - 1 >= 0) { - // existing in redis - opts.db.decr(name); - debug('remaining %s/%s %s', n - 1, opts.max, id); - headers[opts.headers.remaining] = n - 1; - ctx.set(headers); - return next(); - } - if (expires < 0) { - debug(`${name} is stuck. Resetting.`); - debug('remaining %s/%s %s', opts.max - 1, opts.max, id); - opts.db.set(name, opts.max - 1, 'PX', opts.duration || 3600000, 'NX'); - return next(); - } - // user maxed - headers['Retry-After'] = t; - headers[opts.headers.remaining] = n; - ctx.set(headers); - ctx.status = 429; - const retryTime = ms(expires, { long: true }); - ctx.body = `Rate limit exceeded, retry in ${retryTime}`; - if (opts.throw) { - ctx.throw(ctx.status, ctx.body, { headers: headers }); - } - return; - }); - }); + const expires = await pttl(opts.db, name); + if (n - 1 >= 0) { + // existing in redis + opts.db.decr(name); + debug('remaining %s/%s %s', n - 1, opts.max, id); + headers[opts.headers.remaining] = n - 1; + ctx.set(headers); + return next(); + } + if (expires < 0) { + debug(`${name} is stuck. Resetting.`); + debug('remaining %s/%s %s', opts.max - 1, opts.max, id); + opts.db.set(name, opts.max - 1, 'PX', opts.duration || 3600000, 'NX'); + return next(); + } + // user maxed + headers['Retry-After'] = t; + headers[opts.headers.remaining] = n; + ctx.set(headers); + ctx.status = 429; + const retryTime = ms(expires, { long: true }); + ctx.body = `Rate limit exceeded, retry in ${retryTime}`; + if (opts.throw) { + ctx.throw(ctx.status, ctx.body, { headers: headers }); + } }; } diff --git a/package.json b/package.json index 70a9dcb..4ccde54 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "koa-simple-ratelimit", "description": "Simple Rate limiter middleware for koa v2", - "version": "2.1.4", + "version": "2.2.0", "scripts": { "test": "NODE_ENV=test node_modules/mocha/bin/mocha --reporter spec", "lint": "eslint index.js test; exit 0", @@ -13,14 +13,18 @@ "ms": "^0.7.2" }, "devDependencies": { + "babel-eslint": "^7.1.1", + "chai": "^3.5.0", "eslint": "^3.17.1", - "eslint-config-airbnb-base": "^11.1.1", + "eslint-config-standard": "^7.0.1", + "eslint-plugin-babel": "^4.1.1", "eslint-plugin-import": "^2.2.0", + "eslint-plugin-promise": "^3.5.0", + "eslint-plugin-standard": "^2.1.1", "koa": "^2.1.0", "mocha": "^3.2.0", "nyc": "^10.1.2", "redis": "^2.6.5", - "should": "^11.2.0", "supertest": "^3.0.0" }, "nyc": { diff --git a/test/.eslintrc b/test/.eslintrc index 3a4690f..cda2f26 100644 --- a/test/.eslintrc +++ b/test/.eslintrc @@ -1,12 +1,5 @@ -{ - "extends": "../.eslintrc", - "env": { - "mocha": true - }, - "rules": { - "prefer-arrow-callback": 0, - "import/no-extraneous-dependencies": ["error", { "devDependencies": true }], - "func-names": 0, - "no-unused-vars": 0 - } -} \ No newline at end of file +env: + mocha: true + +rules: + space-before-blocks: [2, {functions: never, keywords: always}] diff --git a/test/ratelimit.js b/test/ratelimit.js index 79e157a..fb705b7 100644 --- a/test/ratelimit.js +++ b/test/ratelimit.js @@ -1,8 +1,8 @@ 'use strict'; +const expect = require('chai').expect; const Koa = require('koa'); const request = require('supertest'); -const should = require('should'); const redis = require('redis'); const ratelimit = require('..'); @@ -15,6 +15,9 @@ describe('ratelimit middleware', () => { before((done) => { db.keys('limit:*', (err, rows) => { + if (err) { + throw new Error(err); + } rows.forEach(n => db.del(n)); }); @@ -26,7 +29,7 @@ describe('ratelimit middleware', () => { let app; const routeHitOnlyOnce = () => { - guard.should.be.equal(1); + expect(guard).to.eq(1); }; beforeEach((done) => { @@ -79,10 +82,10 @@ describe('ratelimit middleware', () => { let app; const routeHitOnlyOnce = () => { - guard.should.be.equal(1); + expect(guard).to.eq(1); }; const routeHitTwice = () => { - guard.should.be.equal(2); + expect(guard).to.eq(2); }; beforeEach((done) => { @@ -142,7 +145,7 @@ describe('ratelimit middleware', () => { let app; const routeHitOnlyOnce = () => { - guard.should.be.equal(1); + expect(guard).to.eq(1); }; beforeEach((done) => { @@ -169,6 +172,7 @@ describe('ratelimit middleware', () => { request(app.listen()) .get('/') .expect('X-RateLimit-Remaining', '0') + .expect(routeHitOnlyOnce) .expect(200) .end(done); }); @@ -179,7 +183,7 @@ describe('ratelimit middleware', () => { let app; const routeHitOnlyOnce = () => { - guard.should.be.equal(1); + expect(guard).to.eq(1); }; beforeEach((done) => { @@ -239,12 +243,12 @@ describe('ratelimit middleware', () => { .get('/') .set('foo', 'bar') .expect((res) => { - res.header['x-ratelimit-remaining'].should.equal('0'); + expect(res.header['x-ratelimit-remaining']).to.eq('0'); }) .end(done); }); - it('should not limit if `id` returns `false`', (done) => { + it('should not limit if `id` returns `false`', () => { const app = new Koa(); app.use(ratelimit({ @@ -254,12 +258,9 @@ describe('ratelimit middleware', () => { max: 5, })); - request(app.listen()) + return request(app.listen()) .get('/') - .expect((res) => { - res.header.should.not.have.property('x-ratelimit-remaining'); - }) - .end(done); + .expect((res) => expect(res.header['x-ratelimit-remaining']).to.not.exist); }); it('should limit using the `id` value', (done) => { diff --git a/yarn.lock b/yarn.lock index ebf4773..90de9af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -106,6 +106,10 @@ arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" +assertion-error@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c" + async@^1.4.0, async@^1.4.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -122,6 +126,16 @@ babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: esutils "^2.0.2" js-tokens "^3.0.0" +babel-eslint@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.1.1.tgz#8a6a884f085aa7060af69cfc77341c2f99370fb2" + dependencies: + babel-code-frame "^6.16.0" + babel-traverse "^6.15.0" + babel-types "^6.15.0" + babylon "^6.13.0" + lodash.pickby "^4.6.0" + babel-generator@^6.18.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.23.0.tgz#6b8edab956ef3116f79d8c84c5a3c05f32a74bc5" @@ -158,7 +172,7 @@ babel-template@^6.16.0: babylon "^6.11.0" lodash "^4.2.0" -babel-traverse@^6.18.0, babel-traverse@^6.23.0: +babel-traverse@^6.15.0, babel-traverse@^6.18.0, babel-traverse@^6.23.0: version "6.23.1" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.23.1.tgz#d3cb59010ecd06a97d81310065f966b699e14f48" dependencies: @@ -172,7 +186,7 @@ babel-traverse@^6.18.0, babel-traverse@^6.23.0: invariant "^2.2.0" lodash "^4.2.0" -babel-types@^6.18.0, babel-types@^6.23.0: +babel-types@^6.15.0, babel-types@^6.18.0, babel-types@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.23.0.tgz#bb17179d7538bad38cd0c9e115d340f77e7e9acf" dependencies: @@ -249,6 +263,14 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" +chai@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247" + dependencies: + assertion-error "^1.0.1" + deep-eql "^0.1.3" + type-detect "^1.0.0" + chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -397,6 +419,12 @@ decamelize@^1.0.0, decamelize@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +deep-eql@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2" + dependencies: + type-detect "0.1.1" + deep-equal@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -543,9 +571,9 @@ escope@^3.6.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-config-airbnb-base@^11.1.1: - version "11.1.1" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-11.1.1.tgz#61e9e89e4eb89f474f6913ac817be9fbb59063e0" +eslint-config-standard@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-7.0.1.tgz#6cec96084de9ac862c33ccb953d13a7c59872342" eslint-import-resolver-node@^0.2.0: version "0.2.3" @@ -562,6 +590,10 @@ eslint-module-utils@^2.0.0: debug "2.2.0" pkg-dir "^1.0.0" +eslint-plugin-babel@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-babel/-/eslint-plugin-babel-4.1.1.tgz#ef285c87039b67beb3bbd227f5b0eed4fb376b87" + eslint-plugin-import@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.2.0.tgz#72ba306fad305d67c4816348a4699a4229ac8b4e" @@ -577,6 +609,14 @@ eslint-plugin-import@^2.2.0: minimatch "^3.0.3" pkg-up "^1.0.0" +eslint-plugin-promise@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.5.0.tgz#78fbb6ffe047201627569e85a6c5373af2a68fca" + +eslint-plugin-standard@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-2.1.1.tgz#97960b1537e1718bb633877d0a650050effff3b0" + eslint@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.17.1.tgz#b80ae12d9c406d858406fccda627afce33ea10ea" @@ -1300,6 +1340,10 @@ lodash.keys@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" +lodash.pickby@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.pickby/-/lodash.pickby-4.6.0.tgz#7dea21d8c18d7703a27c704c15d3b84a67e33aff" + lodash@^4.0.0, lodash@^4.2.0, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -1803,44 +1847,6 @@ shelljs@^0.7.5: interpret "^1.0.0" rechoir "^0.6.2" -should-equal@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-1.0.1.tgz#0b6e9516f2601a9fb0bb2dcc369afa1c7e200af7" - dependencies: - should-type "^1.0.0" - -should-format@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/should-format/-/should-format-3.0.3.tgz#9bfc8f74fa39205c53d38c34d717303e277124f1" - dependencies: - should-type "^1.3.0" - should-type-adaptors "^1.0.1" - -should-type-adaptors@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/should-type-adaptors/-/should-type-adaptors-1.0.1.tgz#efe5553cdf68cff66e5c5f51b712dc351c77beaa" - dependencies: - should-type "^1.3.0" - should-util "^1.0.0" - -should-type@^1.0.0, should-type@^1.3.0, should-type@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/should-type/-/should-type-1.4.0.tgz#0756d8ce846dfd09843a6947719dfa0d4cff5cf3" - -should-util@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/should-util/-/should-util-1.0.0.tgz#c98cda374aa6b190df8ba87c9889c2b4db620063" - -should@^11.2.0: - version "11.2.0" - resolved "https://registry.yarnpkg.com/should/-/should-11.2.0.tgz#7afca3182c234781d786d2278a87805b5ecf0409" - dependencies: - should-equal "^1.0.0" - should-format "^3.0.2" - should-type "^1.4.0" - should-type-adaptors "^1.0.1" - should-util "^1.0.0" - signal-exit@^2.0.0: version "2.1.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-2.1.2.tgz#375879b1f92ebc3b334480d038dc546a6d558564" @@ -2018,6 +2024,14 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" +type-detect@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822" + +type-detect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2" + type-is@^1.5.5: version "1.6.14" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2"