sinatra-rate-limiter 0.2.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 +7 -0
- data/LICENSE +20 -0
- data/README.md +87 -0
- data/lib/sinatra/rate-limiter.rb +128 -0
- metadata +74 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 030b21e819260562c4da8c1472360e4d8cdb1c8d
|
4
|
+
data.tar.gz: 456ad3d77b05fe72a5d01aba6ee3699b7da49916
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 920f5b487a6dd84cd220a27869811d834102ff591a68d2446758b851546893af17f1e1dbdf8fbb9b44f9bf88547089263bc30c0af137c786b3bc2db710cd2303
|
7
|
+
data.tar.gz: 14427e370cc5f040287edfba31247480fac29bb2c66a7b06199df0e7f778f9c75c35cb840b926253bca41fe17c3748cabc65dd1c88076fbf50cfdd53a0b36a72
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (C) 2015 Warren Guy <warren@guy.net.au>
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# sinatra/rate-limiter
|
2
|
+
|
3
|
+
A customisable redis backed rate limiter for Sinatra applications.
|
4
|
+
|
5
|
+
## Installing
|
6
|
+
|
7
|
+
* Add the gem to your Gemfile
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
source 'https://rubygems.org'
|
11
|
+
gem 'sinatra-rate-limiter'
|
12
|
+
```
|
13
|
+
|
14
|
+
* Require and enable it in your app after including Sinatra
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
require 'sinatra/rate-limiter'
|
18
|
+
enable :rate_limiter
|
19
|
+
```
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Use `rate_limit` in the pipeline of any route (i.e. in the route itself, or
|
24
|
+
in a `before` filter, or in a Padrino controller, etc. `rate_limit` takes
|
25
|
+
zero to infinite parameters, with the syntax:
|
26
|
+
|
27
|
+
```rate_limit [String], [<Fixnum>, <Fixnum>], [<Fixnum>, <Fixnum>], ...```
|
28
|
+
|
29
|
+
The following route will be limited to 10 requests per minute and 100
|
30
|
+
requests per hour:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
get '/rate-limited' do
|
34
|
+
rate_limit 'default', 10, 60, 100, 60*60
|
35
|
+
|
36
|
+
"now you see me"
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
The following will apply an unnamed limit of 1000 requests per hour to all
|
41
|
+
routes and stricter individual rate limits to two particular routes:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
set :rate_limiter_default_limits, [1000, 60*60]
|
45
|
+
before do
|
46
|
+
rate_limit
|
47
|
+
end
|
48
|
+
|
49
|
+
get '/' do
|
50
|
+
"this route has the global limit applied"
|
51
|
+
end
|
52
|
+
|
53
|
+
get '/rate-limit-1' do
|
54
|
+
rate_limit 'ratelimit1', 2, 5,
|
55
|
+
10, 60
|
56
|
+
end
|
57
|
+
|
58
|
+
get '/rate-limit-2' do
|
59
|
+
rate_limit 'ratelimit2', 1, 10
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
N.B. in the last example, be aware that the more specific rate limits do not
|
64
|
+
override any rate limit already defined during route processing, and the
|
65
|
+
global rate limit will apply additionally. If you call `rate_limit` more than
|
66
|
+
once with the same (or no) name, it will be double counted.
|
67
|
+
|
68
|
+
## Configuration
|
69
|
+
|
70
|
+
All configuration is optional. If no default limits are specified here,
|
71
|
+
you must specify limits with each call of `rate_limit`
|
72
|
+
|
73
|
+
### Defaults
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
set :rate_limiter_default_limits, []
|
77
|
+
set :rate_limiter_environments, [:production]
|
78
|
+
set :rate_limiter_error_code, 429
|
79
|
+
set :rate_limiter_error_template, nil
|
80
|
+
set :rate_limiter_send_headers, true
|
81
|
+
set :rate_limiter_custom_user_id, nil
|
82
|
+
set :rate_limiter_redis_conn, Redis.new
|
83
|
+
set :rate_limiter_redis_namespace, 'rate_limit'
|
84
|
+
set :rate_limiter_redis_expires, 24*60*60
|
85
|
+
```
|
86
|
+
|
87
|
+
TODO: document each setting here explicitly
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'redis'
|
3
|
+
|
4
|
+
module Sinatra
|
5
|
+
|
6
|
+
module RateLimiter
|
7
|
+
|
8
|
+
module Helpers
|
9
|
+
|
10
|
+
def rate_limit(*args)
|
11
|
+
return unless settings.rate_limiter and settings.rate_limiter_environments.include?(settings.environment)
|
12
|
+
|
13
|
+
limit_name, limits = parse_args(args)
|
14
|
+
|
15
|
+
if error_locals = limits_exceeded?(limits, limit_name)
|
16
|
+
rate_limit_headers(limits, limit_name)
|
17
|
+
response.headers['Retry-After'] = error_locals[:try_again] if settings.rate_limiter_send_headers
|
18
|
+
halt settings.rate_limiter_error_code, error_response(error_locals)
|
19
|
+
end
|
20
|
+
|
21
|
+
redis.setex([namespace,user_identifier,limit_name,Time.now.to_f.to_s].join('/'),
|
22
|
+
settings.rate_limiter_redis_expires,
|
23
|
+
request.env['REQUEST_URI'])
|
24
|
+
|
25
|
+
rate_limit_headers(limits, limit_name) if settings.rate_limiter_send_headers
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def parse_args(args)
|
31
|
+
limit_name = args.map{|a| a.class}.first.eql?(String) ? args.shift : 'default'
|
32
|
+
args = settings.rate_limiter_default_limits if args.size < 1
|
33
|
+
|
34
|
+
if (args.size < 1)
|
35
|
+
raise ArgumentError, 'No explicit or default limits values provided.'
|
36
|
+
elsif (args.map{|a| a.class}.select{|a| a != Fixnum}.count > 0)
|
37
|
+
raise ArgumentError, 'Non-Fixnum parameters supplied. All parameters must be Fixnum except the first which may be a String.'
|
38
|
+
elsif ((args.map{|a| a.class}.size % 2) != 0)
|
39
|
+
raise ArgumentError, 'Wrong number of Fixnum parameters supplied.'
|
40
|
+
end
|
41
|
+
|
42
|
+
limits = args.each_slice(2).to_a.map{|a| {requests: a[0], seconds: a[1]}}
|
43
|
+
|
44
|
+
return [limit_name, limits]
|
45
|
+
end
|
46
|
+
|
47
|
+
def redis
|
48
|
+
settings.rate_limiter_redis_conn
|
49
|
+
end
|
50
|
+
|
51
|
+
def namespace
|
52
|
+
settings.rate_limiter_redis_namespace
|
53
|
+
end
|
54
|
+
|
55
|
+
def limit_history(limit_name, seconds=0)
|
56
|
+
redis.
|
57
|
+
keys("#{[namespace,user_identifier,limit_name].join('/')}/*").
|
58
|
+
map{|k| k.split('/')[3].to_f}.
|
59
|
+
select{|t| seconds.eql?(0) ? true : t > (Time.now.to_f - seconds)}
|
60
|
+
end
|
61
|
+
|
62
|
+
def limit_remaining(limit, limit_name)
|
63
|
+
limit[:requests] - limit_history(limit_name, limit[:seconds]).length
|
64
|
+
end
|
65
|
+
|
66
|
+
def limit_reset(limit, limit_name)
|
67
|
+
limit[:seconds] - (Time.now.to_f - limit_history(limit_name, limit[:seconds]).first).to_i
|
68
|
+
end
|
69
|
+
|
70
|
+
def limits_exceeded?(limits, limit_name)
|
71
|
+
exceeded = limits.select {|limit| limit_remaining(limit, limit_name) < 1}.sort_by{|e| e[:seconds]}.last
|
72
|
+
|
73
|
+
if exceeded
|
74
|
+
try_again = limit_reset(exceeded, limit_name)
|
75
|
+
return exceeded.merge({try_again: try_again.to_i})
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def rate_limit_headers(limits, limit_name)
|
80
|
+
header_prefix = 'X-Rate-Limit' + (limit_name.eql?('default') ? '' : '-' + limit_name)
|
81
|
+
limit_no = 0 if limits.length > 1
|
82
|
+
limits.each do |limit|
|
83
|
+
limit_no = limit_no + 1 if limit_no
|
84
|
+
response.headers[header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Limit'] = limit[:requests]
|
85
|
+
response.headers[header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Remaining'] = limit_remaining(limit, limit_name)
|
86
|
+
response.headers[header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Reset'] = limit_reset(limit, limit_name)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def error_response(locals)
|
91
|
+
if settings.rate_limiter_error_template
|
92
|
+
render settings.rate_limiter_error_template, locals: locals
|
93
|
+
else
|
94
|
+
content_type 'text/plain'
|
95
|
+
"Rate limit exceeded (#{locals[:requests]} requests in #{locals[:seconds]} seconds). Try again in #{locals[:try_again]} seconds."
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def user_identifier
|
100
|
+
if settings.rate_limiter_custom_user_id.class == Proc
|
101
|
+
return settings.rate_limiter_custom_user_id.call(request)
|
102
|
+
else
|
103
|
+
return request.ip
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.registered(app)
|
110
|
+
app.helpers RateLimiter::Helpers
|
111
|
+
|
112
|
+
app.set :rate_limiter, false
|
113
|
+
app.set :rate_limiter_default_limits, [] # 10 requests per minute: [{requests: 10, seconds: 60}]
|
114
|
+
app.set :rate_limiter_environments, [:production]
|
115
|
+
app.set :rate_limiter_error_code, 429 # http://tools.ietf.org/html/rfc6585
|
116
|
+
app.set :rate_limiter_error_template, nil # locals: requests, seconds, try_again
|
117
|
+
app.set :rate_limiter_send_headers, true
|
118
|
+
app.set :rate_limiter_custom_user_id, nil # Proc.new { Proc.new{ |request| request.ip } }
|
119
|
+
# must be wrapped with another Proc because Sinatra
|
120
|
+
# evaluates Procs in settings when reading them.
|
121
|
+
app.set :rate_limiter_redis_conn, Redis.new
|
122
|
+
app.set :rate_limiter_redis_namespace, 'rate_limit'
|
123
|
+
app.set :rate_limiter_redis_expires, 24*60*60
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sinatra-rate-limiter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Warren Guy
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sinatra
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: redis
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
description: A redis based rate limiter for Sinatra
|
42
|
+
email: warren@guy.net.au
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- LICENSE
|
48
|
+
- README.md
|
49
|
+
- lib/sinatra/rate-limiter.rb
|
50
|
+
homepage: https://github.com/warrenguy/sinatra-rate-limiter
|
51
|
+
licenses:
|
52
|
+
- MIT
|
53
|
+
metadata: {}
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubyforge_project:
|
70
|
+
rubygems_version: 2.2.2
|
71
|
+
signing_key:
|
72
|
+
specification_version: 4
|
73
|
+
summary: A redis based rate limiter for Sinatra
|
74
|
+
test_files: []
|