sinatra-rate-limiter 0.4.1 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +6 -6
- data/lib/sinatra/rate-limiter.rb +63 -71
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3442a4980f8b129023849abb927d12628d3264a2
|
4
|
+
data.tar.gz: f2a4160d6df1e036eb16d214fff7a05c6fa23705
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
|
data/lib/sinatra/rate-limiter.rb
CHANGED
@@ -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
|
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
|
-
|
99
|
+
class RateLimit
|
100
|
+
attr_accessor :settings, :request, :options
|
100
101
|
|
101
|
-
|
102
|
-
|
102
|
+
def initialize(bucket, limits)
|
103
|
+
@bucket = bucket
|
104
|
+
@limits = limits
|
105
|
+
@time_prefix = get_min_time_prefix(@limits)
|
106
|
+
end
|
103
107
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
113
|
-
end
|
113
|
+
def identifier
|
114
|
+
@identifier ||= @options.identifier.call(request)
|
115
|
+
end
|
114
116
|
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
120
|
-
|
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
|
-
|
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
|
-
|
132
|
-
|
136
|
+
def limit_remaining(limit)
|
137
|
+
limit[:requests] - history(limit[:seconds]).length
|
138
|
+
end
|
133
139
|
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
139
|
-
|
140
|
-
end
|
144
|
+
def limits_exceeded?
|
145
|
+
exceeded = @limits.select {|limit| limit_remaining(limit) < 1}.sort_by{|e| e[:seconds]}.last
|
141
146
|
|
142
|
-
|
143
|
-
|
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
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
161
|
-
|
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
|
-
|
171
|
-
|
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
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
183
|
-
|
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
|