sinatra-rate-limiter 0.2.1 → 0.2.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 +31 -4
  3. data/lib/sinatra/rate-limiter.rb +69 -24
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 030b21e819260562c4da8c1472360e4d8cdb1c8d
4
- data.tar.gz: 456ad3d77b05fe72a5d01aba6ee3699b7da49916
3
+ metadata.gz: 284920f0a11fd397c41ae1bd5e31f6e1f45bb846
4
+ data.tar.gz: 58c8cc97bbab02f53ba781fbc4e1ef0ba0901eb5
5
5
  SHA512:
6
- metadata.gz: 920f5b487a6dd84cd220a27869811d834102ff591a68d2446758b851546893af17f1e1dbdf8fbb9b44f9bf88547089263bc30c0af137c786b3bc2db710cd2303
7
- data.tar.gz: 14427e370cc5f040287edfba31247480fac29bb2c66a7b06199df0e7f778f9c75c35cb840b926253bca41fe17c3748cabc65dd1c88076fbf50cfdd53a0b36a72
6
+ metadata.gz: 589661c3cd2635d4fad8c7ad421ef3ca02cc22c6dff9e7bcbb6dc495820d99c2b1063a530057907499a2725404c198cbf394d38a34362fd9dcc1e4afbe670554
7
+ data.tar.gz: d279915ae972605f568070b3e0d63b2a35963b5a805dc82c3ed75fbe80436d5c9b5cc632fd2fffbb81f316962b94751411a40cf28efe7152af0a6d55c950ed82
data/README.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  A customisable redis backed rate limiter for Sinatra applications.
4
4
 
5
+ This rate limiter extension operates on a leaky bucket principle. Each
6
+ request that the rate limiter sees logs a new item in the redis store. If
7
+ more than the allowable number of requests have been made in the given time
8
+ then no new item is logged and the request is aborted. The items stored in
9
+ redis include a bucket name and timestamp in their key name. This allows
10
+ multiple limit "buckets" to be used and for variable rate limits to be
11
+ applied to different requests using the same bucket. See the _Usage_ section
12
+ below for examples demonstrating this.
13
+
5
14
  ## Installing
6
15
 
7
16
  * Add the gem to your Gemfile
@@ -24,7 +33,14 @@ Use `rate_limit` in the pipeline of any route (i.e. in the route itself, or
24
33
  in a `before` filter, or in a Padrino controller, etc. `rate_limit` takes
25
34
  zero to infinite parameters, with the syntax:
26
35
 
27
- ```rate_limit [String], [<Fixnum>, <Fixnum>], [<Fixnum>, <Fixnum>], ...```
36
+ ```
37
+ rate_limit [String], [[<Fixnum>, <Fixnum>], [<Fixnum>, <Fixnum>], ...]
38
+ ```
39
+
40
+ The `String` optionally defines a name for this rate limiter, allowing you
41
+ to have multiple rate limits within your app. The following pairs of
42
+ `Fixnum`s define `[requests, seconds]`, allowing you to specify how many
43
+ requests per seconds this rate limiter allows.
28
44
 
29
45
  The following route will be limited to 10 requests per minute and 100
30
46
  requests per hour:
@@ -50,20 +66,31 @@ routes and stricter individual rate limits to two particular routes:
50
66
  "this route has the global limit applied"
51
67
  end
52
68
 
53
- get '/rate-limit-1' do
69
+ get '/rate-limit-1/example-1' do
54
70
  rate_limit 'ratelimit1', 2, 5,
55
71
  10, 60
72
+
73
+ "this route is rate limited to 2 requests per 5 seconds and 10 per 60
74
+ seconds"
56
75
  end
57
76
 
77
+ get '/rate-limit-1/example-2' do
78
+ rate_limit 'ratelimit1', 60, 60
79
+
80
+ "this route is rate limited to 60 requests per minute using the same
81
+ bucket as '/rate-limit-1'. "
82
+
58
83
  get '/rate-limit-2' do
59
84
  rate_limit 'ratelimit2', 1, 10
85
+
86
+ "this route is rate limited to 1 request per 10 seconds"
60
87
  end
61
88
  ```
62
89
 
63
90
  N.B. in the last example, be aware that the more specific rate limits do not
64
91
  override any rate limit already defined during route processing, and the
65
- global rate limit will apply additionally. If you call `rate_limit` more than
66
- once with the same (or no) name, it will be double counted.
92
+ global rate limit will apply additionally. If you call `rate_limit` more
93
+ than once with the same (or no) name, it will be double counted.
67
94
 
68
95
  ## Configuration
69
96
 
@@ -12,8 +12,12 @@ module Sinatra
12
12
 
13
13
  limit_name, limits = parse_args(args)
14
14
 
15
- if error_locals = limits_exceeded?(limits, limit_name)
16
- rate_limit_headers(limits, limit_name)
15
+ limiter = RateLimit.new(limit_name, limits)
16
+ limiter.settings = settings
17
+ limiter.request = request
18
+
19
+ if error_locals = limits_exceeded?(limits, limiter)
20
+ rate_limit_headers(limits, limit_name, limiter)
17
21
  response.headers['Retry-After'] = error_locals[:try_again] if settings.rate_limiter_send_headers
18
22
  halt settings.rate_limiter_error_code, error_response(error_locals)
19
23
  end
@@ -22,7 +26,7 @@ module Sinatra
22
26
  settings.rate_limiter_redis_expires,
23
27
  request.env['REQUEST_URI'])
24
28
 
25
- rate_limit_headers(limits, limit_name) if settings.rate_limiter_send_headers
29
+ rate_limit_headers(limits, limit_name, limiter) if settings.rate_limiter_send_headers
26
30
  end
27
31
 
28
32
  private
@@ -37,11 +41,12 @@ module Sinatra
37
41
  raise ArgumentError, 'Non-Fixnum parameters supplied. All parameters must be Fixnum except the first which may be a String.'
38
42
  elsif ((args.map{|a| a.class}.size % 2) != 0)
39
43
  raise ArgumentError, 'Wrong number of Fixnum parameters supplied.'
44
+ elsif !(limit_name =~ /^[a-zA-Z0-9\-]*$/)
45
+ raise ArgumentError, 'Limit name must be a String containing only a-z, A-Z, 0-9, and -.'
40
46
  end
41
47
 
42
- limits = args.each_slice(2).to_a.map{|a| {requests: a[0], seconds: a[1]}}
43
-
44
- return [limit_name, limits]
48
+ return [limit_name,
49
+ args.each_slice(2).map{|a| {requests: a[0], seconds: a[1]}}]
45
50
  end
46
51
 
47
52
  def redis
@@ -52,38 +57,31 @@ module Sinatra
52
57
  settings.rate_limiter_redis_namespace
53
58
  end
54
59
 
55
- def limit_history(limit_name, seconds=0)
56
- redis.
57
- keys("#{[namespace,user_identifier,limit_name].join('/')}/*").
58
- map{|k| k.split('/')[3].to_f}.
59
- select{|t| seconds.eql?(0) ? true : t > (Time.now.to_f - seconds)}
60
- end
61
-
62
- def limit_remaining(limit, limit_name)
63
- limit[:requests] - limit_history(limit_name, limit[:seconds]).length
60
+ def limit_remaining(limit, limiter)
61
+ limit[:requests] - limiter.history(limit[:seconds]).length
64
62
  end
65
63
 
66
- def limit_reset(limit, limit_name)
67
- limit[:seconds] - (Time.now.to_f - limit_history(limit_name, limit[:seconds]).first).to_i
64
+ def limit_reset(limit, limiter)
65
+ limit[:seconds] - (Time.now.to_f - limiter.history(limit[:seconds]).first.to_f).to_i
68
66
  end
69
67
 
70
- def limits_exceeded?(limits, limit_name)
71
- exceeded = limits.select {|limit| limit_remaining(limit, limit_name) < 1}.sort_by{|e| e[:seconds]}.last
68
+ def limits_exceeded?(limits, limiter)
69
+ exceeded = limits.select {|limit| limit_remaining(limit, limiter) < 1}.sort_by{|e| e[:seconds]}.last
72
70
 
73
71
  if exceeded
74
- try_again = limit_reset(exceeded, limit_name)
72
+ try_again = limit_reset(exceeded, limiter)
75
73
  return exceeded.merge({try_again: try_again.to_i})
76
74
  end
77
75
  end
78
76
 
79
- def rate_limit_headers(limits, limit_name)
77
+ def rate_limit_headers(limits, limit_name, limiter)
80
78
  header_prefix = 'X-Rate-Limit' + (limit_name.eql?('default') ? '' : '-' + limit_name)
81
79
  limit_no = 0 if limits.length > 1
82
80
  limits.each do |limit|
83
81
  limit_no = limit_no + 1 if limit_no
84
82
  response.headers[header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Limit'] = limit[:requests]
85
- response.headers[header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Remaining'] = limit_remaining(limit, limit_name)
86
- response.headers[header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Reset'] = limit_reset(limit, limit_name)
83
+ response.headers[header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Remaining'] = limit_remaining(limit, limiter)
84
+ response.headers[header_prefix + (limit_no ? "-#{limit_no}" : '') + '-Reset'] = limit_reset(limit, limiter)
87
85
  end
88
86
  end
89
87
 
@@ -104,6 +102,13 @@ module Sinatra
104
102
  end
105
103
  end
106
104
 
105
+ def get_min_time_prefix(limits)
106
+ now = Time.now.to_f
107
+ oldest = Time.now.to_f - limits.sort_by{|l| -l[:seconds]}.first[:seconds]
108
+
109
+ return now.to_s[0..((now/oldest).to_s.split(/^1\.|[1-9]+/)[1].length)].to_i.to_s
110
+ end
111
+
107
112
  end
108
113
 
109
114
  def self.registered(app)
@@ -120,9 +125,49 @@ module Sinatra
120
125
  # evaluates Procs in settings when reading them.
121
126
  app.set :rate_limiter_redis_conn, Redis.new
122
127
  app.set :rate_limiter_redis_namespace, 'rate_limit'
123
- app.set :rate_limiter_redis_expires, 24*60*60
128
+ app.set :rate_limiter_redis_expires, 24*60*60 # This must be larger than longest limit time period
129
+ end
130
+
131
+ end
132
+
133
+ class RateLimit
134
+ attr_reader :history
135
+
136
+ def initialize(limit_name, limits)
137
+ @limit_name = limit_name
138
+ @limits = limits
139
+ @time_prefix = get_min_time_prefix(@limits)
124
140
  end
125
141
 
142
+ include Sinatra::RateLimiter::Helpers
143
+
144
+ def history(seconds=0)
145
+ redis_history.select{|t| seconds.eql?(0) ? true : t > (Time.now.to_f - seconds)}
146
+ end
147
+
148
+ def redis_history
149
+ if @history
150
+ @history
151
+ else
152
+ @history = redis.
153
+ keys("#{[namespace,user_identifier,@limit_name].join('/')}/#{@time_prefix}*").
154
+ map{|k| k.split('/')[3].to_f}
155
+ end
156
+ end
157
+
158
+ def settings=(settings)
159
+ @settings = settings
160
+ end
161
+ def settings
162
+ @settings
163
+ end
164
+
165
+ def request=(request)
166
+ @request = request
167
+ end
168
+ def request
169
+ @request
170
+ end
126
171
  end
127
172
 
128
173
  end
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.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Warren Guy