rack-pooledthrottle 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f17681ae19c501bb8f4194f2d7e9389f3aae51ee
4
+ data.tar.gz: 6349f697567a2cbc6f3a04248e710dbd36c253eb
5
+ SHA512:
6
+ metadata.gz: 2431de24d6e9c40b2f5e0b2b0f5bfdca57d5ae38b5b99e3c3cec3698c258f3cb56d52efbd30472071ca568046b5c5dcbc1784fff2208880ec31eb080eb8ecf1d
7
+ data.tar.gz: 68eec81e9a722fc99bcbdc66a3cec630ed20500188854bea77ca48f77c83b46a950958b2756a0061f97569ccb4908e5ddbb5cf8e57f3307ad9772552251309f7
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rack-pooledthrottle.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Scott Watermasysk
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
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
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # Rack::Pooledthrottle
2
+
3
+ Rack::Pooledthrottle is middleware which provides rate-limiting of incoming HTTP requests to Rack applications. You should be able to use it with any Ruby web framework (I have only tested Sinatra and Rails).
4
+
5
+ I initially tried to work it into [Rack Throttle](https://github.com/bendiken/rack-throttle), but because of Rack Throttle's
6
+ many backend options I thought it would be too complicated.
7
+
8
+ So how is it different?
9
+
10
+ 1. It uses a pool (via [ConnectionPool](https://github.com/mperham/connection_pool)) of connections instead of creating one on each request
11
+ 1. It uses a sliding TTL for tracking. This means if you limit an IP to 10 requests every hour and the first request comes in at 1:30 the user can make up to 9 more requests until 2:30.
12
+ 1. The TTL is set on middleware declaration. No other subclasses.
13
+ 1. Database support is limited to Memcached (and eventually Redis)
14
+
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem 'rack-pooledthrottle'
22
+ ```
23
+
24
+ And then execute:
25
+
26
+ $ bundle
27
+
28
+ Or install it yourself as:
29
+
30
+ $ gem install rack-pooledthrottle
31
+
32
+ ## Usage
33
+
34
+ ### Adding throttling to a Rails application
35
+
36
+ require 'rack/pooledthrottle'
37
+ require 'dalli' #Dalli is not required. You must add it to your gem file if you want to use it.
38
+ $mc_pool ||= ConnectionPool.new(size: 5) {Dalli::Client.new}
39
+
40
+ class Application < Rails::Application
41
+ config.middleware.use Rack::Throttle::Interval, max: 10, ttl: 3600, pool: $mc_pool
42
+ end
43
+
44
+ ### Adding throttling to a Sinatra application
45
+
46
+ require 'sinatra'
47
+ require 'rack/pooledthrottle'
48
+
49
+ use Rack::Throttle::Interval, max: 5, ttl: 60, pool: $mc_pool #see above for pool
50
+
51
+ get('/hello') { "Hello, world!\n" }
52
+
53
+ ## HAT TIP
54
+
55
+ I just want to make it super clear that a vast majority of this code was based on the excellent work of [Rack Throttle](https://github.com/bendiken/rack-throttle).
56
+
57
+
58
+ ## Contributing
59
+
60
+ 1. Fork it ( https://github.com/[my-github-username]/rack-pooledthrottle/fork )
61
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
62
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
63
+ 4. Push to the branch (`git push origin my-new-feature`)
64
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,11 @@
1
+ require "rack/pooledthrottle/version"
2
+ require 'rack'
3
+ require 'connection_pool'
4
+ require 'rack/pooledthrottle/throttle'
5
+ require 'rack/pooledthrottle/memcached_throttle'
6
+
7
+ module Rack
8
+ module PooledThrottle
9
+
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ module Rack
2
+ module PooledThrottle
3
+ class MemcachedThrottle < Throttle
4
+ def initialize(app, options={})
5
+ super
6
+ end
7
+
8
+ def query_cache?(request)
9
+ ((pool.with {|cache| cache.incr(cache_key(request), 1, ttl, 1)}) <= max)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,107 @@
1
+ module Rack
2
+ module PooledThrottle
3
+ class Throttle
4
+ attr_reader :app, :options
5
+
6
+ def initialize(app, options = {})
7
+ @app, @options = app, options
8
+ end
9
+
10
+ def call(env)
11
+ request = Rack::Request.new(env)
12
+ allowed?(request) ? app.call(env) : rate_limit_exceeded(request)
13
+ end
14
+
15
+ protected
16
+
17
+ def allowed?(request)
18
+ case
19
+ when whitelisted?(request) then true
20
+ when blacklisted?(request) then false
21
+ else
22
+ query_cache?(request)
23
+ end
24
+ end
25
+
26
+ def query_cache?(request)
27
+ false
28
+ end
29
+
30
+ def whitelisted?(request)
31
+ false
32
+ end
33
+
34
+ def blacklisted?(request)
35
+ false
36
+ end
37
+
38
+ def cache_key(request)
39
+ "#{namespace}:#{key_prefix}#{client_identifier(request)}"
40
+ end
41
+
42
+
43
+ ##
44
+ # @param [Rack::Request] request
45
+ # @return [String]
46
+ def client_identifier(request)
47
+ if cio = options[:client_identifier]
48
+ cio.call(request)
49
+ else
50
+ request.ip.to_s
51
+ end
52
+ end
53
+
54
+ def namespace
55
+ options[:namespace] || 'rpt'
56
+ end
57
+
58
+ def key_prefix
59
+ options[:key_prefix]
60
+ end
61
+
62
+ def pool
63
+ options[:pool]
64
+ end
65
+
66
+ def max
67
+ (options[:max] || 10).to_i
68
+ end
69
+
70
+ def ttl
71
+ (options[:ttl] || 60).to_i
72
+ end
73
+
74
+ ##
75
+ # Outputs a `Rate Limit Exceeded` error.
76
+ #
77
+ # @return [Array(Integer, Hash, #each)]
78
+ def rate_limit_exceeded(request)
79
+ options[:rate_limit_exceeded_callback].call(request) if options[:rate_limit_exceeded_callback]
80
+ headers = respond_to?(:retry_after) ? {'Retry-After' => retry_after.to_f.ceil.to_s} : {}
81
+ http_error(options[:code] || 403, options[:message] || 'Rate Limit Exceeded', headers)
82
+ end
83
+
84
+ ##
85
+ # Outputs an HTTP `4xx` or `5xx` response.
86
+ #
87
+ # @param [Integer] code
88
+ # @param [String, #to_s] message
89
+ # @param [Hash{String => String}] headers
90
+ # @return [Array(Integer, Hash, #each)]
91
+ def http_error(code, message = nil, headers = {})
92
+ [code, {'Content-Type' => 'text/plain; charset=utf-8'}.merge(headers),
93
+ [http_status(code), (message.nil? ? "\n" : " (#{message})\n")]]
94
+ end
95
+
96
+ ##
97
+ # Returns the standard HTTP status message for the given status `code`.
98
+ #
99
+ # @param [Integer] code
100
+ # @return [String]
101
+ def http_status(code)
102
+ [code, Rack::Utils::HTTP_STATUS_CODES[code]].join(' ')
103
+ end
104
+
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,5 @@
1
+ module Rack
2
+ module Pooledthrottle
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rack/pooledthrottle/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rack-pooledthrottle"
8
+ spec.version = Rack::Pooledthrottle::VERSION
9
+ spec.authors = ["Scott Watermasysk"]
10
+ spec.email = ["scottwater@gmail.com"]
11
+ spec.summary = %q{Throttle HTTP requests.}
12
+ spec.description = %q{Throttle HTTP requests using a connection pool for all database connections}
13
+ spec.homepage = ""
14
+ spec.license = "Public Domain"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency 'rack', '~> 1'
22
+ spec.add_runtime_dependency 'connection_pool', "~> 2.2"
23
+ spec.add_development_dependency "bundler", "~> 1.7"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "rspec", "~> 3.3"
26
+ spec.add_development_dependency 'rack-test', '0.6.3'
27
+ spec.add_development_dependency 'dalli', '~> 2.7'
28
+
29
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+ require 'dalli'
3
+
4
+ describe Rack::PooledThrottle::MemcachedThrottle do
5
+ include Rack::Test::Methods
6
+
7
+ before(:each) do
8
+ pool.with{|m| m.flush}
9
+ @options = {
10
+ pool: pool,
11
+ max: 3,
12
+ ttl: 30
13
+ }
14
+ end
15
+
16
+ def pool
17
+ @pool ||= ConnectionPool.new(size: 1) do
18
+ Dalli::Client.new((ENV["MEMCACHED_SERVERS"] || "127.0.0.1:11211").split(",").map{|s| s.strip})
19
+ end
20
+ end
21
+
22
+ def app
23
+ @app ||= Rack::PooledThrottle::MemcachedThrottle.new(example_target_app, @options)
24
+ end
25
+
26
+ it 'expect a passing message' do
27
+ get '/foo'
28
+ expect(last_response.body).to show_allowed_response
29
+ end
30
+
31
+ it 'expect a passing message' do
32
+ 4.times {get '/foo'}
33
+ expect(last_response.body).to show_throttled_response
34
+ end
35
+
36
+ it "should return true if whitelisted" do
37
+ allow(app).to receive(:whitelisted?).and_return(true)
38
+ 4.times {get "/foo"}
39
+ expect(last_response.body).to show_allowed_response
40
+ end
41
+
42
+ it "should return true if blacklisted" do
43
+ allow(app).to receive(:blacklisted?).and_return(true)
44
+ get "/foo"
45
+ expect(last_response.body).to show_throttled_response
46
+ end
47
+
48
+
49
+ it 'should allow the client_identifier to be overriden and pass' do
50
+ @options[:max] = 2
51
+ @options[:client_identifier] = ->(request){request.params['email']}
52
+ 2.times {get('/foo', email: 'scottwater@gmail.com')}
53
+ 2.times {get '/foo', email: 'scott@kickofflabs.com'}
54
+ expect(last_response.body).to show_allowed_response
55
+ end
56
+
57
+ it 'should allow the client_identifier to be overriden and fail' do
58
+ @options[:max] = 2
59
+ @options[:client_identifier] = ->(request){request.params['email']}
60
+ 2.times {get('/foo', email: 'scottwater@gmail.com')}
61
+ 3.times {get '/foo', email: 'scott@kickofflabs.com'}
62
+ expect(last_response.body).to show_throttled_response
63
+ end
64
+
65
+ end
@@ -0,0 +1,44 @@
1
+ require "rack/test"
2
+ require "rack/pooledthrottle"
3
+
4
+ def example_target_app
5
+ target_app = Object.new
6
+ allow(target_app).to receive_messages(call: [200, {}, "Example App Body"])
7
+ target_app
8
+ end
9
+
10
+ RSpec::Matchers.define :show_allowed_response do
11
+ match do |body|
12
+ body.include?("Example App Body")
13
+ end
14
+
15
+ failure_message do
16
+ "expected response to show the allowed response"
17
+ end
18
+
19
+ failure_message_when_negated do
20
+ "expected response not to show the allowed response"
21
+ end
22
+
23
+ description do
24
+ "expected the allowed response"
25
+ end
26
+ end
27
+
28
+ RSpec::Matchers.define :show_throttled_response do
29
+ match do |body|
30
+ body.include?("Rate Limit Exceeded")
31
+ end
32
+
33
+ failure_message do
34
+ "expected response to show the throttled response"
35
+ end
36
+
37
+ failure_message_when_negated do
38
+ "expected response not to show the throttled response"
39
+ end
40
+
41
+ description do
42
+ "expected the throttled response"
43
+ end
44
+ end
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-pooledthrottle
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Scott Watermasysk
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: connection_pool
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.7'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.7'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.3'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rack-test
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '='
88
+ - !ruby/object:Gem::Version
89
+ version: 0.6.3
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '='
95
+ - !ruby/object:Gem::Version
96
+ version: 0.6.3
97
+ - !ruby/object:Gem::Dependency
98
+ name: dalli
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.7'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.7'
111
+ description: Throttle HTTP requests using a connection pool for all database connections
112
+ email:
113
+ - scottwater@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - Gemfile
120
+ - LICENSE.txt
121
+ - README.md
122
+ - Rakefile
123
+ - lib/rack/pooledthrottle.rb
124
+ - lib/rack/pooledthrottle/memcached_throttle.rb
125
+ - lib/rack/pooledthrottle/throttle.rb
126
+ - lib/rack/pooledthrottle/version.rb
127
+ - rack-pooledthrottle.gemspec
128
+ - spec/memecached_spec.rb
129
+ - spec/spec_helper.rb
130
+ homepage: ''
131
+ licenses:
132
+ - Public Domain
133
+ metadata: {}
134
+ post_install_message:
135
+ rdoc_options: []
136
+ require_paths:
137
+ - lib
138
+ required_ruby_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ requirements: []
149
+ rubyforge_project:
150
+ rubygems_version: 2.2.2
151
+ signing_key:
152
+ specification_version: 4
153
+ summary: Throttle HTTP requests.
154
+ test_files:
155
+ - spec/memecached_spec.rb
156
+ - spec/spec_helper.rb