sinatra-rate-limiter 0.3.1 → 0.3.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 +20 -1
  3. data/lib/sinatra/rate-limiter.rb +70 -56
  4. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 006ef1d8689384395b156b7c636c4745287d07f0
4
- data.tar.gz: 127f8f5d68dcd6898e8d21e6ac0fdfbaf16858b3
3
+ metadata.gz: 8adddf799a15960cccfb10260b144cd2b44f648d
4
+ data.tar.gz: 39755bb89f40a13226ca738c64f12c4f9c6b5493
5
5
  SHA512:
6
- metadata.gz: 4360d48ffd69e8274b6e2e504547ce58847fa98ba153aedaf7c9e959c4690ac87a01131b006aa023d9b5f0485c2e609cb200d4e6031b5ba7b52c208bef9907ab
7
- data.tar.gz: 20133bd94e537603a00de43971cd840a46d3212d8a046b115a0177801df7ff28299391c126cebafb550849088996fb0ded7ad25af552535c2c758e0ceaae902b
6
+ metadata.gz: fa29b11d746463878859cf05f2a4061a9ae09b9748a5b0cbf79e94fd60c285314a840c2bb680f2de5da1cae1180188ed863e48a5b42ac332bca7a0edb07dc33d
7
+ data.tar.gz: df6410fd0b9c87ac9ffff38ec7670f0db34e58d44b9501447384cc19e93f7b6dff9226c4da6659feb9ad939ef514d80936ac37407f96818e0a03071ba7eb1f30
data/README.md CHANGED
@@ -79,6 +79,7 @@ you must specify limits with each call of `rate_limit`
79
79
  error_code: 429,
80
80
  error_template: nil,
81
81
  send_headers: true,
82
+ header_prefix: 'Rate-Limit',
82
83
  identifier: Proc.new{ |request| request.ip }
83
84
  }
84
85
  ```
@@ -129,10 +130,28 @@ provided (all Integers):
129
130
 
130
131
  ##### `send_headers` (Boolean)
131
132
 
132
- Whether or not to send `X-RateLimit-` headers to the client with each
133
+ Whether or not to send `Rate-Limit-*` headers to the client with each
133
134
  request. A `Retry-After` header is currently always sent when a rate
134
135
  limit is reached regardless of this setting.
135
136
 
137
+ Three headers are sent per defined limit:
138
+
139
+ * `Rate-Limit-Limit` the number of requests allowed per period
140
+ * `Rate-Limit-Remaining` the number of requests left in the current period
141
+ * `Rate-Limit-Reset` the number of seconds remaining until the limit resets
142
+
143
+ If a bucket name is defined, it will be included in the header in the format
144
+ `Rate-Limit-Bucketname-*`. If more than one limit is defined, a number will
145
+ also be added to differentiate them, e.g `Rate-Limit-1-*`, `Rate-Limit-2-*`,
146
+ `Rate-Limit-Bucketname-1-*`, etc.
147
+
148
+ ##### `header_prefix` (String)
149
+
150
+ Prefix for HTTP headers sent to client. Default is `Rate-Limit` (per
151
+ [RFC 6648](https://tools.ietf.org/html/rfc6648) deprecating the `X-` prefix)
152
+ however some users may wish or need to send `X-Rate-Limit` or some other
153
+ arbitrary header prefix instead.
154
+
136
155
  ##### `identifier` (Proc)
137
156
 
138
157
  A `Proc` taking exactly one parameter (`request`) which returns a String
@@ -17,17 +17,18 @@ module Sinatra
17
17
  limiter.request = request
18
18
  limiter.options = options
19
19
 
20
- if error_locals = limits_exceeded?(limits, limiter)
21
- rate_limit_headers(limits, bucket, limiter) if limiter.options.send_headers
20
+ if error_locals = limiter.limits_exceeded?
21
+ limiter.rate_limit_headers.each{|h,v| response.headers[h] = v} if limiter.options.send_headers
22
+
22
23
  response.headers['Retry-After'] = error_locals[:try_again]
23
- halt limiter.options.error_code, error_response(error_locals, limiter)
24
+ halt limiter.options.error_code, error_response(limiter, error_locals)
24
25
  end
25
26
 
26
- redis(limiter).setex([namespace(limiter),user_identifier(limiter),bucket,Time.now.to_f.to_s].join('/'),
27
+ redis.setex([namespace,limiter.user_identifier,bucket,Time.now.to_f.to_s].join('/'),
27
28
  settings.rate_limiter_redis_expires,
28
29
  request.env['REQUEST_URI'])
29
30
 
30
- rate_limit_headers(limits, bucket, limiter) if limiter.options.send_headers
31
+ limiter.rate_limit_headers.each{|h,v| response.headers[h] = v} if limiter.options.send_headers
31
32
  end
32
33
 
33
34
  private
@@ -54,43 +55,22 @@ module Sinatra
54
55
  limits.each_slice(2).map{|a| {requests: a[0], seconds: a[1]}}]
55
56
  end
56
57
 
57
- def redis(limiter)
58
+ def redis
58
59
  settings.rate_limiter_redis_conn
59
60
  end
60
61
 
61
- def namespace(limiter)
62
+ def namespace
62
63
  settings.rate_limiter_redis_namespace
63
64
  end
64
65
 
65
- def limit_remaining(limit, limiter)
66
- limit[:requests] - limiter.history(limit[:seconds]).length
67
- end
68
-
69
- def limit_reset(limit, limiter)
70
- limit[:seconds] - (Time.now.to_f - limiter.history(limit[:seconds]).first.to_f).to_i
71
- end
72
-
73
- def limits_exceeded?(limits, limiter)
74
- exceeded = limits.select {|limit| limit_remaining(limit, limiter) < 1}.sort_by{|e| e[:seconds]}.last
75
-
76
- if exceeded
77
- try_again = limit_reset(exceeded, limiter)
78
- return exceeded.merge({try_again: try_again.to_i})
79
- end
80
- end
66
+ def get_min_time_prefix(limits)
67
+ now = Time.now.to_f
68
+ oldest = Time.now.to_f - limits.sort_by{|l| -l[:seconds]}.first[:seconds]
81
69
 
82
- def rate_limit_headers(limits, bucket, limiter)
83
- header_prefix = 'X-Rate-Limit' + (bucket.eql?('default') ? '' : '-' + bucket)
84
- limit_no = 0 if limits.length > 1
85
- limits.each do |limit|
86
- limit_no = limit_no + 1 if limit_no
87
- response.headers[header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Limit'] = limit[:requests]
88
- response.headers[header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Remaining'] = limit_remaining(limit, limiter)
89
- response.headers[header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Reset'] = limit_reset(limit, limiter)
90
- end
70
+ return now.to_s[0..((now/oldest).to_s.split(/^1\.|[1-9]+/)[1].length)].to_i.to_s
91
71
  end
92
72
 
93
- def error_response(locals, limiter)
73
+ def error_response(limiter, locals)
94
74
  if limiter.options.error_template
95
75
  render limiter.options.error_template, locals: locals
96
76
  else
@@ -99,20 +79,6 @@ module Sinatra
99
79
  end
100
80
  end
101
81
 
102
- def user_identifier(limiter)
103
- if limiter.options.identifier.class == Proc
104
- return limiter.options.identifier.call(request)
105
- else
106
- return request.ip
107
- end
108
- end
109
-
110
- def get_min_time_prefix(limits)
111
- now = Time.now.to_f
112
- oldest = Time.now.to_f - limits.sort_by{|l| -l[:seconds]}.first[:seconds]
113
-
114
- return now.to_s[0..((now/oldest).to_s.split(/^1\.|[1-9]+/)[1].length)].to_i.to_s
115
- end
116
82
 
117
83
  end
118
84
 
@@ -122,23 +88,22 @@ module Sinatra
122
88
  app.set :rate_limiter, false
123
89
  app.set :rate_limiter_environments, [:production]
124
90
  app.set :rate_limiter_default_limits, [10, 20] # 10 requests per 20 seconds
125
- app.set :rate_limiter_redis_conn, Redis.new
126
- app.set :rate_limiter_redis_namespace, 'rate_limit'
127
- app.set :rate_limiter_redis_expires, 24*60*60 # This must be larger than longest limit time period
128
-
129
91
  app.set :rate_limiter_default_options, {
130
92
  error_code: 429,
131
93
  error_template: nil,
132
94
  send_headers: true,
95
+ header_prefix: 'Rate-Limit',
133
96
  identifier: Proc.new{ |request| request.ip }
134
97
  }
98
+
99
+ app.set :rate_limiter_redis_conn, Redis.new
100
+ app.set :rate_limiter_redis_namespace, 'rate_limit'
101
+ app.set :rate_limiter_redis_expires, 24*60*60 # This must be larger than longest limit time period
135
102
  end
136
103
 
137
104
  end
138
105
 
139
106
  class RateLimit
140
- attr_reader :history, :options
141
-
142
107
  def initialize(bucket, limits)
143
108
  @bucket = bucket
144
109
  @limits = limits
@@ -155,14 +120,63 @@ module Sinatra
155
120
  if @history
156
121
  @history
157
122
  else
158
- @history = redis(self).
159
- keys("#{[namespace(self),user_identifier(self),@bucket].join('/')}/#{@time_prefix}*").
123
+ @history = redis.
124
+ keys("#{[namespace,user_identifier,@bucket].join('/')}/#{@time_prefix}*").
160
125
  map{|k| k.split('/')[3].to_f}
161
126
  end
162
127
  end
163
128
 
129
+ def user_identifier
130
+ if options.identifier.class == Proc
131
+ return options.identifier.call(request)
132
+ else
133
+ return request.ip
134
+ end
135
+ end
136
+
137
+ def rate_limit_headers
138
+ headers = []
139
+
140
+ header_prefix = options.header_prefix + (@bucket.eql?('default') ? '' : '-' + @bucket)
141
+ limit_no = 0 if @limits.length > 1
142
+ @limits.each do |limit|
143
+ limit_no = limit_no + 1 if limit_no
144
+ headers << [header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Limit', limit[:requests]]
145
+ headers << [header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Remaining', limit_remaining(limit)]
146
+ headers << [header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Reset', limit_reset(limit)]
147
+ end
148
+
149
+ return headers
150
+ end
151
+
152
+ def limit_remaining(limit)
153
+ limit[:requests] - history(limit[:seconds]).length
154
+ end
155
+
156
+ def limit_reset(limit)
157
+ limit[:seconds] - (Time.now.to_f - history(limit[:seconds]).first.to_f).to_i
158
+ end
159
+
160
+ def limits_exceeded?
161
+ exceeded = @limits.select {|limit| limit_remaining(limit) < 1}.sort_by{|e| e[:seconds]}.last
162
+
163
+ if exceeded
164
+ try_again = limit_reset(exceeded)
165
+ return exceeded.merge({try_again: try_again.to_i})
166
+ end
167
+ end
168
+
169
+ def redis
170
+ settings.rate_limiter_redis_conn
171
+ end
172
+
173
+ def namespace
174
+ settings.rate_limiter_redis_namespace
175
+ end
176
+
164
177
  def options=(options)
165
- @options = OpenStruct.new(settings.rate_limiter_default_options.merge(options))
178
+ options = settings.rate_limiter_default_options.merge(options)
179
+ @options = Struct.new(*options.keys).new(*options.values)
166
180
  end
167
181
  def options
168
182
  @options
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sinatra-rate-limiter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Warren Guy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-30 00:00:00.000000000 Z
11
+ date: 2015-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sinatra