rack-ratelimit 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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: