Commit 71f8fd60369e12848718d475fe99d6ed6d650eff
0 parents
Exists in
master
1st commit
Showing
10 changed files
with
425 additions
and
0 deletions
Show diff stats
1 | +++ a/README.md | |
... | ... | @@ -0,0 +1,7 @@ |
1 | +Sample config values: | |
2 | + | |
3 | +verify_uri 'http://captcha.servicoscorporativos.serpro.gov.br/captchavalidar/1.0.0/validar' | |
4 | + | |
5 | +serpro_client_id 'fdbcdc7a0b754ee7ae9d865fda740f17' | |
6 | + | |
7 | +See http://stdcs.supst.serpro/manual/html/#captcha/page/introducao.html for more details. | ... | ... |
1 | +++ a/controllers/recaptcha_plugin_admin_controller.rb | |
... | ... | @@ -0,0 +1,17 @@ |
1 | +class RecaptchaPluginAdminController < PluginAdminController | |
2 | + | |
3 | + append_view_path File.join(File.dirname(__FILE__) + '/../views') | |
4 | + | |
5 | + def index | |
6 | + end | |
7 | + | |
8 | + def update | |
9 | + if @environment.update_attributes(params[:environment]) | |
10 | + session[:notice] = _('Captcha configuration updated successfully.') | |
11 | + else | |
12 | + session[:notice] = _('Captcha configuration could not be saved.') | |
13 | + end | |
14 | + render :action => 'index' | |
15 | + end | |
16 | + | |
17 | +end | ... | ... |
1 | +++ a/lib/ext/environment.rb | |
... | ... | @@ -0,0 +1,49 @@ |
1 | +require_dependency 'environment' | |
2 | + | |
3 | +class Environment | |
4 | + | |
5 | + #reCAPTCHA settings | |
6 | + settings_items :recaptcha_plugin, :type => ActiveSupport::HashWithIndifferentAccess, :default => {} | |
7 | + attr_accessible :recaptcha_plugin_attributes, :recaptcha_version, :recaptcha_private_key, :recaptcha_site_key | |
8 | + | |
9 | + def recaptcha_plugin_attributes | |
10 | + self.recaptcha_plugin || {} | |
11 | + end | |
12 | + | |
13 | + def recaptcha_version= recaptcha_version | |
14 | + self.recaptcha_plugin = {} if self.recaptcha_plugin.blank? | |
15 | + self.recaptcha_plugin['recaptcha_version'] = recaptcha_version | |
16 | + end | |
17 | + | |
18 | + def recaptcha_version | |
19 | + self.recaptcha_plugin['recaptcha_version'] | |
20 | + end | |
21 | + | |
22 | + def recaptcha_private_key= recaptcha_private_key | |
23 | + self.recaptcha_plugin = {} if self.recaptcha_plugin.blank? | |
24 | + self.recaptcha_plugin['recaptcha_private_key'] = recaptcha_private_key | |
25 | + end | |
26 | + | |
27 | + def recaptcha_private_key | |
28 | + self.recaptcha_plugin['recaptcha_private_key'] | |
29 | + end | |
30 | + | |
31 | + def recaptcha_verify_uri= recaptcha_verify_uri | |
32 | + self.recaptcha_plugin = {} if self.recaptcha_plugin.blank? | |
33 | + self.recaptcha_plugin['recaptcha_verify_uri'] = recaptcha_verify_uri | |
34 | + end | |
35 | + | |
36 | + def recaptcha_verify_uri | |
37 | + self.recaptcha_plugin['recaptcha_verify_uri'] | |
38 | + end | |
39 | + | |
40 | + def recaptcha_site_key= recaptcha_site_key | |
41 | + self.recaptcha_plugin = {} if self.recaptcha_plugin.blank? | |
42 | + self.recaptcha_plugin['recaptcha_site_key'] = recaptcha_site_key | |
43 | + end | |
44 | + | |
45 | + def recaptcha_site_key | |
46 | + self.recaptcha_plugin['recaptcha_site_key'] | |
47 | + end | |
48 | + | |
49 | +end | ... | ... |
1 | +++ a/lib/recaptcha_plugin.rb | |
... | ... | @@ -0,0 +1,41 @@ |
1 | +class RecaptchaPlugin < Noosfero::Plugin | |
2 | + | |
3 | + def self.plugin_name | |
4 | + _('Google reCAPTCHA plugin') | |
5 | + end | |
6 | + | |
7 | + def self.plugin_description | |
8 | + _("Provides a plugin to Google reCAPTCHA.") | |
9 | + end | |
10 | + | |
11 | + def self.api_mount_points | |
12 | + [RecaptchaPlugin::API ] | |
13 | + end | |
14 | + | |
15 | + def test_captcha(*args) | |
16 | + remote_ip = args[0] | |
17 | + params = args[1] | |
18 | + environment = args[2] | |
19 | + | |
20 | + private_key = environment.recaptcha_private_key | |
21 | + version = environment.recaptcha_version | |
22 | + | |
23 | + msg_icve = _('Internal captcha validation error') | |
24 | + msg_esca = 'Environment recaptcha_plugin_attributes' | |
25 | + | |
26 | + return RecaptchaVerification.hash_error(msg_icve, s, nil, "#{msg_eacs} private_key not defined") if private_key.nil? | |
27 | + return RecaptchaVerification.hash_error(msg_icve, s, nil, "#{msg_eacs} version not defined") unless version == 1 || version == 2 | |
28 | + | |
29 | + rv = RecaptchaVerification.new | |
30 | + | |
31 | + if version == 1 | |
32 | + verify_uri = 'https://www.google.com/recaptcha/api/verify' | |
33 | + return rv.verify_recaptcha_v1(remote_ip, private_key, verify_uri, params[:recaptcha_challenge_field], params[:recaptcha_response_field]) | |
34 | + end | |
35 | + if version == 2 | |
36 | + verify_uri = 'https://www.google.com/recaptcha/api/siteverify' | |
37 | + return rv.verify_recaptcha_v2(remote_ip, private_key, verify_uri, params[:g_recaptcha_response]) | |
38 | + end | |
39 | + end | |
40 | + | |
41 | +end | ... | ... |
1 | +++ a/lib/recaptcha_verification.rb | |
... | ... | @@ -0,0 +1,85 @@ |
1 | +class RecaptchaVerification | |
2 | + | |
3 | + def self.hash_error(user_message, status, log_message=nil, javascript_console_message=nil) | |
4 | + {user_message: user_message, status: status, log_message: log_message, javascript_console_message: javascript_console_message} | |
5 | + end | |
6 | + | |
7 | + # return true or a hash with the error | |
8 | + # :user_message, :status, :log_message, :javascript_console_message | |
9 | + def verify_recaptcha_v1(remote_ip, private_key, api_recaptcha_verify_uri, recaptcha_challenge_field, recaptcha_response_field) | |
10 | + if recaptcha_challenge_field == nil || recaptcha_response_field == nil | |
11 | + return render_api_error!(_('Captcha validation error'), 500, nil, _('Missing captcha data')) | |
12 | + end | |
13 | + | |
14 | + verify_hash = { | |
15 | + "privatekey" => private_key, | |
16 | + "remoteip" => remote_ip, | |
17 | + "challenge" => recaptcha_challenge_field, | |
18 | + "response" => recaptcha_response_field | |
19 | + } | |
20 | + uri = URI(api_recaptcha_verify_uri) | |
21 | + https = Net::HTTP.new(uri.host, uri.port) | |
22 | + https.use_ssl = true | |
23 | + request = Net::HTTP::Post.new(uri.path) | |
24 | + request.set_form_data(verify_hash) | |
25 | + begin | |
26 | + result = https.request(request).body.split("\n") | |
27 | + rescue Exception => e | |
28 | + return render_api_error!(_('Internal captcha validation error'), 500, nil, "Error validating Googles' recaptcha version 1: #{e.message}") | |
29 | + end | |
30 | + return true if result[0] == "true" | |
31 | + return render_api_error!(_("Wrong captcha text, please try again"), 403, nil, "Error validating Googles' recaptcha version 1: #{result[1]}") if result[1] == "incorrect-captcha-sol" | |
32 | + #Catches all errors at the end | |
33 | + return render_api_error!(_("Internal recaptcha validation error"), 500, nil, "Error validating Googles' recaptcha version 1: #{result[1]}") | |
34 | + end | |
35 | + | |
36 | + # return true or a hash with the error | |
37 | + # :user_message, :status, :log_message, :javascript_console_message | |
38 | + def verify_recaptcha_v2(remote_ip, private_key, api_recaptcha_verify_uri, g_recaptcha_response) | |
39 | + return render_api_error!(_('Captcha validation error'), 500, nil, _('Missing captcha data')) if g_recaptcha_response == nil | |
40 | + verify_hash = { | |
41 | + "secret" => private_key, | |
42 | + "remoteip" => remote_ip, | |
43 | + "response" => g_recaptcha_response | |
44 | + } | |
45 | + uri = URI(api_recaptcha_verify_uri) | |
46 | + https = Net::HTTP.new(uri.host, uri.port) | |
47 | + https.use_ssl = true | |
48 | + request = Net::HTTP::Post.new(uri.path) | |
49 | + request.set_form_data(verify_hash) | |
50 | + begin | |
51 | + body = https.request(request).body | |
52 | + rescue Exception => e | |
53 | + return render_api_error!(_('Internal captcha validation error'), 500, nil, "recaptcha error: #{e.message}") | |
54 | + end | |
55 | + captcha_result = JSON.parse(body) | |
56 | + captcha_result["success"] ? true : captcha_result | |
57 | + end | |
58 | + | |
59 | + # return true or a hash with the error | |
60 | + # :user_message, :status, :log_message, :javascript_console_message | |
61 | + def verify_recaptcha(client_id, token, captcha_text, verify_uri) | |
62 | + msg_icve = _('Internal captcha validation error') | |
63 | + msg_esca = 'Environment recaptcha_plugin_attributes' | |
64 | + return hash_error(msg_icve, 500, nil, "#{msg_esca} verify_uri not defined") if verify_uri.nil? | |
65 | + return hash_error(msg_icve, 500, nil, "#{msg_esca} client_id not defined") if client_id.nil? | |
66 | + return hash_error(_("Error processing token validation"), 500, nil, _("Missing Serpro's Captcha token")) unless token | |
67 | + return hash_error(_('Captcha text has not been filled'), 403) unless captcha_text | |
68 | + uri = URI(verify_uri) | |
69 | + http = Net::HTTP.new(uri.host, uri.port) | |
70 | + request = Net::HTTP::Post.new(uri.path) | |
71 | + verify_string = "#{client_id}&#{token}&#{captcha_text}" | |
72 | + request.body = verify_string | |
73 | + body = http.request(request).body | |
74 | + return true if body == '1' | |
75 | + return hash_error(_("Internal captcha validation error"), 500, body, "Unable to reach Serpro's Captcha validation service") if body == "Activity timed out" | |
76 | + return hash_error(_("Wrong captcha text, please try again"), 403) if body == '0' | |
77 | + return hash_error(_("Serpro's captcha token not found"), 500) if body == '2' | |
78 | + return hash_error(_("No data sent to validation server or other serious problem"), 500) if body == -1 | |
79 | + #Catches all errors at the end | |
80 | + return hash_error(_("Internal captcha validation error"), 500, nil, "Error validating Serpro's captcha service returned: #{body}") | |
81 | + end | |
82 | + | |
83 | + | |
84 | + | |
85 | +end | ... | ... |
1 | +++ a/test/functional/account_controller_plugin_test.rb | |
... | ... | @@ -0,0 +1,16 @@ |
1 | +# Re-raise errors caught by the controller. | |
2 | +class AccountController; def rescue_action(e) raise e end; end | |
3 | + | |
4 | +class AccountControllerPluginTest < ActionController::TestCase | |
5 | + | |
6 | + def setup | |
7 | + @controller = AccountController.new | |
8 | + @request = ActionController::TestRequest.new | |
9 | + @response = ActionController::TestResponse.new | |
10 | + | |
11 | + @environment = Environment.default | |
12 | + @environment.enabled_plugins = ['RecaptchaPlugin'] | |
13 | + @environment.save! | |
14 | + end | |
15 | + | |
16 | +end | ... | ... |
1 | +++ a/test/test_helper.rb | |
... | ... | @@ -0,0 +1,65 @@ |
1 | +require_relative "../../../lib/noosfero/api/helpers" | |
2 | + | |
3 | +class ActiveSupport::TestCase | |
4 | + | |
5 | + include Rack::Test::Methods | |
6 | + | |
7 | + def app | |
8 | + Noosfero::API::API | |
9 | + end | |
10 | + | |
11 | + def pass_captcha(mocked_url, captcha_verification_body) | |
12 | + stub_request(:post, mocked_url). | |
13 | + with(:body => captcha_verification_body, | |
14 | + :headers => {'Accept'=>'*/*', 'User-Agent'=>'Ruby'}). | |
15 | + to_return(:status => 200, :body => "1", :headers => {'Content-Length' => 1}) | |
16 | + end | |
17 | + | |
18 | + def fail_captcha_text(mocked_url, captcha_verification_body) | |
19 | + stub_request(:post, mocked_url). | |
20 | + with(:body => captcha_verification_body, | |
21 | + :headers => {'Accept'=>'*/*', 'User-Agent'=>'Ruby'}). | |
22 | + to_return(:status => 200, :body => "0", :headers => {'Content-Length' => 1}) | |
23 | + end | |
24 | + | |
25 | + def login_with_captcha | |
26 | + json = do_login_captcha_from_api | |
27 | + @private_token = json["private_token"] | |
28 | + @params = { "private_token" => @private_token} | |
29 | + json | |
30 | + end | |
31 | + | |
32 | + ## Performs a login using the session.rb but mocking the | |
33 | + ## real HTTP request to validate the captcha. | |
34 | + def do_login_captcha_from_api | |
35 | + post "/api/v1/login-captcha" | |
36 | + json = JSON.parse(last_response.body) | |
37 | + json | |
38 | + end | |
39 | + | |
40 | + def login_api | |
41 | + @environment = Environment.default | |
42 | + @user = User.create!(:login => 'testapi', :password => 'testapi', :password_confirmation => 'testapi', :email => 'test@test.org', :environment => @environment) | |
43 | + @user.activate | |
44 | + @person = @user.person | |
45 | + | |
46 | + post "/api/v1/login?login=testapi&password=testapi" | |
47 | + json = JSON.parse(last_response.body) | |
48 | + @private_token = json["private_token"] | |
49 | + unless @private_token | |
50 | + @user.generate_private_token! | |
51 | + @private_token = @user.private_token | |
52 | + end | |
53 | + | |
54 | + @params = {:private_token => @private_token} | |
55 | + end | |
56 | + attr_accessor :private_token, :user, :person, :params, :environment | |
57 | + | |
58 | + private | |
59 | + | |
60 | + def json_response_ids(kind) | |
61 | + json = JSON.parse(last_response.body) | |
62 | + json[kind.to_s].map {|c| c['id']} | |
63 | + end | |
64 | + | |
65 | +end | ... | ... |
1 | +++ a/test/unit/recaptcha_verification_test.rb | |
... | ... | @@ -0,0 +1,110 @@ |
1 | +require 'webmock' | |
2 | +include WebMock::API | |
3 | +require File.dirname(__FILE__) + '/../../../../test/test_helper' | |
4 | +require_relative '../test_helper' | |
5 | + | |
6 | +class RecaptchaVerificationTest < ActiveSupport::TestCase | |
7 | + | |
8 | + def setup | |
9 | + @environment = Environment.default | |
10 | + @environment.enabled_plugins = ['RecaptchaPlugin'] | |
11 | + @environment.recaptcha_verify_uri="http://www.google.com/validate" # do not correct! | |
12 | + @environment.recaptcha_version='2' | |
13 | + @environment.recaptcha_private_key = "private_key" | |
14 | + @environment.save! | |
15 | + @recaptcha_site_key = "64264643" | |
16 | + @captcha_text = "44641441" | |
17 | +# @captcha_verification_body = "#{@environment.recaptcha_client_id}&#{@captcha_token}&#{@captcha_text}" | |
18 | + end | |
19 | + | |
20 | + def login_with_captcha | |
21 | + store = Noosfero::API::SessionStore.create("captcha") | |
22 | + ## Initialize the data for the session store | |
23 | + store.data = [] | |
24 | + ## Put it back in cache | |
25 | + store.store | |
26 | + { "private_token" => "#{store.private_token}" } | |
27 | + end | |
28 | + | |
29 | + def create_article(name) | |
30 | + person = fast_create(Person, :environment_id => @environment.id) | |
31 | + fast_create(Article, :profile_id => person.id, :name => name) | |
32 | + end | |
33 | + | |
34 | + should 'register a user when there are no enabled captcha pluging' do | |
35 | + @environment.enabled_plugins = [] | |
36 | + @environment.save! | |
37 | + Environment.default.enable('skip_new_user_email_confirmation') | |
38 | + params = {:login => "newuserapi", :password => "newuserapi", :password_confirmation => "newuserapi", :email => "newuserapi@email.com" } | |
39 | + post "/api/v1/register?#{params.to_query}" | |
40 | + assert_equal 201, last_response.status | |
41 | + json = JSON.parse(last_response.body) | |
42 | + assert User['newuserapi'].activated? | |
43 | + assert json['user']['private_token'].present? | |
44 | + end | |
45 | + | |
46 | + should 'not register a user if captcha fails' do | |
47 | + fail_captcha_text @environment.recaptcha_verify_uri, @captcha_verification_body | |
48 | + Environment.default.enable('skip_new_user_email_confirmation') | |
49 | + params = {:login => "newuserapi", :password => "newuserapi", :password_confirmation => "newuserapi", :email => "newuserapi@email.com", :txtToken_captcha_serpro_gov_br => @captcha_token, :captcha_text => @captcha_text} | |
50 | + post "/api/v1/register?#{params.to_query}" | |
51 | + assert_equal 403, last_response.status | |
52 | + json = JSON.parse(last_response.body) | |
53 | + assert_equal json["message"], _("Wrong captcha text, please try again") | |
54 | + end | |
55 | + | |
56 | + should 'verify_recaptcha' do | |
57 | + pass_captcha @environment.recaptcha_verify_uri, @captcha_verification_body | |
58 | + scv = RecaptchaVerification.new | |
59 | + assert scv.verify_recaptcha(@environment.recaptcha_client_id, @captcha_token, @captcha_text, @environment.recaptcha_verify_uri) | |
60 | + end | |
61 | + | |
62 | + should 'fail captcha if user has not filled Serpro\' captcha text' do | |
63 | + pass_captcha @environment.recaptcha_verify_uri, @captcha_verification_body | |
64 | + scv = RecaptchaVerification.new | |
65 | + hash = scv.verify_recaptcha(@environment.recaptcha_client_id, @captcha_token, nil, @environment.recaptcha_verify_uri) | |
66 | + assert hash[:user_message], _('Captcha text has not been filled') | |
67 | + end | |
68 | + | |
69 | + should 'fail captcha if Serpro\' captcha token has not been sent' do | |
70 | + pass_captcha @environment.recaptcha_verify_uri, @captcha_verification_body | |
71 | + scv = RecaptchaVerification.new | |
72 | + hash = scv.verify_recaptcha(@environment.recaptcha_client_id, nil, @captcha_text, @environment.recaptcha_verify_uri) | |
73 | + assert hash[:javascript_console_message], _("Missing Serpro's Captcha token") | |
74 | + end | |
75 | + | |
76 | + should 'fail captcha text' do | |
77 | + fail_captcha_text @environment.recaptcha_verify_uri, @captcha_verification_body | |
78 | + scv = RecaptchaVerification.new | |
79 | + hash = scv.verify_recaptcha(@environment.recaptcha_client_id, nil, @captcha_text, @environment.recaptcha_verify_uri) | |
80 | + assert hash[:javascript_console_message], _("Wrong captcha text, please try again") | |
81 | + end | |
82 | + | |
83 | + should 'not perform a vote without authentication' do | |
84 | + article = create_article('Article 1') | |
85 | + params = {} | |
86 | + params[:value] = 1 | |
87 | + | |
88 | + post "/api/v1/articles/#{article.id}/vote?#{params.to_query}" | |
89 | + json = JSON.parse(last_response.body) | |
90 | + assert_equal 401, last_response.status | |
91 | + end | |
92 | + | |
93 | + should 'perform a vote on an article identified by id' do | |
94 | + pass_captcha @environment.recaptcha_verify_uri, @captcha_verification_body | |
95 | + params = {} | |
96 | + params[:txtToken_captcha_serpro_gov_br]= @captcha_token | |
97 | + params[:captcha_text]= @captcha_text | |
98 | + post "/api/v1/login-captcha?#{params.to_query}" | |
99 | + json = JSON.parse(last_response.body) | |
100 | + article = create_article('Article 1') | |
101 | + params = {} | |
102 | + params[:private_token] = json['private_token'] | |
103 | + params[:value] = 1 | |
104 | + post "/api/v1/articles/#{article.id}/vote?#{params.to_query}" | |
105 | + json = JSON.parse(last_response.body) | |
106 | + assert_not_equal 401, last_response.status | |
107 | + assert_equal true, json['vote'] | |
108 | + end | |
109 | + | |
110 | +end | ... | ... |
1 | +++ a/views/recaptcha_plugin_admin/index.html.erb | |
... | ... | @@ -0,0 +1,32 @@ |
1 | +<h1><%= _("reCaptcha Management") %> </h1> | |
2 | + | |
3 | +<%= labelled_form_for(:environment, :url => {:action => 'update'}) do |f| %> | |
4 | + | |
5 | +<table> | |
6 | + <tr> | |
7 | + <th><%= c_('Configuration') %></th> | |
8 | + <th><%= _('Value') %></th> | |
9 | + </tr> | |
10 | + <tr> | |
11 | + <td><%= _('Version (1 or 2)') %></td> | |
12 | + <td><%= text_field :environment, :recaptcha_version %></td> | |
13 | + </tr> | |
14 | + <tr> | |
15 | + <td><%= _('Site key') %></td> | |
16 | + <td><%= text_field :environment, :recaptcha_site_key %></td> | |
17 | + </tr> | |
18 | + <tr> | |
19 | + <td><%= _('Secret key') %></td> | |
20 | + <td><%= text_field :environment, :recaptcha_private_key %></td> | |
21 | + </tr> | |
22 | +</table> | |
23 | + | |
24 | + | |
25 | +<div> | |
26 | + <% button_bar do %> | |
27 | + <%= submit_button('save', c_('Save changes')) %> | |
28 | + <%= button :back, _('Back to plugins administration panel'), :controller => 'plugins' %> | |
29 | + <% end %> | |
30 | +</div> | |
31 | + | |
32 | +<% end %> | ... | ... |