diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..281c0c4 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +group :test do + gem 'webmock' +end diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8f3f9a --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +Sample config values: + +verify_uri 'http://captcha.servicoscorporativos.serpro.gov.br/captchavalidar/1.0.0/validar' + +serpro_client_id 'fdbcdc7a0b754ee7ae9d865fda740f17' + +See http://stdcs.supst.serpro/manual/html/#captcha/page/introducao.html for more details. diff --git a/controllers/recaptcha_plugin_admin_controller.rb b/controllers/recaptcha_plugin_admin_controller.rb new file mode 100644 index 0000000..fba6e1d --- /dev/null +++ b/controllers/recaptcha_plugin_admin_controller.rb @@ -0,0 +1,17 @@ +class RecaptchaPluginAdminController < PluginAdminController + + append_view_path File.join(File.dirname(__FILE__) + '/../views') + + def index + end + + def update + if @environment.update_attributes(params[:environment]) + session[:notice] = _('Captcha configuration updated successfully.') + else + session[:notice] = _('Captcha configuration could not be saved.') + end + render :action => 'index' + end + +end diff --git a/lib/ext/environment.rb b/lib/ext/environment.rb new file mode 100644 index 0000000..9e82724 --- /dev/null +++ b/lib/ext/environment.rb @@ -0,0 +1,49 @@ +require_dependency 'environment' + +class Environment + + #reCAPTCHA settings + settings_items :recaptcha_plugin, :type => ActiveSupport::HashWithIndifferentAccess, :default => {} + attr_accessible :recaptcha_plugin_attributes, :recaptcha_version, :recaptcha_private_key, :recaptcha_site_key + + def recaptcha_plugin_attributes + self.recaptcha_plugin || {} + end + + def recaptcha_version= recaptcha_version + self.recaptcha_plugin = {} if self.recaptcha_plugin.blank? + self.recaptcha_plugin['recaptcha_version'] = recaptcha_version + end + + def recaptcha_version + self.recaptcha_plugin['recaptcha_version'] + end + + def recaptcha_private_key= recaptcha_private_key + self.recaptcha_plugin = {} if self.recaptcha_plugin.blank? + self.recaptcha_plugin['recaptcha_private_key'] = recaptcha_private_key + end + + def recaptcha_private_key + self.recaptcha_plugin['recaptcha_private_key'] + end + + def recaptcha_verify_uri= recaptcha_verify_uri + self.recaptcha_plugin = {} if self.recaptcha_plugin.blank? + self.recaptcha_plugin['recaptcha_verify_uri'] = recaptcha_verify_uri + end + + def recaptcha_verify_uri + self.recaptcha_plugin['recaptcha_verify_uri'] + end + + def recaptcha_site_key= recaptcha_site_key + self.recaptcha_plugin = {} if self.recaptcha_plugin.blank? + self.recaptcha_plugin['recaptcha_site_key'] = recaptcha_site_key + end + + def recaptcha_site_key + self.recaptcha_plugin['recaptcha_site_key'] + end + +end diff --git a/lib/recaptcha_plugin.rb b/lib/recaptcha_plugin.rb new file mode 100644 index 0000000..221ca1f --- /dev/null +++ b/lib/recaptcha_plugin.rb @@ -0,0 +1,41 @@ +class RecaptchaPlugin < Noosfero::Plugin + + def self.plugin_name + _('Google reCAPTCHA plugin') + end + + def self.plugin_description + _("Provides a plugin to Google reCAPTCHA.") + end + + def self.api_mount_points + [RecaptchaPlugin::API ] + end + + def test_captcha(*args) + remote_ip = args[0] + params = args[1] + environment = args[2] + + private_key = environment.recaptcha_private_key + version = environment.recaptcha_version + + msg_icve = _('Internal captcha validation error') + msg_esca = 'Environment recaptcha_plugin_attributes' + + return RecaptchaVerification.hash_error(msg_icve, s, nil, "#{msg_eacs} private_key not defined") if private_key.nil? + return RecaptchaVerification.hash_error(msg_icve, s, nil, "#{msg_eacs} version not defined") unless version == 1 || version == 2 + + rv = RecaptchaVerification.new + + if version == 1 + verify_uri = 'https://www.google.com/recaptcha/api/verify' + return rv.verify_recaptcha_v1(remote_ip, private_key, verify_uri, params[:recaptcha_challenge_field], params[:recaptcha_response_field]) + end + if version == 2 + verify_uri = 'https://www.google.com/recaptcha/api/siteverify' + return rv.verify_recaptcha_v2(remote_ip, private_key, verify_uri, params[:g_recaptcha_response]) + end + end + +end diff --git a/lib/recaptcha_verification.rb b/lib/recaptcha_verification.rb new file mode 100644 index 0000000..86970cc --- /dev/null +++ b/lib/recaptcha_verification.rb @@ -0,0 +1,85 @@ +class RecaptchaVerification + + def self.hash_error(user_message, status, log_message=nil, javascript_console_message=nil) + {user_message: user_message, status: status, log_message: log_message, javascript_console_message: javascript_console_message} + end + + # return true or a hash with the error + # :user_message, :status, :log_message, :javascript_console_message + def verify_recaptcha_v1(remote_ip, private_key, api_recaptcha_verify_uri, recaptcha_challenge_field, recaptcha_response_field) + if recaptcha_challenge_field == nil || recaptcha_response_field == nil + return render_api_error!(_('Captcha validation error'), 500, nil, _('Missing captcha data')) + end + + verify_hash = { + "privatekey" => private_key, + "remoteip" => remote_ip, + "challenge" => recaptcha_challenge_field, + "response" => recaptcha_response_field + } + uri = URI(api_recaptcha_verify_uri) + https = Net::HTTP.new(uri.host, uri.port) + https.use_ssl = true + request = Net::HTTP::Post.new(uri.path) + request.set_form_data(verify_hash) + begin + result = https.request(request).body.split("\n") + rescue Exception => e + return render_api_error!(_('Internal captcha validation error'), 500, nil, "Error validating Googles' recaptcha version 1: #{e.message}") + end + return true if result[0] == "true" + 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" + #Catches all errors at the end + return render_api_error!(_("Internal recaptcha validation error"), 500, nil, "Error validating Googles' recaptcha version 1: #{result[1]}") + end + + # return true or a hash with the error + # :user_message, :status, :log_message, :javascript_console_message + def verify_recaptcha_v2(remote_ip, private_key, api_recaptcha_verify_uri, g_recaptcha_response) + return render_api_error!(_('Captcha validation error'), 500, nil, _('Missing captcha data')) if g_recaptcha_response == nil + verify_hash = { + "secret" => private_key, + "remoteip" => remote_ip, + "response" => g_recaptcha_response + } + uri = URI(api_recaptcha_verify_uri) + https = Net::HTTP.new(uri.host, uri.port) + https.use_ssl = true + request = Net::HTTP::Post.new(uri.path) + request.set_form_data(verify_hash) + begin + body = https.request(request).body + rescue Exception => e + return render_api_error!(_('Internal captcha validation error'), 500, nil, "recaptcha error: #{e.message}") + end + captcha_result = JSON.parse(body) + captcha_result["success"] ? true : captcha_result + end + + # return true or a hash with the error + # :user_message, :status, :log_message, :javascript_console_message + def verify_recaptcha(client_id, token, captcha_text, verify_uri) + msg_icve = _('Internal captcha validation error') + msg_esca = 'Environment recaptcha_plugin_attributes' + return hash_error(msg_icve, 500, nil, "#{msg_esca} verify_uri not defined") if verify_uri.nil? + return hash_error(msg_icve, 500, nil, "#{msg_esca} client_id not defined") if client_id.nil? + return hash_error(_("Error processing token validation"), 500, nil, _("Missing Serpro's Captcha token")) unless token + return hash_error(_('Captcha text has not been filled'), 403) unless captcha_text + uri = URI(verify_uri) + http = Net::HTTP.new(uri.host, uri.port) + request = Net::HTTP::Post.new(uri.path) + verify_string = "#{client_id}&#{token}&#{captcha_text}" + request.body = verify_string + body = http.request(request).body + return true if body == '1' + return hash_error(_("Internal captcha validation error"), 500, body, "Unable to reach Serpro's Captcha validation service") if body == "Activity timed out" + return hash_error(_("Wrong captcha text, please try again"), 403) if body == '0' + return hash_error(_("Serpro's captcha token not found"), 500) if body == '2' + return hash_error(_("No data sent to validation server or other serious problem"), 500) if body == -1 + #Catches all errors at the end + return hash_error(_("Internal captcha validation error"), 500, nil, "Error validating Serpro's captcha service returned: #{body}") + end + + + +end diff --git a/test/functional/account_controller_plugin_test.rb b/test/functional/account_controller_plugin_test.rb new file mode 100644 index 0000000..c14aa06 --- /dev/null +++ b/test/functional/account_controller_plugin_test.rb @@ -0,0 +1,16 @@ +# Re-raise errors caught by the controller. +class AccountController; def rescue_action(e) raise e end; end + +class AccountControllerPluginTest < ActionController::TestCase + + def setup + @controller = AccountController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + + @environment = Environment.default + @environment.enabled_plugins = ['RecaptchaPlugin'] + @environment.save! + end + +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..a3438e7 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,65 @@ +require_relative "../../../lib/noosfero/api/helpers" + +class ActiveSupport::TestCase + + include Rack::Test::Methods + + def app + Noosfero::API::API + end + + def pass_captcha(mocked_url, captcha_verification_body) + stub_request(:post, mocked_url). + with(:body => captcha_verification_body, + :headers => {'Accept'=>'*/*', 'User-Agent'=>'Ruby'}). + to_return(:status => 200, :body => "1", :headers => {'Content-Length' => 1}) + end + + def fail_captcha_text(mocked_url, captcha_verification_body) + stub_request(:post, mocked_url). + with(:body => captcha_verification_body, + :headers => {'Accept'=>'*/*', 'User-Agent'=>'Ruby'}). + to_return(:status => 200, :body => "0", :headers => {'Content-Length' => 1}) + end + + def login_with_captcha + json = do_login_captcha_from_api + @private_token = json["private_token"] + @params = { "private_token" => @private_token} + json + end + + ## Performs a login using the session.rb but mocking the + ## real HTTP request to validate the captcha. + def do_login_captcha_from_api + post "/api/v1/login-captcha" + json = JSON.parse(last_response.body) + json + end + + def login_api + @environment = Environment.default + @user = User.create!(:login => 'testapi', :password => 'testapi', :password_confirmation => 'testapi', :email => 'test@test.org', :environment => @environment) + @user.activate + @person = @user.person + + post "/api/v1/login?login=testapi&password=testapi" + json = JSON.parse(last_response.body) + @private_token = json["private_token"] + unless @private_token + @user.generate_private_token! + @private_token = @user.private_token + end + + @params = {:private_token => @private_token} + end + attr_accessor :private_token, :user, :person, :params, :environment + + private + + def json_response_ids(kind) + json = JSON.parse(last_response.body) + json[kind.to_s].map {|c| c['id']} + end + +end diff --git a/test/unit/recaptcha_verification_test.rb b/test/unit/recaptcha_verification_test.rb new file mode 100644 index 0000000..863885f --- /dev/null +++ b/test/unit/recaptcha_verification_test.rb @@ -0,0 +1,110 @@ +require 'webmock' +include WebMock::API +require File.dirname(__FILE__) + '/../../../../test/test_helper' +require_relative '../test_helper' + +class RecaptchaVerificationTest < ActiveSupport::TestCase + + def setup + @environment = Environment.default + @environment.enabled_plugins = ['RecaptchaPlugin'] + @environment.recaptcha_verify_uri="http://www.google.com/validate" # do not correct! + @environment.recaptcha_version='2' + @environment.recaptcha_private_key = "private_key" + @environment.save! + @recaptcha_site_key = "64264643" + @captcha_text = "44641441" +# @captcha_verification_body = "#{@environment.recaptcha_client_id}&#{@captcha_token}&#{@captcha_text}" + end + + def login_with_captcha + store = Noosfero::API::SessionStore.create("captcha") + ## Initialize the data for the session store + store.data = [] + ## Put it back in cache + store.store + { "private_token" => "#{store.private_token}" } + end + + def create_article(name) + person = fast_create(Person, :environment_id => @environment.id) + fast_create(Article, :profile_id => person.id, :name => name) + end + + should 'register a user when there are no enabled captcha pluging' do + @environment.enabled_plugins = [] + @environment.save! + Environment.default.enable('skip_new_user_email_confirmation') + params = {:login => "newuserapi", :password => "newuserapi", :password_confirmation => "newuserapi", :email => "newuserapi@email.com" } + post "/api/v1/register?#{params.to_query}" + assert_equal 201, last_response.status + json = JSON.parse(last_response.body) + assert User['newuserapi'].activated? + assert json['user']['private_token'].present? + end + + should 'not register a user if captcha fails' do + fail_captcha_text @environment.recaptcha_verify_uri, @captcha_verification_body + Environment.default.enable('skip_new_user_email_confirmation') + params = {:login => "newuserapi", :password => "newuserapi", :password_confirmation => "newuserapi", :email => "newuserapi@email.com", :txtToken_captcha_serpro_gov_br => @captcha_token, :captcha_text => @captcha_text} + post "/api/v1/register?#{params.to_query}" + assert_equal 403, last_response.status + json = JSON.parse(last_response.body) + assert_equal json["message"], _("Wrong captcha text, please try again") + end + + should 'verify_recaptcha' do + pass_captcha @environment.recaptcha_verify_uri, @captcha_verification_body + scv = RecaptchaVerification.new + assert scv.verify_recaptcha(@environment.recaptcha_client_id, @captcha_token, @captcha_text, @environment.recaptcha_verify_uri) + end + + should 'fail captcha if user has not filled Serpro\' captcha text' do + pass_captcha @environment.recaptcha_verify_uri, @captcha_verification_body + scv = RecaptchaVerification.new + hash = scv.verify_recaptcha(@environment.recaptcha_client_id, @captcha_token, nil, @environment.recaptcha_verify_uri) + assert hash[:user_message], _('Captcha text has not been filled') + end + + should 'fail captcha if Serpro\' captcha token has not been sent' do + pass_captcha @environment.recaptcha_verify_uri, @captcha_verification_body + scv = RecaptchaVerification.new + hash = scv.verify_recaptcha(@environment.recaptcha_client_id, nil, @captcha_text, @environment.recaptcha_verify_uri) + assert hash[:javascript_console_message], _("Missing Serpro's Captcha token") + end + + should 'fail captcha text' do + fail_captcha_text @environment.recaptcha_verify_uri, @captcha_verification_body + scv = RecaptchaVerification.new + hash = scv.verify_recaptcha(@environment.recaptcha_client_id, nil, @captcha_text, @environment.recaptcha_verify_uri) + assert hash[:javascript_console_message], _("Wrong captcha text, please try again") + end + + should 'not perform a vote without authentication' do + article = create_article('Article 1') + params = {} + params[:value] = 1 + + post "/api/v1/articles/#{article.id}/vote?#{params.to_query}" + json = JSON.parse(last_response.body) + assert_equal 401, last_response.status + end + + should 'perform a vote on an article identified by id' do + pass_captcha @environment.recaptcha_verify_uri, @captcha_verification_body + params = {} + params[:txtToken_captcha_serpro_gov_br]= @captcha_token + params[:captcha_text]= @captcha_text + post "/api/v1/login-captcha?#{params.to_query}" + json = JSON.parse(last_response.body) + article = create_article('Article 1') + params = {} + params[:private_token] = json['private_token'] + params[:value] = 1 + post "/api/v1/articles/#{article.id}/vote?#{params.to_query}" + json = JSON.parse(last_response.body) + assert_not_equal 401, last_response.status + assert_equal true, json['vote'] + end + +end diff --git a/views/recaptcha_plugin_admin/index.html.erb b/views/recaptcha_plugin_admin/index.html.erb new file mode 100644 index 0000000..e6888dc --- /dev/null +++ b/views/recaptcha_plugin_admin/index.html.erb @@ -0,0 +1,32 @@ +

<%= _("reCaptcha Management") %>

+ +<%= labelled_form_for(:environment, :url => {:action => 'update'}) do |f| %> + + + + + + + + + + + + + + + + + + +
<%= c_('Configuration') %><%= _('Value') %>
<%= _('Version (1 or 2)') %><%= text_field :environment, :recaptcha_version %>
<%= _('Site key') %><%= text_field :environment, :recaptcha_site_key %>
<%= _('Secret key') %><%= text_field :environment, :recaptcha_private_key %>
+ + +
+ <% button_bar do %> + <%= submit_button('save', c_('Save changes')) %> + <%= button :back, _('Back to plugins administration panel'), :controller => 'plugins' %> + <% end %> +
+ +<% end %> -- libgit2 0.21.2