cloudflare-turnstile-rails 1.1.0 → 1.3.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: b7a94bbf6a29069c0d4f5f291ca3e80d368942aad2e03f20ae7b64091f9d900e
4
- data.tar.gz: 3156465de7ffac378b48b61250377536272acd9bd15998d61be263701fe92657
3
+ metadata.gz: 7bdf250ea2814264e89921898dcc1c38b968596fe59e9423914b2d245ee8607c
4
+ data.tar.gz: e72026fff680a00cccfdb6134fbe9dafe0658abc8c135b0c5a4bc73404a1a104
5
5
  SHA512:
6
- metadata.gz: e7304843b0657e901e07f3264e6ab6676914af2b9d6a8f38a0778b53587cff1ba7454c71793fe45cac69bb7741e8264e3495337fb4544bd5f88551e569847a5d
7
- data.tar.gz: 808592cee1ae30ea1a6c19a9887bfe62d33979b991810bb078bec618a0e2e37ec4a93152d7f5552c4786245a25f4497e2ab7f0cdf925c387f0e5588468cb4a52
6
+ metadata.gz: dfe05337cc155cd855b9297c8512d1c173069862a33f3061b685e41db3ec4ac7c20e86466d2c90d82d10795ccb0560b64177a344a5098885506efc53f614872d
7
+ data.tar.gz: aa3d5dacc7e140dedf475881f61522480b30c87e39bf1b228999ed5244fbe33cf1f02af3ffa6575bc2e0a09acb78221375b2fe82863dfcf13274398bac631d3d
data/README.md CHANGED
@@ -5,18 +5,21 @@
5
5
  [![Rails](https://img.shields.io/badge/Rails-5.0%20to%208.1-D30001?logo=rubyonrails&logoColor=white)](https://github.com/vkononov/cloudflare-turnstile-rails/blob/main/Appraisals)
6
6
  [![Test Matrix](https://img.shields.io/github/actions/workflow/status/vkononov/cloudflare-turnstile-rails/test.yml?branch=main&label=Test%20Matrix&logo=github)](https://github.com/vkononov/cloudflare-turnstile-rails/actions/workflows/test.yml)
7
7
  [![Lint](https://img.shields.io/github/actions/workflow/status/vkononov/cloudflare-turnstile-rails/lint.yml?branch=main&label=Lint&logo=github)](https://github.com/vkononov/cloudflare-turnstile-rails/actions/workflows/lint.yml)
8
- [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/license/MIT)
9
9
 
10
10
  Cloudflare Turnstile gem for Ruby on Rails with built-in Turbo and Turbolinks support and CSP compliance.
11
11
 
12
12
  Supports **Rails 5.0 → latest** and **Ruby 2.6 → latest**, with the full Rails/Ruby matrix tested daily in CI.
13
13
 
14
+ > [!TIP]
15
+ > **Using Devise?** [devise-cloudflare-turnstile](https://github.com/vkononov/devise-cloudflare-turnstile) protects every Devise form (sign in, sign up, password reset, and more) with no changes to your Devise views or controllers. It is built on this gem and shares its configuration, so you can use both to protect Devise and non-Devise forms in the same app.
16
+
14
17
  [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://www.buymeacoffee.com/vkononov)
15
18
 
16
19
  ## Features
17
20
 
18
21
  * **One‑line integration**: `<%= cloudflare_turnstile_tag %>` in views, `valid_turnstile?(model:)` in controllers — no extra wiring.
19
- * **Turbo & Turbo Streams aware**: Automatically re‑initializes widgets on `turbo:load`, `turbo:before-stream-render`, and DOM mutations.
22
+ * **Turbo & Turbo Streams aware**: Automatically re‑initializes widgets on `turbo:render` and `turbo:before-stream-render`.
20
23
  * **Legacy Turbolinks support**: Includes a helper for Turbolinks to handle remote form submissions with validation errors.
21
24
  * **CSP nonce support**: Honours Rails' `content_security_policy_nonce` for secure inline scripts.
22
25
  * **Rails Engine & Asset pipeline**: Ships a precompiled JS helper via Railtie — no manual asset setup.
@@ -154,6 +157,22 @@ Supports **Rails 5.0 → latest** and **Ruby 2.6 → latest**, with the full Rai
154
157
  end
155
158
  ```
156
159
 
160
+ The automatic message is a regular flash, so it survives exactly one redirect. If you **re-render the form** instead of redirecting, pass `flash: :now` to scope the message to that render, so it doesn't also appear on the next page the visitor opens:
161
+
162
+ ```ruby
163
+ def create
164
+ if valid_turnstile?(flash: :now)
165
+ # Passed: no message is set either way
166
+ redirect_to dashboard_path, notice: 'Success!'
167
+ else
168
+ # Failed: flash.now[:alert] is automatically set
169
+ render :new, status: :unprocessable_entity
170
+ end
171
+ end
172
+ ```
173
+
174
+ Passing a `model` is the other option for a re-rendered form: the failure goes to `model.errors` and no flash is set at all.
175
+
157
176
  * You may also pass additional **siteverify** parameters (e.g., `secret`, `response`, `remoteip`, `idempotency_key`) supported by Cloudflare's API:
158
177
  [Cloudflare Server-Side Validation Parameters](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#required-parameters)
159
178
 
@@ -168,6 +187,16 @@ Supports **Rails 5.0 → latest** and **Ruby 2.6 → latest**, with the full Rai
168
187
  end
169
188
  ```
170
189
 
190
+ * Requests to Cloudflare time out after 5 seconds to connect, read, or write, so a slow Cloudflare can't tie up your app's request threads for long. A timeout raises `Cloudflare::Turnstile::Rails::ConfigurationError`. To change the limits:
191
+
192
+ ```ruby
193
+ Cloudflare::Turnstile::Rails.configure do |config|
194
+ config.open_timeout = 5 # connecting to siteverify
195
+ config.read_timeout = 5 # reading the response
196
+ config.write_timeout = 5 # sending the verification POST
197
+ end
198
+ ```
199
+
171
200
  #### Advanced Validation
172
201
 
173
202
  * To inspect the entire verification payload, use `verify_turnstile`. It returns a `VerificationResponse` object with detailed information:
@@ -187,6 +216,8 @@ Supports **Rails 5.0 → latest** and **Ruby 2.6 → latest**, with the full Rai
187
216
  end
188
217
  ```
189
218
 
219
+ * A missing, malformed, or oversized token fails immediately with `missing-input-response` or `invalid-input-response`, without a request to Cloudflare. The `VerificationResponse` for these failures only has `success` and `error-codes`; it doesn't carry `hostname`, `challenge_ts`, or the other fields a real siteverify call returns.
220
+
190
221
  * The `VerificationResponse` object contains the raw response from Cloudflare:
191
222
 
192
223
  ```ruby
@@ -302,9 +333,9 @@ Cloudflare provides dummy sitekeys and secret keys for development and testing.
302
333
  | Sitekey | Description | Visibility |
303
334
  |----------------------------|---------------------------------|------------|
304
335
  | `1x00000000000000000000AA` | Always passes | visible |
305
- | `2x00000000000000000000AB` | Always blocks | visible |
336
+ | `2x00000000000000000000AB` | Always fails | visible |
306
337
  | `1x00000000000000000000BB` | Always passes | invisible |
307
- | `2x00000000000000000000BB` | Always blocks | invisible |
338
+ | `2x00000000000000000000BB` | Always fails | invisible |
308
339
  | `3x00000000000000000000FF` | Forces an interactive challenge | visible |
309
340
 
310
341
  ### Dummy Secret Keys
@@ -393,10 +424,11 @@ If you run into any issues after upgrading Rails, please [open an issue](https:/
393
424
 
394
425
  ### Setup
395
426
 
396
- Install dependencies, linters, and prepare everything in one step:
427
+ Install the Ruby dependencies, then the JavaScript dependencies used by the ESLint step:
397
428
 
398
429
  ```bash
399
430
  bin/setup
431
+ npm install
400
432
  ```
401
433
 
402
434
  ### Running the Test Suite
@@ -414,13 +446,13 @@ bundle exec appraisal rake test
414
446
 
415
447
  ### Code Linting
416
448
 
417
- Enforce code style with RuboCop (latest Ruby only):
449
+ Run RuboCop and ESLint together (autocorrecting by default, or pass `--no-fix` to only report):
418
450
 
419
451
  ```bash
420
- bundle exec rubocop
452
+ bin/lint
421
453
  ```
422
454
 
423
- > **CI Note:** We run this via [.github/workflows/lint.yml](https://github.com/vkononov/cloudflare-turnstile-rails/blob/main/.github/workflows/lint.yml) on the latest Ruby only.
455
+ > **CI Note:** We run `bin/lint --no-fix` via [.github/workflows/lint.yml](https://github.com/vkononov/cloudflare-turnstile-rails/blob/main/.github/workflows/lint.yml) on the latest Ruby only.
424
456
 
425
457
  ### Generating Rails Apps Locally
426
458
 
@@ -461,4 +493,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/vkonon
461
493
 
462
494
  ## License
463
495
 
464
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
496
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/license/MIT).
@@ -3,7 +3,8 @@ module Cloudflare
3
3
  module Rails
4
4
  class Configuration
5
5
  attr_writer :script_url
6
- attr_accessor :site_key, :secret_key, :render, :onload, :default_data, :auto_populate_response_in_test_env
6
+ attr_accessor :site_key, :secret_key, :render, :onload, :default_data, :auto_populate_response_in_test_env,
7
+ :open_timeout, :read_timeout, :write_timeout
7
8
 
8
9
  def initialize
9
10
  @script_url = Cloudflare::SCRIPT_URL
@@ -13,6 +14,9 @@ module Cloudflare
13
14
  @onload = nil
14
15
  @default_data = {}
15
16
  @auto_populate_response_in_test_env = true
17
+ @open_timeout = 5
18
+ @read_timeout = 5
19
+ @write_timeout = 5
16
20
  end
17
21
 
18
22
  # Dynamically build the URL every time, so that
@@ -11,6 +11,9 @@ module Cloudflare
11
11
  # Default hidden input field name for Turnstile token submission
12
12
  RESPONSE_FIELD_NAME = 'cf-turnstile-response'.freeze
13
13
 
14
+ # Longest token siteverify accepts; anything longer is rejected without a request
15
+ MAX_RESPONSE_LENGTH = 2048
16
+
14
17
  # Default CSS class applied to Turnstile widget containers
15
18
  WIDGET_CLASS = 'cf-turnstile'.freeze
16
19
  end
@@ -17,10 +17,18 @@ module Cloudflare
17
17
  result
18
18
  end
19
19
 
20
- def valid_turnstile?(model: nil, **opts)
20
+ # flash: :keep carries the automatic failure message into the next request,
21
+ # for a redirect. flash: :now scopes it to the response being rendered. The
22
+ # keyword shadows the controller's own flash method, hence request.flash.
23
+ def valid_turnstile?(model: nil, flash: :keep, **opts)
21
24
  response = verify_turnstile(model: model, **opts)
22
25
  success = response.is_a?(VerificationResponse) && response.success?
23
- flash[:alert] = ErrorMessage.default if !success && model.nil?
26
+
27
+ if !success && model.nil?
28
+ store = flash == :now ? request.flash.now : request.flash
29
+ store[:alert] = ErrorMessage.default
30
+ end
31
+
24
32
  success
25
33
  end
26
34
 
@@ -2,6 +2,7 @@ require 'net/http'
2
2
  require 'openssl'
3
3
 
4
4
  require_relative 'constants/cloudflare'
5
+ require_relative 'constants/error_code'
5
6
 
6
7
  module Cloudflare
7
8
  module Turnstile
@@ -48,15 +49,25 @@ module Cloudflare
48
49
 
49
50
  module Verification
50
51
  def self.verify(response: nil, secret: nil, remoteip: nil, idempotency_key: nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
51
- if (response.nil? || response.strip.empty?) && ::Rails.env.test? && Rails.configuration.auto_populate_response_in_test_env # rubocop:disable Layout/LineLength
52
- response = 'dummy-response'
52
+ if response.is_a?(String) && !response.valid_encoding?
53
+ return VerificationResponse.new('success' => false, 'error-codes' => [ErrorCode::INVALID_INPUT_RESPONSE])
53
54
  end
54
55
 
56
+ response = 'dummy-response' if auto_populate_test_response?(response)
57
+
55
58
  secret ||= Rails.configuration.secret_key
56
59
  if secret.nil? || secret.strip.empty?
57
60
  raise ConfigurationError, ErrorMessage.for(ErrorCode::MISSING_INPUT_SECRET)
58
61
  end
59
62
 
63
+ local_error = if missing_response?(response)
64
+ ErrorCode::MISSING_INPUT_RESPONSE
65
+ elsif invalid_response?(response)
66
+ ErrorCode::INVALID_INPUT_RESPONSE
67
+ end
68
+
69
+ return VerificationResponse.new('success' => false, 'error-codes' => [local_error]) if local_error
70
+
60
71
  body = { 'secret' => secret, 'response' => response }
61
72
  body['remoteip'] = remoteip if remoteip
62
73
  body['idempotency_key'] = idempotency_key if idempotency_key
@@ -67,17 +78,20 @@ module Cloudflare
67
78
 
68
79
  http = Net::HTTP.new(uri.host, uri.port)
69
80
  http.use_ssl = true
81
+ http.open_timeout = Rails.configuration.open_timeout
82
+ http.read_timeout = Rails.configuration.read_timeout
83
+ http.write_timeout = Rails.configuration.write_timeout
70
84
  http.verify_mode = OpenSSL::SSL::VERIFY_PEER
71
85
  http.cert_store = OpenSSL::X509::Store.new
72
86
  http.cert_store.set_default_paths
73
87
 
74
88
  begin
75
89
  res = http.request(request)
76
- rescue Net::OpenTimeout, Net::ReadTimeout => e
90
+ rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Errno::ETIMEDOUT => e
77
91
  raise ConfigurationError, "Turnstile verification timed out: #{e.message}"
78
92
  rescue OpenSSL::SSL::SSLError => e
79
93
  raise ConfigurationError, "SSL verification failed: #{e.message}"
80
- rescue SocketError, Errno::ECONNREFUSED => e
94
+ rescue SocketError, EOFError, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH => e
81
95
  raise ConfigurationError, "Network error during Turnstile verification: #{e.message}"
82
96
  end
83
97
 
@@ -89,6 +103,23 @@ module Cloudflare
89
103
 
90
104
  VerificationResponse.new(json)
91
105
  end
106
+
107
+ def self.auto_populate_test_response?(response)
108
+ (response.nil? || (response.is_a?(String) && response.strip.empty?)) &&
109
+ ::Rails.env.test? &&
110
+ Rails.configuration.auto_populate_response_in_test_env
111
+ end
112
+ private_class_method :auto_populate_test_response?
113
+
114
+ def self.missing_response?(response)
115
+ response.nil? || response == ''
116
+ end
117
+ private_class_method :missing_response?
118
+
119
+ def self.invalid_response?(response)
120
+ !response.is_a?(String) || response.strip.empty? || response.length > Cloudflare::MAX_RESPONSE_LENGTH
121
+ end
122
+ private_class_method :invalid_response?
92
123
  end
93
124
  end
94
125
  end
@@ -1,7 +1,7 @@
1
1
  module Cloudflare
2
2
  module Turnstile
3
3
  module Rails
4
- VERSION = '1.1.0'.freeze
4
+ VERSION = '1.3.0'.freeze
5
5
  end
6
6
  end
7
7
  end
@@ -25,6 +25,12 @@ Cloudflare::Turnstile::Rails.configure do |config|
25
25
  # language: -> { I18n.locale }
26
26
  # }
27
27
 
28
+ # Optional: Seconds to wait when connecting to, and reading from, Cloudflare's siteverify endpoint.
29
+ # Keep these short so a slow Cloudflare can't hold your app's request threads for long.
30
+ # config.open_timeout = 5 # connecting to siteverify
31
+ # config.read_timeout = 5 # reading the response
32
+ # config.write_timeout = 5 # sending the verification POST
33
+
28
34
  # In the Rails Test environment, automatically fill in a dummy response if none was provided.
29
35
  # This lets you keep existing controller tests without having to add
30
36
  # params["cf-turnstile-response"] manually in every test.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cloudflare-turnstile-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vadim Kononov
@@ -23,8 +23,9 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '5.0'
26
- description: Integrates Cloudflare Turnstile into Ruby on Rails applications, transparently
27
- reloads on Turbo and Turbolinks events, and embeds CSP-nonce-compliant scripts.
26
+ description: Cloudflare Turnstile CAPTCHA, a reCAPTCHA alternative, for Ruby on Rails.
27
+ View and controller helpers with Turbo, Turbo Streams, Turbolinks, CSP nonce and
28
+ i18n support.
28
29
  email:
29
30
  - vadim@konoson.com
30
31
  executables: []
@@ -83,8 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
84
  - !ruby/object:Gem::Version
84
85
  version: '0'
85
86
  requirements: []
86
- rubygems_version: 4.0.10
87
+ rubygems_version: 4.0.20
87
88
  specification_version: 4
88
- summary: Cloudflare Turnstile gem for Rails with built-in Turbo and Turbolinks support
89
- and CSP compliance
89
+ summary: Cloudflare Turnstile CAPTCHA for Rails with Turbo, Turbolinks and CSP support
90
90
  test_files: []