rate-limit 0.0.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b6d225525d8976ef1c3c8e7d0fdb1c71a48071357cff8f64633117a13ab714e
4
- data.tar.gz: 057bf093d1271c83795c34472cf31e273353a24621bddebb86a901b48d451e8b
3
+ metadata.gz: b0f1786c2b5fde684e916bcc8733929b7d0d35cdc2807d8f511190f179019611
4
+ data.tar.gz: c216ce4a4f8e2e479854b61b895c87d226cceba9f8a3d6fb476852709a308ed2
5
5
  SHA512:
6
- metadata.gz: 2e6a6fe077419b22d5dcc5357fdf127ab7c944aff6e1bf9d44ece39cf56f1fc1b6b12ac400aa1a2ffdeab9964f90f7739952cda796b3f23a82748410c64b324b
7
- data.tar.gz: 19f68ffd995fca03b3c6787a208649b2c11c2206dd598a6b82feea25b51451817a3f354ca5de7fd9435a6fb6a61b8ac763db37a9dc2535425627eeccfc976333
6
+ metadata.gz: 25bbef68dd61abd1cb2353367af910ee5d61d2ce9f1408c66a36a2741abab62bc9b3c212552e01187c5a95121c611eeb9b16232b1b1bdcc4e486fcdc780b85e0
7
+ data.tar.gz: 5232a6c6f559c5161576b1f54d27a17b590c3b2d25376f8b6f0011d725dbf029bbc7b75e78d4afc6babeaea3a0acac4b9c3b643738f4e9730fbc5105241f208c
data/CHANGELOG.md CHANGED
@@ -0,0 +1,70 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog],
6
+ and this project adheres to [Semantic Versioning].
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [v0.2.0] - 2022-10-05
11
+
12
+ ### Added
13
+
14
+ - added `RateLimit::Errors::LimitExceededError#result`
15
+ - added `Worker#raise_errors`
16
+ - added `Worker#only_failures`
17
+
18
+ ### Changed
19
+
20
+ - changed `RateLimit::Result.initialize`
21
+
22
+ ### Removed
23
+
24
+ - removed `RateLimit::Result#namespace`
25
+ - removed `RateLimit::Worker#namespace`
26
+ - removed `RateLimit::Errors::LimitExceededError#namespace`
27
+ - removed `RateLimit::Errors::LimitExceededError#worker`
28
+ - removed `RateLimit.throttle_with_block!` and `Worker#throttle_with_block!` in favour of `.throttle` with `{ raise_errors: true }` option
29
+ - removed `RateLimit.throttle_only_failures_with_block!` and `Worker#throttle_only_failures_with_block!` in favour of `.throttle` with `{ only_failures: true }` option
30
+
31
+
32
+ ### Fixed
33
+
34
+ - https://github.com/catawiki/rate-limit/issues/16 String Values are not respected
35
+
36
+ ## [v0.1.0] - 2022-09-19
37
+
38
+
39
+ ### Added
40
+
41
+ - https://github.com/catawiki/rate-limit/pull/11 `RateLimit::Result` class
42
+ - https://github.com/catawiki/rate-limit/pull/12 `RateLimit::Worker` class
43
+ - https://github.com/catawiki/rate-limit/pull/13 `RateLimit::Config#on_success` and `RateLimit::Config#on_failure`
44
+
45
+ ### Changed
46
+
47
+ - `RateLimit.throttle` to not accept block
48
+ - `RateLimit.throttle` to return `RateLimit::Result` object
49
+ - `RateLimit::Throttler` from class to module while moving responsibilities to `RateLimit::Worker` class
50
+ - renamed `RateLimit.throttle!` to `RateLimit.throttle_with_block!`
51
+ - renamed `RateLimit.throttle_only_failures` to `RateLimit.throttle_only_failures_with_block!`
52
+
53
+ ### Fixed
54
+
55
+ - https://github.com/catawiki/rate-limit/issues/7 Symbol topic names does not load the correct limits
56
+ - https://github.com/catawiki/rate-limit/issues/6 Main Module (RateLimit) fails to autoload
57
+
58
+
59
+ ## v0.0.1 - 2022-09-09
60
+
61
+ - Initial gem release
62
+
63
+ [Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
64
+ [Semantic Versioning]: https://semver.org/spec/v2.0.0.html
65
+
66
+ <!-- versions -->
67
+
68
+ [Unreleased]: https://github.com/catawiki/rate-limit/compare/v0.1.0...HEAD
69
+ [v0.2.0]: https://github.com/catawiki/rate-limit/compare/v0.1.0...v0.2.0
70
+ [v0.1.0]: https://github.com/catawiki/rate-limit/compare/v0.0.1...v0.1.0
data/README.md CHANGED
@@ -3,12 +3,12 @@
3
3
 
4
4
  Protect your Ruby apps from bad actors. RateLimit allows you to set permissions as to whether certain number of feature calls are valid or not for a specific entity (user, phone number, email address, etc...).
5
5
 
6
- This gem mainly provides brute-force protection by throttling attepmts for a specific entity id (i.e user_id). However it could also be used to throttle based on ip address (we recommend that you consider using [Rack::Attack](https://github.com/rack/rack-attack) for more optimized ip throttling)
6
+ This gem mainly provides brute-force protection by throttling attempts for a specific entity id (i.e user_id). However it could also be used to throttle based on ip address (we recommend that you consider using [Rack::Attack](https://github.com/rack/rack-attack) for more optimized ip throttling)
7
7
 
8
8
  #### Common Use Cases
9
9
  * [Login] Brute-force attempts for a spefic account
10
- * [SMS Spam] Brute-force attempts for requesting Phone Verification SMS for a spefic user_id
11
- * [SMS Spam] Brute-force attempts for requesting Phone Verification SMS for a spefic phone_number
10
+ * [SMS Spam] Brute-force attempts for requesting Phone Verification SMS for a specific user_id
11
+ * [SMS Spam] Brute-force attempts for requesting Phone Verification SMS for a specific phone_number
12
12
  * [Verifications] Brute-force attempts for entering verification codes
13
13
  * [Redeem] Brute-force attempts to redeem voucher codes from a specific account
14
14
 
@@ -28,81 +28,32 @@ Or install it yourself as:
28
28
 
29
29
  $ gem install rate-limit
30
30
 
31
- ## Usage
31
+ ## Basic Usage
32
32
 
33
- #### Basic `RateLimit.throttle`
33
+ ### [`RateLimit.throttle`](https://github.com/catawiki/rate-limit/wiki/Throttling)
34
+ The throttle method expects the following options
34
35
 
35
- ```ruby
36
- if RateLimit.throttle(topic: :login, namespace: :user_id, value: id)
37
- # Do something
38
- end
39
- ```
40
- or
36
+ | Option | Description | Examples |
37
+ | ---------------- | ------------------------------------------------------------------------------- | ------------------------------------- |
38
+ | topic | The topic you would like to throttle | "login", "send_sms", "redeem_voucher" |
39
+ | value | The identifier of the unique entity that is throttled based on the topic limits | user_id, phone_number, voucher_code |
41
40
 
42
- ```ruby
43
- if RateLimit.throttle(topic: :login, value: id)
44
- # Do something
45
- end
46
- ```
47
41
 
48
- #### Basic with exception `RateLimit.throttle!`
42
+ The `throttle` method checks if the given value did exceed the defined limits for the given topic. If the limit is exceeded then it returns [RateLimit::Result](https://github.com/catawiki/rate-limit/wiki/RateLimit::Result) Object, where `result.success?` will be `false`. Otherwise, it increments the attempts counter in the cache and sets `result.success?` to `true`.
49
43
 
44
+ #### Example
50
45
  ```ruby
51
- begin
52
- RateLimit.throttle!(topic: :send_sms, namespace: :user_id, value: id) do
53
- # Logic goes Here
54
- end
55
- rescue RateLimit::Errors::LimitExceededError => e
56
- # Error Handling Logic goes here
57
- e.topic # :login
58
- e.namespace # :user_id
59
- e.value # id
60
- e.threshold # 2
61
- e.interval # 60
62
- end
63
- ```
64
-
65
- #### Advanced
46
+ result = RateLimit.throttle(topic: :login, value: 123)
66
47
 
67
- ```ruby
68
- throttler = RateLimit::Throttler.new(topic: :login, namespace: :user_id, value: id)
69
-
70
- begin
71
- throttler.perform! do
72
- # Logic goes Here
73
- end
74
- rescue RateLimit::Errors::LimitExceededError => e
75
- # Error Handling Logic goes here
76
- end
77
- ```
78
-
79
- #### Manual
80
-
81
- ```ruby
82
- throttler = RateLimit::Throttler.new(topic: :login, namespace: :user_id, value: id)
83
-
84
- unless throttler.limit_exceeded?
85
- # Logic goes Here
86
-
87
- throttler.increment_counters
48
+ if result.success?
49
+ # Do something
88
50
  end
89
51
  ```
90
52
 
91
- #### Nested throttles
92
53
 
93
- ```ruby
94
- begin
95
- RateLimit.throttle!(topic: :send_sms, namespace: :user_id, value: id) do
96
- RateLimit.throttle!(topic: :send_sms, namespace: :phone_number, value: number) do
97
- # Logic goes Here
98
- end
99
- end
100
- rescue RateLimit::Errors::LimitExceededError => e
101
- # Error Handling Logic goes here
102
- end
103
- ```
54
+ Please check the [Wiki](https://github.com/catawiki/rate-limit/wiki) for advanced throttling and options.
104
55
 
105
- ### Config
56
+ ## [Configuration](https://github.com/catawiki/rate-limit/wiki/Configuration)
106
57
 
107
58
  Customize the configuration by adding the following block to `config/initializers/rate_limit.rb`
108
59
 
@@ -113,6 +64,14 @@ RateLimit.configure do |config|
113
64
  config.default_interval = 60
114
65
  config.default_threshold = 2
115
66
  config.limits_file_path = 'config/rate-limit.yml'
67
+ config.on_success = proc { |result|
68
+ # Success Logic Goes HERE
69
+ # result.topic, result.value
70
+ }
71
+ config.on_failure = proc { |result|
72
+ # Failure Logic Goes HERE
73
+ # result.topic, result.value, result.threshold, result.interval
74
+ }
116
75
  end
117
76
  ```
118
77
 
data/lib/rate-limit.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rate_limit'
@@ -3,29 +3,21 @@
3
3
  module RateLimit
4
4
  module Base
5
5
  def throttle(**args)
6
- throttle!(**args) { yield if block_given? }
7
- rescue Errors::LimitExceededError => _e
8
- false
9
- end
10
-
11
- def throttle!(**args)
12
- Throttler.new(**args).perform! { yield if block_given? }
13
- end
14
-
15
- def throttle_only_failures!(**args)
16
- Throttler.new(**args).perform_only_failures! { yield if block_given? }
6
+ worker = Worker.new(**args)
7
+ worker.throttle { yield if block_given? }
8
+ worker.result
17
9
  end
18
10
 
19
11
  def limit_exceeded?(**args)
20
- Throttler.new(**args).limit_exceeded?
12
+ Worker.new(**args).reloaded_limit_exceeded?
21
13
  end
22
14
 
23
15
  def reset_counters(**args)
24
- Throttler.new(**args).clear_cache_counter
16
+ Worker.new(**args).clear_cache_counter
25
17
  end
26
18
 
27
19
  def increment_counters(**args)
28
- Throttler.new(**args).increment_cache_counter
20
+ Worker.new(**args).increment_cache_counter
29
21
  end
30
22
  end
31
23
  end
@@ -9,6 +9,8 @@ module RateLimit
9
9
  attr_accessor :default_interval,
10
10
  :default_threshold,
11
11
  :limits_file_path,
12
+ :on_success,
13
+ :on_failure,
12
14
  :fail_safe,
13
15
  :redis
14
16
 
@@ -24,6 +26,14 @@ module RateLimit
24
26
  raw_limits[topic] || Defaults.raw_limits
25
27
  end
26
28
 
29
+ def success_callback(*args)
30
+ on_success&.call(*args)
31
+ end
32
+
33
+ def failure_callback(*args)
34
+ on_failure&.call(*args)
35
+ end
36
+
27
37
  private
28
38
 
29
39
  def raw_limits
@@ -3,18 +3,18 @@
3
3
  module RateLimit
4
4
  module Errors
5
5
  class LimitExceededError < StandardError
6
- attr_reader :window
6
+ attr_reader :result
7
7
 
8
- delegate :topic, :namespace, :value, :threshold, :interval, to: :window
8
+ delegate :topic, :value, :threshold, :interval, to: :result
9
9
 
10
- def initialize(window)
11
- @window = window
10
+ def initialize(result)
11
+ @result = result
12
12
 
13
13
  super(custom_message)
14
14
  end
15
15
 
16
16
  def custom_message
17
- "#{topic}: #{namespace} has exceeded #{threshold} in #{interval} seconds"
17
+ "#{result.topic}: has exceeded #{result.threshold} in #{result.interval} seconds"
18
18
  end
19
19
  end
20
20
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RateLimit
4
+ class Result
5
+ # Attributes
6
+ attr_accessor :topic, :value, :threshold, :interval
7
+
8
+ # Methods
9
+ def initialize(topic:, value:)
10
+ @topic = topic
11
+ @value = value
12
+ end
13
+
14
+ def success?
15
+ @success
16
+ end
17
+
18
+ def success!
19
+ @success = true
20
+ end
21
+
22
+ def failure!(worker)
23
+ @success = false
24
+ @threshold = worker.exceeded_window&.threshold
25
+ @interval = worker.exceeded_window&.interval
26
+ end
27
+ end
28
+ end
@@ -1,57 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RateLimit
4
- class Throttler
5
- attr_accessor :topic, :namespace, :value, :limits, :windows
6
-
7
- def initialize(topic:, value:, namespace: nil)
8
- @topic = topic.to_s
9
- @value = value.to_i
10
- @namespace = namespace&.to_s
11
- @windows = Limit.fetch(topic).map { |limit| Window.new(self, limit) }
12
- end
13
-
14
- def perform!
15
- validate_limit!
4
+ module Throttler
5
+ def throttle
6
+ return failure! if reloaded_limit_exceeded?
16
7
 
17
8
  yield if block_given?
18
9
 
19
- increment_cache_counter
20
-
21
- true
22
- end
23
-
24
- def perform_only_failures!
25
- validate_limit!
26
-
27
- begin
28
- yield if block_given?
29
- rescue StandardError => e
30
- increment_cache_counter
31
- raise e
32
- end
33
-
34
- true
35
- end
36
-
37
- def limit_exceeded?
38
- Window.find_exceeded(windows).present?
39
- end
40
-
41
- def increment_cache_counter
42
- Window.increment_cache_counter(windows)
43
- end
44
-
45
- def clear_cache_counter
46
- Window.clear_cache_counter(windows)
47
- end
48
-
49
- private
50
-
51
- def validate_limit!
52
- exceeded_window = Window.find_exceeded(windows)
53
-
54
- raise Errors::LimitExceededError, exceeded_window if exceeded_window
10
+ return success! unless only_failures
11
+ rescue StandardError => e
12
+ success! unless e.is_a?(Errors::LimitExceededError)
13
+ raise e
55
14
  end
56
15
  end
57
16
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RateLimit
4
- VERSION = '0.0.1'
4
+ VERSION = '0.2.0'
5
5
  end
@@ -2,18 +2,18 @@
2
2
 
3
3
  module RateLimit
4
4
  class Window
5
- attr_accessor :throttler, :limit
5
+ attr_accessor :worker, :limit
6
6
 
7
- delegate :topic, :namespace, :value, to: :throttler
7
+ delegate :topic, :value, to: :worker
8
8
  delegate :threshold, :interval, to: :limit
9
9
 
10
- def initialize(throttler, limit)
11
- @throttler = throttler
12
- @limit = limit
10
+ def initialize(worker, limit)
11
+ @worker = worker
12
+ @limit = limit
13
13
  end
14
14
 
15
15
  def key
16
- @key ||= [topic, namespace, value, interval].join(':')
16
+ @key ||= [topic, value, interval].join(':')
17
17
  end
18
18
 
19
19
  def cached_counter
@@ -21,6 +21,10 @@ module RateLimit
21
21
  end
22
22
 
23
23
  class << self
24
+ def find_all(topic:, worker:)
25
+ Limit.fetch(topic).map { |limit| Window.new(worker, limit) }
26
+ end
27
+
24
28
  def find_exceeded(windows)
25
29
  windows.find { |w| w.cached_counter >= w.threshold }
26
30
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RateLimit
4
+ class Worker
5
+ include Throttler
6
+
7
+ attr_accessor :topic, :value, :limits, :windows, :exceeded_window, :result, :raise_errors, :only_failures
8
+
9
+ def initialize(topic:, value:, raise_errors: false, only_failures: false)
10
+ @topic = topic.to_s
11
+ @value = value.to_s
12
+ @windows = Window.find_all(worker: self, topic: @topic)
13
+ @result = Result.new(topic: @topic, value: @value)
14
+ @raise_errors = raise_errors
15
+ @only_failures = only_failures
16
+ end
17
+
18
+ def increment_cache_counter
19
+ Window.increment_cache_counter(windows)
20
+ end
21
+
22
+ def clear_cache_counter
23
+ Window.clear_cache_counter(windows)
24
+ end
25
+
26
+ def reloaded_limit_exceeded?
27
+ @exceeded_window = Window.find_exceeded(windows)
28
+
29
+ limit_exceeded?
30
+ end
31
+
32
+ def limit_exceeded?
33
+ exceeded_window.present?
34
+ end
35
+
36
+ def success!
37
+ increment_cache_counter
38
+ result.success!
39
+ RateLimit.config.success_callback(result)
40
+ end
41
+
42
+ def failure!
43
+ result.failure!(self)
44
+ RateLimit.config.failure_callback(result)
45
+
46
+ raise Errors::LimitExceededError, result if raise_errors
47
+ end
48
+ end
49
+ end
data/lib/rate_limit.rb CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  require 'active_support/core_ext/module'
4
4
  require_relative 'rate_limit/configurable'
5
+ require_relative 'rate_limit/result'
5
6
  require_relative 'rate_limit/cache'
6
7
  require_relative 'rate_limit/window'
7
8
  require_relative 'rate_limit/throttler'
9
+ require_relative 'rate_limit/worker'
8
10
  require_relative 'rate_limit/limit'
9
11
  require_relative 'rate_limit/errors/limit_exceeded_error'
10
12
  require_relative 'rate_limit/base'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rate-limit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mohamed Motaweh
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-09-09 00:00:00.000000000 Z
11
+ date: 2022-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -60,6 +60,7 @@ files:
60
60
  - CHANGELOG.md
61
61
  - LICENSE
62
62
  - README.md
63
+ - lib/rate-limit.rb
63
64
  - lib/rate_limit.rb
64
65
  - lib/rate_limit/base.rb
65
66
  - lib/rate_limit/cache.rb
@@ -69,9 +70,11 @@ files:
69
70
  - lib/rate_limit/configurable.rb
70
71
  - lib/rate_limit/errors/limit_exceeded_error.rb
71
72
  - lib/rate_limit/limit.rb
73
+ - lib/rate_limit/result.rb
72
74
  - lib/rate_limit/throttler.rb
73
75
  - lib/rate_limit/version.rb
74
76
  - lib/rate_limit/window.rb
77
+ - lib/rate_limit/worker.rb
75
78
  homepage: https://github.com/catawiki/rate-limit
76
79
  licenses:
77
80
  - MIT
@@ -95,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
95
98
  - !ruby/object:Gem::Version
96
99
  version: '0'
97
100
  requirements: []
98
- rubygems_version: 3.1.6
101
+ rubygems_version: 3.2.3
99
102
  signing_key:
100
103
  specification_version: 4
101
104
  summary: A Rate Limiting Gem