From db96d22d02b2aeca5dbfb3e6649c812a15e4668a Mon Sep 17 00:00:00 2001 From: Anton Podviaznikov Date: Tue, 23 Sep 2014 20:47:33 -0700 Subject: [PATCH 1/2] rate-limit initial implementation --- README.md | 2 +- app.json | 2 +- package.json | 2 ++ server/config.coffee | 4 ++++ server/redis.coffee | 11 +++++++++++ server/routes/admin.coffee | 3 +++ server/routes/api/index.coffee | 34 ++++++++++++++++++++++++++++++++-- 7 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 server/redis.coffee diff --git a/README.md b/README.md index 016b4d7..29c46fe 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ If you plan on contributing to Buckets' development, you can install Buckets dir ### Pre-requirements -[MongoDB](http://www.mongodb.org), [Node.js](http://nodejs.org) and the following global: +[MongoDB](http://www.mongodb.org), [Node.js](http://nodejs.org), [Redis](http://redis.io) and the following global: ``` npm install -g grunt-cli diff --git a/app.json b/app.json index 8018ea7..7d86657 100644 --- a/app.json +++ b/app.json @@ -15,5 +15,5 @@ "BUILDPACK_URL": "https://github.com/mbuchetics/heroku-buildpack-nodejs-grunt.git", "NODE_ENV": "production" }, - "addons": ["mongohq", "mandrill"] + "addons": ["mongohq", "mandrill", "redistogo"] } diff --git a/package.json b/package.json index fcf1193..b1b1612 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,8 @@ "passport": "^0.2.1", "passport-local": "^1.0.0", "path-to-regexp": "^1.0.0", + "ratelimiter": "^1.0.3", + "redis": "^0.12.1", "response-time": "^2.0.1", "serve-favicon": "^2.1.1", "serve-static": "^1.4.3", diff --git a/server/config.coffee b/server/config.coffee index 89a38e3..3eae8bf 100644 --- a/server/config.coffee +++ b/server/config.coffee @@ -15,6 +15,9 @@ config = autoStart: yes cloudinary: process.env.CLOUDINARY_URL db: "mongodb://localhost/buckets_#{env}" + apiHourlyLimit: 100 + redisURL: "redis://127.0.0.1:6379" + loginFailLimit: 10 production: smtp: service: 'Mandrill' @@ -22,6 +25,7 @@ config = user: process.env.MANDRILL_USERNAME pass: process.env.MANDRILL_APIKEY db: process.env.MONGOHQ_URL + redisURL: process.env.REDISTOGO_URL fastly: api_key: process.env.FASTLY_API_KEY cdn_url: process.env.FASTLY_CDN_URL diff --git a/server/redis.coffee b/server/redis.coffee new file mode 100644 index 0000000..d67b8ed --- /dev/null +++ b/server/redis.coffee @@ -0,0 +1,11 @@ +url = require 'url' +redis = require 'redis' +config = require './config' + +redisURL = url.parse config.redisURL +redisDb = redis.createClient(redisURL.port, redisURL.hostname) + +if redisURL.auth + redisDb.auth redisURL.auth.split(':')[1] + +module.exports = redisDb diff --git a/server/routes/admin.coffee b/server/routes/admin.coffee index 3f4d948..fddfab2 100644 --- a/server/routes/admin.coffee +++ b/server/routes/admin.coffee @@ -15,6 +15,8 @@ passport = require '../lib/auth' pkg = require '../../package' User = require '../models/user' + + module.exports = app = express() {adminSegment} = config @@ -40,6 +42,7 @@ app.post '/login', passport.authenticate('local', failureRedirect: "/#{adminSegm "/#{adminSegment}/" app.post '/checkLogin', (req, res) -> + passport.authenticate('local', (err, user, authErr) -> if authErr res.status(401).send errors: [authErr] diff --git a/server/routes/api/index.coffee b/server/routes/api/index.coffee index 2c9e100..9300e54 100644 --- a/server/routes/api/index.coffee +++ b/server/routes/api/index.coffee @@ -1,6 +1,36 @@ -express = require 'express' +express = require 'express' -module.exports = app = express() +Limiter = require 'ratelimiter' +config = require '../../config' +redisDb = require '../../redis' + + + +# use router instead of just express app. +module.exports = app = express.Router() + +app.use (req, res, next) -> + if req.user? + limit = new Limiter({ id: req.user._id, db: redisDb, max: config.apiHourlyLimit }) + limit.get (err, limit) -> + if err? + return next err + + res.set 'X-RateLimit-Limit', limit.total + res.set 'X-RateLimit-Remaining', limit.remaining + res.set 'X-RateLimit-Reset', limit.reset + + # all good + if limit.remaining + return next() + + # not good + delta = (limit.reset * 1000) - Date.now() | 0 + after = limit.reset - (Date.now() / 1000) | 0 + res.set 'Retry-After', after + res.status(429).send('Rate limit exceeded, retry in ' + delta) + else + next() app.use require './buckets' app.use require './entries' From b8cb1f34994af23c99e13c88fdf25a48aeb4ad61 Mon Sep 17 00:00:00 2001 From: Anton Podviaznikov Date: Tue, 23 Sep 2014 20:57:31 -0700 Subject: [PATCH 2/2] add redis to travis config file --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 83caf4b..7aefa07 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: node_js -services: mongodb +services: + - redis-server + - mongodb before_install: - npm install -g grunt-cli