sinatra-rate-limiter 0.3.1 → 0.3.2

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