Skip to content
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Introduction

This plugin logs failed login attempts and requires users to go through
a reCAPTCHA verification process when the number of failed attempts go
a reCAPTCHA|hCaptcha verification process when the number of failed attempts go
too high. It provides protection against automated attacks.

Failed attempts are logged by IP and stored in a database table.
Expand All @@ -12,8 +12,8 @@ IPs are also released after a certain expire amount of time.

## Installation

<big>**IMPORTANT: This plugin requires reCAPTCHA API keys to work properly.**</big>
<br>These can be obtained from https://www.google.com/recaptcha.
<big>**IMPORTANT: This plugin requires reCAPTCHA|hCaptcha API keys to work properly.**</big>
<br>These can be obtained from https://www.google.com/recaptcha or https://dashboard.hcaptcha.com/.


#### With Composer
Expand Down Expand Up @@ -44,15 +44,16 @@ the table `rcguard` accordingly.

You may customize the following in the `config.inc.php` file:

- the API version: `v3`, `v2invisible` or `v2`;
- the API version: `v3`, `v2invisible`, `v2` or `v2hcaptcha`;
- the v2 widget theme: `light` or `dark`;
- the v2 widget size: `normal` or `compact`.

For more information about the widget please check the [documentation about reCAPTCHA][recaptcha-doc].
For more information about the widget please check the [documentation about reCAPTCHA][recaptcha-doc]
or [documentation about hCaptcha][hcaptcha-doc].

The plugin configuration file has several other options you may configure, please take at look.

Since May 2018, you can define a proxy (anonymous or authenticated) to request the recaptcha widget.
Since May 2018, you can define a proxy (anonymous or authenticated) to request the reCAPTCHA|hCaptcha widget.


## Supported databases
Expand All @@ -79,12 +80,13 @@ Email: [Diana Soares][email]
[email]: mailto:diana.soares@gmail.com
[dennylin]: https://github.com/dennylin93
[recaptcha-doc]: https://developers.google.com/recaptcha/intro
[hcaptcha-doc]: https://docs.hcaptcha.com/


## License

This plugin is distributed under the GPL-3.0+ license.

This plugin also contains a PHP library for reCAPTCHA that is
distributed under its own license. See the library file for the exact details.
This plugin also contains PHP libraries for reCAPTCHA and hCaptcha that is
distributed under its own license. See the library files for the exact details.

18 changes: 10 additions & 8 deletions config.inc.php.dist
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,22 @@ $config['expire_time'] = 30;
// Reset failure count after successfull login (see bratkartoffel/rcguard@670395e)
$config['rcguard_reset_after_success'] = true;

// reCAPTCHA API version and url
$config['recaptcha_api_version'] = 'v2'; // v3 | v2 | v2invisible
$config['recaptcha_api_url'] = 'https://www.google.com/recaptcha/api.js';
// reCAPTCHA|hCaptcha API version and url
$config['recaptcha_api_version'] = 'v2'; // v3 | v2 | v2invisible | v2hcaptcha
$config['recaptcha_api_url'] = 'https://www.google.com/recaptcha/api.js'; // reCAPTCHA
//$config['recaptcha_api_url'] = 'https://js.hcaptcha.com/1/api.js'; // hCaptcha

// !!! DEPRECATED - not used anymore !!!
//$config['recaptcha_api'] = 'http://www.google.com/recaptcha/api.js';
//$config['recaptcha_api_secure'] = 'https://www.google.com/recaptcha/api.js';
//$config['recaptcha_https'] = true;

// Keys can be obtained from http://www.google.com/recaptcha/
// Keys can be obtained from http://www.google.com/recaptcha/ or https://dashboard.hcaptcha.com/

// reCAPTCHA site key
// reCAPTCHA|hCaptcha site key
$config['recaptcha_publickey'] = '';

// reCAPTCHA secret key
// reCAPTCHA|hCaptcha secret key
$config['recaptcha_privatekey'] = '';

// Send client IP to Google for reCAPTCHA verification
Expand All @@ -53,8 +54,9 @@ $config['recaptcha_size'] = 'normal';
// Parameter expansion:
// %r - Remote IP
// %u - Username
$config['recaptcha_log_success'] = 'Verification succeeded for %u. [%r]';
$config['recaptcha_log_failure'] = 'Error: Verification failed for %u. [%r]';
// %v - API version
$config['recaptcha_log_success'] = 'Verification succeeded for %u. [%r] via %v';
$config['recaptcha_log_failure'] = 'Error: Verification failed for %u. [%r] via %v';
$config['recaptcha_log_unknown'] = 'Error: Unknown log type.';

// Block IPv6 clients based on prefix length
Expand Down
196 changes: 196 additions & 0 deletions lib/hcaptchalib.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<?php
/**
* This is a PHP library that handles calling hCaptcha.
* - See also
* https://docs.hcaptcha.com/
* - Get a hCaptcha Site/Secret Key
* https://dashboard.hcaptcha.com/
*
* THIS IS AN ADJUSTED VERSION SUPPORTING hCaptcha
*
* based on recaptchalib.php (php_1.1.1)
* from https://developers.google.com/recaptcha/docs/php
*
* @copyright Copyright (c) 2014, Google Inc.
* @link http://www.google.com/recaptcha
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

/**
* hCaptcha client
*/
class ReCaptcha
{
private static $version = "php_1.1.1";
private static $signupUrl = "https://dashboard.hcaptcha.com/";
private static $siteVerifyUrl = "https://hcaptcha.com/siteverify";
private $_secret;
private $_options;

/**
* Constructor.
*
* @param string $secret shared secret between site and ReCAPTCHA server.
* @param array $extra_options Extra options to pass to the stream context.
*/
function __construct($secret, $extra_options = null)
{
if (empty($secret)) {
die('To use hCaptcha you must get an API key from <a href="' .
self::$signupUrl . '">' . self::$signupUrl . '</a>');
}

$this->_secret = $secret;
$this->_options = $extra_options;
}


/**
* Submit the POST request with the specified parameters.
*
* @param array $params Request parameters
* @return string Body of the hCaptcha response
*/
private function _submit($params)
{
// PHP 5.6.0 changed the way you specify the peer name for SSL context options.
// Using "CN_name" will still work, but it will raise deprecated errors.
$peer_key = version_compare(PHP_VERSION, '5.6.0', '<') ? 'CN_name' : 'peer_name';
$options = array(
'http' => array(
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($params, '', '&'),
// Force the peer to validate (not needed in 5.6.0+, but still works)
'verify_peer' => true,
$peer_key => 'hcaptcha.com',
)
);

if ($this->_options) {
$options = self::mergeOptions($options, $this->_options);
}

// REMEMBER: this is only for this kind of RequestMethod\Post
if (isset($options['http']['proxy'])
&& strpos($options['http']['proxy'], 'tcp://') === false) {
$options['http']['proxy'] = 'tcp://' . $options['http']['proxy'];
}

$context = stream_context_create($options);
return file_get_contents(self::$siteVerifyUrl, false, $context);
}

/**
* Recursively merge options without appending values.
*
* @param array $opts1 Options array (the default options)
* @param array $opts2 Options array (the given options)
* @return array The merged options
*/
private static function mergeOptions($opts1, $opts2)
{
if (is_array($opts2)) {
foreach ($opts2 as $key => $val) {
$opts1[$key] = (
is_array($val) && isset($opts1[$key]) && is_array($opts1[$key])
? self::mergeOptions($opts1[$key], $val) : $val
);
}
}
return $opts1;
}

/**
* Calls the hCaptcha siteverify API to verify whether the user passes
* CAPTCHA test. (hCaptcha version php_1.1.1)
*
* @param string $response The value of 'h-captcha-response' in the submitted form.
* @param string $remoteIp The end user's IP address.
* @param string $sitekey assigned site key
* @return ReCaptchaResponse Response from the service.
*/
public function verify($response, $remoteIp = null, $sitekey = null)
{
if (empty($response)) { // Discard empty solution submissions
return new ReCaptchaResponse(false, array('missing-input'));
}

$params = array('secret' => $this->_secret,
'sitekey' => $sitekey,
'remoteip' => $remoteIp,
'response' => $response
);

$rawResponse = $this->_submit($params);

return ReCaptchaResponse::fromJson($rawResponse);
}
}


/**
* The response returned from the service.
*/
class ReCaptchaResponse
{
public $success;
public $errorCodes;

/**
* Constructor.
*
* @param boolean $success
* @param array $errorCodes
*/
function __construct($success, $errorCodes=array())
{
$this->success = $success;
$this->errorCodes = $errorCodes;
}

/**
* Build the response from the expected JSON returned by the service.
*
* @param string $json
* @return ReCaptchaResponse
*/
public static function fromJson($json)
{
$responseData = json_decode($json, true);

if (!$responseData) {
$reCaptchaResponse = new ReCaptchaResponse(false, array('invalid-json'));
}
else if (isset($responseData['success']) && $responseData['success'] == true) {
$reCaptchaResponse = new ReCaptchaResponse(true);
}
else if (isset($responseData['error-codes']) && is_array($responseData['error-codes'])) {
$reCaptchaResponse = new ReCaptchaResponse(false, $responseData['error-codes']);
}
else {
$reCaptchaResponse = new ReCaptchaResponse(false);
}

return $reCaptchaResponse;
}
}

?>
38 changes: 32 additions & 6 deletions rcguard.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,14 @@ public function authenticate($args)
}

$msg = 'rcguard.recaptchaempty';
$response = rcube_utils::get_input_value('g-recaptcha-response', rcube_utils::INPUT_POST);

$api_version = $this->rc->config->get('recaptcha_api_version', 'v2');
$input_value = 'g-recaptcha-response';
if ($api_version == 'v2hcaptcha') {
$input_value = 'h-captcha-response';
};

$response = rcube_utils::get_input_value($input_value, rcube_utils::INPUT_POST);

if ($response) {
if ($this->verify_recaptcha($response, $client_ip)) {
Expand Down Expand Up @@ -310,12 +317,20 @@ private function show_recaptcha_v2invisible()
private function show_recaptcha_v2($size = null)
{
$api = $this->rc->config->get('recaptcha_api_url');
$src = sprintf('%s?hl=%s', $api, $this->rc->user->language);
$lang = $this->rc->user->language;
$lang_territory_separator_pos = strpos($lang, '_');
if ($lang_territory_separator_pos > 0) {
// hCaptcha is not supporting 'territory' appendix
$lang = substr($lang, 0, $lang_territory_separator_pos);
};
$src = sprintf('%s?hl=%s', $api, $lang);
$this->include_script($src);

$api_version = $this->rc->config->get('recaptcha_api_version', 'v2');
$html = sprintf(
'<div class="g-recaptcha" ' .
'<div class="%s" ' .
'data-sitekey="%s" data-theme="%s" data-size="%s"></div>',
($api_version == 'v2hcaptcha') ? 'h-captcha' : 'g-recaptcha',
$this->rc->config->get('recaptcha_publickey'),
$this->rc->config->get('recaptcha_theme'),
$size ?: $this->rc->config->get('recaptcha_size')
Expand Down Expand Up @@ -348,10 +363,20 @@ private function verify_recaptcha($response, $client_ip = null)
}
}

require_once $this->home . '/lib/recaptchalib.php';
$api_version = $this->rc->config->get('recaptcha_api_version', 'v2');
if ($api_version == 'v2hcaptcha') {
require_once $this->home . '/lib/hcaptchalib.php';
} else {
require_once $this->home . '/lib/recaptchalib.php';
};

$reCaptcha = new ReCaptcha($config->get('recaptcha_privatekey'), $options);
$resp = $reCaptcha->verify($response, $client_ip);

if ($api_version == 'v2hcaptcha') {
$resp = $reCaptcha->verify($response, $client_ip, $config->get('recaptcha_publickey'));
} else {
$resp = $reCaptcha->verify($response, $client_ip);
};

return $resp != null && $resp->success;
}
Expand Down Expand Up @@ -379,7 +404,8 @@ private function log_recaptcha($log_type, $username)
}

if (!empty($log_entry)) {
$log_entry = str_replace(['%r', '%u'], [$client_ip, $username], $log_entry);
$api_version = $this->rc->config->get('recaptcha_api_version', 'v2');
$log_entry = str_replace(['%r', '%u', '%v'], [$client_ip, $username, $api_version], $log_entry);
rcube::write_log('rcguard', $log_entry);
}
}
Expand Down