prorate 0.5.0 → 0.6.0
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.
- 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
|
[](https://travis-ci.org/WeTransfer/prorate)
|
8
10
|
[](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
|