panmind-recaptcha 1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|