rate_limit 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 +4 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +33 -0
- data/Rakefile +1 -0
- data/lib/rate_limit.rb +84 -0
- data/lib/rate_limit/loader.rb +18 -0
- data/lib/rate_limit/version.rb +3 -0
- data/lib/rate_limit/visit.rb +15 -0
- data/rate_limit.gemspec +24 -0
- metadata +64 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Copyright (c) 2012 Geoffroy Couprie
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
21
|
+
|
data/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Rate Limit
|
|
2
|
+
======
|
|
3
|
+
|
|
4
|
+
Filter requests to your controllers, and block bruteforce attacks on your forms.
|
|
5
|
+
|
|
6
|
+
By limiting the rate of requests, you could, without harming usability:
|
|
7
|
+
* prevent password bruteforce on login forms
|
|
8
|
+
* prevent user enumeration on password reset forms
|
|
9
|
+
* slow significantly site scraping
|
|
10
|
+
|
|
11
|
+
Features
|
|
12
|
+
--------
|
|
13
|
+
|
|
14
|
+
* Block requests if they exceed a specified rate (blockBy*)
|
|
15
|
+
* Add a growing delay before accepting requests (slowBy*)
|
|
16
|
+
* Reset the filter if successful use (resetBlockerByKey/resetSlowerByKey)
|
|
17
|
+
|
|
18
|
+
Code example
|
|
19
|
+
------------
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
class HelloController < ApplicationController
|
|
23
|
+
def index
|
|
24
|
+
if RateLimit.slowByIp(request)
|
|
25
|
+
puts "not blocking the page"
|
|
26
|
+
else
|
|
27
|
+
puts "blocking the page"
|
|
28
|
+
render :nothing => true, :status => 403
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
data/Rakefile
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/rate_limit.rb
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
require "rate_limit/version"
|
|
2
|
+
require "rate_limit/loader"
|
|
3
|
+
require "rate_limit/visit"
|
|
4
|
+
|
|
5
|
+
module RateLimit
|
|
6
|
+
|
|
7
|
+
##
|
|
8
|
+
# Block requests by IP
|
|
9
|
+
# the request argument is given by the request method of Rails controllers
|
|
10
|
+
|
|
11
|
+
def self.limitByIp(request, requests_per_seconds = 1, cache = Rails.cache)
|
|
12
|
+
key = request.remote_ip+request.fullpath
|
|
13
|
+
blockByKey(key, requests_per_seconds, cache)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
##
|
|
17
|
+
# Slow requests by IP
|
|
18
|
+
# the factor parameter is the multiplicating factor for the growing time between requests
|
|
19
|
+
def self.slowByIp(request, requests_per_seconds = 1, factor = 1, cache = Rails.cache)
|
|
20
|
+
key = request.remote_ip+request.fullpath
|
|
21
|
+
slowByKey(key, requests_per_seconds, factor, cache)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# Block requests by key
|
|
26
|
+
# You can define wich eky is used to discriminate between requests
|
|
27
|
+
def self.blockByKey(key, requests_per_seconds = 1, cache = Rails.cache)
|
|
28
|
+
loader = Loader.new cache
|
|
29
|
+
|
|
30
|
+
key = key+"b"
|
|
31
|
+
timestamp = Time.new.to_time.to_i
|
|
32
|
+
counter = loader.read key
|
|
33
|
+
if counter.nil?
|
|
34
|
+
loader.write key, BlockedVisit.new(1, timestamp)
|
|
35
|
+
true
|
|
36
|
+
else
|
|
37
|
+
counter.visits += 1
|
|
38
|
+
time = timestamp - counter.start_time
|
|
39
|
+
loader.write key, counter
|
|
40
|
+
requests_per_seconds > counter.visits.to_f/time.to_f
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# Slow requests by key
|
|
46
|
+
# You can define wich eky is used to discriminate between requests
|
|
47
|
+
def self.slowByKey(key, requests_per_seconds = 0.5, factor = 1, cache = Rails.cache)
|
|
48
|
+
loader = Loader.new cache
|
|
49
|
+
|
|
50
|
+
key = key+"s"
|
|
51
|
+
timestamp = Time.new.to_time.to_i
|
|
52
|
+
counter = loader.read key
|
|
53
|
+
if counter.nil?
|
|
54
|
+
loader.write key, SlowedVisit.new(1, timestamp)
|
|
55
|
+
true
|
|
56
|
+
else
|
|
57
|
+
time = timestamp - counter.previous
|
|
58
|
+
counter.previous = timestamp
|
|
59
|
+
wait_time = requests_per_seconds*factor*counter.visits
|
|
60
|
+
if wait_time < time
|
|
61
|
+
loader.write key, counter
|
|
62
|
+
true
|
|
63
|
+
else
|
|
64
|
+
counter.visits += 1
|
|
65
|
+
loader.write key, counter
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Reset the counters for blocked requests
|
|
73
|
+
def self.resetBlockerByKey(key)
|
|
74
|
+
loader = Loader.new cache
|
|
75
|
+
cache.delete(key+"b")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
##
|
|
79
|
+
# Reset the counters for slowed requests
|
|
80
|
+
def self.resetSlowerByKey(key)
|
|
81
|
+
loader = Loader.new cache
|
|
82
|
+
cache.delete(key+"s")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class RateLimit::Loader
|
|
2
|
+
def initialize(cache)
|
|
3
|
+
@cache = cache
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def read(key)
|
|
7
|
+
data = @cache.read(key)
|
|
8
|
+
Marshal::load(data) unless data.nil?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def write(key, data)
|
|
12
|
+
@cache.write(key, Marshal::dump(data))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def delete(key)
|
|
16
|
+
@cache.delete(key)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class RateLimit::BlockedVisit
|
|
2
|
+
attr_accessor :visits, :start_time
|
|
3
|
+
def initialize(visits, date)
|
|
4
|
+
@visits = visits
|
|
5
|
+
@start_time = date
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class RateLimit::SlowedVisit
|
|
10
|
+
attr_accessor :visits, :previous
|
|
11
|
+
def initialize(visits = 0, previous)
|
|
12
|
+
@visits = visits
|
|
13
|
+
@previous = previous
|
|
14
|
+
end
|
|
15
|
+
end
|
data/rate_limit.gemspec
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
+
require "rate_limit/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |s|
|
|
6
|
+
s.name = "rate_limit"
|
|
7
|
+
s.version = RateLimit::VERSION
|
|
8
|
+
s.authors = ["Geoffroy Couprie"]
|
|
9
|
+
s.email = ["geo.couprie@gmail.com"]
|
|
10
|
+
s.homepage = "https://github.com/Geal/rate_limit"
|
|
11
|
+
s.summary = %q{rate_limit filters page access in your Rails application}
|
|
12
|
+
s.description = %q{Limit request frequence by IP, by page, for login or password reset forms}
|
|
13
|
+
|
|
14
|
+
s.rubyforge_project = "rate_limit"
|
|
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
|
+
# specify any dependencies here; for example:
|
|
22
|
+
# s.add_development_dependency "rspec"
|
|
23
|
+
# s.add_runtime_dependency "rest-client"
|
|
24
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rate_limit
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
prerelease:
|
|
5
|
+
version: 0.0.1
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Geoffroy Couprie
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
|
|
13
|
+
date: 2012-07-12 00:00:00 Z
|
|
14
|
+
dependencies: []
|
|
15
|
+
|
|
16
|
+
description: Limit request frequence by IP, by page, for login or password reset forms
|
|
17
|
+
email:
|
|
18
|
+
- geo.couprie@gmail.com
|
|
19
|
+
executables: []
|
|
20
|
+
|
|
21
|
+
extensions: []
|
|
22
|
+
|
|
23
|
+
extra_rdoc_files: []
|
|
24
|
+
|
|
25
|
+
files:
|
|
26
|
+
- .gitignore
|
|
27
|
+
- Gemfile
|
|
28
|
+
- LICENSE
|
|
29
|
+
- README.md
|
|
30
|
+
- Rakefile
|
|
31
|
+
- lib/rate_limit.rb
|
|
32
|
+
- lib/rate_limit/loader.rb
|
|
33
|
+
- lib/rate_limit/version.rb
|
|
34
|
+
- lib/rate_limit/visit.rb
|
|
35
|
+
- rate_limit.gemspec
|
|
36
|
+
homepage: https://github.com/Geal/rate_limit
|
|
37
|
+
licenses: []
|
|
38
|
+
|
|
39
|
+
post_install_message:
|
|
40
|
+
rdoc_options: []
|
|
41
|
+
|
|
42
|
+
require_paths:
|
|
43
|
+
- lib
|
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
45
|
+
none: false
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: "0"
|
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
51
|
+
none: false
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: "0"
|
|
56
|
+
requirements: []
|
|
57
|
+
|
|
58
|
+
rubyforge_project: rate_limit
|
|
59
|
+
rubygems_version: 1.8.8
|
|
60
|
+
signing_key:
|
|
61
|
+
specification_version: 3
|
|
62
|
+
summary: rate_limit filters page access in your Rails application
|
|
63
|
+
test_files: []
|
|
64
|
+
|