prorate 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +6 -3
- data/README.md +94 -17
- data/lib/prorate/rate_limit.lua +1 -1
- data/lib/prorate/throttle.rb +19 -0
- data/lib/prorate/version.rb +1 -1
- data/prorate.gemspec +4 -3
- metadata +24 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dab868f09fd0b191abe56fdfdb156271331d397e
|
4
|
+
data.tar.gz: 0c9d8670999217b9b14f662a53b589f7c3b0b93d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c7c9909a1ebf831a56b3216a3387598d1c72f20463962329a9d61ea993533f5298769af63f6e16d837a2b22f1f1d1008b7ae2b1413ac80ae58b264f08b72eeb5
|
7
|
+
data.tar.gz: b9c3b0b86240e6d8dd9b504a3c8bc2341bd230138e263edad0fc8e03db900cb9c1ca8e777f3ac22bea5e44f00750bad13568d47c315315d09608ab4bc597cc1c
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# Prorate
|
2
2
|
|
3
|
-
Provides a low-level time-based throttle. Is mainly meant for situations where
|
4
|
-
|
5
|
-
|
3
|
+
Provides a low-level time-based throttle. Is mainly meant for situations where
|
4
|
+
using something like Rack::Attack is not very useful since you need access to
|
5
|
+
more variables. Under the hood, this uses a Lua script that implements the
|
6
|
+
[Leaky Bucket](https://en.wikipedia.org/wiki/Leaky_bucket) algorithm in a single
|
7
|
+
threaded and race condition safe way.
|
6
8
|
|
7
9
|
[![Build Status](https://travis-ci.org/WeTransfer/prorate.svg?branch=master)](https://travis-ci.org/WeTransfer/prorate)
|
8
10
|
[![Gem Version](https://badge.fury.io/rb/prorate.svg)](https://badge.fury.io/rb/prorate)
|
@@ -17,29 +19,106 @@ gem 'prorate'
|
|
17
19
|
|
18
20
|
And then execute:
|
19
21
|
|
20
|
-
|
22
|
+
```shell
|
23
|
+
bundle install
|
24
|
+
```
|
21
25
|
|
22
26
|
Or install it yourself as:
|
23
27
|
|
24
|
-
|
28
|
+
```shell
|
29
|
+
gem install prorate
|
30
|
+
```
|
25
31
|
|
26
32
|
## Usage
|
27
33
|
|
34
|
+
The simplest mode of operation is throttling an endpoint, using the throttler
|
35
|
+
before the action happens.
|
36
|
+
|
28
37
|
Within your Rails controller:
|
29
38
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
39
|
+
```ruby
|
40
|
+
t = Prorate::Throttle.new(
|
41
|
+
redis: Redis.new,
|
42
|
+
logger: Rails.logger,
|
43
|
+
name: "throttle-login-email",
|
44
|
+
limit: 20,
|
45
|
+
period: 5.seconds
|
46
|
+
)
|
47
|
+
# Add all the parameters that function as a discriminator.
|
48
|
+
t << request.ip << params.require(:email)
|
49
|
+
# ...and call the throttle! method
|
50
|
+
t.throttle! # Will raise a Prorate::Throttled exception if the limit has been reached
|
51
|
+
#
|
52
|
+
# Your regular action happens after this point
|
53
|
+
```
|
54
|
+
|
55
|
+
To capture that exception, in the controller
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
rescue_from Prorate::Throttled do |e|
|
59
|
+
response.set_header('Retry-After', e.retry_in_seconds.to_s)
|
60
|
+
render nothing: true, status: 429
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
### Throttling and checking of its status
|
65
|
+
|
66
|
+
More exquisite control can be achieved by combining throttling (see previous
|
67
|
+
step) and - in subsequent calls - checking the status of the throttle before
|
68
|
+
invoking the throttle.
|
69
|
+
|
70
|
+
Let's say you have an endpoint that not only needs throttling, but you want to
|
71
|
+
ban [credential stuffers](https://en.wikipedia.org/wiki/Credential_stuffing)
|
72
|
+
outright. This is a multi-step process:
|
73
|
+
|
74
|
+
1. Respond with a 429 if the discriminators of the request would land in an
|
75
|
+
already blocking 'credential-stuffing'-throttle
|
76
|
+
1. Run your regular throttling
|
77
|
+
1. Perform your sign in action
|
78
|
+
1. If the sign in was unsuccessful, add the discriminators to the
|
79
|
+
'credential-stuffing'-throttle
|
80
|
+
|
81
|
+
In your controller that would look like this:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
t = Prorate::Throttle.new(
|
85
|
+
redis: Redis.new,
|
86
|
+
logger: Rails.logger,
|
87
|
+
name: "credential-stuffing",
|
88
|
+
limit: 20,
|
89
|
+
period: 20.minutes
|
90
|
+
)
|
91
|
+
# Add all the parameters that function as a discriminator.
|
92
|
+
t << request.ip
|
93
|
+
# And before anything else, check whether it is throttled
|
94
|
+
if t.status.throttled?
|
95
|
+
response.set_header('Retry-After', t.status.remaining_throttle_seconds.to_s)
|
96
|
+
render(nothing: true, status: 429) and return
|
97
|
+
end
|
98
|
+
|
99
|
+
# run your regular throttles for the endpoint
|
100
|
+
other_throttles.map(:throttle!)
|
101
|
+
# Perform your sign in logic..
|
102
|
+
|
103
|
+
user = YourSignInLogic.valid?(
|
104
|
+
email: params[:email],
|
105
|
+
password: params[:password]
|
106
|
+
)
|
107
|
+
|
108
|
+
# Add the request to the credential stuffing throttle if we didn't succeed
|
109
|
+
t.throttle! unless user
|
110
|
+
|
111
|
+
# the rest of your action
|
112
|
+
```
|
36
113
|
|
37
114
|
To capture that exception, in the controller
|
38
115
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
116
|
+
```ruby
|
117
|
+
rescue_from Prorate::Throttled do |e|
|
118
|
+
response.set_header('Retry-After', e.retry_in_seconds.to_s)
|
119
|
+
render nothing: true, status: 429
|
120
|
+
end
|
121
|
+
```
|
43
122
|
|
44
123
|
## Development
|
45
124
|
|
@@ -51,8 +130,6 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
51
130
|
|
52
131
|
Bug reports and pull requests are welcome on GitHub at https://github.com/WeTransfer/prorate.
|
53
132
|
|
54
|
-
|
55
133
|
## License
|
56
134
|
|
57
135
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
58
|
-
|
data/lib/prorate/rate_limit.lua
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
-- Single threaded Leaky Bucket implementation.
|
2
|
-
-- args: key_base, leak_rate, max_bucket_capacity, block_duration
|
2
|
+
-- args: key_base, leak_rate, max_bucket_capacity, block_duration, n_tokens
|
3
3
|
-- returns: an array of two integers, the first of which indicates the remaining block time.
|
4
4
|
-- if the block time is nonzero, the second integer is always zero. If the block time is zero,
|
5
5
|
-- the second integer indicates the level of the bucket
|
data/lib/prorate/throttle.rb
CHANGED
@@ -96,6 +96,19 @@ module Prorate
|
|
96
96
|
end
|
97
97
|
end
|
98
98
|
|
99
|
+
def status
|
100
|
+
discriminator = Digest::SHA1.hexdigest(Marshal.dump(@discriminators))
|
101
|
+
identifier = [name, discriminator].join(':')
|
102
|
+
|
103
|
+
redis.with do |r|
|
104
|
+
is_blocked = r.exists("#{identifier}.block")
|
105
|
+
return Status.new(is_throttled: false, remaining_throttle_seconds: 0) unless is_blocked
|
106
|
+
|
107
|
+
remaining_seconds = r.get("#{identifier}.block").to_i - Time.now.to_i
|
108
|
+
Status.new(is_throttled: true, remaining_throttle_seconds: remaining_seconds)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
99
112
|
private
|
100
113
|
|
101
114
|
def run_lua_throttler(redis:, identifier:, bucket_capacity:, leak_rate:, block_for:, n_tokens:)
|
@@ -110,5 +123,11 @@ module Prorate
|
|
110
123
|
raise e
|
111
124
|
end
|
112
125
|
end
|
126
|
+
|
127
|
+
class Status < Ks.strict(:is_throttled, :remaining_throttle_seconds)
|
128
|
+
def throttled?
|
129
|
+
is_throttled
|
130
|
+
end
|
131
|
+
end
|
113
132
|
end
|
114
133
|
end
|
data/lib/prorate/version.rb
CHANGED
data/prorate.gemspec
CHANGED
@@ -30,9 +30,10 @@ Gem::Specification.new do |spec|
|
|
30
30
|
spec.add_dependency "ks"
|
31
31
|
spec.add_dependency "redis", ">= 2"
|
32
32
|
spec.add_development_dependency "connection_pool", "~> 2"
|
33
|
-
spec.add_development_dependency "bundler"
|
34
|
-
spec.add_development_dependency "rake", "~>
|
33
|
+
spec.add_development_dependency "bundler"
|
34
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
35
35
|
spec.add_development_dependency "rspec", "~> 3.0"
|
36
|
-
spec.add_development_dependency 'wetransfer_style', '0.6.
|
36
|
+
spec.add_development_dependency 'wetransfer_style', '0.6.5'
|
37
37
|
spec.add_development_dependency 'yard', '~> 0.9'
|
38
|
+
spec.add_development_dependency 'pry', '~> 0.12.2'
|
38
39
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: prorate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-02-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ks
|
@@ -56,30 +56,30 @@ dependencies:
|
|
56
56
|
name: bundler
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
68
|
+
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rake
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
75
|
+
version: '13.0'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
82
|
+
version: '13.0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: rspec
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -100,14 +100,14 @@ dependencies:
|
|
100
100
|
requirements:
|
101
101
|
- - '='
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: 0.6.
|
103
|
+
version: 0.6.5
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - '='
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: 0.6.
|
110
|
+
version: 0.6.5
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: yard
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,6 +122,20 @@ dependencies:
|
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0.9'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: pry
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.12.2
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 0.12.2
|
125
139
|
description: Can be used to implement all kinds of throttles
|
126
140
|
email:
|
127
141
|
- me@julik.nl
|