turnstile-ruby 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/README.md +166 -0
- data/lib/turnstile/adapters/controller_methods.rb +81 -0
- data/lib/turnstile/adapters/view_methods.rb +17 -0
- data/lib/turnstile/configuration.rb +74 -0
- data/lib/turnstile/helpers.rb +139 -0
- data/lib/turnstile/rails.rb +4 -0
- data/lib/turnstile/railtie.rb +35 -0
- data/lib/turnstile/version.rb +5 -0
- data/lib/turnstile.rb +118 -0
- data/rails/locales/en.yml +5 -0
- data/rails/locales/fr.yml +5 -0
- data/rails/locales/ja.yml +5 -0
- data/rails/locales/nl.yml +5 -0
- metadata +201 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9c562e5825290bbab8c92309f410fdeedbf3f1a4a87abd3243130886f8ffc07e
|
4
|
+
data.tar.gz: 2dd53d7bc2dde10c1493b8a5ab7be75ec96e4d107055052e3c511c23ef946841
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ed59df2922741050d4632c697cf576a48379014ea62ca4f5bab12f80e71757d5b97dceb556447bd3c984f0fb80c4ec3f8f6aac451b307d90d0fbadd8354b9e1c
|
7
|
+
data.tar.gz: c546b14454b5cb81788d15e716e1cb4ab1d13687dbbf07bc6730c7a61e8db156cf5e7ae1b744c30881b51f8e09cfe718bb23e4331e808292a1284426241e1006
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
# Turnstile Ruby
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/turnstile-ruby)
|
4
|
+
|
5
|
+
A simple Ruby client for verifying [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/) tokens.
|
6
|
+
Turnstile is a CAPTCHA alternative from Cloudflare, similar to Google reCAPTCHA, but lightweight and privacy-friendly.
|
7
|
+
|
8
|
+
This gem makes it easy to integrate Turnstile into Ruby and Rails applications.
|
9
|
+
|
10
|
+
---
|
11
|
+
|
12
|
+
## ✨ Features
|
13
|
+
|
14
|
+
- Verify Turnstile tokens with Cloudflare’s API
|
15
|
+
- Lightweight, no heavy dependencies
|
16
|
+
- Works with plain Ruby and Ruby on Rails
|
17
|
+
- Provides meaningful error codes and success flags
|
18
|
+
- Simple configuration via environment variables or initializer
|
19
|
+
|
20
|
+
---
|
21
|
+
|
22
|
+
## 📦 Installation
|
23
|
+
|
24
|
+
Add this line to your Gemfile:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
gem "turnstile-ruby"
|
28
|
+
```
|
29
|
+
|
30
|
+
And install:
|
31
|
+
|
32
|
+
```bash
|
33
|
+
bundle install
|
34
|
+
```
|
35
|
+
|
36
|
+
Or install directly with:
|
37
|
+
|
38
|
+
```bash
|
39
|
+
gem install turnstile-ruby
|
40
|
+
```
|
41
|
+
|
42
|
+
---
|
43
|
+
|
44
|
+
## ⚙️ Configuration
|
45
|
+
|
46
|
+
Set your **Cloudflare Turnstile Site Key** and **Secret Key** in environment variables:
|
47
|
+
|
48
|
+
```bash
|
49
|
+
export TURNSTILE_SITE_KEY="your_site_key"
|
50
|
+
export TURNSTILE_SECRET_KEY="your_secret_key"
|
51
|
+
```
|
52
|
+
|
53
|
+
For Rails, you can create an initializer (`config/initializers/turnstile.rb`):
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
Turnstile.configure do |config|
|
57
|
+
config.site_key = ENV["TURNSTILE_SITE_KEY"]
|
58
|
+
config.secret_key = ENV["TURNSTILE_SECRET_KEY"]
|
59
|
+
config.timeout = 5 # optional, in seconds
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
---
|
64
|
+
|
65
|
+
## 🛠 Usage
|
66
|
+
|
67
|
+
### Rails Views
|
68
|
+
|
69
|
+
Embed the Turnstile widget:
|
70
|
+
|
71
|
+
```erb
|
72
|
+
<form action="/signup" method="POST">
|
73
|
+
<!-- Your form fields -->
|
74
|
+
|
75
|
+
<div class="cf-turnstile"
|
76
|
+
data-sitekey="<%= Turnstile.site_key %>">
|
77
|
+
</div>
|
78
|
+
|
79
|
+
<button type="submit">Submit</button>
|
80
|
+
</form>
|
81
|
+
|
82
|
+
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
83
|
+
```
|
84
|
+
|
85
|
+
### Rails Controller
|
86
|
+
|
87
|
+
Verify the token on form submission:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
def create
|
91
|
+
token = params["cf-turnstile-response"]
|
92
|
+
|
93
|
+
result = Turnstile.verify(token, remote_ip: request.remote_ip)
|
94
|
+
|
95
|
+
if result.success?
|
96
|
+
# proceed with signup
|
97
|
+
else
|
98
|
+
flash[:error] = "Turnstile verification failed: #{result.error_codes.join(", ")}"
|
99
|
+
render :new
|
100
|
+
end
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
### Plain Ruby Example
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
require "turnstile"
|
108
|
+
|
109
|
+
token = "client-submitted-token"
|
110
|
+
result = Turnstile.verify(token)
|
111
|
+
|
112
|
+
if result.success?
|
113
|
+
puts "Verification passed!"
|
114
|
+
else
|
115
|
+
puts "Verification failed: #{result.error_codes.inspect}"
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
---
|
120
|
+
|
121
|
+
## ✅ Response Object
|
122
|
+
|
123
|
+
`Turnstile.verify` returns a `Turnstile::Response` object with:
|
124
|
+
|
125
|
+
- `success?` → `true` or `false`
|
126
|
+
- `error_codes` → array of error codes
|
127
|
+
- `hostname` → hostname (if provided)
|
128
|
+
- `challenge_ts` → challenge timestamp (if provided)
|
129
|
+
|
130
|
+
---
|
131
|
+
|
132
|
+
## 🚀 Development
|
133
|
+
|
134
|
+
Clone the repo and install dependencies:
|
135
|
+
|
136
|
+
```bash
|
137
|
+
git clone https://github.com/urkkv/turnstile-ruby.git
|
138
|
+
cd turnstile-ruby
|
139
|
+
bundle install
|
140
|
+
```
|
141
|
+
|
142
|
+
Run tests:
|
143
|
+
|
144
|
+
```bash
|
145
|
+
bundle exec rspec
|
146
|
+
```
|
147
|
+
|
148
|
+
---
|
149
|
+
|
150
|
+
## 📜 License
|
151
|
+
|
152
|
+
This project is licensed under the [MIT License](LICENSE).
|
153
|
+
|
154
|
+
---
|
155
|
+
|
156
|
+
## 🙌 Contributing
|
157
|
+
|
158
|
+
Bug reports and pull requests are welcome at
|
159
|
+
[https://github.com/urkkv/turnstile-ruby](https://github.com/urkkv/turnstile-ruby).
|
160
|
+
|
161
|
+
---
|
162
|
+
|
163
|
+
## 🔗 Resources
|
164
|
+
|
165
|
+
- [Cloudflare Turnstile Documentation](https://developers.cloudflare.com/turnstile/)
|
166
|
+
- [RubyGems.org – turnstile-ruby](https://rubygems.org/gems/turnstile-ruby)
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Turnstile
|
4
|
+
module Adapters
|
5
|
+
module ControllerMethods
|
6
|
+
private
|
7
|
+
|
8
|
+
# Your private API can be specified in the +options+ hash or preferably
|
9
|
+
# using the Configuration.
|
10
|
+
def verify_turnstile(options = {})
|
11
|
+
options = {model: options} unless options.is_a? Hash
|
12
|
+
return true if Turnstile.skip_env?(options[:env])
|
13
|
+
|
14
|
+
model = options[:model]
|
15
|
+
attribute = options.fetch(:attribute, :base)
|
16
|
+
turnstile_response = options[:response] || params['cf-turnstile-response']
|
17
|
+
|
18
|
+
begin
|
19
|
+
verified = if Turnstile.invalid_response?(turnstile_response)
|
20
|
+
false
|
21
|
+
else
|
22
|
+
unless options[:skip_remote_ip]
|
23
|
+
remoteip = (request.respond_to?(:remote_ip) && request.remote_ip) || (env && env['REMOTE_ADDR'])
|
24
|
+
options = options.merge(remote_ip: remoteip.to_s) if remoteip
|
25
|
+
end
|
26
|
+
|
27
|
+
success, @_turnstile_reply =
|
28
|
+
Turnstile.verify_via_api_call(turnstile_response, options.merge(with_reply: true))
|
29
|
+
success
|
30
|
+
end
|
31
|
+
|
32
|
+
if verified
|
33
|
+
flash.delete(:turnstile_error) if turnstile_flash_supported? && !model
|
34
|
+
true
|
35
|
+
else
|
36
|
+
turnstile_error(
|
37
|
+
model,
|
38
|
+
attribute,
|
39
|
+
options.fetch(:message) { Turnstile::Helpers.to_error_message(:verification_failed) }
|
40
|
+
)
|
41
|
+
false
|
42
|
+
end
|
43
|
+
rescue Timeout::Error
|
44
|
+
if Turnstile.configuration.handle_timeouts_gracefully
|
45
|
+
turnstile_error(
|
46
|
+
model,
|
47
|
+
attribute,
|
48
|
+
options.fetch(:message) { Turnstile::Helpers.to_error_message(:turnstile_unreachable) }
|
49
|
+
)
|
50
|
+
false
|
51
|
+
else
|
52
|
+
raise TurnstileError, 'Turnstile unreachable.'
|
53
|
+
end
|
54
|
+
rescue StandardError => e
|
55
|
+
raise TurnstileError, e.message, e.backtrace
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def verify_turnstile!(options = {})
|
60
|
+
verify_turnstile(options) || raise(VerifyError, turnstile_reply)
|
61
|
+
end
|
62
|
+
|
63
|
+
def turnstile_reply
|
64
|
+
@_turnstile_reply if defined?(@_turnstile_reply)
|
65
|
+
end
|
66
|
+
|
67
|
+
def turnstile_error(model, attribute, message)
|
68
|
+
if model
|
69
|
+
model.new.errors.add(attribute, message)
|
70
|
+
elsif turnstile_flash_supported?
|
71
|
+
flash[:turnstile_error] = message
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def turnstile_flash_supported?
|
76
|
+
request.respond_to?(:format) && request.format == :html && respond_to?(:flash)
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Turnstile
|
4
|
+
module Adapters
|
5
|
+
module ViewMethods
|
6
|
+
# Renders a turnstile [Checkbox](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/) widget
|
7
|
+
def turnstile_tags(options = {})
|
8
|
+
::Turnstile::Helpers.turnstile_tags(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Renders a Turnstile [Invisible Turnstile captcha](https://developers.cloudflare.com/turnstile/reference/widget-types/#invisible)
|
12
|
+
def invisible_turnstile_tags(options = {})
|
13
|
+
::Turnstile::Helpers.invisible_turnstile_tags(options)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Turnstile
|
4
|
+
# This class enables detailed configuration of the turnstile captcha services.
|
5
|
+
#
|
6
|
+
# By calling
|
7
|
+
#
|
8
|
+
# Turnstile.configuration # => instance of Turnstile::Configuration
|
9
|
+
#
|
10
|
+
# or
|
11
|
+
# Turnstile.configure do |config|
|
12
|
+
# config # => instance of Turnstile::Configuration
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# you are able to perform configuration updates.
|
16
|
+
#
|
17
|
+
# Your are able to customize all attributes listed below. All values have
|
18
|
+
# sensitive default and will very likely not need to be changed.
|
19
|
+
#
|
20
|
+
# Please note that the site and secret key for the Turnstile captcha API Access
|
21
|
+
# have no useful default value. The keys may be set via the Shell environment
|
22
|
+
# or using this configuration. Settings within this configuration always take
|
23
|
+
# precedence.
|
24
|
+
#
|
25
|
+
# Setting the keys with this Configuration
|
26
|
+
#
|
27
|
+
# Turnstile.configure do |config|
|
28
|
+
# config.site_key = '0x4AAAAAAAC1z764FTJAewGm'
|
29
|
+
# config.secret_key = '0x4AAAAAAAC1z_XZkOcOamwjRONkb-xoXMU'
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
class Configuration
|
33
|
+
DEFAULTS = {
|
34
|
+
'server_url' => 'https://challenges.cloudflare.com/turnstile/v0/api.js',
|
35
|
+
'verify_url' => 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
|
36
|
+
}.freeze
|
37
|
+
|
38
|
+
attr_accessor :default_env, :skip_verify_env, :proxy, :secret_key, :site_key, :handle_timeouts_gracefully,
|
39
|
+
:hostname, :response_limit
|
40
|
+
attr_writer :api_server_url, :verify_url
|
41
|
+
|
42
|
+
def initialize # :nodoc:
|
43
|
+
@default_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || (Rails.env if defined? Rails.env)
|
44
|
+
@skip_verify_env = %w[test cucumber rspec]
|
45
|
+
@handle_timeouts_gracefully = true
|
46
|
+
|
47
|
+
@secret_key = ENV['TURNSTILE_SECRET_KEY']
|
48
|
+
@site_key = ENV['TURNSTILE_SITE_KEY']
|
49
|
+
|
50
|
+
@verify_url = ENV['TURNSTILE_VERIFY_URL']
|
51
|
+
@api_server_url = ENV['TURNSTILE_SERVER_URL']
|
52
|
+
|
53
|
+
# Default response token size
|
54
|
+
# https://developers.cloudflare.com/turnstile/frequently-asked-questions/#what-is-the-length-of-a-turnstile-token
|
55
|
+
@response_limit = 2048
|
56
|
+
end
|
57
|
+
|
58
|
+
def secret_key!
|
59
|
+
secret_key || raise(TurnstileError, "No secret key specified.")
|
60
|
+
end
|
61
|
+
|
62
|
+
def site_key!
|
63
|
+
site_key || raise(TurnstileError, "No site key specified.")
|
64
|
+
end
|
65
|
+
|
66
|
+
def api_server_url
|
67
|
+
@api_server_url || DEFAULTS.fetch('server_url')
|
68
|
+
end
|
69
|
+
|
70
|
+
def verify_url
|
71
|
+
@verify_url || DEFAULTS.fetch('verify_url')
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Turnstile
|
4
|
+
module Helpers
|
5
|
+
DEFAULT_ERRORS = {
|
6
|
+
turnstile_unreachable: 'Oops, we failed to validate your Turnstile response. Please try again.',
|
7
|
+
verification_failed: 'Turnstile verification failed, please try again.'
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
def self.to_error_message(key)
|
11
|
+
default = DEFAULT_ERRORS.fetch(key) { raise ArgumentError "Unknown Turnstile captcha error - #{key}" }
|
12
|
+
to_message("turnstile.errors.#{key}", default)
|
13
|
+
end
|
14
|
+
|
15
|
+
if defined?(I18n)
|
16
|
+
def self.to_message(key, default)
|
17
|
+
I18n.translate(key, default: default)
|
18
|
+
end
|
19
|
+
else
|
20
|
+
def self.to_message(_key, default)
|
21
|
+
default
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.turnstile_tags(options)
|
26
|
+
if options.key?(:ssl)
|
27
|
+
raise(TurnstileError, "SSL is now always true. Please remove 'ssl' from your calls to turnstile_tags.")
|
28
|
+
end
|
29
|
+
|
30
|
+
html, tag_attributes = components(options.dup)
|
31
|
+
html << %(<div #{tag_attributes}></div>\n)
|
32
|
+
|
33
|
+
html.respond_to?(:html_safe) ? html.html_safe : html
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.invisible_turnstile_tags(custom)
|
37
|
+
options = { callback: 'javascriptCallback', ui: :button }.merge(custom)
|
38
|
+
text = options.delete(:text)
|
39
|
+
html, tag_attributes = components(options.dup)
|
40
|
+
html << default_callback(options) if default_callback_required?(options)
|
41
|
+
|
42
|
+
case options[:ui]
|
43
|
+
when :button
|
44
|
+
html << %(<button type="submit" #{tag_attributes}>#{text}</button>\n)
|
45
|
+
when :input
|
46
|
+
html << %(<input type="submit" #{tag_attributes} value="#{text}"/>\n)
|
47
|
+
else
|
48
|
+
raise(TurnstileError, "Turnstile ui `#{options[:ui]}` is not valid.")
|
49
|
+
end
|
50
|
+
html.respond_to?(:html_safe) ? html.html_safe : html
|
51
|
+
end
|
52
|
+
|
53
|
+
private_class_method def self.components(options)
|
54
|
+
html = +''
|
55
|
+
attributes = {}
|
56
|
+
|
57
|
+
options = options.dup
|
58
|
+
env = options.delete(:env)
|
59
|
+
class_attribute = options.delete(:class)
|
60
|
+
site_key = options.delete(:site_key)
|
61
|
+
hl = options.delete(:hl)
|
62
|
+
onload = options.delete(:onload)
|
63
|
+
render = options.delete(:render)
|
64
|
+
script_async = options.delete(:script_async)
|
65
|
+
script_defer = options.delete(:script_defer)
|
66
|
+
skip_script = (options.delete(:script) == false) || (options.delete(:external_script) == false)
|
67
|
+
ui = options.delete(:ui)
|
68
|
+
|
69
|
+
data_attribute_keys = %i[sitekey action cData callback expired_callback
|
70
|
+
timeout_callback error_callback theme language response_field
|
71
|
+
response_field_name size retry retry_interval refresh_expired]
|
72
|
+
# data_attribute_keys << :tabindex unless ui == :button # TODO:: Remove me
|
73
|
+
data_attributes = {}
|
74
|
+
data_attribute_keys.each do |data_attribute|
|
75
|
+
value = options.delete(data_attribute)
|
76
|
+
data_attributes["data-#{data_attribute.to_s.tr('_', '-')}"] = value if value
|
77
|
+
end
|
78
|
+
|
79
|
+
unless Turnstile.skip_env?(env)
|
80
|
+
site_key ||= Turnstile.configuration.site_key!
|
81
|
+
script_url = Turnstile.configuration.api_server_url
|
82
|
+
query_params = hash_to_query(
|
83
|
+
hl: hl,
|
84
|
+
onload: onload,
|
85
|
+
render: render
|
86
|
+
)
|
87
|
+
script_url += "?#{query_params}" unless query_params.empty?
|
88
|
+
async_attr = "async" if script_async != false
|
89
|
+
defer_attr = "defer" if script_defer != false
|
90
|
+
html << %(<script src="#{script_url}" #{async_attr} #{defer_attr}></script>\n) unless skip_script
|
91
|
+
attributes["data-sitekey"] = site_key
|
92
|
+
attributes.merge! data_attributes
|
93
|
+
end
|
94
|
+
|
95
|
+
# The remaining options will be added as attributes on the tag.
|
96
|
+
attributes["class"] = "cf-turnstile #{class_attribute}"
|
97
|
+
tag_attributes = attributes.merge(options).map { |k, v| %(#{k}="#{v}") }.join(" ")
|
98
|
+
|
99
|
+
[html, tag_attributes]
|
100
|
+
end
|
101
|
+
|
102
|
+
private_class_method def self.hash_to_query(hash)
|
103
|
+
hash.delete_if { |_, val| val.nil? || val.empty? }.to_a.map { |pair| pair.join('=') }.join('&')
|
104
|
+
end
|
105
|
+
|
106
|
+
private_class_method def self.default_callback_required?(options)
|
107
|
+
options[:callback] == 'javascriptCallback' &&
|
108
|
+
!Turnstile.skip_env?(options[:env]) &&
|
109
|
+
options[:script] != false &&
|
110
|
+
options[:inline_script] != false
|
111
|
+
end
|
112
|
+
|
113
|
+
private_class_method def self.default_callback(options = {})
|
114
|
+
selector_attr = options[:id] ? "##{options[:id]}" : ".cf-turnstile"
|
115
|
+
|
116
|
+
<<-HTML
|
117
|
+
<script>
|
118
|
+
var javascriptCallback = function () {
|
119
|
+
var closestForm = function (ele) {
|
120
|
+
var curEle = ele.parentNode;
|
121
|
+
while (curEle.nodeName !== 'FORM' && curEle.nodeName !== 'BODY'){
|
122
|
+
curEle = curEle.parentNode;
|
123
|
+
}
|
124
|
+
return curEle.nodeName === 'FORM' ? curEle : null
|
125
|
+
};
|
126
|
+
|
127
|
+
var el = document.querySelector("#{selector_attr}")
|
128
|
+
if (!!el) {
|
129
|
+
var form = closestForm(el);
|
130
|
+
if (form) {
|
131
|
+
form.submit();
|
132
|
+
}
|
133
|
+
}
|
134
|
+
};
|
135
|
+
</script>
|
136
|
+
HTML
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Turnstile
|
4
|
+
class Railtie < Rails::Railtie
|
5
|
+
ActiveSupport.on_load(:action_view) do
|
6
|
+
include Turnstile::Adapters::ViewMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
ActiveSupport.on_load(:action_controller) do
|
10
|
+
include Turnstile::Adapters::ControllerMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
initializer 'turnstile' do |app|
|
14
|
+
Turnstile::Railtie.instance_eval do
|
15
|
+
pattern = pattern_from app.config.i18n.available_locales
|
16
|
+
|
17
|
+
add("rails/locales/#{pattern}.yml")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
protected
|
23
|
+
|
24
|
+
def add(pattern)
|
25
|
+
files = Dir[File.join(File.dirname(__FILE__), '../..', pattern)]
|
26
|
+
I18n.load_path.concat(files)
|
27
|
+
end
|
28
|
+
|
29
|
+
def pattern_from(args)
|
30
|
+
array = Array(args || [])
|
31
|
+
array.blank? ? '*' : "{#{array.join ','}}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/turnstile.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'net/http'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
require 'turnstile/configuration'
|
8
|
+
require 'turnstile/helpers'
|
9
|
+
require 'turnstile/adapters/controller_methods'
|
10
|
+
require 'turnstile/adapters/view_methods'
|
11
|
+
|
12
|
+
if defined?(Rails)
|
13
|
+
require 'turnstile/railtie'
|
14
|
+
end
|
15
|
+
|
16
|
+
module Turnstile
|
17
|
+
DEFAULT_TIMEOUT = 3
|
18
|
+
|
19
|
+
class TurnstileError < StandardError
|
20
|
+
end
|
21
|
+
|
22
|
+
class VerifyError < TurnstileError
|
23
|
+
end
|
24
|
+
|
25
|
+
# Gives access to the current Configuration.
|
26
|
+
def self.configuration
|
27
|
+
@configuration ||= Configuration.new
|
28
|
+
end
|
29
|
+
|
30
|
+
# Allows easy setting of multiple configuration options. See Configuration
|
31
|
+
# for all available options.
|
32
|
+
#--
|
33
|
+
# The temp assignment is only used to get a nicer rdoc. Feel free to remove
|
34
|
+
# this hack.
|
35
|
+
#++
|
36
|
+
def self.configure
|
37
|
+
config = configuration
|
38
|
+
yield(config)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.with_configuration(config)
|
42
|
+
original_config = {}
|
43
|
+
|
44
|
+
config.each do |key, value|
|
45
|
+
original_config[key] = configuration.send(key)
|
46
|
+
configuration.send("#{key}=", value)
|
47
|
+
end
|
48
|
+
|
49
|
+
yield if block_given?
|
50
|
+
ensure
|
51
|
+
original_config.each { |key, value| configuration.send("#{key}=", value) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.skip_env?(env)
|
55
|
+
configuration.skip_verify_env.include?(env || configuration.default_env)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.invalid_response?(resp)
|
59
|
+
resp.empty? || resp.length > configuration.response_limit
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.verify_via_api_call(response, options)
|
63
|
+
secret_key = options.fetch(:secret_key) { configuration.secret_key! }
|
64
|
+
verify_hash = { 'secret' => secret_key, 'response' => response }
|
65
|
+
verify_hash['remoteip'] = options[:remote_ip] if options.key?(:remote_ip)
|
66
|
+
|
67
|
+
reply = api_verification(verify_hash, timeout: options[:timeout])
|
68
|
+
success = reply['success'].to_s == 'true' &&
|
69
|
+
hostname_valid?(reply['hostname'], options[:hostname]) &&
|
70
|
+
action_valid?(reply['action'], options[:action])
|
71
|
+
|
72
|
+
if options[:with_reply] == true
|
73
|
+
[success, reply]
|
74
|
+
else
|
75
|
+
success
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.hostname_valid?(hostname, validation)
|
80
|
+
validation ||= configuration.hostname
|
81
|
+
|
82
|
+
case validation
|
83
|
+
when nil, FalseClass then true
|
84
|
+
when String then validation == hostname
|
85
|
+
else validation.call(hostname)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.action_valid?(action, expected_action)
|
90
|
+
case expected_action
|
91
|
+
when nil, FalseClass then true
|
92
|
+
else action == expected_action
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.http_client_for(uri:, timeout: nil)
|
97
|
+
timeout ||= DEFAULT_TIMEOUT
|
98
|
+
http = if configuration.proxy
|
99
|
+
proxy_server = URI.parse(configuration.proxy)
|
100
|
+
Net::HTTP::Proxy(proxy_server.host, proxy_server.port, proxy_server.user, proxy_server.password)
|
101
|
+
else
|
102
|
+
Net::HTTP
|
103
|
+
end
|
104
|
+
instance = http.new(uri.host, uri.port)
|
105
|
+
instance.read_timeout = instance.open_timeout = timeout
|
106
|
+
instance.use_ssl = true if uri.port == 443
|
107
|
+
|
108
|
+
instance
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.api_verification(verify_hash, timeout: nil)
|
112
|
+
uri = URI.parse(configuration.verify_url)
|
113
|
+
http_instance = http_client_for(uri: uri, timeout: nil)
|
114
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
115
|
+
request.set_form_data verify_hash
|
116
|
+
JSON.parse(http_instance.request(request).body)
|
117
|
+
end
|
118
|
+
end
|
metadata
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: turnstile-ruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- urkkv
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-09-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: json
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bump
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: i18n
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: maxitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: mocha
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry-byebug
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubocop
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: webmock
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
description: Helper for Turnstile Captcha API
|
154
|
+
email:
|
155
|
+
- urkv@outlook.com
|
156
|
+
executables: []
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files: []
|
159
|
+
files:
|
160
|
+
- CHANGELOG.md
|
161
|
+
- README.md
|
162
|
+
- lib/turnstile.rb
|
163
|
+
- lib/turnstile/adapters/controller_methods.rb
|
164
|
+
- lib/turnstile/adapters/view_methods.rb
|
165
|
+
- lib/turnstile/configuration.rb
|
166
|
+
- lib/turnstile/helpers.rb
|
167
|
+
- lib/turnstile/rails.rb
|
168
|
+
- lib/turnstile/railtie.rb
|
169
|
+
- lib/turnstile/version.rb
|
170
|
+
- rails/locales/en.yml
|
171
|
+
- rails/locales/fr.yml
|
172
|
+
- rails/locales/ja.yml
|
173
|
+
- rails/locales/nl.yml
|
174
|
+
homepage: http://github.com/urkkv/turnstile-ruby
|
175
|
+
licenses:
|
176
|
+
- MIT
|
177
|
+
metadata:
|
178
|
+
allowed_push_host: https://rubygems.org
|
179
|
+
homepage_uri: http://github.com/urkkv/turnstile-ruby
|
180
|
+
source_code_uri: http://github.com/urkkv/turnstile-ruby
|
181
|
+
changelog_uri: http://github.com/urkkv/turnstile-ruby
|
182
|
+
post_install_message:
|
183
|
+
rdoc_options: []
|
184
|
+
require_paths:
|
185
|
+
- lib
|
186
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
187
|
+
requirements:
|
188
|
+
- - ">="
|
189
|
+
- !ruby/object:Gem::Version
|
190
|
+
version: 2.6.0
|
191
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
192
|
+
requirements:
|
193
|
+
- - ">="
|
194
|
+
- !ruby/object:Gem::Version
|
195
|
+
version: '0'
|
196
|
+
requirements: []
|
197
|
+
rubygems_version: 3.5.9
|
198
|
+
signing_key:
|
199
|
+
specification_version: 4
|
200
|
+
summary: Helper for Turnstile Captcha API
|
201
|
+
test_files: []
|