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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -6
  3. data/lib/sinatra/rate-limiter.rb +63 -71
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e2eb98d3fb4708f2dde94dc7aa8fac5e790eea55
4
- data.tar.gz: eff8f03476da900eff856f94b20989608cd07c1d
3
+ metadata.gz: 3442a4980f8b129023849abb927d12628d3264a2
4
+ data.tar.gz: f2a4160d6df1e036eb16d214fff7a05c6fa23705
5
5
  SHA512:
6
- metadata.gz: cc4c520d384178af38c646dd55aedfcf08ee89d96a464825b4c26f3ddc66e3084e9a0a2831d2e68772073c75dd583b97636acb9a29b0fd96aec670ba2c22661b
7
- data.tar.gz: 409c06eefea02caf1cea1c6a7b8a5fa662abaa7647a11660011c6a30e2c5ff2fada3e04e8f565a6e5f75e3e5452353922476b1bf4d2c4e1e2b568d07a996cb26
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 three values for the exceeded limit:
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
 
@@ -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 or nil' if (!value.nil? and value.class != 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
- end
99
+ class RateLimit
100
+ attr_accessor :settings, :request, :options
100
101
 
101
- class RateLimit
102
- attr_accessor :settings, :request, :options
102
+ def initialize(bucket, limits)
103
+ @bucket = bucket
104
+ @limits = limits
105
+ @time_prefix = get_min_time_prefix(@limits)
106
+ end
103
107
 
104
- def initialize(bucket, limits)
105
- @bucket = bucket
106
- @limits = limits
107
- @time_prefix = get_min_time_prefix(@limits)
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
- def options=(options)
111
- options = settings.rate_limiter_default_options.merge(options)
112
- @options = Struct.new(*options.keys).new(*options.values)
113
- end
113
+ def identifier
114
+ @identifier ||= @options.identifier.call(request)
115
+ end
114
116
 
115
- def history(seconds=0)
116
- redis_history.select{|t| seconds.eql?(0) ? true : t > (Time.now.to_f - seconds)}
117
- end
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
- def headers
120
- headers = []
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
- header_prefix = @options.header_prefix + (@bucket.eql?('default') ? '' : '-' + @bucket)
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
- return headers
132
- end
136
+ def limit_remaining(limit)
137
+ limit[:requests] - history(limit[:seconds]).length
138
+ end
133
139
 
134
- def limit_remaining(limit)
135
- limit[:requests] - history(limit[:seconds]).length
136
- end
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
- def limit_reset(limit)
139
- limit[:seconds] - (Time.now.to_f - history(limit[:seconds]).first.to_f).to_i
140
- end
144
+ def limits_exceeded?
145
+ exceeded = @limits.select {|limit| limit_remaining(limit) < 1}.sort_by{|e| e[:seconds]}.last
141
146
 
142
- def limits_exceeded?
143
- exceeded = @limits.select {|limit| limit_remaining(limit) < 1}.sort_by{|e| e[:seconds]}.last
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
- if exceeded
146
- try_again = limit_reset(exceeded)
147
- return exceeded.merge({try_again: try_again.to_i, bucket: @bucket})
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
- def log_request
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
- private
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
- def redis_history
161
- if @history
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
- def user_identifier
171
- if @options.identifier.class == Proc
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
- def redis
179
- @settings.rate_limiter_redis_conn
180
- end
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
- def namespace
183
- @settings.rate_limiter_redis_namespace
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sinatra-rate-limiter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Warren Guy