sinatra-rate-limiter 0.3.1 → 0.3.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.
- 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
|