limiter 0.0.2

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,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *~
19
+ nbproject/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in limiter.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 wangxz
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,37 @@
1
+ # Limiter
2
+
3
+ Rack middleware for rate-limiting incoming HTTP requests with black_list and white_list support.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'limiter', :git => "git://github.com/csdn-dev/limiter.git"
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ ## Usage
16
+
17
+ ```ruby
18
+ # config/initializers/limiter.rb
19
+ require File.expand_path("../redis", __FILE__)
20
+ Rails.configuration.app_middleware.insert_before(Rack::MethodOverride,
21
+ Limiter::RateLimiter,
22
+ :black_list => Limiter::BlackList.new($redis),
23
+ :white_list => Limiter::WhiteList.new($redis),
24
+ :allow_path => Rails.env.development? ? /^\/(assets|human_validations|simple_captcha)/ :
25
+ /^\/(human_validations|simple_captcha)/,
26
+ :message => "<a href='/human_validations/new'>我不是机器人</a>",
27
+ :visit_counter => Limiter::VisitCounter.new($redis)
28
+ )
29
+ ```
30
+
31
+ ## Contributing
32
+
33
+ 1. Fork it
34
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
35
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
36
+ 4. Push to the branch (`git push origin my-new-feature`)
37
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/lib/limiter.rb ADDED
@@ -0,0 +1,7 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require "limiter/version"
3
+ require "limiter/base"
4
+ require "limiter/white_list"
5
+ require "limiter/black_list"
6
+ require "limiter/rate_limiter"
7
+ require "limiter/visit_counter"
@@ -0,0 +1,142 @@
1
+ # -*- encoding : utf-8 -*-
2
+ # This is the base class for rate limiter implementations.
3
+ #
4
+ # @example Defining a rate limiter subclass
5
+ # class MyLimiter < Limiter::Base
6
+ # def allowed?(request)
7
+ # # TODO: custom logic goes here
8
+ # end
9
+ # end
10
+ #
11
+ module Limiter
12
+ class Base
13
+ attr_reader :app
14
+ attr_reader :options
15
+ attr_reader :white_list
16
+ attr_reader :black_list
17
+ attr_reader :allow_path
18
+ attr_reader :allow_agent
19
+
20
+ ##
21
+ # @param [#call] app
22
+ # @param [Hash{Symbol => Object}] options
23
+ # @option options [BlackList] :black_list (BlackList.new($redis))
24
+ # @option options [WhiteList] :white_list (WhiteList.new($redis))
25
+ # @option options [String/Regexp] :allow_path ("/human_test")
26
+ # @option options [Regex] :allow_agent (/agent1|agent2/)
27
+ # @option options [Integer] :code (403)
28
+ # @option options [String] :message ("Rate Limit Exceeded")
29
+
30
+ def initialize(app, options = {})
31
+ @black_list = options[:black_list]
32
+ @white_list = options[:white_list]
33
+ @allow_path = options[:allow_path]
34
+ @allow_agent = options[:allow_agent]
35
+ @app, @options = app, options
36
+ end
37
+
38
+ ##
39
+ # @param [Hash{String => String}] env
40
+ # @return [Array(Integer, Hash, #each)]
41
+ # @see http://rack.rubyforge.org/doc/SPEC.html
42
+ def call(env)
43
+ request = Rack::Request.new(env)
44
+ allowed?(request) ? app.call(env) : rate_limit_exceeded
45
+ end
46
+
47
+ ##
48
+ # Returns `false` if the rate limit has been exceeded for the given
49
+ # `request`, or `true` otherwise.
50
+ #
51
+ # Override this method in subclasses that implement custom rate limiter
52
+ # strategies.
53
+ #
54
+ # @param [Rack::Request] request
55
+ # @return [Boolean]
56
+ def allowed?(request)
57
+ case
58
+ when allow_path?(request) then true
59
+ when allow_agent?(request) then true
60
+ when whitelisted?(request) then true
61
+ when blacklisted?(request) then false
62
+ else nil # override in subclasses
63
+ end
64
+ end
65
+
66
+ def whitelisted?(request)
67
+ white_list.member?(client_identifier(request))
68
+ end
69
+
70
+ def blacklisted?(request)
71
+ black_list.member?(client_identifier(request))
72
+ end
73
+
74
+ def allow_path?(request)
75
+ if allow_path.is_a?(Regexp)
76
+ request.path =~ allow_path
77
+ else
78
+ request.path == allow_path
79
+ end
80
+ end
81
+
82
+ def allow_agent?(request)
83
+ return false unless allow_agent
84
+ request.user_agent.to_s =~ allow_agent
85
+ end
86
+
87
+ protected
88
+
89
+ ##
90
+ # @param [Rack::Request] request
91
+ # @return [String]
92
+ def client_identifier(request)
93
+ request.ip.to_s
94
+ end
95
+
96
+ ##
97
+ # @param [Rack::Request] request
98
+ # @return [Float]
99
+ def request_start_time(request)
100
+ case
101
+ when request.env.has_key?('HTTP_X_REQUEST_START')
102
+ request.env['HTTP_X_REQUEST_START'].to_f / 1000
103
+ else
104
+ Time.now.to_f
105
+ end
106
+ end
107
+
108
+ ##
109
+ # Outputs a `Rate Limit Exceeded` error.
110
+ #
111
+ # @return [Array(Integer, Hash, #each)]
112
+ def rate_limit_exceeded
113
+ headers = respond_to?(:retry_after) ? {'Retry-After' => retry_after.to_f.ceil.to_s} : {}
114
+ http_error(options[:code] || 403, options[:message], headers)
115
+ end
116
+
117
+ ##
118
+ # Outputs an HTTP `4xx` or `5xx` response.
119
+ #
120
+ # @param [Integer] code
121
+ # @param [String, #to_s] message
122
+ # @param [Hash{String => String}] headers
123
+ # @return [Array(Integer, Hash, #each)]
124
+ def http_error(code, message = nil, headers = {})
125
+ body = if message
126
+ [message]
127
+ else
128
+ [http_status(code) + " : Rate Limit Exceeded\n"]
129
+ end
130
+ [code, {'Content-Type' => 'text/html; charset=utf-8'}.merge(headers), body]
131
+ end
132
+
133
+ ##
134
+ # Returns the standard HTTP status message for the given status `code`.
135
+ #
136
+ # @param [Integer] code
137
+ # @return [String]
138
+ def http_status(code)
139
+ [code, Rack::Utils::HTTP_STATUS_CODES[code]].join(' ')
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,28 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Limiter
3
+ class BlackList
4
+ def initialize(cache_store)
5
+ @cache_store = cache_store
6
+ end
7
+
8
+ def list
9
+ @cache_store.smembers(key)
10
+ end
11
+
12
+ def add(ip)
13
+ @cache_store.sadd(key, ip)
14
+ end
15
+
16
+ def remove(ip)
17
+ @cache_store.srem(key, ip)
18
+ end
19
+
20
+ def member?(ip)
21
+ @cache_store.sismember(key, ip)
22
+ end
23
+
24
+ def key
25
+ "limiter/black_list"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,54 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Limiter
3
+ class RateLimiter < Base
4
+ GET_TTL = 20.minutes
5
+ MAX_GET_NUM = 1000
6
+
7
+ POST_TTL = 5.seconds
8
+ MAX_POST_NUM = 20
9
+
10
+ def initialize(app, options = {})
11
+ super
12
+ end
13
+
14
+ def visit_counter
15
+ @visit_counter ||= options[:visit_counter]
16
+ end
17
+
18
+ def allowed?(request)
19
+ common_allowed = super
20
+ return true if common_allowed == true
21
+ return false if common_allowed == false
22
+
23
+ client_id = client_identifier(request)
24
+ post_count = read_and_incr_post_num(request, client_id)
25
+ get_count = read_and_incr_get_num(request, client_id)
26
+
27
+ return false if (get_count > MAX_GET_NUM || post_count > MAX_POST_NUM)
28
+ return true
29
+ end
30
+
31
+ def client_identifier(request)
32
+ # 61.135.163.4 -> 61.135.163.0
33
+ request.ip.to_s.sub(/\.\d+$/, ".0")
34
+ end
35
+
36
+ private
37
+
38
+ def read_and_incr_post_num(request, client_id)
39
+ if request.post?
40
+ post_count = visit_counter.count(client_id, "POST")
41
+ visit_counter.incr(client_id, "POST", POST_TTL)
42
+ return post_count
43
+ end
44
+ return 0
45
+ end
46
+
47
+ def read_and_incr_get_num(request, client_id)
48
+ get_count = visit_counter.count(client_id, "GET")
49
+ visit_counter.incr(client_id, "GET", GET_TTL)
50
+ return get_count
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,4 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Limiter
3
+ VERSION = "0.0.2"
4
+ end
@@ -0,0 +1,35 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Limiter
3
+ class VisitCounter
4
+ def initialize(cache_store)
5
+ @cache_store = cache_store
6
+ end
7
+
8
+ def remove(ip, method)
9
+ cache_key = [key, ip, method].join("/")
10
+ @cache_store.del(cache_key)
11
+ end
12
+
13
+ def incr(ip, method, ttl)
14
+ cache_key = [key, ip, method].join("/")
15
+ @cache_store.multi do
16
+ @cache_store.incr(cache_key)
17
+ @cache_store.expire(cache_key, ttl)
18
+ end
19
+ end
20
+
21
+ def count(ip, method)
22
+ cache_key = [key, ip, method].join("/")
23
+ @cache_store.get(cache_key).to_i
24
+ end
25
+
26
+ def set(ip, method, ttl, num)
27
+ cache_key = [key, ip, method].join("/")
28
+ @cache_store.setex(cache_key, ttl, num)
29
+ end
30
+
31
+ def key
32
+ "limiter/vc"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,28 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Limiter
3
+ class WhiteList
4
+ def initialize(cache_store)
5
+ @cache_store = cache_store
6
+ end
7
+
8
+ def list
9
+ @cache_store.smembers(key)
10
+ end
11
+
12
+ def add(ip)
13
+ @cache_store.sadd(key, ip)
14
+ end
15
+
16
+ def remove(ip)
17
+ @cache_store.srem(key, ip)
18
+ end
19
+
20
+ def member?(ip)
21
+ @cache_store.sismember(key, ip)
22
+ end
23
+
24
+ def key
25
+ "limiter/white_list"
26
+ end
27
+ end
28
+ end
data/limiter.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'limiter/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "limiter"
8
+ gem.version = Limiter::VERSION
9
+ gem.authors = ["wangxz"]
10
+ gem.email = ["wangxz@csdn.net"]
11
+ gem.description = %q{Write a gem description}
12
+ gem.summary = %q{Write a gem summary}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: limiter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - wangxz
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-26 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Write a gem description
15
+ email:
16
+ - wangxz@csdn.net
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - Gemfile
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - lib/limiter.rb
27
+ - lib/limiter/base.rb
28
+ - lib/limiter/black_list.rb
29
+ - lib/limiter/rate_limiter.rb
30
+ - lib/limiter/version.rb
31
+ - lib/limiter/visit_counter.rb
32
+ - lib/limiter/white_list.rb
33
+ - limiter.gemspec
34
+ homepage: ''
35
+ licenses: []
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ none: false
48
+ requirements:
49
+ - - ! '>='
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubyforge_project:
54
+ rubygems_version: 1.8.24
55
+ signing_key:
56
+ specification_version: 3
57
+ summary: Write a gem summary
58
+ test_files: []