rate-limit 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9b6d225525d8976ef1c3c8e7d0fdb1c71a48071357cff8f64633117a13ab714e
4
+ data.tar.gz: 057bf093d1271c83795c34472cf31e273353a24621bddebb86a901b48d451e8b
5
+ SHA512:
6
+ metadata.gz: 2e6a6fe077419b22d5dcc5357fdf127ab7c944aff6e1bf9d44ece39cf56f1fc1b6b12ac400aa1a2ffdeab9964f90f7739952cda796b3f23a82748410c64b324b
7
+ data.tar.gz: 19f68ffd995fca03b3c6787a208649b2c11c2206dd598a6b82feea25b51451817a3f354ca5de7fd9435a6fb6a61b8ac763db37a9dc2535425627eeccfc976333
data/CHANGELOG.md ADDED
File without changes
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Catawiki B.V.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # RateLimit
2
+ ![rspec](https://github.com/catawiki/rate-limit/actions/workflows/main.yml/badge.svg)
3
+
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
+
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)
7
+
8
+ #### Common Use Cases
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
12
+ * [Verifications] Brute-force attempts for entering verification codes
13
+ * [Redeem] Brute-force attempts to redeem voucher codes from a specific account
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'rate-limit'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ $ bundle install
26
+
27
+ Or install it yourself as:
28
+
29
+ $ gem install rate-limit
30
+
31
+ ## Usage
32
+
33
+ #### Basic `RateLimit.throttle`
34
+
35
+ ```ruby
36
+ if RateLimit.throttle(topic: :login, namespace: :user_id, value: id)
37
+ # Do something
38
+ end
39
+ ```
40
+ or
41
+
42
+ ```ruby
43
+ if RateLimit.throttle(topic: :login, value: id)
44
+ # Do something
45
+ end
46
+ ```
47
+
48
+ #### Basic with exception `RateLimit.throttle!`
49
+
50
+ ```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
66
+
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
88
+ end
89
+ ```
90
+
91
+ #### Nested throttles
92
+
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
+ ```
104
+
105
+ ### Config
106
+
107
+ Customize the configuration by adding the following block to `config/initializers/rate_limit.rb`
108
+
109
+ ```ruby
110
+ RateLimit.configure do |config|
111
+ config.redis = Redis.new
112
+ config.fail_safe = true
113
+ config.default_interval = 60
114
+ config.default_threshold = 2
115
+ config.limits_file_path = 'config/rate-limit.yml'
116
+ end
117
+ ```
118
+
119
+ #### Define Limits
120
+
121
+ The `config/rate-limit.yml` should include the limits you want to enforce on each given topic. In the following format:
122
+
123
+ ```yaml
124
+ topic:
125
+ threshold: interval
126
+ ```
127
+
128
+ ##### Example
129
+
130
+ * maximum `2` login attempts per `60` seconds
131
+ * maximum `1` send sms attempts per `60` seconds
132
+ * maximum `5` send sms attempts per `300` seconds
133
+ * maximum `10` send sms attempts per `3000` seconds
134
+
135
+ ```yaml
136
+ login:
137
+ 2: 60
138
+ send_sms:
139
+ 1: 60
140
+ 5: 300
141
+ 10: 3000
142
+ ```
143
+
144
+ ## Development
145
+
146
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
147
+
148
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
149
+
150
+ ## Contributing
151
+
152
+ Bug reports and pull requests are welcome on GitHub at https://github.com/catawiki/rate-limit. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/catawiki/rate-limit/blob/master/CODE_OF_CONDUCT.md).
153
+
154
+ ## License
155
+
156
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
157
+
158
+ ## Code of Conduct
159
+
160
+ Everyone interacting in the RateLimit project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/catawiki/rate-limit/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RateLimit
4
+ module Base
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? }
17
+ end
18
+
19
+ def limit_exceeded?(**args)
20
+ Throttler.new(**args).limit_exceeded?
21
+ end
22
+
23
+ def reset_counters(**args)
24
+ Throttler.new(**args).clear_cache_counter
25
+ end
26
+
27
+ def increment_counters(**args)
28
+ Throttler.new(**args).increment_cache_counter
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RateLimit
4
+ module Cache
5
+ class << self
6
+ def write(options)
7
+ RateLimit.config.redis.multi do |redis|
8
+ options.each do |key, value|
9
+ redis.incr(key)
10
+ redis.expire(key, value)
11
+ end
12
+ end
13
+ rescue ::Redis::BaseError => e
14
+ return true if RateLimit.config.fail_safe
15
+
16
+ raise e
17
+ end
18
+
19
+ def read(key)
20
+ RateLimit.config.redis.get(key)
21
+ rescue ::Redis::BaseError => e
22
+ return 0 if RateLimit.config.fail_safe
23
+
24
+ raise e
25
+ end
26
+
27
+ def clear(keys)
28
+ RateLimit.config.redis.multi do |redis|
29
+ keys.each { |k| redis.del(k) }
30
+ end
31
+ rescue ::Redis::BaseError => e
32
+ return true if RateLimit.config.fail_safe
33
+
34
+ raise e
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RateLimit
4
+ class Config
5
+ module Defaults
6
+ # Limits File Path
7
+ LIMITS_FILE_PATH = 'config/rate-limit.yml'
8
+
9
+ # Fixed Window Defaults
10
+ WINDOW_INTERVAL = 60
11
+ WINDOW_THRESHOLD = 2
12
+
13
+ class << self
14
+ def raw_limits
15
+ {
16
+ RateLimit.config.default_threshold => \
17
+ RateLimit.config.default_interval
18
+ }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module RateLimit
6
+ class Config
7
+ module FileLoader
8
+ def self.fetch
9
+ return {} unless File.exist?(RateLimit.config.limits_file_path)
10
+
11
+ YAML.load_file(RateLimit.config.limits_file_path)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis'
4
+ require_relative 'config/defaults'
5
+ require_relative 'config/file_loader'
6
+
7
+ module RateLimit
8
+ class Config
9
+ attr_accessor :default_interval,
10
+ :default_threshold,
11
+ :limits_file_path,
12
+ :fail_safe,
13
+ :redis
14
+
15
+ def initialize
16
+ @redis = Redis.new
17
+ @fail_safe = true
18
+ @limits_file_path = Defaults::LIMITS_FILE_PATH
19
+ @default_interval = Defaults::WINDOW_INTERVAL
20
+ @default_threshold = Defaults::WINDOW_THRESHOLD
21
+ end
22
+
23
+ def raw_limits_for(topic)
24
+ raw_limits[topic] || Defaults.raw_limits
25
+ end
26
+
27
+ private
28
+
29
+ def raw_limits
30
+ @raw_limits ||= FileLoader.fetch
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'config'
4
+
5
+ module RateLimit
6
+ module Configurable
7
+ def config
8
+ @config ||= Config.new
9
+ end
10
+
11
+ def configure
12
+ yield(config)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RateLimit
4
+ module Errors
5
+ class LimitExceededError < StandardError
6
+ attr_reader :window
7
+
8
+ delegate :topic, :namespace, :value, :threshold, :interval, to: :window
9
+
10
+ def initialize(window)
11
+ @window = window
12
+
13
+ super(custom_message)
14
+ end
15
+
16
+ def custom_message
17
+ "#{topic}: #{namespace} has exceeded #{threshold} in #{interval} seconds"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RateLimit
4
+ class Limit
5
+ attr_accessor :threshold, :interval
6
+
7
+ def initialize(threshold, interval)
8
+ @threshold = threshold
9
+ @interval = interval
10
+ end
11
+
12
+ class << self
13
+ def fetch(topic)
14
+ RateLimit.config.raw_limits_for(topic).map do |threshold, interval|
15
+ Limit.new(threshold, interval)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
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!
16
+
17
+ yield if block_given?
18
+
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
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RateLimit
4
+ VERSION = '0.0.1'
5
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RateLimit
4
+ class Window
5
+ attr_accessor :throttler, :limit
6
+
7
+ delegate :topic, :namespace, :value, to: :throttler
8
+ delegate :threshold, :interval, to: :limit
9
+
10
+ def initialize(throttler, limit)
11
+ @throttler = throttler
12
+ @limit = limit
13
+ end
14
+
15
+ def key
16
+ @key ||= [topic, namespace, value, interval].join(':')
17
+ end
18
+
19
+ def cached_counter
20
+ Cache.read(key).to_i || 0
21
+ end
22
+
23
+ class << self
24
+ def find_exceeded(windows)
25
+ windows.find { |w| w.cached_counter >= w.threshold }
26
+ end
27
+
28
+ def increment_cache_counter(windows)
29
+ Cache.write(
30
+ windows.each_with_object({}) { |w, h| h[w.key] = w.interval }
31
+ )
32
+ end
33
+
34
+ def clear_cache_counter(windows)
35
+ Cache.clear(windows.map(&:key))
36
+ end
37
+ end
38
+ end
39
+ end
data/lib/rate_limit.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/module'
4
+ require_relative 'rate_limit/configurable'
5
+ require_relative 'rate_limit/cache'
6
+ require_relative 'rate_limit/window'
7
+ require_relative 'rate_limit/throttler'
8
+ require_relative 'rate_limit/limit'
9
+ require_relative 'rate_limit/errors/limit_exceeded_error'
10
+ require_relative 'rate_limit/base'
11
+ require_relative 'rate_limit/version'
12
+
13
+ module RateLimit
14
+ extend RateLimit::Configurable
15
+ extend RateLimit::Base
16
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rate-limit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Mohamed Motaweh
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-09-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.2'
20
+ - - "<="
21
+ - !ruby/object:Gem::Version
22
+ version: 7.0.4
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '5.2'
30
+ - - "<="
31
+ - !ruby/object:Gem::Version
32
+ version: 7.0.4
33
+ - !ruby/object:Gem::Dependency
34
+ name: redis
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 3.0.0
40
+ - - "<="
41
+ - !ruby/object:Gem::Version
42
+ version: 5.1.0
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 3.0.0
50
+ - - "<="
51
+ - !ruby/object:Gem::Version
52
+ version: 5.1.0
53
+ description:
54
+ email:
55
+ - opensource@catawiki.nl
56
+ executables: []
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - CHANGELOG.md
61
+ - LICENSE
62
+ - README.md
63
+ - lib/rate_limit.rb
64
+ - lib/rate_limit/base.rb
65
+ - lib/rate_limit/cache.rb
66
+ - lib/rate_limit/config.rb
67
+ - lib/rate_limit/config/defaults.rb
68
+ - lib/rate_limit/config/file_loader.rb
69
+ - lib/rate_limit/configurable.rb
70
+ - lib/rate_limit/errors/limit_exceeded_error.rb
71
+ - lib/rate_limit/limit.rb
72
+ - lib/rate_limit/throttler.rb
73
+ - lib/rate_limit/version.rb
74
+ - lib/rate_limit/window.rb
75
+ homepage: https://github.com/catawiki/rate-limit
76
+ licenses:
77
+ - MIT
78
+ metadata:
79
+ homepage_uri: https://github.com/catawiki/rate-limit
80
+ source_code_uri: https://github.com/catawiki/rate-limit
81
+ changelog_uri: https://github.com/catawiki/rate-limit/blob/master/CHANGELOG.md
82
+ rubygems_mfa_required: 'true'
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '2.7'
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubygems_version: 3.1.6
99
+ signing_key:
100
+ specification_version: 4
101
+ summary: A Rate Limiting Gem
102
+ test_files: []