rate-limit 0.0.1 → 0.2.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 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