recaptcha 4.14.0 → 5.14.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/recaptcha.rb CHANGED
@@ -1,26 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'recaptcha/configuration'
4
- require 'uri'
3
+ require 'json'
5
4
  require 'net/http'
5
+ require 'uri'
6
6
 
7
+ require 'recaptcha/configuration'
8
+ require 'recaptcha/helpers'
9
+ require 'recaptcha/adapters/controller_methods'
10
+ require 'recaptcha/adapters/view_methods'
7
11
  if defined?(Rails)
8
12
  require 'recaptcha/railtie'
9
- else
10
- require 'recaptcha/client_helper'
11
- require 'recaptcha/verify'
12
13
  end
13
14
 
14
15
  module Recaptcha
15
- CONFIG = {
16
- 'server_url' => 'https://www.google.com/recaptcha/api.js',
17
- 'verify_url' => 'https://www.google.com/recaptcha/api/siteverify'
18
- }.freeze
19
-
20
- USE_SSL_BY_DEFAULT = false
21
- HANDLE_TIMEOUTS_GRACEFULLY = true
22
16
  DEFAULT_TIMEOUT = 3
23
17
 
18
+ class RecaptchaError < StandardError
19
+ end
20
+
21
+ class VerifyError < RecaptchaError
22
+ end
23
+
24
24
  # Gives access to the current Configuration.
25
25
  def self.configuration
26
26
  @configuration ||= Configuration.new
@@ -50,33 +50,123 @@ module Recaptcha
50
50
  original_config.each { |key, value| configuration.send("#{key}=", value) }
51
51
  end
52
52
 
53
- def self.get(verify_hash, options)
54
- http = if Recaptcha.configuration.proxy
55
- proxy_server = URI.parse(Recaptcha.configuration.proxy)
56
- Net::HTTP::Proxy(proxy_server.host, proxy_server.port, proxy_server.user, proxy_server.password)
53
+ def self.skip_env?(env)
54
+ configuration.skip_verify_env.include?(env || configuration.default_env)
55
+ end
56
+
57
+ def self.invalid_response?(resp)
58
+ resp.empty? || resp.length > configuration.response_limit
59
+ end
60
+
61
+ def self.verify_via_api_call(response, options)
62
+ if Recaptcha.configuration.enterprise
63
+ verify_via_api_call_enterprise(response, options)
57
64
  else
58
- Net::HTTP
65
+ verify_via_api_call_free(response, options)
59
66
  end
60
- query = URI.encode_www_form(verify_hash)
61
- uri = URI.parse(Recaptcha.configuration.verify_url + '?' + query)
62
- http_instance = http.new(uri.host, uri.port)
63
- http_instance.read_timeout = http_instance.open_timeout = options[:timeout] || DEFAULT_TIMEOUT
64
- http_instance.use_ssl = true if uri.port == 443
65
- request = Net::HTTP::Get.new(uri.request_uri)
66
- http_instance.request(request).body
67
67
  end
68
68
 
69
- def self.i18n(key, default)
70
- if defined?(I18n)
71
- I18n.translate(key, default: default)
69
+ def self.verify_via_api_call_enterprise(response, options)
70
+ site_key = options.fetch(:site_key) { configuration.site_key! }
71
+ api_key = options.fetch(:enterprise_api_key) { configuration.enterprise_api_key! }
72
+ project_id = options.fetch(:enterprise_project_id) { configuration.enterprise_project_id! }
73
+
74
+ query_params = { 'key' => api_key }
75
+ body = { 'event' => { 'token' => response, 'siteKey' => site_key } }
76
+ body['event']['expectedAction'] = options[:action] if options.key?(:action)
77
+ body['event']['userIpAddress'] = options[:remote_ip] if options.key?(:remote_ip)
78
+
79
+ reply = api_verification_enterprise(query_params, body, project_id, timeout: options[:timeout])
80
+ score = reply.dig('riskAnalysis', 'score')
81
+ token_properties = reply['tokenProperties']
82
+ success = !token_properties.nil? &&
83
+ token_properties['valid'].to_s == 'true' &&
84
+ hostname_valid?(token_properties['hostname'], options[:hostname]) &&
85
+ action_valid?(token_properties['action'], options[:action]) &&
86
+ score_above_threshold?(score, options[:minimum_score]) &&
87
+ score_below_threshold?(score, options[:maximum_score])
88
+
89
+ if options[:with_reply] == true
90
+ [success, reply]
72
91
  else
73
- default
92
+ success
74
93
  end
75
94
  end
76
95
 
77
- class RecaptchaError < StandardError
96
+ def self.verify_via_api_call_free(response, options)
97
+ secret_key = options.fetch(:secret_key) { configuration.secret_key! }
98
+ verify_hash = { 'secret' => secret_key, 'response' => response }
99
+ verify_hash['remoteip'] = options[:remote_ip] if options.key?(:remote_ip)
100
+
101
+ reply = api_verification_free(verify_hash, timeout: options[:timeout])
102
+ success = reply['success'].to_s == 'true' &&
103
+ hostname_valid?(reply['hostname'], options[:hostname]) &&
104
+ action_valid?(reply['action'], options[:action]) &&
105
+ score_above_threshold?(reply['score'], options[:minimum_score]) &&
106
+ score_below_threshold?(reply['score'], options[:maximum_score])
107
+
108
+ if options[:with_reply] == true
109
+ [success, reply]
110
+ else
111
+ success
112
+ end
78
113
  end
79
114
 
80
- class VerifyError < RecaptchaError
115
+ def self.hostname_valid?(hostname, validation)
116
+ validation ||= configuration.hostname
117
+
118
+ case validation
119
+ when nil, FalseClass then true
120
+ when String then validation == hostname
121
+ else validation.call(hostname)
122
+ end
123
+ end
124
+
125
+ def self.action_valid?(action, expected_action)
126
+ case expected_action
127
+ when nil, FalseClass then true
128
+ else action == expected_action.to_s
129
+ end
130
+ end
131
+
132
+ def self.score_above_threshold?(score, minimum_score)
133
+ !minimum_score || (score && score >= minimum_score)
134
+ end
135
+
136
+ def self.score_below_threshold?(score, maximum_score)
137
+ !maximum_score || (score && score <= maximum_score)
138
+ end
139
+
140
+ def self.http_client_for(uri:, timeout: nil)
141
+ timeout ||= DEFAULT_TIMEOUT
142
+ http = if configuration.proxy
143
+ proxy_server = URI.parse(configuration.proxy)
144
+ Net::HTTP::Proxy(proxy_server.host, proxy_server.port, proxy_server.user, proxy_server.password)
145
+ else
146
+ Net::HTTP
147
+ end
148
+ instance = http.new(uri.host, uri.port)
149
+ instance.read_timeout = instance.open_timeout = timeout
150
+ instance.use_ssl = true if uri.port == 443
151
+
152
+ instance
153
+ end
154
+
155
+ def self.api_verification_free(verify_hash, timeout: nil)
156
+ query = URI.encode_www_form(verify_hash)
157
+ uri = URI.parse("#{configuration.verify_url}?#{query}")
158
+ http_instance = http_client_for(uri: uri, timeout: timeout)
159
+ request = Net::HTTP::Get.new(uri.request_uri)
160
+ JSON.parse(http_instance.request(request).body)
161
+ end
162
+
163
+ def self.api_verification_enterprise(query_params, body, project_id, timeout: nil)
164
+ query = URI.encode_www_form(query_params)
165
+ uri = URI.parse("#{configuration.verify_url}/#{project_id}/assessments?#{query}")
166
+ http_instance = http_client_for(uri: uri, timeout: timeout)
167
+ request = Net::HTTP::Post.new(uri.request_uri)
168
+ request['Content-Type'] = 'application/json; charset=utf-8'
169
+ request.body = JSON.generate(body)
170
+ JSON.parse(http_instance.request(request).body)
81
171
  end
82
172
  end
@@ -0,0 +1,5 @@
1
+ en:
2
+ recaptcha:
3
+ errors:
4
+ verification_failed: reCAPTCHA verification failed, please try again.
5
+ recaptcha_unreachable: Oops, we failed to validate your reCAPTCHA response. Please try again.
@@ -0,0 +1,5 @@
1
+ fr:
2
+ recaptcha:
3
+ errors:
4
+ verification_failed: La vérification reCAPTCHA a échoué, veuillez essayer à nouveau.
5
+ recaptcha_unreachable: Oops, nous n'avons pas pu valider votre réponse reCAPTCHA. Veuillez essayer à nouveau.
@@ -0,0 +1,5 @@
1
+ ja:
2
+ recaptcha:
3
+ errors:
4
+ verification_failed: reCAPTCHA認証に失敗しました。もう一度お試しください。
5
+ recaptcha_unreachable: reCAPTCHAのレスポンスを検証できませんでした。もう一度お試しください。
@@ -0,0 +1,5 @@
1
+ nl:
2
+ recaptcha:
3
+ errors:
4
+ verification_failed: reCAPTCHA-verificatie mislukt, probeer het opnieuw.
5
+ recaptcha_unreachable: Oeps, we hebben uw reCAPTCHA-antwoord niet kunnen valideren. Probeer het opnieuw.
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: recaptcha
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.14.0
4
+ version: 5.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason L Perry
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-01 00:00:00.000000000 Z
11
+ date: 2023-04-19 00:00:00.000000000 Z
12
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
13
  - !ruby/object:Gem::Dependency
28
14
  name: mocha
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -52,20 +38,6 @@ dependencies:
52
38
  - - ">="
53
39
  - !ruby/object:Gem::Version
54
40
  version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: activesupport
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
41
  - !ruby/object:Gem::Dependency
70
42
  name: i18n
71
43
  requirement: !ruby/object:Gem::Requirement
@@ -161,17 +133,23 @@ files:
161
133
  - LICENSE
162
134
  - README.md
163
135
  - lib/recaptcha.rb
164
- - lib/recaptcha/client_helper.rb
136
+ - lib/recaptcha/adapters/controller_methods.rb
137
+ - lib/recaptcha/adapters/view_methods.rb
165
138
  - lib/recaptcha/configuration.rb
139
+ - lib/recaptcha/helpers.rb
166
140
  - lib/recaptcha/rails.rb
167
141
  - lib/recaptcha/railtie.rb
168
- - lib/recaptcha/verify.rb
169
142
  - lib/recaptcha/version.rb
143
+ - rails/locales/en.yml
144
+ - rails/locales/fr.yml
145
+ - rails/locales/ja.yml
146
+ - rails/locales/nl.yml
170
147
  homepage: http://github.com/ambethia/recaptcha
171
148
  licenses:
172
149
  - MIT
173
- metadata: {}
174
- post_install_message:
150
+ metadata:
151
+ source_code_uri: https://github.com/ambethia/recaptcha
152
+ post_install_message:
175
153
  rdoc_options: []
176
154
  require_paths:
177
155
  - lib
@@ -179,16 +157,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
179
157
  requirements:
180
158
  - - ">="
181
159
  - !ruby/object:Gem::Version
182
- version: 2.3.0
160
+ version: 2.7.0
183
161
  required_rubygems_version: !ruby/object:Gem::Requirement
184
162
  requirements:
185
163
  - - ">="
186
164
  - !ruby/object:Gem::Version
187
165
  version: '0'
188
166
  requirements: []
189
- rubyforge_project:
190
- rubygems_version: 2.7.6
191
- signing_key:
167
+ rubygems_version: 3.3.3
168
+ signing_key:
192
169
  specification_version: 4
193
170
  summary: Helpers for the reCAPTCHA API
194
171
  test_files: []
@@ -1,157 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Recaptcha
4
- module ClientHelper
5
- # Your public API can be specified in the +options+ hash or preferably
6
- # using the Configuration.
7
- def recaptcha_tags(options = {})
8
- if options.key?(:stoken)
9
- raise(RecaptchaError, "Secure Token is deprecated. Please remove 'stoken' from your calls to recaptcha_tags.")
10
- end
11
- if options.key?(:ssl)
12
- raise(RecaptchaError, "SSL is now always true. Please remove 'ssl' from your calls to recaptcha_tags.")
13
- end
14
-
15
- noscript = options.delete(:noscript)
16
-
17
- html, tag_attributes, fallback_uri = Recaptcha::ClientHelper.recaptcha_components(options)
18
- html << %(<div #{tag_attributes}></div>\n)
19
-
20
- if noscript != false
21
- html << <<-HTML
22
- <noscript>
23
- <div>
24
- <div style="width: 302px; height: 422px; position: relative;">
25
- <div style="width: 302px; height: 422px; position: absolute;">
26
- <iframe
27
- src="#{fallback_uri}"
28
- name="ReCAPTCHA"
29
- style="width: 302px; height: 422px; border-style: none; border: 0; overflow: hidden;">
30
- </iframe>
31
- </div>
32
- </div>
33
- <div style="width: 300px; height: 60px; border-style: none;
34
- bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px;
35
- background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
36
- <textarea id="g-recaptcha-response" name="g-recaptcha-response"
37
- class="g-recaptcha-response"
38
- style="width: 250px; height: 40px; border: 1px solid #c1c1c1;
39
- margin: 10px 25px; padding: 0px; resize: none;">
40
- </textarea>
41
- </div>
42
- </div>
43
- </noscript>
44
- HTML
45
- end
46
-
47
- html.respond_to?(:html_safe) ? html.html_safe : html
48
- end
49
-
50
- # Invisible reCAPTCHA implementation
51
- def invisible_recaptcha_tags(options = {})
52
- options = {callback: 'invisibleRecaptchaSubmit', ui: :button}.merge options
53
- text = options.delete(:text)
54
- html, tag_attributes = Recaptcha::ClientHelper.recaptcha_components(options)
55
- html << recaptcha_default_callback(options) if recaptcha_default_callback_required?(options)
56
- case options[:ui]
57
- when :button
58
- html << %(<button type="submit" #{tag_attributes}>#{text}</button>\n)
59
- when :invisible
60
- html << %(<div data-size="invisible" #{tag_attributes}></div>\n)
61
- when :input
62
- html << %(<input type="submit" #{tag_attributes} value="#{text}"/>\n)
63
- else
64
- raise(RecaptchaError, "ReCAPTCHA ui `#{options[:ui]}` is not valid.")
65
- end
66
- html.respond_to?(:html_safe) ? html.html_safe : html
67
- end
68
-
69
- def self.recaptcha_components(options = {})
70
- html = +''
71
- attributes = {}
72
- fallback_uri = +''
73
-
74
- # Since leftover options get passed directly through as tag
75
- # attributes, we must unconditionally delete all our options
76
- options = options.dup
77
- env = options.delete(:env)
78
- class_attribute = options.delete(:class)
79
- site_key = options.delete(:site_key)
80
- hl = options.delete(:hl)
81
- onload = options.delete(:onload)
82
- render = options.delete(:render)
83
- nonce = options.delete(:nonce)
84
- skip_script = (options.delete(:script) == false)
85
- ui = options.delete(:ui)
86
-
87
- data_attribute_keys = [:badge, :theme, :type, :callback, :expired_callback, :error_callback, :size]
88
- data_attribute_keys << :tabindex unless ui == :button
89
- data_attributes = {}
90
- data_attribute_keys.each do |data_attribute|
91
- value = options.delete(data_attribute)
92
- data_attributes["data-#{data_attribute.to_s.tr('_', '-')}"] = value if value
93
- end
94
-
95
- unless Recaptcha::Verify.skip?(env)
96
- site_key ||= Recaptcha.configuration.site_key!
97
- script_url = Recaptcha.configuration.api_server_url
98
- query_params = hash_to_query(
99
- hl: hl,
100
- onload: onload,
101
- render: render
102
- )
103
- script_url += "?#{query_params}" unless query_params.empty?
104
- nonce_attr = " nonce='#{nonce}'" if nonce
105
- html << %(<script src="#{script_url}" async defer#{nonce_attr}></script>\n) unless skip_script
106
- fallback_uri = %(#{script_url.chomp(".js")}/fallback?k=#{site_key})
107
- attributes["data-sitekey"] = site_key
108
- attributes.merge! data_attributes
109
- end
110
-
111
- # Append whatever that's left of options to be attributes on the tag.
112
- attributes["class"] = "g-recaptcha #{class_attribute}"
113
- tag_attributes = attributes.merge(options).map { |k, v| %(#{k}="#{v}") }.join(" ")
114
-
115
- [html, tag_attributes, fallback_uri]
116
- end
117
-
118
- private
119
-
120
- def recaptcha_default_callback(options = {})
121
- nonce = options[:nonce]
122
- nonce_attr = " nonce='#{nonce}'" if nonce
123
-
124
- <<-HTML
125
- <script#{nonce_attr}>
126
- var invisibleRecaptchaSubmit = function () {
127
- var closestForm = function (ele) {
128
- var curEle = ele.parentNode;
129
- while (curEle.nodeName !== 'FORM' && curEle.nodeName !== 'BODY'){
130
- curEle = curEle.parentNode;
131
- }
132
- return curEle.nodeName === 'FORM' ? curEle : null
133
- };
134
-
135
- var eles = document.getElementsByClassName('g-recaptcha');
136
- if (eles.length > 0) {
137
- var form = closestForm(eles[0]);
138
- if (form) {
139
- form.submit();
140
- }
141
- }
142
- };
143
- </script>
144
- HTML
145
- end
146
-
147
- def recaptcha_default_callback_required?(options)
148
- options[:callback] == 'invisibleRecaptchaSubmit' &&
149
- !Recaptcha::Verify.skip?(options[:env]) &&
150
- options[:script] != false
151
- end
152
-
153
- private_class_method def self.hash_to_query(hash)
154
- hash.delete_if { |_, val| val.nil? || val.empty? }.to_a.map { |pair| pair.join('=') }.join('&')
155
- end
156
- end
157
- end
@@ -1,108 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
-
5
- module Recaptcha
6
- module Verify
7
- G_RESPONSE_LIMIT = 4000
8
- # Your private API can be specified in the +options+ hash or preferably
9
- # using the Configuration.
10
- def verify_recaptcha(options = {})
11
- options = {model: options} unless options.is_a? Hash
12
- return true if Recaptcha::Verify.skip?(options[:env])
13
-
14
- model = options[:model]
15
- attribute = options[:attribute] || :base
16
- recaptcha_response = options[:response] || params['g-recaptcha-response'].to_s
17
-
18
- begin
19
- verified = if recaptcha_response.empty? || recaptcha_response.length > G_RESPONSE_LIMIT
20
- false
21
- else
22
- recaptcha_verify_via_api_call(request, recaptcha_response, options)
23
- end
24
-
25
- if verified
26
- flash.delete(:recaptcha_error) if recaptcha_flash_supported? && !model
27
- true
28
- else
29
- recaptcha_error(
30
- model,
31
- attribute,
32
- options[:message],
33
- "recaptcha.errors.verification_failed",
34
- "reCAPTCHA verification failed, please try again."
35
- )
36
- false
37
- end
38
- rescue Timeout::Error
39
- if Recaptcha.configuration.handle_timeouts_gracefully
40
- recaptcha_error(
41
- model,
42
- attribute,
43
- options[:message],
44
- "recaptcha.errors.recaptcha_unreachable",
45
- "Oops, we failed to validate your reCAPTCHA response. Please try again."
46
- )
47
- false
48
- else
49
- raise RecaptchaError, "Recaptcha unreachable."
50
- end
51
- rescue StandardError => e
52
- raise RecaptchaError, e.message, e.backtrace
53
- end
54
- end
55
-
56
- def verify_recaptcha!(options = {})
57
- verify_recaptcha(options) || raise(VerifyError)
58
- end
59
-
60
- def self.skip?(env)
61
- env ||= ENV['RAILS_ENV'] || ENV['RACK_ENV'] || (Rails.env if defined? Rails.env)
62
- Recaptcha.configuration.skip_verify_env.include? env
63
- end
64
-
65
- private
66
-
67
- def recaptcha_verify_via_api_call(request, recaptcha_response, options)
68
- secret_key = options[:secret_key] || Recaptcha.configuration.secret_key!
69
-
70
- verify_hash = {
71
- "secret" => secret_key,
72
- "response" => recaptcha_response
73
- }
74
-
75
- unless options[:skip_remote_ip]
76
- remoteip = (request.respond_to?(:remote_ip) && request.remote_ip) || (env && env['REMOTE_ADDR'])
77
- verify_hash["remoteip"] = remoteip.to_s
78
- end
79
-
80
- reply = JSON.parse(Recaptcha.get(verify_hash, options))
81
- reply['success'].to_s == "true" &&
82
- recaptcha_hostname_valid?(reply['hostname'], options[:hostname])
83
- end
84
-
85
- def recaptcha_hostname_valid?(hostname, validation)
86
- validation ||= Recaptcha.configuration.hostname
87
-
88
- case validation
89
- when nil, FalseClass then true
90
- when String then validation == hostname
91
- else validation.call(hostname)
92
- end
93
- end
94
-
95
- def recaptcha_error(model, attribute, message, key, default)
96
- message ||= Recaptcha.i18n(key, default)
97
- if model
98
- model.errors.add attribute, message
99
- else
100
- flash[:recaptcha_error] = message if recaptcha_flash_supported?
101
- end
102
- end
103
-
104
- def recaptcha_flash_supported?
105
- request.respond_to?(:format) && request.format == :html && respond_to?(:flash)
106
- end
107
- end
108
- end