rack-ratelimit 1.1.2 → 1.2.0
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 +5 -5
- data/lib/rack/ratelimit.rb +48 -19
- metadata +8 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f495171388825bee8e7248a597406cad160d6d0fe2adfb03f9d33617ab136832
|
4
|
+
data.tar.gz: 4b6256bf56d9384a80265ac5b78a9f63809e29e08bc33918b87c22ae3717cc5d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c8eb47b3e77c43c16df7c984463366f78d0be4c5f46b425e15d027932d5d43be4771e16a8338325c631408229bc3011576daf888aaff0d021ba505a080e06a9f
|
7
|
+
data.tar.gz: 467d0a44fab4f3b6490a912ad1ed8b699b315d4158a2ce709ce2743bfa16ba9c614e151d941674c2b830e4ac406ff1305fc09ee6fb2612bf7176d21477852d55
|
data/lib/rack/ratelimit.rb
CHANGED
@@ -24,7 +24,7 @@ module Rack
|
|
24
24
|
# cache: a Dalli::Client instance
|
25
25
|
# redis: a Redis instance
|
26
26
|
# counter: Your own custom counter. Must respond to
|
27
|
-
# `#increment(classification_string,
|
27
|
+
# `#increment(classification_string, end_of_time_window_epoch_timestamp)`
|
28
28
|
# and return the counter value after increment.
|
29
29
|
#
|
30
30
|
# Optional configuration:
|
@@ -39,7 +39,10 @@ module Rack
|
|
39
39
|
# subsequently blocked requests.
|
40
40
|
# error_message: the message returned in the response body when the rate
|
41
41
|
# limit is exceeded. Defaults to "<name> rate limit exceeded. Please
|
42
|
-
# wait
|
42
|
+
# wait %d seconds then retry your request." The number of seconds
|
43
|
+
# until the end of the rate-limiting window is interpolated into the
|
44
|
+
# message string, but the %d placeholder is optional if you wish to
|
45
|
+
# omit it.
|
43
46
|
#
|
44
47
|
# Example:
|
45
48
|
#
|
@@ -78,7 +81,7 @@ module Rack
|
|
78
81
|
end
|
79
82
|
|
80
83
|
@logger = options[:logger]
|
81
|
-
@error_message = options.fetch(:error_message, "#{@name} rate limit exceeded. Please wait
|
84
|
+
@error_message = options.fetch(:error_message, "#{@name} rate limit exceeded. Please wait %d seconds then retry your request.")
|
82
85
|
|
83
86
|
@conditions = Array(options[:conditions])
|
84
87
|
@exceptions = Array(options[:exceptions])
|
@@ -120,33 +123,34 @@ module Rack
|
|
120
123
|
# * If it's the first request that exceeds the limit, log it.
|
121
124
|
# * If the count doesn't exceed the limit, pass through the request.
|
122
125
|
def call(env)
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
timestamp = @period * (Time.now.to_f / @period).ceil
|
127
|
-
time = Time.at(timestamp).utc.xmlschema
|
126
|
+
# Accept an optional start-of-request timestamp from the Rack env for
|
127
|
+
# upstream timing and for testing.
|
128
|
+
now = env.fetch('ratelimit.timestamp', Time.now).to_f
|
128
129
|
|
130
|
+
if apply_rate_limit?(env) && classification = classify(env)
|
129
131
|
# Increment the request counter.
|
130
|
-
|
132
|
+
epoch = ratelimit_epoch(now)
|
133
|
+
count = @counter.increment(classification, epoch)
|
131
134
|
remaining = @max - count
|
132
135
|
|
133
|
-
json = %({"name":"#{@name}","period":#{@period},"limit":#{@max},"remaining":#{remaining < 0 ? 0 : remaining},"until":"#{time}"})
|
134
|
-
|
135
136
|
# If exceeded, return a 429 Rate Limit Exceeded response.
|
136
137
|
if remaining < 0
|
137
138
|
# Only log the first hit that exceeds the limit.
|
138
139
|
if @logger && remaining == -1
|
139
|
-
@logger.info '%s: %s exceeded %d request limit for %s' % [@name, classification, @max,
|
140
|
+
@logger.info '%s: %s exceeded %d request limit for %s' % [@name, classification, @max, format_epoch(epoch)]
|
140
141
|
end
|
141
142
|
|
143
|
+
retry_after = seconds_until_epoch(epoch)
|
144
|
+
|
142
145
|
[ @status,
|
143
|
-
{ 'X-Ratelimit' =>
|
144
|
-
|
146
|
+
{ 'X-Ratelimit' => ratelimit_json(remaining, epoch),
|
147
|
+
'Retry-After' => retry_after.to_s },
|
148
|
+
[ @error_message % retry_after ] ]
|
145
149
|
|
146
150
|
# Otherwise, pass through then add some informational headers.
|
147
151
|
else
|
148
152
|
@app.call(env).tap do |status, headers, body|
|
149
|
-
headers
|
153
|
+
amend_headers headers, 'X-Ratelimit', ratelimit_json(remaining, epoch)
|
150
154
|
end
|
151
155
|
end
|
152
156
|
else
|
@@ -154,14 +158,39 @@ module Rack
|
|
154
158
|
end
|
155
159
|
end
|
156
160
|
|
161
|
+
private
|
162
|
+
# Calculate the end of the current rate-limiting window.
|
163
|
+
def ratelimit_epoch(timestamp)
|
164
|
+
@period * (timestamp / @period).ceil
|
165
|
+
end
|
166
|
+
|
167
|
+
def ratelimit_json(remaining, epoch)
|
168
|
+
%({"name":"#{@name}","period":#{@period},"limit":#{@max},"remaining":#{remaining < 0 ? 0 : remaining},"until":"#{format_epoch(epoch)}"})
|
169
|
+
end
|
170
|
+
|
171
|
+
def format_epoch(epoch)
|
172
|
+
Time.at(epoch).utc.xmlschema
|
173
|
+
end
|
174
|
+
|
175
|
+
# Clamp negative durations in case we're in a new rate-limiting window.
|
176
|
+
def seconds_until_epoch(epoch)
|
177
|
+
sec = (epoch - Time.now.to_f).ceil
|
178
|
+
sec = 0 if sec < 0
|
179
|
+
sec
|
180
|
+
end
|
181
|
+
|
182
|
+
def amend_headers(headers, name, value)
|
183
|
+
headers[name] = [headers[name], value].compact.join("\n")
|
184
|
+
end
|
185
|
+
|
157
186
|
class MemcachedCounter
|
158
187
|
def initialize(cache, name, period)
|
159
188
|
@cache, @name, @period = cache, name, period
|
160
189
|
end
|
161
190
|
|
162
191
|
# Increment the request counter and return the current count.
|
163
|
-
def increment(classification,
|
164
|
-
key = 'rack-ratelimit/%s/%s/%i' % [@name, classification,
|
192
|
+
def increment(classification, epoch)
|
193
|
+
key = 'rack-ratelimit/%s/%s/%i' % [@name, classification, epoch]
|
165
194
|
|
166
195
|
# Try to increment the counter if it's present.
|
167
196
|
if count = @cache.incr(key, 1)
|
@@ -184,8 +213,8 @@ module Rack
|
|
184
213
|
end
|
185
214
|
|
186
215
|
# Increment the request counter and return the current count.
|
187
|
-
def increment(classification,
|
188
|
-
key = 'rack-ratelimit/%s/%s/%i' % [@name, classification,
|
216
|
+
def increment(classification, epoch)
|
217
|
+
key = 'rack-ratelimit/%s/%s/%i' % [@name, classification, epoch]
|
189
218
|
|
190
219
|
# Returns [count, expire_ok] response for each multi command.
|
191
220
|
# Return the first, the count.
|
metadata
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-ratelimit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Daer
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-01-22 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description:
|
13
|
+
description:
|
14
14
|
email: jeremydaer@gmail.com
|
15
15
|
executables: []
|
16
16
|
extensions: []
|
@@ -22,7 +22,7 @@ homepage: https://github.com/jeremy/rack-ratelimit
|
|
22
22
|
licenses:
|
23
23
|
- MIT
|
24
24
|
metadata: {}
|
25
|
-
post_install_message:
|
25
|
+
post_install_message:
|
26
26
|
rdoc_options: []
|
27
27
|
require_paths:
|
28
28
|
- lib
|
@@ -30,17 +30,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '2.0'
|
34
34
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
35
35
|
requirements:
|
36
36
|
- - ">="
|
37
37
|
- !ruby/object:Gem::Version
|
38
38
|
version: '0'
|
39
39
|
requirements: []
|
40
|
-
|
41
|
-
|
42
|
-
signing_key:
|
40
|
+
rubygems_version: 3.1.4
|
41
|
+
signing_key:
|
43
42
|
specification_version: 4
|
44
43
|
summary: Flexible rate limits for your Rack apps
|
45
44
|
test_files: []
|
46
|
-
has_rdoc:
|