sinatra-rate-limiter 0.4.1 → 0.4.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 +6 -6
- data/lib/sinatra/rate-limiter.rb +63 -71
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3442a4980f8b129023849abb927d12628d3264a2
|
4
|
+
data.tar.gz: f2a4160d6df1e036eb16d214fff7a05c6fa23705
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 014e4148653366b3bb5bdfd8bfe42f378688c13803dbeacfa80a5ba88b9485377f2cb9fe1da7f68c9f799c40411ca43822db32b370ef48971ba62a7c3e91e3bb
|
7
|
+
data.tar.gz: ba1a7361fc46fe41d97ca057ffce1ac547ad49d456ea8b25760dd49a2baeed9b747b65a61c7b8d71e332020a66e66194978202298821f9c16fa2a2ed5de5d201
|
data/README.md
CHANGED
@@ -52,7 +52,7 @@ Use `rate_limit` in the pipeline of any route (i.e. in the route itself, or
|
|
52
52
|
in a `before` filter, or in a Padrino controller, etc. `rate_limit` takes
|
53
53
|
zero to infinite parameters, with the syntax:
|
54
54
|
|
55
|
-
```
|
55
|
+
```ruby
|
56
56
|
rate_limit [BucketName], [[<Requests>, <Seconds>], ...], [[<Key>: <Value>], ...]
|
57
57
|
```
|
58
58
|
|
@@ -67,11 +67,10 @@ See the _Examples_ section below for usage examples.
|
|
67
67
|
|
68
68
|
When a rate limit is exceeded, the exception `Sinatra::RateLimiter::Exceeded`
|
69
69
|
is thrown. By default, this sends an response code `429` with a simple plain
|
70
|
-
text error message. You can use Sinatra's error handling to customise this
|
71
|
-
|
72
|
-
E.g.:
|
70
|
+
text error message. You can use Sinatra's error handling to customise this,
|
71
|
+
for example to provide a JSON response with status code 400:
|
73
72
|
|
74
|
-
```
|
73
|
+
```ruby
|
75
74
|
error Sinatra::RateLimiter::Exceeded do
|
76
75
|
status 400
|
77
76
|
content_type :json
|
@@ -82,11 +81,12 @@ E.g.:
|
|
82
81
|
|
83
82
|
As well as the default error message being available in
|
84
83
|
`env['sinatra.error'].message`, `the env['sinatra.error.rate_limiter']`
|
85
|
-
object contains
|
84
|
+
object contains four values for the exceeded limit:
|
86
85
|
|
87
86
|
* `.requests` Integer of the number of requests allowed
|
88
87
|
* `.seconds` Integer of the number of seconds the request limit applies to
|
89
88
|
* `.try_again` Integer the number of seconds until the limit resets
|
89
|
+
* `.bucket` Name of the triggering bucket
|
90
90
|
|
91
91
|
## Configuration
|
92
92
|
|
data/lib/sinatra/rate-limiter.rb
CHANGED
@@ -56,7 +56,7 @@ module Sinatra
|
|
56
56
|
when :header_prefix
|
57
57
|
raise ArgumentError, 'header_prefix must be a String' if value.class != String
|
58
58
|
when :identifier
|
59
|
-
raise ArgumentError, 'identifier must be a Proc
|
59
|
+
raise ArgumentError, 'identifier must be a Proc' if value.class != Proc
|
60
60
|
else
|
61
61
|
raise ArgumentError, "Invalid option #{option}"
|
62
62
|
end
|
@@ -96,99 +96,91 @@ module Sinatra
|
|
96
96
|
end
|
97
97
|
end
|
98
98
|
|
99
|
-
|
99
|
+
class RateLimit
|
100
|
+
attr_accessor :settings, :request, :options
|
100
101
|
|
101
|
-
|
102
|
-
|
102
|
+
def initialize(bucket, limits)
|
103
|
+
@bucket = bucket
|
104
|
+
@limits = limits
|
105
|
+
@time_prefix = get_min_time_prefix(@limits)
|
106
|
+
end
|
103
107
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
end
|
108
|
+
def options=(options)
|
109
|
+
options = settings.rate_limiter_default_options.merge(options)
|
110
|
+
@options = Struct.new(*options.keys).new(*options.values)
|
111
|
+
end
|
109
112
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
end
|
113
|
+
def identifier
|
114
|
+
@identifier ||= @options.identifier.call(request)
|
115
|
+
end
|
114
116
|
|
115
|
-
|
116
|
-
|
117
|
-
|
117
|
+
def history(seconds=0)
|
118
|
+
redis_history.select{|t| seconds.eql?(0) ? true : t > (Time.now.to_f - seconds)}
|
119
|
+
end
|
120
|
+
|
121
|
+
def headers
|
122
|
+
headers = []
|
118
123
|
|
119
|
-
|
120
|
-
|
124
|
+
header_prefix = @options.header_prefix + (@bucket.eql?('default') ? '' : '-' + @bucket)
|
125
|
+
limit_no = 0 if @limits.length > 1
|
126
|
+
@limits.each do |limit|
|
127
|
+
limit_no = limit_no + 1 if limit_no
|
128
|
+
headers << [header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Limit', limit[:requests]]
|
129
|
+
headers << [header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Remaining', limit_remaining(limit)]
|
130
|
+
headers << [header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Reset', limit_reset(limit)]
|
131
|
+
end
|
121
132
|
|
122
|
-
|
123
|
-
limit_no = 0 if @limits.length > 1
|
124
|
-
@limits.each do |limit|
|
125
|
-
limit_no = limit_no + 1 if limit_no
|
126
|
-
headers << [header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Limit', limit[:requests]]
|
127
|
-
headers << [header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Remaining', limit_remaining(limit)]
|
128
|
-
headers << [header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Reset', limit_reset(limit)]
|
133
|
+
return headers
|
129
134
|
end
|
130
135
|
|
131
|
-
|
132
|
-
|
136
|
+
def limit_remaining(limit)
|
137
|
+
limit[:requests] - history(limit[:seconds]).length
|
138
|
+
end
|
133
139
|
|
134
|
-
|
135
|
-
|
136
|
-
|
140
|
+
def limit_reset(limit)
|
141
|
+
limit[:seconds] - (Time.now.to_f - history(limit[:seconds]).first.to_f).to_i
|
142
|
+
end
|
137
143
|
|
138
|
-
|
139
|
-
|
140
|
-
end
|
144
|
+
def limits_exceeded?
|
145
|
+
exceeded = @limits.select {|limit| limit_remaining(limit) < 1}.sort_by{|e| e[:seconds]}.last
|
141
146
|
|
142
|
-
|
143
|
-
|
147
|
+
if exceeded
|
148
|
+
try_again = limit_reset(exceeded)
|
149
|
+
return exceeded.merge({try_again: try_again.to_i, bucket: @bucket})
|
150
|
+
end
|
151
|
+
end
|
144
152
|
|
145
|
-
|
146
|
-
|
147
|
-
|
153
|
+
def log_request
|
154
|
+
redis.setex(
|
155
|
+
[namespace, identifier, @bucket, Time.now.to_f.to_s].join('/'),
|
156
|
+
@settings.rate_limiter_redis_expires,
|
157
|
+
nil)
|
148
158
|
end
|
149
|
-
end
|
150
159
|
|
151
|
-
|
152
|
-
redis.setex(
|
153
|
-
[namespace, user_identifier, @bucket, Time.now.to_f.to_s].join('/'),
|
154
|
-
@settings.rate_limiter_redis_expires,
|
155
|
-
nil)
|
156
|
-
end
|
160
|
+
private
|
157
161
|
|
158
|
-
|
162
|
+
def redis_history
|
163
|
+
@history ||= redis.
|
164
|
+
keys("#{[namespace,identifier,@bucket].join('/')}/#{@time_prefix}*").
|
165
|
+
map{|k| k.split('/')[3].to_f}
|
166
|
+
end
|
159
167
|
|
160
|
-
|
161
|
-
|
162
|
-
@history
|
163
|
-
else
|
164
|
-
@history = redis.
|
165
|
-
keys("#{[namespace,user_identifier,@bucket].join('/')}/#{@time_prefix}*").
|
166
|
-
map{|k| k.split('/')[3].to_f}
|
168
|
+
def redis
|
169
|
+
@settings.rate_limiter_redis_conn
|
167
170
|
end
|
168
|
-
end
|
169
171
|
|
170
|
-
|
171
|
-
|
172
|
-
return @options.identifier.call(request)
|
173
|
-
else
|
174
|
-
return request.ip
|
172
|
+
def namespace
|
173
|
+
@settings.rate_limiter_redis_namespace
|
175
174
|
end
|
176
|
-
end
|
177
175
|
|
178
|
-
|
179
|
-
|
180
|
-
|
176
|
+
def get_min_time_prefix(limits)
|
177
|
+
now = Time.now.to_f
|
178
|
+
oldest = Time.now.to_f - limits.sort_by{|l| -l[:seconds]}.first[:seconds]
|
181
179
|
|
182
|
-
|
183
|
-
|
180
|
+
return now.to_s[0..((now/oldest).to_s.split(/^1\.|[1-9]+/)[1].length)].to_i.to_s
|
181
|
+
end
|
184
182
|
end
|
185
183
|
|
186
|
-
def get_min_time_prefix(limits)
|
187
|
-
now = Time.now.to_f
|
188
|
-
oldest = Time.now.to_f - limits.sort_by{|l| -l[:seconds]}.first[:seconds]
|
189
|
-
|
190
|
-
return now.to_s[0..((now/oldest).to_s.split(/^1\.|[1-9]+/)[1].length)].to_i.to_s
|
191
|
-
end
|
192
184
|
end
|
193
185
|
|
194
186
|
register RateLimiter
|