redis_knock 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.sw*
2
+ *.gem
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg/*
6
+ vendor/bundle/*
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,80 @@
1
+ Ruby HTTP Throttle Control Engine using Redis
2
+ =============================================
3
+
4
+ This gem implements a simple HTTP throttle control using [Redis][] as database engine to store rate-limiting IP's.
5
+
6
+ Why not rack-throttle?
7
+ ------------------------
8
+
9
+ The gem [rack-throttle][] works with the same principle and does very well the job, but stores IP's and never clears the database.
10
+ The main goal of Redis Knock is the use of Redis **[expire][]** command, that removes the key after X seconds.
11
+
12
+ Usage
13
+ -----
14
+
15
+ ### Using in your Rails Controller with before_filter
16
+
17
+ # app/controllers/simple_controller.rb
18
+
19
+ require 'redis_knock'
20
+
21
+ class SimpleController < ApplicationController
22
+ before_filter :check_throttle
23
+
24
+ private
25
+ def check_throttle
26
+ control = RedisKnock::Control.new limit: 1000, interval: 1.hour, redis: { host: 'localhost', port: 6379, db: 1 }
27
+
28
+ render(text: 'Rate limit exceeded', status: :forbidden) and return unless control.allowed?(request)
29
+ end
30
+ end
31
+
32
+ ### Using as a Rack Middleware in your Rails app
33
+
34
+ # config/application.rb
35
+
36
+ class Application < Rails::Application
37
+ config.middleware.use RedisKnock::Middleware, limit: 1000, interval: 1.hour, redis: { host: 'localhost', port: 6379, db: 1 }
38
+ end
39
+
40
+ ### Using in your Sinatra app
41
+
42
+ # a_sinatra_application.rb
43
+
44
+ require 'sinatra'
45
+ require 'redis_knock'
46
+
47
+ use RedisKnock::Middleware, limit: 1000, interval: 3600, redis: { host: 'localhost', port: 6379, db: 1 }
48
+
49
+ get '/' do
50
+ 'Hello World!'
51
+ end
52
+
53
+ Dependencies
54
+ ------------
55
+
56
+ * redis gem
57
+ * Redis server version >= 2.1
58
+
59
+ Installation
60
+ ------------
61
+
62
+ ### With rubygems:
63
+
64
+ $ [sudo] gem install redis_knock
65
+
66
+ Authors
67
+ -------
68
+
69
+ * Marcelo Correia Pinheiro - <http://salizzar.net/>
70
+
71
+ License
72
+ -------
73
+
74
+ RedisKnock is free and unencumbered pubic domain software. For more
75
+ information, see <http://unlicense.org/> or the accompanying UNLICENSE file.
76
+
77
+ [rack-throttle]: https://raw.github.com/datagraph/rack-throttle
78
+ [Redis]: http://redis.io/
79
+ [expire]: http://redis.io/commands/expire
80
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/UNLICENSE ADDED
@@ -0,0 +1,25 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <http://unlicense.org/>
25
+
@@ -0,0 +1,4 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'redis_knock'
4
+
@@ -0,0 +1,7 @@
1
+ # encoding: UTF-8
2
+
3
+ module RedisKnock
4
+ class ConnectionError < Exception
5
+ end
6
+ end
7
+
@@ -0,0 +1,48 @@
1
+ # encoding: UTF-8
2
+
3
+ module RedisKnock
4
+ class Control
5
+ def initialize(options)
6
+ check! options
7
+
8
+ @client = get_connection options[:redis]
9
+ @limit = options[:limit]
10
+ @interval = options[:interval]
11
+ end
12
+
13
+ def allowed?(request)
14
+ cache_key = get_cache_key request
15
+
16
+ count = @client.incr cache_key
17
+ @client.expire(cache_key, @interval) if count == 1
18
+
19
+ count <= @limit
20
+ end
21
+
22
+ private
23
+
24
+ def check!(options)
25
+ raise ArgumentError.new('A limit must be supplied') if options[:limit].nil?
26
+ raise ArgumentError.new('A interval must be supplied') if options[:interval].nil?
27
+
28
+ redis_opts = options[:redis]
29
+ raise ArgumentError.new('Redis connection params must be supplied') if redis_opts.nil? || redis_opts.empty?
30
+ raise ArgumentError.new('Redis host must be supplied') if redis_opts[:host].nil?
31
+ raise ArgumentError.new('Redis port must be supplied') if redis_opts[:port].nil?
32
+ raise ArgumentError.new('Redis database must be supplied') if redis_opts[:db].nil?
33
+ end
34
+
35
+ def get_connection(options)
36
+ begin
37
+ Redis.new options
38
+ rescue Exception => e
39
+ raise ConnectionError.new("Cannot connect to Redis server: #{e.message}")
40
+ end
41
+ end
42
+
43
+ def get_cache_key(request)
44
+ request.ip.to_s
45
+ end
46
+ end
47
+ end
48
+
@@ -0,0 +1,30 @@
1
+ # encoding: UTF-8
2
+
3
+ module RedisKnock
4
+ class Middleware
5
+ attr_reader :app, :options
6
+
7
+ def initialize(app, options = {})
8
+ @app, @options = app, options
9
+ end
10
+
11
+ def call(env)
12
+ request = Rack::Request.new env
13
+ control = RedisKnock::Control.new options
14
+ control.allowed?(request) ? app.call(env) : limit_exceeded
15
+ end
16
+
17
+ private
18
+
19
+ def limit_exceeded
20
+ message = 'Rate limit exceeded'
21
+
22
+ headers = {}
23
+ headers['Content-Type'] = 'text/plain; charset=utf-8'
24
+ headers['Content-Length'] = message.length.to_s
25
+
26
+ [ 403, headers, [ message ] ]
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,5 @@
1
+ # encoding: UTF-8
2
+
3
+ module RedisKnock
4
+ VERSION = '0.0.1'
5
+ end
@@ -0,0 +1,12 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'redis'
4
+
5
+ module RedisKnock
6
+ autoload :ConnectionFactory, 'redis_knock/connection_factory'
7
+ autoload :Control, 'redis_knock/control'
8
+ autoload :Middleware, 'redis_knock/middleware'
9
+ autoload :ConnectionError, 'redis_knock/connection_error'
10
+ autoload :Version, 'redis_knock/version'
11
+ end
12
+
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "redis_knock/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "redis_knock"
7
+ s.version = RedisKnock::VERSION
8
+ s.authors = ["Marcelo Correia Pinheiro"]
9
+ s.email = ["salizzar@gmail.com"]
10
+ s.homepage = "https://github.com/salizzar/redis_knock"
11
+ s.summary = %q{A Ruby HTTP Throttle Control}
12
+ s.description = %q{The gem redis_knock implements a HTTP Throttle Control engine using Redis to store rate-limiting IP's.}
13
+
14
+ s.rubyforge_project = "redis_knock"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency 'rake'
22
+ s.add_development_dependency 'rspec'
23
+ s.add_development_dependency 'rack-test'
24
+ s.add_runtime_dependency 'redis'
25
+ end
@@ -0,0 +1,103 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe RedisKnock::Control do
6
+ let(:subject) { RedisKnock::Control }
7
+ let(:host) { 'localhost' }
8
+ let(:port) { 6379 }
9
+ let(:db) { 1 }
10
+ let(:interval) { 3600 }
11
+ let(:limit) { 1000 }
12
+ let(:ip) { '127.0.0.1' }
13
+ let(:request) { mock(:ip => ip) }
14
+ let(:redis_client) { mock 'An Redis connection' }
15
+ let(:redis_options) { { :host => host, :port => port, :db => db } }
16
+ let(:options) { { :limit => limit, :interval => interval, :redis => redis_options } }
17
+
18
+ context 'invalid arguments validation' do
19
+ it 'raises ArgumentError if limit is not informed' do
20
+ options.delete :limit
21
+
22
+ error = ArgumentError.new 'A limit must be supplied'
23
+ expect { subject.new options }.to raise_error(error.class, error.message)
24
+ end
25
+
26
+ it 'raises ArgumentErroro if interval is not informed' do
27
+ options.delete :interval
28
+
29
+ error = ArgumentError.new 'A interval must be supplied'
30
+ expect { subject.new options }.to raise_error(error.class, error.message)
31
+ end
32
+
33
+ context 'Redis connection params' do
34
+ it 'raises ArgumentError if redis connection params are not informed' do
35
+ options.delete :redis
36
+
37
+ error = ArgumentError.new 'Redis connection params must be supplied'
38
+ expect { subject.new options }.to raise_error(error.class, error.message)
39
+ end
40
+
41
+ it 'raises ArgumentError if host is not informed' do
42
+ redis_options.delete :host
43
+
44
+ error = ArgumentError.new 'Redis host must be supplied'
45
+ expect { subject.new options }.to raise_error(error.class, error.message)
46
+ end
47
+
48
+ it 'raises ArgumentError if port is not informed' do
49
+ redis_options.delete :port
50
+
51
+ error = ArgumentError.new 'Redis port must be supplied'
52
+ expect { subject.new options }.to raise_error(error.class, error.message)
53
+ end
54
+
55
+ it 'raises ArgumentError if database is not informed' do
56
+ redis_options.delete :db
57
+
58
+ error = ArgumentError.new 'Redis database must be supplied'
59
+ expect { subject.new options }.to raise_error(error.class, error.message)
60
+ end
61
+ end
62
+ end
63
+
64
+ context 'when cannot connect to Redis server' do
65
+ it 'raises ConnectionError' do
66
+ Redis.should_receive(:new).and_raise('An error')
67
+
68
+ error = RedisKnock::ConnectionError.new 'Cannot connect to Redis server: An error'
69
+ expect { subject.new options }.to raise_error(error.class, error.message)
70
+ end
71
+ end
72
+
73
+ context 'checking for allowed requests' do
74
+ before :each do
75
+ Redis.should_receive(:new).with(redis_options).and_return(redis_client)
76
+ end
77
+
78
+ it 'expires key if is first request' do
79
+ redis_client.should_receive(:incr).and_return(1)
80
+ redis_client.should_receive(:expire).with(ip, interval)
81
+
82
+ control = subject.new options
83
+ control.should be_allowed(request)
84
+ end
85
+
86
+ it 'returns true when limit is not reached' do
87
+ redis_client.should_not_receive(:expire)
88
+ redis_client.should_receive(:incr).and_return(limit - 1)
89
+
90
+ control = subject.new options
91
+ control.should be_allowed(request)
92
+ end
93
+
94
+ it 'returns false when limit is reached' do
95
+ redis_client.should_not_receive(:expire)
96
+ redis_client.should_receive(:incr).and_return(limit + 1)
97
+
98
+ control = subject.new options
99
+ control.should_not be_allowed(request)
100
+ end
101
+ end
102
+ end
103
+
@@ -0,0 +1,47 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe RedisKnock::Middleware do
6
+ let(:redis_options) { { :host => 'localhost', :port => 6379, :db => 1 } }
7
+ let(:options) { { :limit => 1000, :interval => 3600, :redis => redis_options } }
8
+ let(:target_app) { get_app }
9
+ let(:app) { RedisKnock::Middleware.new target_app, options }
10
+ let(:control) { mock 'A Redis Throttle Control' }
11
+
12
+ context 'performing throttle check' do
13
+ before :each do
14
+ RedisKnock::Control.should_receive(:new).and_return(control)
15
+ end
16
+
17
+ context 'with a allowed request' do
18
+ it 'process if limit is not reached' do
19
+ target_app.should_receive(:call).and_return([ 200, {}, [ 'An response body' ] ])
20
+ control.should_receive(:allowed?).and_return(true)
21
+
22
+ get '/'
23
+ last_response.status.should == 200
24
+ last_response.body.should == 'An response body'
25
+ end
26
+ end
27
+
28
+ context 'with a not allowed request' do
29
+ before :each do
30
+ control.should_receive(:allowed?).and_return(false)
31
+ end
32
+
33
+ it 'returns HTTP 403 Forbidden if limit was reached' do
34
+ get '/'
35
+ last_response.status.should == 403
36
+ last_response.body.should == 'Rate limit exceeded'
37
+ end
38
+
39
+ it 'returns content-type and retry-after HTTP headers when is not allowed' do
40
+ get '/'
41
+ last_response.headers['Content-Type'].should == 'text/plain; charset=utf-8'
42
+ last_response.headers['Content-Length'].should == 'Rate limit exceeded'.length.to_s
43
+ end
44
+ end
45
+ end
46
+ end
47
+
@@ -0,0 +1,14 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'bundler'
4
+ require 'rspec'
5
+ require 'rack/test'
6
+ require 'redis_knock'
7
+
8
+ RSpec.configure do |config|
9
+ config.include Rack::Test::Methods
10
+
11
+ def get_app
12
+ @app ||= mock 'An app'
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis_knock
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Marcelo Correia Pinheiro
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-01-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: &70260527478660 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70260527478660
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &70260527403560 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70260527403560
36
+ - !ruby/object:Gem::Dependency
37
+ name: rack-test
38
+ requirement: &70260527402640 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70260527402640
47
+ - !ruby/object:Gem::Dependency
48
+ name: redis
49
+ requirement: &70260527401620 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70260527401620
58
+ description: The gem redis_knock implements a HTTP Throttle Control engine using Redis
59
+ to store rate-limiting IP's.
60
+ email:
61
+ - salizzar@gmail.com
62
+ executables: []
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - .gitignore
67
+ - .rspec
68
+ - Gemfile
69
+ - README.md
70
+ - Rakefile
71
+ - UNLICENSE
72
+ - lib/redis-knock.rb
73
+ - lib/redis_knock.rb
74
+ - lib/redis_knock/connection_error.rb
75
+ - lib/redis_knock/control.rb
76
+ - lib/redis_knock/middleware.rb
77
+ - lib/redis_knock/version.rb
78
+ - redis_knock.gemspec
79
+ - spec/lib/redis_knock/control_spec.rb
80
+ - spec/lib/redis_knock/middleware_spec.rb
81
+ - spec/spec_helper.rb
82
+ homepage: https://github.com/salizzar/redis_knock
83
+ licenses: []
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ! '>='
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubyforge_project: redis_knock
102
+ rubygems_version: 1.8.10
103
+ signing_key:
104
+ specification_version: 3
105
+ summary: A Ruby HTTP Throttle Control
106
+ test_files:
107
+ - spec/lib/redis_knock/control_spec.rb
108
+ - spec/lib/redis_knock/middleware_spec.rb
109
+ - spec/spec_helper.rb