From c78f4c0b7684b81c6b273911f6b7847e2dbe19e2 Mon Sep 17 00:00:00 2001 From: Manuel Delgado Date: Thu, 4 Dec 2014 17:26:00 -0600 Subject: [PATCH 01/12] Adds support for "No CAPTCHA reCAPTCHA" (APIv2) --- lib/recaptchalib.php | 331 +++++++++++--------------------------- rcguard.php | 30 ++-- skins/default/rcguard.css | 6 +- 3 files changed, 115 insertions(+), 252 deletions(-) diff --git a/lib/recaptchalib.php b/lib/recaptchalib.php index 32c4f4d..ae467a2 100644 --- a/lib/recaptchalib.php +++ b/lib/recaptchalib.php @@ -1,17 +1,15 @@ $value ) - $req .= $key . '=' . urlencode( stripslashes($value) ) . '&'; - - // Cut the last '&' - $req=substr($req,0,strlen($req)-1); - return $req; -} - - - -/** - * Submits an HTTP POST to a reCAPTCHA server - * @param string $host - * @param string $path - * @param array $data - * @param int port - * @return array response + * A ReCaptchaResponse is returned from checkAnswer(). */ -function _recaptcha_http_post($host, $path, $data, $port = 80) { - - $req = _recaptcha_qsencode ($data); - - $http_request = "POST $path HTTP/1.0\r\n"; - $http_request .= "Host: $host\r\n"; - $http_request .= "Content-Type: application/x-www-form-urlencoded;\r\n"; - $http_request .= "Content-Length: " . strlen($req) . "\r\n"; - $http_request .= "User-Agent: reCAPTCHA/PHP\r\n"; - $http_request .= "\r\n"; - $http_request .= $req; - - $response = ''; - if( false == ( $fs = @fsockopen($host, $port, $errno, $errstr, 10) ) ) { - die ('Could not open socket'); - } - - fwrite($fs, $http_request); - - while ( !feof($fs) ) - $response .= fgets($fs, 1160); // One TCP-IP packet - fclose($fs); - $response = explode("\r\n\r\n", $response, 2); - - return $response; +class ReCaptchaResponse +{ + public $success; + public $errorCodes; } - - -/** - * Gets the challenge HTML (javascript and non-javascript version). - * This is called from the browser, and the resulting reCAPTCHA HTML widget - * is embedded within the HTML form it was called from. - * @param string $pubkey A public key for reCAPTCHA - * @param string $error The error given by reCAPTCHA (optional, default is null) - * @param boolean $use_ssl Should the request be made over ssl? (optional, default is false) - - * @return string - The HTML to be embedded in the user's form. - */ -function recaptcha_get_html ($pubkey, $error = null, $use_ssl = false) +class ReCaptcha { - if ($pubkey == null || $pubkey == '') { - die ("To use reCAPTCHA you must get an API key from https://www.google.com/recaptcha/admin/create"); - } - - if ($use_ssl) { - $server = RECAPTCHA_API_SECURE_SERVER; - } else { - $server = RECAPTCHA_API_SERVER; + private static $_signupUrl = "https://www.google.com/recaptcha/admin"; + private static $_siteVerifyUrl = + "https://www.google.com/recaptcha/api/siteverify?"; + private $_secret; + private static $_version = "php_1.0"; + + /** + * Constructor. + * + * @param string $secret shared secret between site and ReCAPTCHA server. + */ + function ReCaptcha($secret) + { + if ($secret == null || $secret == "") { + die("To use reCAPTCHA you must get an API key from " . self::$_signupUrl . ""); } - - $errorpart = ""; - if ($error) { - $errorpart = "&error=" . $error; + $this->_secret=$secret; + } + + /** + * Encodes the given data into a query string format. + * + * @param array $data array of string elements to be encoded. + * + * @return string - encoded request. + */ + private function _encodeQS($data) + { + $req = ""; + foreach ($data as $key => $value) { + $req .= $key . '=' . urlencode(stripslashes($value)) . '&'; } - return ' - - '; -} - - - - -/** - * A ReCaptchaResponse is returned from recaptcha_check_answer() - */ -class ReCaptchaResponse { - var $is_valid; - var $error; -} - -/** - * Calls an HTTP POST function to verify if the user's guess was correct - * @param string $privkey - * @param string $remoteip - * @param string $challenge - * @param string $response - * @param array $extra_params an array of extra variables to post to the server - * @return ReCaptchaResponse - */ -function recaptcha_check_answer ($privkey, $remoteip, $challenge, $response, $extra_params = array()) -{ - if ($privkey == null || $privkey == '') { - die ("To use reCAPTCHA you must get an API key from https://www.google.com/recaptcha/admin/create"); - } - - if ($remoteip == null || $remoteip == '') { - die ("For security reasons, you must pass the remote ip to reCAPTCHA"); - } - - - - //discard spam submissions - if ($challenge == null || strlen($challenge) == 0 || $response == null || strlen($response) == 0) { - $recaptcha_response = new ReCaptchaResponse(); - $recaptcha_response->is_valid = false; - $recaptcha_response->error = 'incorrect-captcha-sol'; - return $recaptcha_response; + // Cut the last '&' + $req=substr($req, 0, strlen($req)-1); + return $req; + } + + /** + * Submits an HTTP GET to a reCAPTCHA server. + * + * @param string $path url path to recaptcha server. + * @param array $data array of parameters to be sent. + * + * @return array response + */ + private function _submitHTTPGet($path, $data) + { + $req = $this->_encodeQS($data); + $response = file_get_contents($path . $req); + return $response; + } + + /** + * Calls the reCAPTCHA siteverify API to verify whether the user passes + * CAPTCHA test. + * + * @param string $remoteIp IP address of end user. + * @param string $response response string from recaptcha verification. + * + * @return ReCaptchaResponse + */ + public function verifyResponse($remoteIp, $response) + { + // Discard empty solution submissions + if ($response == null || strlen($response) == 0) { + $recaptchaResponse = new ReCaptchaResponse(); + $recaptchaResponse->success = false; + $recaptchaResponse->errorCodes = 'missing-input'; + return $recaptchaResponse; } - $response = _recaptcha_http_post (RECAPTCHA_VERIFY_SERVER, "/recaptcha/api/verify", - array ( - 'privatekey' => $privkey, - 'remoteip' => $remoteip, - 'challenge' => $challenge, - 'response' => $response - ) + $extra_params - ); - - $answers = explode ("\n", $response [1]); - $recaptcha_response = new ReCaptchaResponse(); - - if (trim ($answers [0]) == 'true') { - $recaptcha_response->is_valid = true; - } - else { - $recaptcha_response->is_valid = false; - $recaptcha_response->error = $answers [1]; + $getResponse = $this->_submitHttpGet( + self::$_siteVerifyUrl, + array ( + 'secret' => $this->_secret, + 'remoteip' => $remoteIp, + 'v' => self::$_version, + 'response' => $response + ) + ); + $answers = json_decode($getResponse, true); + $recaptchaResponse = new ReCaptchaResponse(); + + if (trim($answers ['success']) == true) { + $recaptchaResponse->success = true; + } else { + $recaptchaResponse->success = false; + $recaptchaResponse->errorCodes = $answers [error-codes]; } - return $recaptcha_response; - -} - -/** - * gets a URL where the user can sign up for reCAPTCHA. If your application - * has a configuration page where you enter a key, you should provide a link - * using this function. - * @param string $domain The domain where the page is hosted - * @param string $appname The name of your application - */ -function recaptcha_get_signup_url ($domain = null, $appname = null) { - return "https://www.google.com/recaptcha/admin/create?" . _recaptcha_qsencode (array ('domains' => $domain, 'app' => $appname)); -} -function _recaptcha_aes_pad($val) { - $block_size = 16; - $numpad = $block_size - (strlen ($val) % $block_size); - return str_pad($val, strlen ($val) + $numpad, chr($numpad)); + return $recaptchaResponse; + } } -/* Mailhide related code */ - -function _recaptcha_aes_encrypt($val,$ky) { - if (! function_exists ("mcrypt_encrypt")) { - die ("To use reCAPTCHA Mailhide, you need to have the mcrypt php module installed."); - } - $mode=MCRYPT_MODE_CBC; - $enc=MCRYPT_RIJNDAEL_128; - $val=_recaptcha_aes_pad($val); - return mcrypt_encrypt($enc, $ky, $val, $mode, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"); -} - - -function _recaptcha_mailhide_urlbase64 ($x) { - return strtr(base64_encode ($x), '+/', '-_'); -} - -/* gets the reCAPTCHA Mailhide url for a given email, public key and private key */ -function recaptcha_mailhide_url($pubkey, $privkey, $email) { - if ($pubkey == '' || $pubkey == null || $privkey == "" || $privkey == null) { - die ("To use reCAPTCHA Mailhide, you have to sign up for a public and private key, " . - "you can do so at http://www.google.com/recaptcha/mailhide/apikey"); - } - - - $ky = pack('H*', $privkey); - $cryptmail = _recaptcha_aes_encrypt ($email, $ky); - - return "http://www.google.com/recaptcha/mailhide/d?k=" . $pubkey . "&c=" . _recaptcha_mailhide_urlbase64 ($cryptmail); -} - -/** - * gets the parts of the email to expose to the user. - * eg, given johndoe@example,com return ["john", "example.com"]. - * the email is then displayed as john...@example.com - */ -function _recaptcha_mailhide_email_parts ($email) { - $arr = preg_split("/@/", $email ); - - if (strlen ($arr[0]) <= 4) { - $arr[0] = substr ($arr[0], 0, 1); - } else if (strlen ($arr[0]) <= 6) { - $arr[0] = substr ($arr[0], 0, 3); - } else { - $arr[0] = substr ($arr[0], 0, 4); - } - return $arr; -} - -/** - * Gets html to display an email address given a public an private key. - * to get a key, go to: - * - * http://www.google.com/recaptcha/mailhide/apikey - */ -function recaptcha_mailhide_html($pubkey, $privkey, $email) { - $emailparts = _recaptcha_mailhide_email_parts ($email); - $url = recaptcha_mailhide_url ($pubkey, $privkey, $email); - - return htmlentities($emailparts[0]) . "...@" . htmlentities ($emailparts [1]); - -} - - ?> diff --git a/rcguard.php b/rcguard.php index 70a42f9..ae90221 100644 --- a/rcguard.php +++ b/rcguard.php @@ -88,9 +88,8 @@ function authenticate($args) } } - if (($challenge = $_POST['recaptcha_challenge_field']) - && ($response = $_POST['recaptcha_response_field'])) { - if ($this->verify_recaptcha($client_ip, $challenge, $response)) { + if ($response = $_POST['g-recaptcha-response']) { + if ($this->verify_recaptcha($client_ip, $response)) { $this->log_recaptcha(RCGUARD_RECAPTCHA_SUCCESS, $args['user']); return $args; @@ -210,24 +209,24 @@ private function show_recaptcha($loginform) { $this->load_config(); $rcmail = rcmail::get_instance(); - $recaptcha_api = 'http://www.google.com/recaptcha/api'; - $recaptcha_api_secure = 'https://www.google.com/recaptcha/api'; - + $recaptcha_api = 'http://www.google.com/recaptcha/api.js'; + $recaptcha_api_secure = 'https://www.google.com/recaptcha/api.js'; + $skin_path = $this->local_skin_path(); $this->include_stylesheet($skin_path . '/rcguard.css'); $this->include_script('rcguard.js'); - $src = sprintf("%s/challenge?k=%s", + $src = sprintf("%s?hl=%s", $rcmail->config->get('recaptcha_https') || $_SERVER['HTTPS'] ? - $recaptcha_api_secure : $recaptcha_api, - $rcmail->config->get('recaptcha_publickey')); + $recaptcha_api_secure : $recaptcha_api, + $rcmail->user->language); $script = html::tag('script', array('type' => "text/javascript", 'src' => $src)); + $this->include_script($src); $tmp = $loginform['content']; $tmp = str_ireplace('', - '' . $script . ' - + '
', $tmp); $loginform['content'] = $tmp; @@ -235,7 +234,7 @@ private function show_recaptcha($loginform) return $loginform; } - private function verify_recaptcha($client_ip, $challenge, $response) + private function verify_recaptcha($client_ip, $response) { $this->load_config(); $rcmail = rcmail::get_instance(); @@ -244,10 +243,11 @@ private function verify_recaptcha($client_ip, $challenge, $response) require_once($this->home . '/lib/recaptchalib.php'); $resp = null; $error = null; + + $reCaptcha = new ReCaptcha($privatekey); + $resp = $reCaptcha->verifyResponse($client_ip, $response); - $resp = recaptcha_check_answer($privatekey, $client_ip, $challenge, $response); - - if ($resp->is_valid) + if ($resp != null && $resp->success) return true; else return false; diff --git a/skins/default/rcguard.css b/skins/default/rcguard.css index 5bf3edc..6a6b1ce 100644 --- a/skins/default/rcguard.css +++ b/skins/default/rcguard.css @@ -4,7 +4,7 @@ background-size: cover; } -#recaptcha_area { - margin: 0 auto; - padding-left: 24px; +.g-recaptcha div div { + margin: 10px auto 0; + padding-left: 24px; } From 49ea9b27ceb4b377da8fad7ef45408d9314bbf7a Mon Sep 17 00:00:00 2001 From: Diana Soares Date: Wed, 13 May 2015 14:49:28 +0100 Subject: [PATCH 02/12] Always include the CSS from "default" skin. --- rcguard.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rcguard.php b/rcguard.php index bd570b7..5ba74b5 100644 --- a/rcguard.php +++ b/rcguard.php @@ -212,7 +212,7 @@ private function show_recaptcha($loginform) $recaptcha_api = 'http://www.google.com/recaptcha/api'; $recaptcha_api_secure = 'https://www.google.com/recaptcha/api'; - $skin_path = $this->local_skin_path(); + $skin_path = 'skins/default'; //$this->local_skin_path(); $this->include_stylesheet($skin_path . '/rcguard.css'); $this->include_script('rcguard.js'); From 03c0fd09de5bed381eda7ee5d94926dc47177202 Mon Sep 17 00:00:00 2001 From: Diana Soares Date: Wed, 13 May 2015 14:50:40 +0100 Subject: [PATCH 03/12] Add a composer.json file. --- composer.json | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 composer.json diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a4177ab --- /dev/null +++ b/composer.json @@ -0,0 +1,31 @@ +{ + "name": "dsoares/rcguard", + "description": "Roundcube plugin that enforces reCAPTCHA for users that have too many failed logins", + "keywords": ["mail","security","captcha"], + "homepage": "https://github.com/dennylin93/rcguard", + "license": "GPL-3.0", + "type": "roundcube-plugin", + "version": "0.6-git", + "authors": [ + { + "name": "Denny Lin", + "homepage": "https://github.com/dennylin93", + "role": "Developer" + } + ], + "repositories": [ + { + "type": "composer", + "url": "http://plugins.roundcube.net" + } + ], + "require": { + "php": ">=5.2.1", + "roundcube/plugin-installer": ">=0.1.2" + }, + "extra": { + "roundcube": { + "min-version": "1.0" + } + } +} From fec58dc7e0337d5026e22d3ba2dc4d000a0ce016 Mon Sep 17 00:00:00 2001 From: Diana Soares Date: Wed, 13 May 2015 15:56:24 +0100 Subject: [PATCH 04/12] Corrected the name to the real author name. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a4177ab..73e00b9 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "dsoares/rcguard", + "name": "dennylin93/rcguard", "description": "Roundcube plugin that enforces reCAPTCHA for users that have too many failed logins", "keywords": ["mail","security","captcha"], "homepage": "https://github.com/dennylin93/rcguard", From 464c733f0aee06bb8b2006a171d7efbd0908a380 Mon Sep 17 00:00:00 2001 From: Diana Soares Date: Tue, 30 Jun 2015 10:08:26 +0100 Subject: [PATCH 05/12] Use Roundcube API to get the client IP. --- rcguard.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/rcguard.php b/rcguard.php index 5ba74b5..27da01a 100644 --- a/rcguard.php +++ b/rcguard.php @@ -48,7 +48,7 @@ function loginform($loginform) { $this->load_config(); $rcmail = rcmail::get_instance(); - $client_ip = $_SERVER['REMOTE_ADDR']; + $client_ip = $this->_get_client_ip(); $query = $rcmail->db->query( "SELECT " . $this->unixtimestamp('last') . " AS last, " . $this->unixtimestamp('NOW()') . " as time @@ -69,7 +69,7 @@ function authenticate($args) $this->load_config(); $this->add_texts('localization/'); $rcmail = rcmail::get_instance(); - $client_ip = $_SERVER['REMOTE_ADDR']; + $client_ip = $this->_get_client_ip(); $query = $rcmail->db->query( "SELECT ip @@ -118,7 +118,7 @@ function authenticate($args) function login_after($args) { - $client_ip = $_SERVER['REMOTE_ADDR']; + $client_ip = $this->_get_client_ip(); $this->delete_rcguard('', $client_ip, true); @@ -129,7 +129,7 @@ function login_failed($args) { $rcmail = rcmail::get_instance(); - $client_ip = $_SERVER['REMOTE_ADDR']; + $client_ip = $this->_get_client_ip(); $query = $rcmail->db->query( "SELECT hits @@ -256,7 +256,7 @@ private function log_recaptcha($log_type, $username) { $this->load_config(); $rcmail = rcmail::get_instance(); - $client_ip = $_SERVER['REMOTE_ADDR']; + $client_ip = $this->_get_client_ip(); $username = (empty($username)) ? 'empty username' : $username; if (!$rcmail->config->get('recaptcha_log')) @@ -314,6 +314,11 @@ private function pl_authenticate($args) { return $args; } + + private function _get_client_ip() + { + return rcube_utils::remote_addr(); + } } ?> From 916bec781fe5da9c362065cc2f20c94276168335 Mon Sep 17 00:00:00 2001 From: Diana Soares Date: Tue, 30 Jun 2015 10:36:32 +0100 Subject: [PATCH 06/12] Update composer.json. --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 73e00b9..c199d0e 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { - "name": "dennylin93/rcguard", + "name": "dsoares/rcguard", "description": "Roundcube plugin that enforces reCAPTCHA for users that have too many failed logins", "keywords": ["mail","security","captcha"], - "homepage": "https://github.com/dennylin93/rcguard", + "homepage": "https://github.com/dsoares/rcguard", "license": "GPL-3.0", "type": "roundcube-plugin", "version": "0.6-git", From d68810caddef4eb81df6b31af27049013f311440 Mon Sep 17 00:00:00 2001 From: Diana Soares Date: Tue, 30 Jun 2015 11:14:17 +0100 Subject: [PATCH 07/12] Remove extra whitespace. Check if skin path exists, otherwise defaults to "default". --- rcguard.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rcguard.php b/rcguard.php index e1c9a7e..b869b55 100644 --- a/rcguard.php +++ b/rcguard.php @@ -210,14 +210,15 @@ private function show_recaptcha($loginform) $rcmail = rcmail::get_instance(); $recaptcha_api = 'http://www.google.com/recaptcha/api.js'; $recaptcha_api_secure = 'https://www.google.com/recaptcha/api.js'; - + $skin_path = $this->local_skin_path(); + if (!file_exists(INSTALL_PATH . '/plugins/rcguard/'.$skin_path)) { $skin_path = 'skins/default'; } $this->include_stylesheet($skin_path . '/rcguard.css'); $this->include_script('rcguard.js'); $src = sprintf("%s?hl=%s", $rcmail->config->get('recaptcha_https') || $_SERVER['HTTPS'] ? - $recaptcha_api_secure : $recaptcha_api, + $recaptcha_api_secure : $recaptcha_api, $rcmail->user->language); $script = html::tag('script', array('type' => "text/javascript", 'src' => $src)); @@ -242,7 +243,7 @@ private function verify_recaptcha($client_ip, $response) require_once($this->home . '/lib/recaptchalib.php'); $resp = null; $error = null; - + $reCaptcha = new ReCaptcha($privatekey); $resp = $reCaptcha->verifyResponse($client_ip, $response); @@ -315,7 +316,7 @@ private function pl_authenticate($args) { return $args; } - private function _get_client_ip() + private function _get_client_ip() { return rcube_utils::remote_addr(); } From a8b6e07bd63c2b1dbde17f163cac6f2d6c89ffa6 Mon Sep 17 00:00:00 2001 From: Maxqia Date: Thu, 2 Jul 2015 16:41:39 -0700 Subject: [PATCH 08/12] Update Larry Skin for NoCapcha ReCapcha --- skins/larry/rcguard.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skins/larry/rcguard.css b/skins/larry/rcguard.css index 5bf3edc..6a6b1ce 100644 --- a/skins/larry/rcguard.css +++ b/skins/larry/rcguard.css @@ -4,7 +4,7 @@ background-size: cover; } -#recaptcha_area { - margin: 0 auto; - padding-left: 24px; +.g-recaptcha div div { + margin: 10px auto 0; + padding-left: 24px; } From e2a1d97bd9cbd15a2686fa084ac6fa2359f7a9e4 Mon Sep 17 00:00:00 2001 From: Diana Soares Date: Fri, 3 Jul 2015 12:30:14 +0100 Subject: [PATCH 09/12] Set default skin to larry; rename skin 'default' to 'classic'. Move API urls info config file. Cleaning some of the original code. IMPORTANT: You must set recaptcha_api* configuration settings in config.inc.php !!! See config.inc.php-dist for predefined values. --- config.inc.php.dist | 6 +- rcguard.php | 479 ++++++++++++------------- skins/{default => classic}/rcguard.css | 0 3 files changed, 226 insertions(+), 259 deletions(-) rename skins/{default => classic}/rcguard.css (100%) diff --git a/config.inc.php.dist b/config.inc.php.dist index 3e2e930..20ea0ae 100644 --- a/config.inc.php.dist +++ b/config.inc.php.dist @@ -10,10 +10,14 @@ $rcmail_config['failed_attempts'] = 5; // Release IP after how many minutes (after last failed attempt) $rcmail_config['expire_time'] = 30; +// reCAPTCHA API +$rcmail_config['recaptcha_api'] = 'http://www.google.com/recaptcha/api.js'; +$rcmail_config['recaptcha_api_secure'] = 'https://www.google.com/recaptcha/api.js'; + // Use HTTPS for reCAPTCHA $rcmail_config['recaptcha_https'] = false; -// Keys can be obtained from http://recaptcha.net/whyrecaptcha.html +// Keys can be obtained from http://www.google.com/recaptcha/ // Public key for reCAPTCHA $rcmail_config['recaptcha_publickey'] = ''; diff --git a/rcguard.php b/rcguard.php index b869b55..2a39743 100644 --- a/rcguard.php +++ b/rcguard.php @@ -36,290 +36,253 @@ class rcguard extends rcube_plugin { - function init() - { - $this->add_hook('template_object_loginform', array($this, 'loginform')); - $this->add_hook('authenticate', array($this, 'authenticate')); - $this->add_hook('login_after', array($this, 'login_after')); - $this->add_hook('login_failed', array($this, 'login_failed')); - } - - function loginform($loginform) - { - $this->load_config(); - $rcmail = rcmail::get_instance(); - $client_ip = $this->_get_client_ip(); - - $query = $rcmail->db->query( - "SELECT " . $this->unixtimestamp('last') . " AS last, " . $this->unixtimestamp('NOW()') . " as time - FROM rcguard - WHERE ip = ? AND hits >= ?", - $client_ip, $rcmail->config->get('failed_attempts')); - $result = $rcmail->db->fetch_assoc($query); - - if ((!$result || $this->delete_rcguard($result, $client_ip)) && - $rcmail->config->get('failed_attempts') != 0) - return $loginform; - - return $this->show_recaptcha($loginform); - } - - function authenticate($args) - { - $this->load_config(); - $this->add_texts('localization/'); - $rcmail = rcmail::get_instance(); - $client_ip = $this->_get_client_ip(); - - $query = $rcmail->db->query( - "SELECT ip - FROM rcguard - WHERE ip = ? AND hits >= ?", - $client_ip, $rcmail->config->get('failed_attempts')); - $result = $rcmail->db->fetch_assoc($query); - - if (!$result && $rcmail->config->get('failed_attempts') != 0) - return $args; - - if ($rcmail->config->get('pl_plugin')) { - if (!empty($_COOKIE[$rcmail->config->get('pl_cookie_name')])) { - $args = $this->pl_authenticate($args); - return $args; - } + function init() + { + $this->load_config(); + $this->add_hook('template_object_loginform', array($this, 'loginform')); + $this->add_hook('authenticate', array($this, 'authenticate')); + $this->add_hook('login_after', array($this, 'login_after')); + $this->add_hook('login_failed', array($this, 'login_failed')); } - if ($response = $_POST['g-recaptcha-response']) { - if ($this->verify_recaptcha($client_ip, $response)) { - $this->log_recaptcha(RCGUARD_RECAPTCHA_SUCCESS, $args['user']); + function loginform($loginform) + { + $rcmail = rcmail::get_instance(); + $client_ip = $this->_get_client_ip(); - return $args; - } - else { - $this->log_recaptcha(RCGUARD_RECAPTCHA_FAILURE, $args['user']); + $query = $rcmail->db->query("SELECT " . $this->unixtimestamp('last') . " AS last, " . $this->unixtimestamp('NOW()') . " as time " . + " FROM rcguard WHERE ip = ? AND hits >= ?", + $client_ip, $rcmail->config->get('failed_attempts')); + $result = $rcmail->db->fetch_assoc($query); - $rcmail->output->show_message('rcguard.recaptchafailed', 'error'); - $rcmail->output->set_env('task', 'login'); - $rcmail->output->send('login'); + if ((!$result || $this->delete_rcguard($result, $client_ip)) && + $rcmail->config->get('failed_attempts') != 0) { + return $loginform; + } - exit; - } + return $this->show_recaptcha($loginform); } - else { - $this->log_recaptcha(RCGUARD_RECAPTCHA_FAILURE, $args['user']); - $rcmail->output->show_message('rcguard.recaptchaempty', 'error'); - $rcmail->output->set_env('task', 'login'); - $rcmail->output->send('login'); + function authenticate($args) + { + $this->add_texts('localization/'); + $rcmail = rcmail::get_instance(); + $client_ip = $this->_get_client_ip(); + + $query = $rcmail->db->query("SELECT ip FROM rcguard WHERE ip = ? AND hits >= ?", + $client_ip, $rcmail->config->get('failed_attempts')); + $result = $rcmail->db->fetch_assoc($query); + + if (!$result && $rcmail->config->get('failed_attempts') != 0) { + return $args; + } + + if ($rcmail->config->get('pl_plugin')) { + if (!empty($_COOKIE[$rcmail->config->get('pl_cookie_name')])) { + $args = $this->pl_authenticate($args); + return $args; + } + } + + if ($response = $_POST['g-recaptcha-response']) { + if ($this->verify_recaptcha($client_ip, $response)) { + $this->log_recaptcha(RCGUARD_RECAPTCHA_SUCCESS, $args['user']); + return $args; + } + else { + $this->log_recaptcha(RCGUARD_RECAPTCHA_FAILURE, $args['user']); + + $rcmail->output->show_message('rcguard.recaptchafailed', 'error'); + $rcmail->output->set_env('task', 'login'); + $rcmail->output->send('login'); + } + } + else { + $this->log_recaptcha(RCGUARD_RECAPTCHA_FAILURE, $args['user']); + + $rcmail->output->show_message('rcguard.recaptchaempty', 'error'); + $rcmail->output->set_env('task', 'login'); + $rcmail->output->send('login'); + } + + return null; + } - exit; + function login_after($args) + { + $client_ip = $this->_get_client_ip(); + $this->delete_rcguard('', $client_ip, true); + + return $args; } - } - - function login_after($args) - { - $client_ip = $this->_get_client_ip(); - - $this->delete_rcguard('', $client_ip, true); - - return $args; - } - - function login_failed($args) - { - $rcmail = rcmail::get_instance(); - - $client_ip = $this->_get_client_ip(); - - $query = $rcmail->db->query( - "SELECT hits - FROM rcguard - WHERE ip = ?", - $client_ip); - $result = $rcmail->db->fetch_assoc($query); - - if ($result) - $this->update_rcguard($result['hits'], $client_ip); - else - $this->insert_rcguard($client_ip); - } - - private function insert_rcguard($client_ip) - { - $rcmail = rcmail::get_instance(); - - $query = $rcmail->db->query( - "INSERT INTO rcguard - (ip, first, last, hits) - VALUES (?, NOW(), NOW(), ?)", - $client_ip, 1); - } - - private function update_rcguard($hits, $client_ip) - { - $rcmail = rcmail::get_instance(); - - $query = $rcmail->db->query( - "UPDATE rcguard - SET last = NOW(), hits = ? - WHERE ip = ?", - $hits + 1, $client_ip); - } - - private function delete_rcguard($result, $client_ip, $force = false) - { - $this->load_config(); - $rcmail = rcmail::get_instance(); - - if ($force) { - $query = $rcmail->db->query( - "DELETE FROM rcguard - WHERE ip = ?", - $client_ip); - - $this->flush_rcguard(); - - return; + + function login_failed($args) + { + $rcmail = rcmail::get_instance(); + $client_ip = $this->_get_client_ip(); + + $query = $rcmail->db->query("SELECT hits FROM rcguard WHERE ip = ?", $client_ip); + $result = $rcmail->db->fetch_assoc($query); + + if ($result) { + $this->update_rcguard($result['hits'], $client_ip); + } + else { + $this->insert_rcguard($client_ip); + } } - $last = $result['last']; - $time = $result['time']; + private function insert_rcguard($client_ip) + { + $rcmail = rcmail::get_instance(); + $query = $rcmail->db->query("INSERT INTO rcguard (ip, first, last, hits) VALUES (?, NOW(), NOW(), ?)", + $client_ip, 1); + } + + private function update_rcguard($hits, $client_ip) + { + $rcmail = rcmail::get_instance(); + $query = $rcmail->db->query("UPDATE rcguard SET last = NOW(), hits = ? WHERE ip = ?", + $hits + 1, $client_ip); + } + + private function delete_rcguard($result, $client_ip, $force = false) + { + $rcmail = rcmail::get_instance(); + + if ($force) { + $query = $rcmail->db->query("DELETE FROM rcguard WHERE ip = ?", $client_ip); + $this->flush_rcguard(); + return true; + } + + $last = $result['last']; + $time = $result['time']; + + if ($last + $rcmail->config->get('expire_time') * 60 < $time) { + $this->flush_rcguard(); + return true; + } + + return false; + } - if ($last + $rcmail->config->get('expire_time') * 60 < $time) { - $this->flush_rcguard(); + private function flush_rcguard() + { + $rcmail = rcmail::get_instance(); - return true; + $query = $rcmail->db->query("DELETE FROM rcguard " . + " WHERE " . $this->unixtimestamp('last') . " + ? < " . $this->unixtimestamp('NOW()'), + $rcmail->config->get('expire_time') * 60); } - else - return false; - } - - private function flush_rcguard() - { - $this->load_config(); - $rcmail = rcmail::get_instance(); - - $query = $rcmail->db->query( - "DELETE FROM rcguard - WHERE " . $this->unixtimestamp('last') . " + ? < " . $this->unixtimestamp('NOW()'), - $rcmail->config->get('expire_time') * 60); - } - - private function show_recaptcha($loginform) - { - $this->load_config(); - $rcmail = rcmail::get_instance(); - $recaptcha_api = 'http://www.google.com/recaptcha/api.js'; - $recaptcha_api_secure = 'https://www.google.com/recaptcha/api.js'; - - $skin_path = $this->local_skin_path(); - if (!file_exists(INSTALL_PATH . '/plugins/rcguard/'.$skin_path)) { $skin_path = 'skins/default'; } - $this->include_stylesheet($skin_path . '/rcguard.css'); - $this->include_script('rcguard.js'); - - $src = sprintf("%s?hl=%s", - $rcmail->config->get('recaptcha_https') || $_SERVER['HTTPS'] ? - $recaptcha_api_secure : $recaptcha_api, - $rcmail->user->language); - - $script = html::tag('script', array('type' => "text/javascript", 'src' => $src)); - $this->include_script($src); - - $tmp = $loginform['content']; - $tmp = str_ireplace('', - '
+ + private function show_recaptcha($loginform) + { + $rcmail = rcmail::get_instance(); + + $skin_path = $this->local_skin_path(); + if (!file_exists(INSTALL_PATH . '/plugins/rcguard/'.$skin_path)) { $skin_path = 'skins/larry'; } + $this->include_stylesheet($skin_path . '/rcguard.css'); + $this->include_script('rcguard.js'); + + $recaptcha_api = ($rcmail->config->get('recaptcha_https') || $_SERVER['HTTPS']) ? + $rcmail->config->get('recaptcha_api_secure') : $rcmail->config->get('recaptcha_api'); + + $src = sprintf("%s?hl=%s", $recaptcha_api, $rcmail->user->language); + $script = html::tag('script', array('type' => "text/javascript", 'src' => $src)); + $this->include_script($src); + + $tmp = $loginform['content']; + $tmp = str_ireplace('', + '
', $tmp); - $loginform['content'] = $tmp; - - return $loginform; - } - - private function verify_recaptcha($client_ip, $response) - { - $this->load_config(); - $rcmail = rcmail::get_instance(); - $privatekey = $rcmail->config->get('recaptcha_privatekey'); - - require_once($this->home . '/lib/recaptchalib.php'); - $resp = null; - $error = null; - - $reCaptcha = new ReCaptcha($privatekey); - $resp = $reCaptcha->verifyResponse($client_ip, $response); - - if ($resp != null && $resp->success) - return true; - else - return false; - } - - private function log_recaptcha($log_type, $username) - { - $this->load_config(); - $rcmail = rcmail::get_instance(); - $client_ip = $this->_get_client_ip(); - $username = (empty($username)) ? 'empty username' : $username; - - if (!$rcmail->config->get('recaptcha_log')) - return; - - switch ($log_type) { - case RCGUARD_RECAPTCHA_SUCCESS: - $log_entry = $rcmail->config->get('recaptcha_log_success'); - break; - case RCGUARD_RECAPTCHA_FAILURE: - $log_entry = $rcmail->config->get('recaptcha_log_failure'); - break; - default: - $log_entry = $rcmail->config->get('recaptcha_log_unknown'); + $loginform['content'] = $tmp; + + return $loginform; } - if (empty($log_entry)) - return; + private function verify_recaptcha($client_ip, $response) + { + $rcmail = rcmail::get_instance(); - $log_entry = str_replace(array('%r', '%u'), array($client_ip, $username), $log_entry); + $privatekey = $rcmail->config->get('recaptcha_privatekey'); + require_once($this->home . '/lib/recaptchalib.php'); - write_log('rcguard', $log_entry); - } + $reCaptcha = new ReCaptcha($privatekey); + $resp = $reCaptcha->verifyResponse($client_ip, $response); - private function unixtimestamp($field) - { - $rcmail = rcmail::get_instance(); + return ($resp != null && $resp->success); + } - switch ($rcmail->db->db_provider) { - case 'pgsql': - case 'postgres': - return "EXTRACT (EPOCH FROM $field)"; - default: - return "UNIX_TIMESTAMP($field)"; + private function log_recaptcha($log_type, $username) + { + $rcmail = rcmail::get_instance(); + $client_ip = $this->_get_client_ip(); + $username = (empty($username)) ? 'empty username' : $username; + + if (!$rcmail->config->get('recaptcha_log')) { + return; + } + + switch ($log_type) { + case RCGUARD_RECAPTCHA_SUCCESS: + $log_entry = $rcmail->config->get('recaptcha_log_success'); + break; + case RCGUARD_RECAPTCHA_FAILURE: + $log_entry = $rcmail->config->get('recaptcha_log_failure'); + break; + default: + $log_entry = $rcmail->config->get('recaptcha_log_unknown'); + } + + if (!empty($log_entry)) { + $log_entry = str_replace(array('%r', '%u'), array($client_ip, $username), $log_entry); + write_log('rcguard', $log_entry); + } } - } - - private function pl_authenticate($args) { - $this->load_config(); - $rcmail = rcmail::get_instance(); - - // Code from persistent login plugin - $plain_token = $rcmail->decrypt($_COOKIE[$this->cookie_name]); - $token_parts = explode('|', $plain_token); - - if (!empty($token_parts) && is_array($token_parts) && count($token_parts == 5)) { - if (time() <= $token_parts[4]) { - $args['user'] = $token_parts[1]; - $args['pass'] = $rcmail->decrypt($token_parts[2]); - $args['host'] = $token_parts[3]; - $args['cookiecheck'] = false; - $args['valid'] = true; - } + + private function unixtimestamp($field) + { + $rcmail = rcmail::get_instance(); + $ts = ''; + + switch ($rcmail->db->db_provider) { + case 'pgsql': + case 'postgres': + $ts = "EXTRACT (EPOCH FROM $field)"; + default: + $ts = "UNIX_TIMESTAMP($field)"; + } + + return $ts; } - return $args; - } + private function pl_authenticate($args) + { + $rcmail = rcmail::get_instance(); + + // Code from persistent login plugin + $plain_token = $rcmail->decrypt($_COOKIE[$this->cookie_name]); + $token_parts = explode('|', $plain_token); + + if (!empty($token_parts) && is_array($token_parts) && count($token_parts == 5)) { + if (time() <= $token_parts[4]) { + $args['user'] = $token_parts[1]; + $args['pass'] = $rcmail->decrypt($token_parts[2]); + $args['host'] = $token_parts[3]; + $args['cookiecheck'] = false; + $args['valid'] = true; + } + } - private function _get_client_ip() - { - return rcube_utils::remote_addr(); - } + return $args; + } + + private function _get_client_ip() + { + return rcube_utils::remote_addr(); + } } ?> diff --git a/skins/default/rcguard.css b/skins/classic/rcguard.css similarity index 100% rename from skins/default/rcguard.css rename to skins/classic/rcguard.css From 225f278f93a67a4464ee6845bb538eab56c432e7 Mon Sep 17 00:00:00 2001 From: Diana Soares Date: Fri, 3 Jul 2015 17:51:07 +0100 Subject: [PATCH 10/12] Update the reCaptcha client to use the new reCAPTCHA API version 2.0 --- lib/recaptchalib.php | 154 ++++++++++++++++++++++++------------------- rcguard.php | 2 +- 2 files changed, 88 insertions(+), 68 deletions(-) diff --git a/lib/recaptchalib.php b/lib/recaptchalib.php index ae467a2..1824fd1 100644 --- a/lib/recaptchalib.php +++ b/lib/recaptchalib.php @@ -31,110 +31,130 @@ */ /** - * A ReCaptchaResponse is returned from checkAnswer(). + * reCAPTCHA client */ -class ReCaptchaResponse -{ - public $success; - public $errorCodes; -} - class ReCaptcha { - private static $_signupUrl = "https://www.google.com/recaptcha/admin"; - private static $_siteVerifyUrl = - "https://www.google.com/recaptcha/api/siteverify?"; + private static $version = "php_1.1.1"; + private static $signupUrl = "https://www.google.com/recaptcha/admin"; + private static $siteVerifyUrl = "https://www.google.com/recaptcha/api/siteverify"; private $_secret; - private static $_version = "php_1.0"; /** * Constructor. * * @param string $secret shared secret between site and ReCAPTCHA server. */ - function ReCaptcha($secret) + function __construct($secret) { - if ($secret == null || $secret == "") { - die("To use reCAPTCHA you must get an API key from " . self::$_signupUrl . ""); + if (empty($secret)) { + die('To use reCAPTCHA you must get an API key from ' . self::$signupUrl . ''); } - $this->_secret=$secret; + + $this->_secret = $secret; } + /** - * Encodes the given data into a query string format. + * Submit the POST request with the specified parameters. * - * @param array $data array of string elements to be encoded. + * @param array $params Request parameters + * @return string Body of the reCAPTCHA 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 => 'www.google.com', + ) + ); + + $context = stream_context_create($options); + return file_get_contents(self::$siteVerifyUrl, false, $context); + } + + + /** + * Calls the reCAPTCHA siteverify API to verify whether the user passes + * CAPTCHA test. (reCAPTCHA version php_1.1.1) * - * @return string - encoded request. + * @param string $response The value of 'g-recaptcha-response' in the submitted form. + * @param string $remoteIp The end user's IP address. + * @return ReCaptchaResponse Response from the service. */ - private function _encodeQS($data) + public function verify($response, $remoteIp = null) { - $req = ""; - foreach ($data as $key => $value) { - $req .= $key . '=' . urlencode(stripslashes($value)) . '&'; + if (empty($response)) { // Discard empty solution submissions + return new ReCaptchaResponse(false, array('missing-input')); } + + $params = array('version' => self::$version, + 'secret' => $this->_secret, + 'remoteip' => $remoteIp, + 'response' => $response + ); + + $rawResponse = $this->_submit($params); - // Cut the last '&' - $req=substr($req, 0, strlen($req)-1); - return $req; + return ReCaptchaResponse::fromJson($rawResponse); } +} + + +/** + * The response returned from the service. + */ +class ReCaptchaResponse +{ + public $success; + public $errorCodes; /** - * Submits an HTTP GET to a reCAPTCHA server. - * - * @param string $path url path to recaptcha server. - * @param array $data array of parameters to be sent. + * Constructor. * - * @return array response + * @param boolean $success + * @param array $errorCodes */ - private function _submitHTTPGet($path, $data) + function __construct($success, $errorCodes=array()) { - $req = $this->_encodeQS($data); - $response = file_get_contents($path . $req); - return $response; + $this->success = $success; + $this->errorCodes = $errorCodes; } /** - * Calls the reCAPTCHA siteverify API to verify whether the user passes - * CAPTCHA test. - * - * @param string $remoteIp IP address of end user. - * @param string $response response string from recaptcha verification. + * Build the response from the expected JSON returned by the service. * + * @param string $json * @return ReCaptchaResponse */ - public function verifyResponse($remoteIp, $response) + public static function fromJson($json) { - // Discard empty solution submissions - if ($response == null || strlen($response) == 0) { - $recaptchaResponse = new ReCaptchaResponse(); - $recaptchaResponse->success = false; - $recaptchaResponse->errorCodes = 'missing-input'; - return $recaptchaResponse; - } + $responseData = json_decode($json, true); - $getResponse = $this->_submitHttpGet( - self::$_siteVerifyUrl, - array ( - 'secret' => $this->_secret, - 'remoteip' => $remoteIp, - 'v' => self::$_version, - 'response' => $response - ) - ); - $answers = json_decode($getResponse, true); - $recaptchaResponse = new ReCaptchaResponse(); - - if (trim($answers ['success']) == true) { - $recaptchaResponse->success = true; - } else { - $recaptchaResponse->success = false; - $recaptchaResponse->errorCodes = $answers [error-codes]; + 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; + return $reCaptchaResponse; } } -?> +?> \ No newline at end of file diff --git a/rcguard.php b/rcguard.php index 2a39743..cd2fe42 100644 --- a/rcguard.php +++ b/rcguard.php @@ -210,7 +210,7 @@ private function verify_recaptcha($client_ip, $response) require_once($this->home . '/lib/recaptchalib.php'); $reCaptcha = new ReCaptcha($privatekey); - $resp = $reCaptcha->verifyResponse($client_ip, $response); + $resp = $reCaptcha->verify($response, $client_ip); return ($resp != null && $resp->success); } From 08f85e26aabf0f49257b964d6d99af8e7894b9b1 Mon Sep 17 00:00:00 2001 From: dsoares Date: Fri, 7 Aug 2015 12:39:36 +0100 Subject: [PATCH 11/12] Update composer to do automatic database initialization on install. --- SQL/mysql.initial.sql | 2 +- SQL/mysql.update.sql | 23 ----------------------- composer.json | 11 ++++++----- 3 files changed, 7 insertions(+), 29 deletions(-) delete mode 100644 SQL/mysql.update.sql diff --git a/SQL/mysql.initial.sql b/SQL/mysql.initial.sql index 21d4411..ad47297 100644 --- a/SQL/mysql.initial.sql +++ b/SQL/mysql.initial.sql @@ -1,6 +1,6 @@ -- MySQL table for rcguard -CREATE TABLE `rcguard` ( +CREATE TABLE IF NOT EXISTS `rcguard` ( `ip` VARCHAR(40) NOT NULL, `first` DATETIME NOT NULL, `last` DATETIME NOT NULL, diff --git a/SQL/mysql.update.sql b/SQL/mysql.update.sql deleted file mode 100644 index 5b305a7..0000000 --- a/SQL/mysql.update.sql +++ /dev/null @@ -1,23 +0,0 @@ --- MySQL table updates for rcguard - --- 0.1.0 -> 0.1.1 -TRUNCATE TABLE `rcguard`; - -ALTER TABLE `rcguard` - DROP INDEX `time`, - DROP INDEX `hits`; - -ALTER TABLE `rcguard` - ADD INDEX (`last`), - ADD INDEX (`hits`); - --- 0.2.0 -> 0.2.1 -TRUNCATE TABLE `rcguard`; - -ALTER TABLE `rcguard` - DROP INDEX `last`, - DROP INDEX `hits`; - -ALTER TABLE `rcguard` - ADD INDEX `last_index` (`last`), - ADD INDEX `hits_index` (`hits`); diff --git a/composer.json b/composer.json index c199d0e..562ed79 100644 --- a/composer.json +++ b/composer.json @@ -1,14 +1,14 @@ { "name": "dsoares/rcguard", + "type": "roundcube-plugin", "description": "Roundcube plugin that enforces reCAPTCHA for users that have too many failed logins", "keywords": ["mail","security","captcha"], - "homepage": "https://github.com/dsoares/rcguard", - "license": "GPL-3.0", - "type": "roundcube-plugin", - "version": "0.6-git", + "homepage": "https://github.com/dsoares/rcguard/", + "license": "GPL-3.0+", "authors": [ { "name": "Denny Lin", + "email": "dennylin93@hs.ntnu.edu.tw", "homepage": "https://github.com/dennylin93", "role": "Developer" } @@ -25,7 +25,8 @@ }, "extra": { "roundcube": { - "min-version": "1.0" + "min-version": "1.0", + "sql-dir": "SQL" } } } From dc0272b4690cb69b8cfebdcd7be25e4c3da92347 Mon Sep 17 00:00:00 2001 From: steelAXIS Date: Wed, 12 Aug 2015 18:19:42 +0200 Subject: [PATCH 12/12] Fix missing Break Statement --- rcguard.php | 1 + 1 file changed, 1 insertion(+) diff --git a/rcguard.php b/rcguard.php index cd2fe42..d9ec8f7 100644 --- a/rcguard.php +++ b/rcguard.php @@ -251,6 +251,7 @@ private function unixtimestamp($field) case 'pgsql': case 'postgres': $ts = "EXTRACT (EPOCH FROM $field)"; + break; default: $ts = "UNIX_TIMESTAMP($field)"; }