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.
Files changed (3) hide show
  1. checksums.yaml +5 -5
  2. data/lib/rack/ratelimit.rb +48 -19
  3. metadata +8 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: da76df57ad5b54ed09e61c19e80a8d16ecce6a2c
4
- data.tar.gz: 8a393903a899cfa721e35597b3730a29a1edbf74
2
+ SHA256:
3
+ metadata.gz: f495171388825bee8e7248a597406cad160d6d0fe2adfb03f9d33617ab136832
4
+ data.tar.gz: 4b6256bf56d9384a80265ac5b78a9f63809e29e08bc33918b87c22ae3717cc5d
5
5
  SHA512:
6
- metadata.gz: 7602ad3237ac2e8263fed3209dc7b6830a4383338ff0f7fb213ccb325b642faa46eae336bce2a84d5e3468e9a4e45bd40eba7ef9274ca8f9a81d4be5929306ef
7
- data.tar.gz: 78559b12b276864f97dce761ed7d192c36fbc4c256f880aef53236a119c6c7b8db37febf9be90c3a5ff3ce8d07847fe46140862c61e076f0be43e21124c2611a
6
+ metadata.gz: c8eb47b3e77c43c16df7c984463366f78d0be4c5f46b425e15d027932d5d43be4771e16a8338325c631408229bc3011576daf888aaff0d021ba505a080e06a9f
7
+ data.tar.gz: 467d0a44fab4f3b6490a912ad1ed8b699b315d4158a2ce709ce2743bfa16ba9c614e151d941674c2b830e4ac406ff1305fc09ee6fb2612bf7176d21477852d55
@@ -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, end_of_time_window_timestamp)`
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 <period> seconds then retry your request."
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 #{@period} seconds then retry your request.")
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
- if apply_rate_limit?(env) && classification = classify(env)
124
-
125
- # Marks the end of the current rate-limiting window.
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
- count = @counter.increment(classification, timestamp)
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, time]
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' => json, 'Retry-After' => @period.to_s },
144
- [@error_message] ]
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['X-Ratelimit'] = [headers['X-Ratelimit'], json].compact.join("\n")
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, timestamp)
164
- key = 'rack-ratelimit/%s/%s/%i' % [@name, classification, timestamp]
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, timestamp)
188
- key = 'rack-ratelimit/%s/%s/%i' % [@name, classification, timestamp]
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.1.2
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: 2016-06-02 00:00:00.000000000 Z
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: '1.8'
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
- rubyforge_project:
41
- rubygems_version: 2.6.4
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: