panmind-recaptcha 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.
- data/README.md +144 -0
- data/Rakefile +49 -0
- data/lib/panmind/recaptcha.rb +136 -0
- data/rails/init.rb +7 -0
- metadata +84 -0
data/README.md
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
ReCaptcha for Rails - With AJAX Validation
|
2
|
+
==========================================
|
3
|
+
|
4
|
+
Purpose
|
5
|
+
-------
|
6
|
+
|
7
|
+
This plugin implements view helpers to generate ReCaptcha code,
|
8
|
+
to interface with the HTTP API for captcha verification, a DSL
|
9
|
+
to generate a `before_filter` and the necessary hacky code to
|
10
|
+
implement AJAX captcha validation.
|
11
|
+
|
12
|
+
Implementation
|
13
|
+
--------------
|
14
|
+
|
15
|
+
For AJAX validation, a request to ReCaptcha HTTP server must be
|
16
|
+
made in order to verify the user input, but captchas can be
|
17
|
+
checked only once.
|
18
|
+
|
19
|
+
The plugin, thus, in case of a successful verification, saves
|
20
|
+
uses the Rails `flash` to temporarily save this status in the
|
21
|
+
session and then the before filter skips the verification via
|
22
|
+
the ReCaptcha HTTP service.
|
23
|
+
|
24
|
+
Installation
|
25
|
+
------------
|
26
|
+
|
27
|
+
Via RubyGems:
|
28
|
+
|
29
|
+
gem install panmind-recaptcha
|
30
|
+
|
31
|
+
Or via Rails Plugin:
|
32
|
+
|
33
|
+
script/plugin install git://github.com/Panmind/recaptcha.git
|
34
|
+
|
35
|
+
Usage
|
36
|
+
-----
|
37
|
+
|
38
|
+
In your config/environment.rb:
|
39
|
+
|
40
|
+
Panmind::Recaptcha.set(
|
41
|
+
:private_key => 'your private key',
|
42
|
+
:public_key => 'your public key
|
43
|
+
)
|
44
|
+
|
45
|
+
In your controller, say, the `UsersController` for a signup action:
|
46
|
+
|
47
|
+
require_valid_captcha :only => :create
|
48
|
+
|
49
|
+
private
|
50
|
+
def invalid_captcha
|
51
|
+
@user = User.new params[:user]
|
52
|
+
@user.errors.add_to_base('Captcha failed')
|
53
|
+
|
54
|
+
render :new, :layout => 'login'
|
55
|
+
end
|
56
|
+
|
57
|
+
The `invalid_captcha` method is called by the plugin when captcha
|
58
|
+
verification fails, and *must* be overwritten or a `NotImplementedError`
|
59
|
+
exception will be thrown.
|
60
|
+
|
61
|
+
In your view:
|
62
|
+
|
63
|
+
<%= recaptcha :label => 'Are you human?', :theme => 'clean' %>
|
64
|
+
|
65
|
+
You can pass any `RecaptchaOptions` valid option, as stated by the
|
66
|
+
service documentation. The only nonstandard option `:label` is used
|
67
|
+
by the plugin to print a label before the captcha widget.
|
68
|
+
|
69
|
+
|
70
|
+
AJAX Validation
|
71
|
+
---------------
|
72
|
+
|
73
|
+
To cache the results of a successful captcha verification, you need
|
74
|
+
simply to pass the `:ajax => true` option to the `require_valid_captcha`
|
75
|
+
controller method.
|
76
|
+
|
77
|
+
require_valid_captcha :only => :create, :ajax => true
|
78
|
+
|
79
|
+
When the form is validated via AJAX, the maybe successful result will
|
80
|
+
be saved in the `flash` (thus set in the session store); when the form is
|
81
|
+
then submitted via a plain HTTP request, verification will be skipped.
|
82
|
+
|
83
|
+
On Panmind we use our `jquery.ajax-validation` plugin, that you
|
84
|
+
can download from http://github.com/Panmind/jquery-ajax-nav and
|
85
|
+
the Javascript code located in the `js/signup-sample.js` file.
|
86
|
+
|
87
|
+
On the backend, an example checker that returns different HTTP
|
88
|
+
status code follows:
|
89
|
+
|
90
|
+
def signup_checker
|
91
|
+
# If no email was provided, return 400
|
92
|
+
if params[:email].blank?
|
93
|
+
render :nothing => true, :status => :bad_request and return
|
94
|
+
end
|
95
|
+
|
96
|
+
email = CGI.unescape(params[:email])
|
97
|
+
|
98
|
+
# more thorough checks on email should go here
|
99
|
+
|
100
|
+
# If an user with this email already exist, return 406
|
101
|
+
if User.exists?(['email = ?', email])
|
102
|
+
render :nothing => true, :status => :not_acceptable and return
|
103
|
+
end
|
104
|
+
|
105
|
+
unless valid_captcha?
|
106
|
+
invalid_captcha and return
|
107
|
+
end
|
108
|
+
|
109
|
+
save_solved_captcha # This method sets a flag in the flash
|
110
|
+
render :nothing => true, :status => :ok
|
111
|
+
end
|
112
|
+
|
113
|
+
Moreover, the client Javascript code should be informed via an
|
114
|
+
HTTP status when validation fails, thus the `invalid_captcha`
|
115
|
+
must contain a special `render` when the request comes from XHR:
|
116
|
+
|
117
|
+
def invalid_captcha
|
118
|
+
# If the captcha is not valid, return a 412 (precondition failed)
|
119
|
+
render :nothing => true, :status => 412 and return true if request.xhr?
|
120
|
+
|
121
|
+
# Same invalid_captcha code as above
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
The latest XHR specification from the w3c states that cookies set
|
126
|
+
by responses to requests sent via XHR are to be honored by the browser.
|
127
|
+
|
128
|
+
The code was tested with IE (6,7,8), Safari (4, 5), Firefox 3, Chrome 5
|
129
|
+
and Opera 10.
|
130
|
+
|
131
|
+
|
132
|
+
Security
|
133
|
+
--------
|
134
|
+
|
135
|
+
As long as you use a session store backed on the server or cryptographically
|
136
|
+
sign the cookies used by the session cookie store (as Rails does by default)
|
137
|
+
there is no way to bypass the captcha when AJAX validation is enabled.
|
138
|
+
|
139
|
+
|
140
|
+
Compatibility
|
141
|
+
-------------
|
142
|
+
|
143
|
+
Tested with Rails 2.3.8 with the `rails_xss` plugin installed,
|
144
|
+
running under Ruby 1.9.1-p378.
|
data/Rakefile
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
|
4
|
+
require 'lib/panmind/recaptcha'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'jeweler'
|
8
|
+
Jeweler::Tasks.new do |gemspec|
|
9
|
+
gemspec.name = 'panmind-recaptcha'
|
10
|
+
|
11
|
+
gemspec.summary = 'ReCaptcha for Rails - With AJAX Validation'
|
12
|
+
gemspec.description = 'ReCaptcha implements view helpers to generate ReCaptcha code, ' \
|
13
|
+
'with the noscript counterpart, required methods that interface ' \
|
14
|
+
'with the HTTP API for captcha verification, a DSL to generate a ' \
|
15
|
+
'before_filter and the code to implement AJAX captcha validation.'
|
16
|
+
|
17
|
+
gemspec.authors = ['Marcello Barnaba']
|
18
|
+
gemspec.email = 'vjt@openssl.it'
|
19
|
+
gemspec.homepage = 'http://github.com/Panmind/recaptcha'
|
20
|
+
|
21
|
+
gemspec.files = %w( README.md Rakefile rails/init.rb ) + Dir['lib/**/*']
|
22
|
+
gemspec.extra_rdoc_files = %w( README.md )
|
23
|
+
gemspec.has_rdoc = true
|
24
|
+
|
25
|
+
gemspec.version = Panmind::Recaptcha::Version
|
26
|
+
gemspec.date = '2010-11-17'
|
27
|
+
|
28
|
+
gemspec.require_path = 'lib'
|
29
|
+
|
30
|
+
gemspec.add_dependency('rails', '~> 2.3.8')
|
31
|
+
end
|
32
|
+
rescue LoadError
|
33
|
+
puts 'Jeweler not available. Install it with: gem install jeweler'
|
34
|
+
end
|
35
|
+
|
36
|
+
desc 'Generate the rdoc'
|
37
|
+
Rake::RDocTask.new do |rdoc|
|
38
|
+
rdoc.rdoc_files.add %w( README.md lib/**/*.rb )
|
39
|
+
|
40
|
+
rdoc.main = 'README.md'
|
41
|
+
rdoc.title = 'ReCaptcha for Rails - With AJAX Validation'
|
42
|
+
end
|
43
|
+
|
44
|
+
desc 'Will someone help write tests?'
|
45
|
+
task :default do
|
46
|
+
puts
|
47
|
+
puts 'Can you help in writing tests? Please do :-)'
|
48
|
+
puts
|
49
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
if defined?(Rails) && Rails.env.test?
|
4
|
+
begin
|
5
|
+
require 'mocha'
|
6
|
+
rescue LoadError
|
7
|
+
print "\n!!\n!! ReCaptcha: to use the test helpers you should gem install mocha\n!!\n\n"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module Panmind
|
12
|
+
module Recaptcha
|
13
|
+
Version = 1.0
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_accessor :private_key, :public_key, :request_timeout
|
17
|
+
|
18
|
+
def set(options)
|
19
|
+
self.private_key, self.public_key =
|
20
|
+
options.values_at(:private_key, :public_key)
|
21
|
+
|
22
|
+
# Defaults
|
23
|
+
#
|
24
|
+
self.request_timeout = options[:timeout] || 5
|
25
|
+
end
|
26
|
+
|
27
|
+
def enabled?
|
28
|
+
Rails.env.production?
|
29
|
+
end
|
30
|
+
end # << self
|
31
|
+
|
32
|
+
class ConfigurationError < StandardError; end
|
33
|
+
|
34
|
+
module Controller
|
35
|
+
def self.included(base)
|
36
|
+
base.instance_eval do
|
37
|
+
def require_valid_captcha(options = {})
|
38
|
+
if options.delete(:ajax)
|
39
|
+
options.update(:unless => :captcha_already_solved?)
|
40
|
+
end
|
41
|
+
|
42
|
+
before_filter :validate_recaptcha, options
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
protected
|
48
|
+
def validate_recaptcha
|
49
|
+
invalid_captcha unless valid_captcha?
|
50
|
+
end
|
51
|
+
|
52
|
+
def captcha_already_solved?
|
53
|
+
flash[:skip_captcha_check]
|
54
|
+
end
|
55
|
+
|
56
|
+
def save_solved_captcha
|
57
|
+
flash[:skip_captcha_check] = true
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
def valid_captcha?
|
62
|
+
return true unless Recaptcha.enabled?
|
63
|
+
|
64
|
+
challenge, response = params.values_at(
|
65
|
+
:recaptcha_challenge_field, :recaptcha_response_field)
|
66
|
+
|
67
|
+
return false if challenge.blank? || response.blank?
|
68
|
+
|
69
|
+
req =
|
70
|
+
Timeout.timeout(Recaptcha.request_timeout) do
|
71
|
+
uri = URI.parse("http://api-verify.recaptcha.net/verify")
|
72
|
+
Net::HTTP.post_form(uri,
|
73
|
+
:privatekey => Recaptcha.private_key,
|
74
|
+
:remoteip => request.remote_ip,
|
75
|
+
:challenge => challenge,
|
76
|
+
:response => response
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
res = req.body.split("\n")
|
81
|
+
return res.first == 'true'
|
82
|
+
|
83
|
+
rescue Timeout::Error
|
84
|
+
# Let it go...
|
85
|
+
true
|
86
|
+
end
|
87
|
+
|
88
|
+
def invalid_captcha
|
89
|
+
raise NotImplementedError, 'You must implement invalid_captcha in your controller'
|
90
|
+
end
|
91
|
+
end # Controller
|
92
|
+
|
93
|
+
module Helpers
|
94
|
+
def recaptcha(options = {})
|
95
|
+
return unless Recaptcha.enabled?
|
96
|
+
|
97
|
+
if Recaptcha.private_key.blank? || Recaptcha.public_key.blank?
|
98
|
+
raise ConfigurationError, 'ReCaptcha keys are missing'
|
99
|
+
end
|
100
|
+
|
101
|
+
label_text = options.delete(:label) || 'Enter the following words'
|
102
|
+
|
103
|
+
noscript_options = {:width => 420, :height => 320}.merge(
|
104
|
+
options.delete(:noscript) || {})
|
105
|
+
|
106
|
+
recaptcha_options =
|
107
|
+
options.empty? ? '' :
|
108
|
+
javascript_tag(%[var RecaptchaOptions = #{options.to_json}])
|
109
|
+
|
110
|
+
label_tag('recaptcha_response_field', label_text) + recaptcha_options +
|
111
|
+
%[<script type="text/javascript"
|
112
|
+
src="https://api-secure.recaptcha.net/challenge?k=#{Recaptcha.public_key}">
|
113
|
+
</script>
|
114
|
+
|
115
|
+
<noscript>
|
116
|
+
<iframe src="https://api-secure.recaptcha.net/noscript?k=#{Recaptcha.public_key}"
|
117
|
+
height="#{noscript_options[:width]}" width="#{noscript_options[:height]}" frameborder="0"></iframe><br>
|
118
|
+
<input type="text" class="text" name="recaptcha_challenge_field" tabindex="#{options[:tabindex]}"/>
|
119
|
+
<input type="hidden" name="recaptcha_response_field" value="manual_challenge" />
|
120
|
+
</noscript>
|
121
|
+
].html_safe
|
122
|
+
end
|
123
|
+
end # Helpers
|
124
|
+
|
125
|
+
module TestHelpers
|
126
|
+
def mock_valid_captcha
|
127
|
+
@controller.stubs(:valid_captcha?).returns(true)
|
128
|
+
end
|
129
|
+
|
130
|
+
def mock_invalid_captcha
|
131
|
+
@controller.stubs(:valid_captcha?).returns(false)
|
132
|
+
end
|
133
|
+
end # TestHelpers
|
134
|
+
|
135
|
+
end # Recaptcha
|
136
|
+
end # Panmind
|
data/rails/init.rb
ADDED
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: panmind-recaptcha
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 15
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: "1.0"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Marcello Barnaba
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-11-17 00:00:00 +01:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rails
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 19
|
29
|
+
segments:
|
30
|
+
- 2
|
31
|
+
- 3
|
32
|
+
- 8
|
33
|
+
version: 2.3.8
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
description: ReCaptcha implements view helpers to generate ReCaptcha code, with the noscript counterpart, required methods that interface with the HTTP API for captcha verification, a DSL to generate a before_filter and the code to implement AJAX captcha validation.
|
37
|
+
email: vjt@openssl.it
|
38
|
+
executables: []
|
39
|
+
|
40
|
+
extensions: []
|
41
|
+
|
42
|
+
extra_rdoc_files:
|
43
|
+
- README.md
|
44
|
+
files:
|
45
|
+
- README.md
|
46
|
+
- Rakefile
|
47
|
+
- lib/panmind/recaptcha.rb
|
48
|
+
- rails/init.rb
|
49
|
+
has_rdoc: true
|
50
|
+
homepage: http://github.com/Panmind/recaptcha
|
51
|
+
licenses: []
|
52
|
+
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options:
|
55
|
+
- --charset=UTF-8
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
hash: 3
|
64
|
+
segments:
|
65
|
+
- 0
|
66
|
+
version: "0"
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
hash: 3
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
version: "0"
|
76
|
+
requirements: []
|
77
|
+
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 1.3.7
|
80
|
+
signing_key:
|
81
|
+
specification_version: 3
|
82
|
+
summary: ReCaptcha for Rails - With AJAX Validation
|
83
|
+
test_files: []
|
84
|
+
|