sinatra-rate-limiter 0.3.1 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +20 -1
- data/lib/sinatra/rate-limiter.rb +70 -56
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8adddf799a15960cccfb10260b144cd2b44f648d
|
4
|
+
data.tar.gz: 39755bb89f40a13226ca738c64f12c4f9c6b5493
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fa29b11d746463878859cf05f2a4061a9ae09b9748a5b0cbf79e94fd60c285314a840c2bb680f2de5da1cae1180188ed863e48a5b42ac332bca7a0edb07dc33d
|
7
|
+
data.tar.gz: df6410fd0b9c87ac9ffff38ec7670f0db34e58d44b9501447384cc19e93f7b6dff9226c4da6659feb9ad939ef514d80936ac37407f96818e0a03071ba7eb1f30
|
data/README.md
CHANGED
@@ -79,6 +79,7 @@ you must specify limits with each call of `rate_limit`
|
|
79
79
|
error_code: 429,
|
80
80
|
error_template: nil,
|
81
81
|
send_headers: true,
|
82
|
+
header_prefix: 'Rate-Limit',
|
82
83
|
identifier: Proc.new{ |request| request.ip }
|
83
84
|
}
|
84
85
|
```
|
@@ -129,10 +130,28 @@ provided (all Integers):
|
|
129
130
|
|
130
131
|
##### `send_headers` (Boolean)
|
131
132
|
|
132
|
-
Whether or not to send `
|
133
|
+
Whether or not to send `Rate-Limit-*` headers to the client with each
|
133
134
|
request. A `Retry-After` header is currently always sent when a rate
|
134
135
|
limit is reached regardless of this setting.
|
135
136
|
|
137
|
+
Three headers are sent per defined limit:
|
138
|
+
|
139
|
+
* `Rate-Limit-Limit` the number of requests allowed per period
|
140
|
+
* `Rate-Limit-Remaining` the number of requests left in the current period
|
141
|
+
* `Rate-Limit-Reset` the number of seconds remaining until the limit resets
|
142
|
+
|
143
|
+
If a bucket name is defined, it will be included in the header in the format
|
144
|
+
`Rate-Limit-Bucketname-*`. If more than one limit is defined, a number will
|
145
|
+
also be added to differentiate them, e.g `Rate-Limit-1-*`, `Rate-Limit-2-*`,
|
146
|
+
`Rate-Limit-Bucketname-1-*`, etc.
|
147
|
+
|
148
|
+
##### `header_prefix` (String)
|
149
|
+
|
150
|
+
Prefix for HTTP headers sent to client. Default is `Rate-Limit` (per
|
151
|
+
[RFC 6648](https://tools.ietf.org/html/rfc6648) deprecating the `X-` prefix)
|
152
|
+
however some users may wish or need to send `X-Rate-Limit` or some other
|
153
|
+
arbitrary header prefix instead.
|
154
|
+
|
136
155
|
##### `identifier` (Proc)
|
137
156
|
|
138
157
|
A `Proc` taking exactly one parameter (`request`) which returns a String
|
data/lib/sinatra/rate-limiter.rb
CHANGED
@@ -17,17 +17,18 @@ module Sinatra
|
|
17
17
|
limiter.request = request
|
18
18
|
limiter.options = options
|
19
19
|
|
20
|
-
if error_locals = limits_exceeded?
|
21
|
-
rate_limit_headers
|
20
|
+
if error_locals = limiter.limits_exceeded?
|
21
|
+
limiter.rate_limit_headers.each{|h,v| response.headers[h] = v} if limiter.options.send_headers
|
22
|
+
|
22
23
|
response.headers['Retry-After'] = error_locals[:try_again]
|
23
|
-
halt limiter.options.error_code, error_response(
|
24
|
+
halt limiter.options.error_code, error_response(limiter, error_locals)
|
24
25
|
end
|
25
26
|
|
26
|
-
redis
|
27
|
+
redis.setex([namespace,limiter.user_identifier,bucket,Time.now.to_f.to_s].join('/'),
|
27
28
|
settings.rate_limiter_redis_expires,
|
28
29
|
request.env['REQUEST_URI'])
|
29
30
|
|
30
|
-
rate_limit_headers
|
31
|
+
limiter.rate_limit_headers.each{|h,v| response.headers[h] = v} if limiter.options.send_headers
|
31
32
|
end
|
32
33
|
|
33
34
|
private
|
@@ -54,43 +55,22 @@ module Sinatra
|
|
54
55
|
limits.each_slice(2).map{|a| {requests: a[0], seconds: a[1]}}]
|
55
56
|
end
|
56
57
|
|
57
|
-
def redis
|
58
|
+
def redis
|
58
59
|
settings.rate_limiter_redis_conn
|
59
60
|
end
|
60
61
|
|
61
|
-
def namespace
|
62
|
+
def namespace
|
62
63
|
settings.rate_limiter_redis_namespace
|
63
64
|
end
|
64
65
|
|
65
|
-
def
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
def limit_reset(limit, limiter)
|
70
|
-
limit[:seconds] - (Time.now.to_f - limiter.history(limit[:seconds]).first.to_f).to_i
|
71
|
-
end
|
72
|
-
|
73
|
-
def limits_exceeded?(limits, limiter)
|
74
|
-
exceeded = limits.select {|limit| limit_remaining(limit, limiter) < 1}.sort_by{|e| e[:seconds]}.last
|
75
|
-
|
76
|
-
if exceeded
|
77
|
-
try_again = limit_reset(exceeded, limiter)
|
78
|
-
return exceeded.merge({try_again: try_again.to_i})
|
79
|
-
end
|
80
|
-
end
|
66
|
+
def get_min_time_prefix(limits)
|
67
|
+
now = Time.now.to_f
|
68
|
+
oldest = Time.now.to_f - limits.sort_by{|l| -l[:seconds]}.first[:seconds]
|
81
69
|
|
82
|
-
|
83
|
-
header_prefix = 'X-Rate-Limit' + (bucket.eql?('default') ? '' : '-' + bucket)
|
84
|
-
limit_no = 0 if limits.length > 1
|
85
|
-
limits.each do |limit|
|
86
|
-
limit_no = limit_no + 1 if limit_no
|
87
|
-
response.headers[header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Limit'] = limit[:requests]
|
88
|
-
response.headers[header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Remaining'] = limit_remaining(limit, limiter)
|
89
|
-
response.headers[header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Reset'] = limit_reset(limit, limiter)
|
90
|
-
end
|
70
|
+
return now.to_s[0..((now/oldest).to_s.split(/^1\.|[1-9]+/)[1].length)].to_i.to_s
|
91
71
|
end
|
92
72
|
|
93
|
-
def error_response(
|
73
|
+
def error_response(limiter, locals)
|
94
74
|
if limiter.options.error_template
|
95
75
|
render limiter.options.error_template, locals: locals
|
96
76
|
else
|
@@ -99,20 +79,6 @@ module Sinatra
|
|
99
79
|
end
|
100
80
|
end
|
101
81
|
|
102
|
-
def user_identifier(limiter)
|
103
|
-
if limiter.options.identifier.class == Proc
|
104
|
-
return limiter.options.identifier.call(request)
|
105
|
-
else
|
106
|
-
return request.ip
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
def get_min_time_prefix(limits)
|
111
|
-
now = Time.now.to_f
|
112
|
-
oldest = Time.now.to_f - limits.sort_by{|l| -l[:seconds]}.first[:seconds]
|
113
|
-
|
114
|
-
return now.to_s[0..((now/oldest).to_s.split(/^1\.|[1-9]+/)[1].length)].to_i.to_s
|
115
|
-
end
|
116
82
|
|
117
83
|
end
|
118
84
|
|
@@ -122,23 +88,22 @@ module Sinatra
|
|
122
88
|
app.set :rate_limiter, false
|
123
89
|
app.set :rate_limiter_environments, [:production]
|
124
90
|
app.set :rate_limiter_default_limits, [10, 20] # 10 requests per 20 seconds
|
125
|
-
app.set :rate_limiter_redis_conn, Redis.new
|
126
|
-
app.set :rate_limiter_redis_namespace, 'rate_limit'
|
127
|
-
app.set :rate_limiter_redis_expires, 24*60*60 # This must be larger than longest limit time period
|
128
|
-
|
129
91
|
app.set :rate_limiter_default_options, {
|
130
92
|
error_code: 429,
|
131
93
|
error_template: nil,
|
132
94
|
send_headers: true,
|
95
|
+
header_prefix: 'Rate-Limit',
|
133
96
|
identifier: Proc.new{ |request| request.ip }
|
134
97
|
}
|
98
|
+
|
99
|
+
app.set :rate_limiter_redis_conn, Redis.new
|
100
|
+
app.set :rate_limiter_redis_namespace, 'rate_limit'
|
101
|
+
app.set :rate_limiter_redis_expires, 24*60*60 # This must be larger than longest limit time period
|
135
102
|
end
|
136
103
|
|
137
104
|
end
|
138
105
|
|
139
106
|
class RateLimit
|
140
|
-
attr_reader :history, :options
|
141
|
-
|
142
107
|
def initialize(bucket, limits)
|
143
108
|
@bucket = bucket
|
144
109
|
@limits = limits
|
@@ -155,14 +120,63 @@ module Sinatra
|
|
155
120
|
if @history
|
156
121
|
@history
|
157
122
|
else
|
158
|
-
@history = redis
|
159
|
-
keys("#{[namespace
|
123
|
+
@history = redis.
|
124
|
+
keys("#{[namespace,user_identifier,@bucket].join('/')}/#{@time_prefix}*").
|
160
125
|
map{|k| k.split('/')[3].to_f}
|
161
126
|
end
|
162
127
|
end
|
163
128
|
|
129
|
+
def user_identifier
|
130
|
+
if options.identifier.class == Proc
|
131
|
+
return options.identifier.call(request)
|
132
|
+
else
|
133
|
+
return request.ip
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def rate_limit_headers
|
138
|
+
headers = []
|
139
|
+
|
140
|
+
header_prefix = options.header_prefix + (@bucket.eql?('default') ? '' : '-' + @bucket)
|
141
|
+
limit_no = 0 if @limits.length > 1
|
142
|
+
@limits.each do |limit|
|
143
|
+
limit_no = limit_no + 1 if limit_no
|
144
|
+
headers << [header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Limit', limit[:requests]]
|
145
|
+
headers << [header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Remaining', limit_remaining(limit)]
|
146
|
+
headers << [header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Reset', limit_reset(limit)]
|
147
|
+
end
|
148
|
+
|
149
|
+
return headers
|
150
|
+
end
|
151
|
+
|
152
|
+
def limit_remaining(limit)
|
153
|
+
limit[:requests] - history(limit[:seconds]).length
|
154
|
+
end
|
155
|
+
|
156
|
+
def limit_reset(limit)
|
157
|
+
limit[:seconds] - (Time.now.to_f - history(limit[:seconds]).first.to_f).to_i
|
158
|
+
end
|
159
|
+
|
160
|
+
def limits_exceeded?
|
161
|
+
exceeded = @limits.select {|limit| limit_remaining(limit) < 1}.sort_by{|e| e[:seconds]}.last
|
162
|
+
|
163
|
+
if exceeded
|
164
|
+
try_again = limit_reset(exceeded)
|
165
|
+
return exceeded.merge({try_again: try_again.to_i})
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def redis
|
170
|
+
settings.rate_limiter_redis_conn
|
171
|
+
end
|
172
|
+
|
173
|
+
def namespace
|
174
|
+
settings.rate_limiter_redis_namespace
|
175
|
+
end
|
176
|
+
|
164
177
|
def options=(options)
|
165
|
-
|
178
|
+
options = settings.rate_limiter_default_options.merge(options)
|
179
|
+
@options = Struct.new(*options.keys).new(*options.values)
|
166
180
|
end
|
167
181
|
def options
|
168
182
|
@options
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sinatra-rate-limiter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Warren Guy
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-05-
|
11
|
+
date: 2015-05-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sinatra
|