spam_protect 0.0.4
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 +7 -0
- data/LICENSE +18 -0
- data/README.md +106 -0
- data/lib/generators/spam_protect/install/install_generator.rb +17 -0
- data/lib/generators/spam_protect/install/templates/spam_protect.rb +24 -0
- data/lib/spam_protect/controller_helpers.rb +25 -0
- data/lib/spam_protect/current_time.rb +11 -0
- data/lib/spam_protect/encryption/payload.rb +44 -0
- data/lib/spam_protect/encryption/secret_key.rb +35 -0
- data/lib/spam_protect/encryption.rb +23 -0
- data/lib/spam_protect/errors/encryption_unavailable.rb +11 -0
- data/lib/spam_protect/errors/error.rb +8 -0
- data/lib/spam_protect/errors/no_secret_key.rb +11 -0
- data/lib/spam_protect/form_builder.rb +24 -0
- data/lib/spam_protect/guardian.rb +63 -0
- data/lib/spam_protect/policies/base_policy.rb +15 -0
- data/lib/spam_protect/policies/cookie_policy.rb +20 -0
- data/lib/spam_protect/policies/encryption_policy.rb +28 -0
- data/lib/spam_protect/policies/honeypot_policy.rb +15 -0
- data/lib/spam_protect/policies/timestamp_policy.rb +22 -0
- data/lib/spam_protect/railtie.rb +23 -0
- data/lib/spam_protect/version.rb +5 -0
- data/lib/spam_protect/view_helpers.rb +19 -0
- data/lib/spam_protect.rb +54 -0
- metadata +134 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 642e4962310843445840e8799d51e75a6f02ee4eaa16b3f3be8298446fb9ed28
|
|
4
|
+
data.tar.gz: e2b35c61e0a5e20c270a327b2eb63ec1cc0505604ff7422307e8e25cc943b187
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 40e5f2fc28e7761b82051430b5c66e09656ac93faf0fcb63925bcb43f26092226206d47e244606e091b3c453637b726bdebb0ca776fdf1c409e19fd43210b40a
|
|
7
|
+
data.tar.gz: 1f4feae42a54fd4e64dbe93e2320f4f6417172155beba6defc987493d34e7d414d1f6fb5b58153df9e47b03434bf5b6266af900d42dc2881af47bbfab04d64d7
|
data/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Full Fat Software Ltd
|
|
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 SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
13
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
14
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
15
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
16
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
17
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
18
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# spam_protect
|
|
2
|
+
|
|
3
|
+
A lightweight Ruby gem to help reduce spam in Rails applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'spam_protect'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
bundle install
|
|
16
|
+
|
|
17
|
+
Then add the initializer to your Rails application:
|
|
18
|
+
|
|
19
|
+
bin/rails generate spam_protect:install
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Use the form helper method `spam_protect_field` within your forms to include the necessary spam protection fields:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
<%= form_for @comment do |f| >
|
|
27
|
+
<%= f.spam_protect_field %>
|
|
28
|
+
<% end %>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This generates output similar to:
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<input type="text" name="comment[hp_field]" class="sp_hp" autocomplete="off" tabindex="-1" />
|
|
35
|
+
<input type="hidden" name="comment[sp_timestamp]" value="encrypted_token_here" />
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Visually hide the honeypot field with CSS:
|
|
39
|
+
|
|
40
|
+
```css
|
|
41
|
+
.sp_hp {
|
|
42
|
+
display: none !important;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Include the JavaScript tag in any views where forms with spam protection are used:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
<%= spam_protect_javascript_tag %>
|
|
50
|
+
```
|
|
51
|
+
Check the results in your controller:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
class CommentsController < ApplicationController
|
|
55
|
+
def create
|
|
56
|
+
if validate_spam_protect_params(params[:comment])
|
|
57
|
+
@comment = Comment.new(comment_params)
|
|
58
|
+
...
|
|
59
|
+
else
|
|
60
|
+
# handle spam case
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
68
|
+
You can customize the behavior of the gem by modifying the configuration file located at `config/initializers/spam_protect.rb`. Here you can set options such as encryption keys, token expiration times, and honeypot field names.
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
# Example initializer for spam_protect
|
|
72
|
+
SpamProtect.configure do |config|
|
|
73
|
+
# Custom field names (symbols)
|
|
74
|
+
config.honeypot_field = :hp_phone
|
|
75
|
+
config.timestamp_field = :hp_ts
|
|
76
|
+
|
|
77
|
+
# CSS class applied to the honeypot field
|
|
78
|
+
config.honeypot_class = "sp_hp"
|
|
79
|
+
config.wrapper_class = "spam_protect"
|
|
80
|
+
|
|
81
|
+
# Require JavaScript checks
|
|
82
|
+
config.require_js = true # Default is true
|
|
83
|
+
|
|
84
|
+
# Minimum seconds required between form render and submission
|
|
85
|
+
config.min_seconds = 3
|
|
86
|
+
|
|
87
|
+
# Secret key for signing/encrypting timestamps
|
|
88
|
+
config.signature_secret = SecureRandom.hex(64) # Will default to Rails secret_key_base if nil
|
|
89
|
+
|
|
90
|
+
# Expiry duration for the signature
|
|
91
|
+
config.signature_expiry = 6.hours
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
Clone the repository and run:
|
|
99
|
+
|
|
100
|
+
bundle install
|
|
101
|
+
bundle exec rspec
|
|
102
|
+
bundle exec rubocop # for linting
|
|
103
|
+
|
|
104
|
+
### Requirements
|
|
105
|
+
|
|
106
|
+
This gem requires Ruby 3.3 or newer. The repository `.ruby-version` is set to `3.4.1` but any Ruby >= 3.3 should work.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module SpamProtect
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Creates a SpamProtect initializer in config/initializers"
|
|
11
|
+
|
|
12
|
+
def copy_initializer
|
|
13
|
+
copy_file "spam_protect.rb", "config/initializers/spam_protect.rb"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example initializer for spam_protect
|
|
4
|
+
SpamProtect.configure do |config|
|
|
5
|
+
# Custom field names (symbols)
|
|
6
|
+
# config.honeypot_field = :hp_phone
|
|
7
|
+
# config.timestamp_field = :hp_ts
|
|
8
|
+
|
|
9
|
+
# CSS class applied to the honeypot field
|
|
10
|
+
# config.honeypot_class = "sp_hp"
|
|
11
|
+
# config.wrapper_class = "spam_protect"
|
|
12
|
+
|
|
13
|
+
# Require JavaScript checks
|
|
14
|
+
# config.require_js = true # Default is true
|
|
15
|
+
|
|
16
|
+
# Minimum seconds required between form render and submission
|
|
17
|
+
# config.min_seconds = 3
|
|
18
|
+
|
|
19
|
+
# Secret key for signing/encrypting timestamps
|
|
20
|
+
# config.signature_secret = SecureRandom.hex(64) # Will default to Rails secret_key_base if nil
|
|
21
|
+
|
|
22
|
+
# Expiry duration for the signature
|
|
23
|
+
# config.signature_expiry = 6.hours
|
|
24
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpamProtect
|
|
4
|
+
module ControllerHelpers
|
|
5
|
+
def validate_spam_protect_params(params, honeypot_key: nil, timestamp_key: nil, min_seconds: nil)
|
|
6
|
+
honeypot_key ||= SpamProtect.config.honeypot_field
|
|
7
|
+
timestamp_key ||= SpamProtect.config.timestamp_field
|
|
8
|
+
min_seconds ||= SpamProtect.config.min_seconds
|
|
9
|
+
|
|
10
|
+
unless params.is_a?(ActionController::Parameters)
|
|
11
|
+
raise ArgumentError, "params must be an instance of ActionController::Parameters"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
unless params.key?(honeypot_key) && params.key?(timestamp_key)
|
|
15
|
+
raise ArgumentError, "params must include both #{honeypot_key} and #{timestamp_key} keys. Have you passed in params[:<model_name>]?"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
honeypot_value = params[honeypot_key]
|
|
19
|
+
encrypted_timestamp = params[timestamp_key]
|
|
20
|
+
|
|
21
|
+
guardian = SpamProtect::Guardian.new(honeypot_value, encrypted_timestamp, cookies["spam_protect_token"], min_seconds)
|
|
22
|
+
guardian.valid?
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpamProtect
|
|
4
|
+
module Encryption
|
|
5
|
+
class Payload
|
|
6
|
+
VALID_METHODS = %w[timestamp expires_at].freeze
|
|
7
|
+
|
|
8
|
+
def self.generate
|
|
9
|
+
new({
|
|
10
|
+
timestamp: CurrentTime.now.to_i,
|
|
11
|
+
expires_at: CurrentTime.now.to_i + SpamProtect.config.signature_expiry.to_i
|
|
12
|
+
})
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(hash)
|
|
16
|
+
@hash = hash
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def [](key)
|
|
20
|
+
if VALID_METHODS.include?(key.to_s)
|
|
21
|
+
send(key)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_h
|
|
26
|
+
{
|
|
27
|
+
"timestamp" => timestamp,
|
|
28
|
+
"expires_at" => expires_at
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def expires_at
|
|
33
|
+
@hash[:expires_at] || @hash["expires_at"]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def timestamp
|
|
37
|
+
@hash[:timestamp] || @hash["timestamp"]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
alias_method :expires_at?, :expires_at
|
|
41
|
+
alias_method :timestamp?, :timestamp
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpamProtect
|
|
4
|
+
module Encryption
|
|
5
|
+
class SecretKey
|
|
6
|
+
class << self
|
|
7
|
+
def relevant_key!
|
|
8
|
+
result = from_configuration || from_rails
|
|
9
|
+
|
|
10
|
+
unless result.present?
|
|
11
|
+
raise Errors::NoSecretKey
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
unless defined?(ActiveSupport::KeyGenerator) && defined?(ActiveSupport::MessageEncryptor)
|
|
15
|
+
raise Errors::EncryptionUnavailable
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
key_len = ActiveSupport::MessageEncryptor.key_len
|
|
19
|
+
|
|
20
|
+
ActiveSupport::KeyGenerator.new(result).generate_key("spam_protect", key_len)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def from_configuration
|
|
26
|
+
SpamProtect.config.signature_secret
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def from_rails
|
|
30
|
+
(defined?(Rails) && Rails.application.respond_to?(:secret_key_base)) ? Rails.application.secret_key_base : nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpamProtect
|
|
4
|
+
module Encryption
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
# Encrypt and sign a payload (hash). Returns a token string or nil on failure.
|
|
8
|
+
def encrypt(payload)
|
|
9
|
+
encryptor = ActiveSupport::MessageEncryptor.new(secret_key)
|
|
10
|
+
encryptor.encrypt_and_sign(payload)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Decrypt and verify a token. Returns the payload (usually a Hash) or nil.
|
|
14
|
+
def decrypt(token)
|
|
15
|
+
encryptor = ActiveSupport::MessageEncryptor.new(secret_key)
|
|
16
|
+
encryptor.decrypt_and_verify(token)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def secret_key
|
|
20
|
+
SecretKey.relevant_key!
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpamProtect
|
|
4
|
+
module Errors
|
|
5
|
+
class EncryptionUnavailable < Error
|
|
6
|
+
def message
|
|
7
|
+
"ActiveSupport encryption helpers are not available. Please ensure the 'activesupport' gem is included in your project."
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpamProtect
|
|
4
|
+
module Errors
|
|
5
|
+
class NoSecretKey < Error
|
|
6
|
+
def message
|
|
7
|
+
"No secret key available for signing/encryption. Please set `SpamProtect.config.signature_secret` or ensure Rails is loaded with a valid `secret_key_base`."
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpamProtect
|
|
4
|
+
module FormBuilderMethods
|
|
5
|
+
def spam_protect_field(name: nil, timestamp_name: nil, wrapper: false)
|
|
6
|
+
name ||= SpamProtect.config.honeypot_field
|
|
7
|
+
timestamp_name ||= SpamProtect.config.timestamp_field
|
|
8
|
+
honeypot_class = SpamProtect.config.honeypot_class
|
|
9
|
+
wrapper_class = SpamProtect.config.wrapper_class
|
|
10
|
+
|
|
11
|
+
payload = Encryption::Payload.generate
|
|
12
|
+
token = Encryption.encrypt(payload.to_h)
|
|
13
|
+
|
|
14
|
+
honeypot = @template.text_field_tag("#{@object_name}[#{name}]", nil, class: honeypot_class, autocomplete: "off", tabindex: "-1")
|
|
15
|
+
signature_input = @template.hidden_field_tag("#{@object_name}[#{timestamp_name}]", token)
|
|
16
|
+
|
|
17
|
+
if wrapper
|
|
18
|
+
@template.content_tag(:div, honeypot + signature_input, class: wrapper_class)
|
|
19
|
+
else
|
|
20
|
+
(honeypot + signature_input).html_safe
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpamProtect
|
|
4
|
+
class Guardian
|
|
5
|
+
attr_reader :errors
|
|
6
|
+
|
|
7
|
+
def initialize(honeypot_value, encrypted_timestamp, cookie, min_seconds)
|
|
8
|
+
@honeypot_value = honeypot_value
|
|
9
|
+
@encrypted_timestamp = encrypted_timestamp
|
|
10
|
+
@cookie = cookie
|
|
11
|
+
@min_seconds = min_seconds
|
|
12
|
+
@errors = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def valid?
|
|
16
|
+
payload = Encryption::Payload.new(
|
|
17
|
+
Encryption.decrypt(@encrypted_timestamp)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
cookie_payload = if SpamProtect.config.require_js
|
|
21
|
+
Encryption::Payload.new(
|
|
22
|
+
Encryption.decrypt(@cookie)
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
payloads = [
|
|
27
|
+
payload,
|
|
28
|
+
cookie_payload
|
|
29
|
+
].compact
|
|
30
|
+
|
|
31
|
+
honeypot_policy = Policies::HoneypotPolicy.new(@honeypot_value)
|
|
32
|
+
if honeypot_policy.invalid?
|
|
33
|
+
@errors.append "Honeypot field is filled in"
|
|
34
|
+
return false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
cookie_policy = Policies::CookiePolicy.new(@cookie)
|
|
38
|
+
if cookie_policy.invalid?
|
|
39
|
+
@errors.append "Cookie is invalid"
|
|
40
|
+
return false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
payloads.each do |p|
|
|
44
|
+
encryption_policy = Policies::EncryptionPolicy.new(p)
|
|
45
|
+
if encryption_policy.invalid?
|
|
46
|
+
@errors.append "Payload encryption is invalid or expired"
|
|
47
|
+
return false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
timestamp_policy = Policies::TimestampPolicy.new(p["timestamp"], @min_seconds)
|
|
51
|
+
if timestamp_policy.invalid?
|
|
52
|
+
@errors.append "Form submitted too quickly"
|
|
53
|
+
return false
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
true
|
|
58
|
+
rescue
|
|
59
|
+
@errors.append "Encryption failure"
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpamProtect
|
|
4
|
+
module Policies
|
|
5
|
+
class CookiePolicy < BasePolicy
|
|
6
|
+
def initialize(cookie)
|
|
7
|
+
@cookie = cookie
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def valid?
|
|
11
|
+
unless SpamProtect.config.require_js
|
|
12
|
+
return true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Decryption/validity is checked elsewhere
|
|
16
|
+
@cookie.to_s.strip.present?
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpamProtect
|
|
4
|
+
module Policies
|
|
5
|
+
class EncryptionPolicy < BasePolicy
|
|
6
|
+
def initialize(payload)
|
|
7
|
+
@payload = payload
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def valid?
|
|
11
|
+
correct_shape? && between_timestamps?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
protected
|
|
15
|
+
|
|
16
|
+
def correct_shape?
|
|
17
|
+
@payload.timestamp? && @payload.expires_at?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def between_timestamps?
|
|
21
|
+
CurrentTime.now.between?(
|
|
22
|
+
Time.at(@payload["timestamp"].to_i),
|
|
23
|
+
Time.at(@payload["expires_at"].to_i)
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpamProtect
|
|
4
|
+
module Policies
|
|
5
|
+
class TimestampPolicy < BasePolicy
|
|
6
|
+
def initialize(timestamp, min_seconds)
|
|
7
|
+
@timestamp = timestamp
|
|
8
|
+
@min_seconds = min_seconds
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def valid?
|
|
12
|
+
return false if @timestamp.blank?
|
|
13
|
+
|
|
14
|
+
now = CurrentTime.now
|
|
15
|
+
|
|
16
|
+
submitted_at = Time.at(@timestamp.to_i)
|
|
17
|
+
|
|
18
|
+
(now - submitted_at) >= @min_seconds
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module SpamProtect
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
initializer "spam_protect.action_view" do
|
|
8
|
+
ActiveSupport.on_load(:action_view) do
|
|
9
|
+
require_relative "form_builder"
|
|
10
|
+
require_relative "view_helpers"
|
|
11
|
+
ActionView::Helpers::FormBuilder.include(SpamProtect::FormBuilderMethods)
|
|
12
|
+
ActionView::Base.include(SpamProtect::ViewHelpers)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
initializer "spam_protect.action_controller" do
|
|
17
|
+
ActiveSupport.on_load(:action_controller) do
|
|
18
|
+
require_relative "controller_helpers"
|
|
19
|
+
ActionController::Base.include(SpamProtect::ControllerHelpers) if defined?(ActionController::Base)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpamProtect
|
|
4
|
+
module ViewHelpers
|
|
5
|
+
def spam_protect_javascript_tag
|
|
6
|
+
payload = Encryption::Payload.generate
|
|
7
|
+
token = Encryption.encrypt(payload.to_h)
|
|
8
|
+
|
|
9
|
+
js = <<~JS
|
|
10
|
+
(function(){
|
|
11
|
+
var token = #{token.to_json};
|
|
12
|
+
document.cookie = "spam_protect_token=" + encodeURIComponent(token) + "; path=/; SameSite=Lax; Secure";
|
|
13
|
+
})();
|
|
14
|
+
JS
|
|
15
|
+
|
|
16
|
+
%(<script>#{js}</script>).html_safe
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/spam_protect.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_view"
|
|
4
|
+
require "active_support"
|
|
5
|
+
require "active_support/core_ext/numeric/time"
|
|
6
|
+
require "active_support/message_encryptor"
|
|
7
|
+
require "active_support/key_generator"
|
|
8
|
+
|
|
9
|
+
require_relative "spam_protect/errors/error"
|
|
10
|
+
require_relative "spam_protect/errors/encryption_unavailable"
|
|
11
|
+
require_relative "spam_protect/errors/no_secret_key"
|
|
12
|
+
require_relative "spam_protect/encryption"
|
|
13
|
+
require_relative "spam_protect/encryption/payload"
|
|
14
|
+
require_relative "spam_protect/encryption/secret_key"
|
|
15
|
+
require_relative "spam_protect/policies/base_policy"
|
|
16
|
+
require_relative "spam_protect/policies/honeypot_policy"
|
|
17
|
+
require_relative "spam_protect/policies/timestamp_policy"
|
|
18
|
+
require_relative "spam_protect/policies/encryption_policy"
|
|
19
|
+
require_relative "spam_protect/policies/cookie_policy"
|
|
20
|
+
require_relative "spam_protect/guardian"
|
|
21
|
+
require_relative "spam_protect/current_time"
|
|
22
|
+
require_relative "spam_protect/form_builder"
|
|
23
|
+
require_relative "spam_protect/view_helpers"
|
|
24
|
+
require_relative "spam_protect/version"
|
|
25
|
+
require_relative "spam_protect/railtie"
|
|
26
|
+
require_relative "spam_protect/controller_helpers"
|
|
27
|
+
|
|
28
|
+
module SpamProtect
|
|
29
|
+
# Configuration object for the gem
|
|
30
|
+
class Config
|
|
31
|
+
attr_accessor :honeypot_field, :timestamp_field, :honeypot_class,
|
|
32
|
+
:wrapper_class, :require_js, :min_seconds, :signature_secret,
|
|
33
|
+
:signature_expiry
|
|
34
|
+
|
|
35
|
+
def initialize
|
|
36
|
+
@honeypot_field = :hp_phone
|
|
37
|
+
@timestamp_field = :hp_ts
|
|
38
|
+
@honeypot_class = "sp_hp"
|
|
39
|
+
@wrapper_class = "spam_protect"
|
|
40
|
+
@require_js = true
|
|
41
|
+
@min_seconds = 2
|
|
42
|
+
@signature_secret = nil
|
|
43
|
+
@signature_expiry = 6.hours
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.config
|
|
48
|
+
@config ||= Config.new
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.configure
|
|
52
|
+
yield config if block_given?
|
|
53
|
+
end
|
|
54
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: spam_protect
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.4
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Full Fat Software
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2025-11-14 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rspec
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rubocop
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: standard
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rubocop-performance
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rails
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '7.2'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '7.2'
|
|
82
|
+
description: spam_protect stops contact message spam in rails applications by adding
|
|
83
|
+
honeypot fields and timestamp checks.
|
|
84
|
+
email:
|
|
85
|
+
- hey@fullfatsoftware.com
|
|
86
|
+
executables: []
|
|
87
|
+
extensions: []
|
|
88
|
+
extra_rdoc_files: []
|
|
89
|
+
files:
|
|
90
|
+
- LICENSE
|
|
91
|
+
- README.md
|
|
92
|
+
- lib/generators/spam_protect/install/install_generator.rb
|
|
93
|
+
- lib/generators/spam_protect/install/templates/spam_protect.rb
|
|
94
|
+
- lib/spam_protect.rb
|
|
95
|
+
- lib/spam_protect/controller_helpers.rb
|
|
96
|
+
- lib/spam_protect/current_time.rb
|
|
97
|
+
- lib/spam_protect/encryption.rb
|
|
98
|
+
- lib/spam_protect/encryption/payload.rb
|
|
99
|
+
- lib/spam_protect/encryption/secret_key.rb
|
|
100
|
+
- lib/spam_protect/errors/encryption_unavailable.rb
|
|
101
|
+
- lib/spam_protect/errors/error.rb
|
|
102
|
+
- lib/spam_protect/errors/no_secret_key.rb
|
|
103
|
+
- lib/spam_protect/form_builder.rb
|
|
104
|
+
- lib/spam_protect/guardian.rb
|
|
105
|
+
- lib/spam_protect/policies/base_policy.rb
|
|
106
|
+
- lib/spam_protect/policies/cookie_policy.rb
|
|
107
|
+
- lib/spam_protect/policies/encryption_policy.rb
|
|
108
|
+
- lib/spam_protect/policies/honeypot_policy.rb
|
|
109
|
+
- lib/spam_protect/policies/timestamp_policy.rb
|
|
110
|
+
- lib/spam_protect/railtie.rb
|
|
111
|
+
- lib/spam_protect/version.rb
|
|
112
|
+
- lib/spam_protect/view_helpers.rb
|
|
113
|
+
homepage: https://github.com/fullfatsoftware/spam_protect
|
|
114
|
+
licenses:
|
|
115
|
+
- MIT
|
|
116
|
+
metadata: {}
|
|
117
|
+
rdoc_options: []
|
|
118
|
+
require_paths:
|
|
119
|
+
- lib
|
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '3.3'
|
|
125
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - ">="
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: '0'
|
|
130
|
+
requirements: []
|
|
131
|
+
rubygems_version: 3.6.2
|
|
132
|
+
specification_version: 4
|
|
133
|
+
summary: A lightweight Ruby gem to help reduce spam in rails applications
|
|
134
|
+
test_files: []
|