rate_limiter 0.0.6 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE.txt +20 -0
  4. data/README.md +18 -11
  5. data/lib/generators/rate_limiter/install_generator.rb +22 -0
  6. data/lib/generators/rate_limiter/templates/rate_limiter.rb +6 -0
  7. data/lib/rate_limiter.rb +51 -51
  8. data/lib/rate_limiter/config.rb +18 -3
  9. data/lib/rate_limiter/frameworks/rails.rb +3 -0
  10. data/lib/rate_limiter/frameworks/rails/controller.rb +69 -0
  11. data/lib/rate_limiter/model.rb +30 -48
  12. data/lib/rate_limiter/model_config.rb +34 -0
  13. data/lib/rate_limiter/request.rb +112 -0
  14. data/lib/rate_limiter/throttle.rb +79 -0
  15. data/lib/rate_limiter/validator.rb +29 -0
  16. data/lib/rate_limiter/version.rb +3 -1
  17. data/rate_limiter.gemspec +24 -18
  18. metadata +98 -183
  19. data/.gitignore +0 -11
  20. data/.rspec +0 -3
  21. data/.rvmrc +0 -80
  22. data/Gemfile +0 -3
  23. data/Gemfile.lock +0 -150
  24. data/Rakefile +0 -6
  25. data/lib/rate_limiter/controller.rb +0 -36
  26. data/spec/dummy/Rakefile +0 -7
  27. data/spec/dummy/app/assets/images/rails.png +0 -0
  28. data/spec/dummy/app/assets/javascripts/application.js +0 -15
  29. data/spec/dummy/app/assets/stylesheets/application.css +0 -13
  30. data/spec/dummy/app/controllers/application_controller.rb +0 -7
  31. data/spec/dummy/app/controllers/messages_controller.rb +0 -75
  32. data/spec/dummy/app/helpers/application_helper.rb +0 -2
  33. data/spec/dummy/app/mailers/.gitkeep +0 -0
  34. data/spec/dummy/app/models/message.rb +0 -5
  35. data/spec/dummy/app/views/layouts/application.html.erb +0 -15
  36. data/spec/dummy/app/views/messages/_form.html.erb +0 -22
  37. data/spec/dummy/app/views/messages/_message.html.erb +0 -8
  38. data/spec/dummy/app/views/messages/edit.html.erb +0 -3
  39. data/spec/dummy/app/views/messages/index.html.erb +0 -6
  40. data/spec/dummy/app/views/messages/new.html.erb +0 -3
  41. data/spec/dummy/app/views/messages/show.html.erb +0 -4
  42. data/spec/dummy/config.ru +0 -4
  43. data/spec/dummy/config/application.rb +0 -70
  44. data/spec/dummy/config/boot.rb +0 -11
  45. data/spec/dummy/config/database.yml +0 -25
  46. data/spec/dummy/config/environment.rb +0 -5
  47. data/spec/dummy/config/environments/development.rb +0 -37
  48. data/spec/dummy/config/environments/production.rb +0 -67
  49. data/spec/dummy/config/environments/test.rb +0 -37
  50. data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -7
  51. data/spec/dummy/config/initializers/inflections.rb +0 -15
  52. data/spec/dummy/config/initializers/mime_types.rb +0 -5
  53. data/spec/dummy/config/initializers/secret_token.rb +0 -7
  54. data/spec/dummy/config/initializers/session_store.rb +0 -8
  55. data/spec/dummy/config/initializers/wrap_parameters.rb +0 -14
  56. data/spec/dummy/config/locales/en.yml +0 -5
  57. data/spec/dummy/config/routes.rb +0 -5
  58. data/spec/dummy/db/migrate/20121213101512_create_messages.rb +0 -21
  59. data/spec/dummy/db/schema.rb +0 -33
  60. data/spec/dummy/db/seeds.rb +0 -7
  61. data/spec/dummy/lib/assets/.gitkeep +0 -0
  62. data/spec/dummy/lib/tasks/.gitkeep +0 -0
  63. data/spec/dummy/log/.gitkeep +0 -0
  64. data/spec/dummy/public/404.html +0 -26
  65. data/spec/dummy/public/422.html +0 -26
  66. data/spec/dummy/public/500.html +0 -25
  67. data/spec/dummy/public/favicon.ico +0 -0
  68. data/spec/dummy/script/rails +0 -6
  69. data/spec/rate_limiter/model_spec.rb +0 -20
  70. data/spec/rate_limiter_spec.rb +0 -10
  71. data/spec/spec_helper.rb +0 -30
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 59402eb858231d2bb8f7478a18c82a49a30268a5d9d7c4842f383a67d9e43ce8
4
+ data.tar.gz: ee860589d525fb08ab112d60065fc5d44aa76ef2a8f1df32c5edb0f334d0f95a
5
+ SHA512:
6
+ metadata.gz: fced90a375bdc2787e8a45da94e054379556a747bd38bdca81b46d7055332e267382fd2ef52e9151d0f7e627b61f7d3404b47354c2e42885a875fab93f964a29
7
+ data.tar.gz: 3deedcb193a1472ef5bdc47da33f995d848c43616165512836ff898d57f6eb82851dd3fcf53be210cc34970528c7944c8c45d387203b155b733cd30836906ff1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## v0.1.0
2
+
3
+ * Updated for Rails 5 (and 6).
4
+ * Use `validate` instead of `before_create`.
5
+ * Use minitest for tests.
6
+
1
7
  ## v0.0.6
2
8
 
3
9
  * Updated dependencies to allow for Rails 4.
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012-2019 Sean Eshbaugh
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,10 +1,15 @@
1
1
  # Rate Limiter
2
2
 
3
- A gem that limits the rate at which ActiveRecord model instances can be created.
3
+ [![Gem Version](https://badge.fury.io/rb/rate_limiter.svg)](https://badge.fury.io/rb/rate_limiter)
4
+ [![Travis](https://travis-ci.com/seaneshbaugh/rate_limiter.svg?branch=master)](https://travis-ci.org/seaneshbaugh/rate_limiter)
5
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/287d36cd30f34a818738/test_coverage)](https://codeclimate.com/github/seaneshbaugh/rate_limiter/test_coverage)
6
+ [![Maintainability](https://api.codeclimate.com/v1/badges/287d36cd30f34a818738/maintainability)](https://codeclimate.com/github/seaneshbaugh/rate_limiter/maintainability)
4
7
 
5
- ## Rails Version
8
+ Limit the rate at which ActiveRecord model instances can be created.
6
9
 
7
- This gem has only been tested on Rails 3.2. There is no reason that I am aware of that would prevent it from working on all versions of Rails 3 (and Rails 4 when it is released).
10
+ ## Compatibility
11
+
12
+ This gem is compatible with Ruby 2.5 or greater and Rails 5 or greater. For Ruby versions older than 2.5 or for Rails 3 and 4 use [version 0.0.6](https://rubygems.org/gems/rate_limiter/versions/0.0.6).
8
13
 
9
14
  ## Installation
10
15
 
@@ -14,19 +19,19 @@ Add the gem to your project's Gemfile:
14
19
 
15
20
  ## Basic Usage
16
21
 
17
- In the models you want to rate limit simply call the `rate_limit` method inside the model.
22
+ In the models you want to rate limit simply call the `rate_limit` class method inside the model.
18
23
 
19
24
  ```ruby
20
- class ProductReview < ActiveRecord::Base
25
+ class ProductReview < ApplicationRecord
21
26
  rate_limit
22
27
  end
23
28
  ```
24
29
 
25
- By default this will rate limit creation of instances using the `ip_address` attribute with an interval of one minute. This is kind of a bold assumption (that may change in future versions) since there's a good chance you don't have an `ip_address` attribute on your model. If that's the case then you can do the following:
30
+ By default this will rate limit creation of instances using the `ip_address` attribute with an interval of one minute. This is a pretty big assumption that may change in future versions. To use a different attribute do the following:
26
31
 
27
32
  ```ruby
28
- class ProductReview < ActiveRecord::Base
29
- rate_limit :on => :username
33
+ class ProductReview < ApplicationRecord
34
+ rate_limit on: :username
30
35
  end
31
36
  ```
32
37
 
@@ -36,10 +41,14 @@ Because you may want to increase or decrease the interval between creating insta
36
41
 
37
42
  ```ruby
38
43
  class ProductReview < ActiveRecord::Base
39
- rate_limit :interval => 3.hours
44
+ rate_limit interval: 3.hours
40
45
  end
41
46
  ```
42
47
 
48
+ ## Credit Where Credit Is Due
49
+
50
+ Large portions of this gem are copied almost verbatim from the excellent [paper_trail](https://github.com/paper-trail-gem/paper_trail) gem; in particular the overall structure and all of the stuff that handles whether or not rate limiting is active.
51
+
43
52
  ## Contributing
44
53
 
45
54
  If you feel like you can add something useful to rate_limiter then don't hesitate to contribute! To make sure your fix/feature has a high chance of being included, please do the following:
@@ -61,5 +70,3 @@ Some things that will increase the chance that your pull request is accepted, ta
61
70
  * Use Rails idioms and helpers
62
71
  * Include tests that fail without your code, and pass with it
63
72
  * Update the documentation, guides, or whatever is affected by your contribution
64
-
65
- Yes, I am well aware of the irony of asking for tests when there are effectively none right now. This gem is a work in progress.
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/base'
4
+
5
+ module RateLimiter
6
+ module Generators
7
+ # Rails generator for installing the default RateLimiter initializer.
8
+ class InstallGenerator < ::Rails::Generators::Base
9
+ source_root File.expand_path('./templates', __dir__)
10
+
11
+ desc 'Creates a RateLimiter initializer and copies a default locale file to your application.'
12
+
13
+ def copy_initializer
14
+ template('rate_limiter.rb', 'config/initializers/rate_limiter.rb')
15
+ end
16
+
17
+ def copy_locale
18
+ copy_file('../../../../config/locales/en.yml', 'config/locales/rate_limiter.en.yml')
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configure the default behavior of RateLimiter here.
4
+ RateLimiter.config do |config|
5
+ # config.timestamp_field = :created_at
6
+ end
data/lib/rate_limiter.rb CHANGED
@@ -1,63 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/all'
4
+ require 'active_record'
5
+
1
6
  require 'rate_limiter/config'
2
- require 'rate_limiter/controller'
3
7
  require 'rate_limiter/model'
8
+ require 'rate_limiter/request'
4
9
 
10
+ # Extend your ActiveRecord models with the ability to limit the rate at which
11
+ # they are saved.
5
12
  module RateLimiter
6
- def self.enabled=(value)
7
- RateLimiter.config.enabled = value
8
- end
9
-
10
- def self.enabled?
11
- !!RateLimiter.config.enabled
12
- end
13
-
14
- def self.enabled_for_controller=(value)
15
- rate_limiter_store[:request_enabled_for_controller] = value
16
- end
17
-
18
- def self.enabled_for_controller?
19
- !!rate_limiter_store[:request_enabled_for_controller]
20
- end
21
-
22
- def self.timestamp_field=(field_name)
23
- RateLimiter.config.timestamp_field = field_name
24
- end
25
-
26
- def self.timestamp_field
27
- RateLimiter.config.timestamp_field
28
- end
29
-
30
- def self.source=(value)
31
- rate_limiter_store[:source] = value
32
- end
33
-
34
- def self.source
35
- rate_limiter_store[:source]
36
- end
37
-
38
- def self.controller_info=(value)
39
- rate_limiter_store[:controller_info] = value
40
- end
41
-
42
- def self.controller_info
43
- rate_limiter_store[:controller_info]
44
- end
45
-
46
- private
47
-
48
- def self.rate_limiter_store
49
- Thread.current[:rate_limiter] ||= { :request_enabled_for_controller => true }
50
- end
51
-
52
- def self.config
53
- @@config ||= RateLimiter::Config.instance
13
+ class << self
14
+ # Return the RateLimiter singleton configuration object. This is for all
15
+ # threads.
16
+ def config
17
+ @config ||= Config.instance
18
+ yield @config if block_given?
19
+ @config
20
+ end
21
+ alias configure config
22
+
23
+ # Switches RateLimiter on or off, for all threads.
24
+ def enabled=(value)
25
+ config.enabled = value
26
+ end
27
+
28
+ # Returns `true` if RateLimiter is on, `false if it is off. This is for all
29
+ # threads.
30
+ def enabled?
31
+ config.enabled
32
+ end
33
+
34
+ # Gets the options local to the current request.
35
+ #
36
+ # If given a block the options passed in are set, the block is executed,
37
+ # previous options are restored, and the return value of the block is
38
+ # returned.
39
+ def request(options = nil, &block)
40
+ if options.nil? && !block_given?
41
+ Request
42
+ else
43
+ Request.with(options, &block)
44
+ end
45
+ end
54
46
  end
55
47
  end
56
48
 
49
+ # See https://guides.rubyonrails.org/engines.html#what-are-on-load-hooks-questionmark
50
+ # for more information on `on_load` hooks.
57
51
  ActiveSupport.on_load(:active_record) do
58
52
  include RateLimiter::Model
59
53
  end
60
54
 
61
- ActiveSupport.on_load(:action_controller) do
62
- include RateLimiter::Controller
55
+ # Load Rails controller extensions if RateLimiter is being used in a Rails
56
+ # application.
57
+ if defined?(::Rails)
58
+ if defined?(::Rails.application)
59
+ require 'rate_limiter/frameworks/rails'
60
+ else
61
+ Kernel.warn('RateLimiter has been loaded before Rails.')
62
+ end
63
63
  end
@@ -1,13 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'singleton'
2
4
 
3
5
  module RateLimiter
6
+ # Global configuration that affects all threads. Thread-specific configuration
7
+ # can be found in `/lib/rate_limiter.rb` and in
8
+ # `/lib/rate_limite/frameworks/rails/controller.rb`.
4
9
  class Config
5
10
  include Singleton
6
- attr_accessor :enabled, :timestamp_field
11
+
12
+ attr_accessor :rate_limit_defaults
7
13
 
8
14
  def initialize
9
- @enabled = true
10
- @timestamp_field = :created_at
15
+ @mutex = Mutex.new
16
+ @enabled = true
17
+ @rate_limit_defaults = {}
18
+ end
19
+
20
+ def enabled
21
+ @mutex.synchronize { !!@enabled }
22
+ end
23
+
24
+ def enabled=(enable)
25
+ @mutex.synchronize { @enabled = enable }
11
26
  end
12
27
  end
13
28
  end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rate_limiter/frameworks/rails/controller'
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RateLimiter
4
+ module Rails
5
+ # Extensions for Rails controllers. Allows for rate limiting to be turned on
6
+ # or off without disabling it on the model.
7
+ module Controller
8
+ def self.included(base)
9
+ base.before_action(
10
+ :set_rate_limiter_enabled_for_controller,
11
+ :set_rate_limiter_source
12
+ )
13
+ end
14
+
15
+ protected
16
+
17
+ # Get the user to use for the source for the current request. By default this will
18
+ # attempt to return the value of `current_user` since that is what Devise uses. If
19
+ # that assumption is incorrect this method can be overridden to return the correct
20
+ # user or ID (or nothing at all).
21
+ #
22
+ # ```
23
+ # def user_for_rate_limiter
24
+ # logged_in_user.id
25
+ # end
26
+ # ```
27
+ def user_for_rate_limiter
28
+ return nil unless respond_to?(:current_user)
29
+
30
+ current_user
31
+ end
32
+
33
+ # Returns `true` or `false` depending on whether rate imiting should be
34
+ # active for the current request for all models.
35
+ #
36
+ # Override this method in your controller to turn rate limiting on or off.
37
+ #
38
+ # ```
39
+ # def rate_limiter_enabled_for_controller
40
+ # # It is recommended that you always call `super` here unless simply
41
+ # # returning `false`.
42
+ # super && !user_for_rate_limiter.has_role?(:admin)
43
+ # end
44
+ # ```
45
+ def rate_limiter_enabled_for_controller
46
+ RateLimiter.enabled?
47
+ end
48
+
49
+ private
50
+
51
+ # Tells RateLimiter whether rate limiting should be enabled for the
52
+ # current request.
53
+ def set_rate_limiter_enabled_for_controller
54
+ RateLimiter.request.enabled = rate_limiter_enabled_for_controller
55
+ end
56
+
57
+ # Set the request store's source.
58
+ def set_rate_limiter_source
59
+ RateLimiter.request.source = user_for_rate_limiter if rate_limiter_enabled_for_controller
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ if defined?(::ActionController)
66
+ ::ActiveSupport.on_load(:action_controller) do
67
+ include RateLimiter::Rails::Controller
68
+ end
69
+ end
@@ -1,66 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rate_limiter/model_config'
4
+ require 'rate_limiter/throttle'
5
+
1
6
  module RateLimiter
7
+ # Mixin module for ActiveRecord models.
2
8
  module Model
3
9
  def self.included(base)
4
- base.send :extend, ClassMethods
10
+ base.send(:extend, ClassMethods)
5
11
  end
6
12
 
13
+ # Class methods available to models after RateLimiter has been loaded.
7
14
  module ClassMethods
15
+ # Tell the model to limit creation of records based on an attribute for a
16
+ # given interval of time.
17
+ #
18
+ # Options:
19
+ #
20
+ # - :on - The attribute to limit on. Defaults to `:ip_address`. Set to an
21
+ # array to limit on multiple attributes (e.g. `:ip_address` or `:user_id`.
22
+ # - :interval - The amount of time that must have elapses since the last
23
+ # record that has the same value as the attribute indicated by the `:on`
24
+ # option in seconds. Defaults to 1 minute.
25
+ # - :if, :unless - Procs that specify the conditions for when record
26
+ # creation rate limiting should occur.
8
27
  def rate_limit(options = {})
9
- send :include, InstanceMethods
10
-
11
- class_attribute :rate_limit_on
12
- self.rate_limit_on = options[:on] || :ip_address
13
-
14
- class_attribute :rate_limit_interval
15
- self.rate_limit_interval = options[:interval] || 1.minute
16
-
17
- class_attribute :rate_limit_if_condition
18
- self.rate_limit_if_condition = options[:if]
19
-
20
- class_attribute :rate_limit_unless_condition
21
- self.rate_limit_unless_condition = options[:unless]
22
-
23
- class_attribute :rate_limit_enabled_for_model
24
- self.rate_limit_enabled_for_model = true
25
-
26
- self.before_create :check_rate_limit
28
+ defaults = RateLimiter.config.rate_limit_defaults
29
+ rate_limiter.setup(defaults.merge(options))
27
30
  end
28
31
 
29
- def rate_limit_off
30
- self.rate_limit_enabled_for_model = false
31
- end
32
-
33
- def rate_limit_on
34
- self.rate_limit_enabled_for_model = true
32
+ def rate_limiter
33
+ ModelConfig.new(self)
35
34
  end
36
35
  end
37
36
 
37
+ # Instance methods available to models after RateLimiter has been initialized by
38
+ # calling `rate_limit`.
38
39
  module InstanceMethods
39
- def check_rate_limit
40
- if switched_on? && rate_limit?
41
- klass = self.class
42
-
43
- others = klass.where("#{klass.rate_limit_on.to_s} = ? AND #{RateLimiter.config.timestamp_field.to_s} >= ?", self.send(klass.rate_limit_on), Time.now - klass.rate_limit_interval)
44
-
45
- if others.present?
46
- # TODO: Come up with a better error message.
47
- self.errors.add(:base, "You cannot create a new #{klass.name.downcase} yet.")
48
-
49
- false
50
- else
51
- true
52
- end
53
- else
54
- true
55
- end
56
- end
57
-
58
- def switched_on?
59
- RateLimiter.enabled? && RateLimiter.enabled_for_controller? && self.class.rate_limit_enabled_for_model
40
+ def rate_limit_exceeded?
41
+ throttle.exceeded?
60
42
  end
61
43
 
62
- def rate_limit?
63
- (rate_limit_if_condition.blank? || rate_limit_if_condition.call(self)) && !rate_limit_unless_condition.try(:call, self)
44
+ def throttle
45
+ Throttle.new(self, self.class.rate_limiter_options)
64
46
  end
65
47
  end
66
48
  end