From b4665845d02d675ec0362d72dc07ba4c86ac8949 Mon Sep 17 00:00:00 2001 From: Christian Beer Date: Thu, 19 Nov 2015 16:53:48 +0100 Subject: [PATCH 1/2] Web: update the reCAPTCHA PHP client library to 1.1.2 This version allows the use of alternative RequestMethods in case get_file_contents() is disabled for security reasons. Currently it uses fsockopen() but it can also use php-curl. Include inc/recaptchalib.php into the file where the reCaptcha is shown or validated. The function boinc_recaptcha_isValidated() is a convenient wrapper that can be updated for future versions or changes of the RequestMethod used. --- html/inc/ReCaptcha/ReCaptcha.php | 97 +++++++++ html/inc/ReCaptcha/RequestMethod.php | 42 ++++ html/inc/ReCaptcha/RequestMethod/Curl.php | 74 +++++++ html/inc/ReCaptcha/RequestMethod/CurlPost.php | 88 +++++++++ html/inc/ReCaptcha/RequestMethod/Post.php | 70 +++++++ html/inc/ReCaptcha/RequestMethod/Socket.php | 104 ++++++++++ .../ReCaptcha/RequestMethod/SocketPost.php | 121 ++++++++++++ html/inc/ReCaptcha/RequestParameters.php | 103 ++++++++++ html/inc/ReCaptcha/Response.php | 102 ++++++++++ html/inc/profile.inc | 1 - html/inc/recaptcha_loader.php | 42 ++++ html/inc/recaptchalib.php | 185 +++++------------- html/inc/util.inc | 23 --- html/user/create_account_action.php | 4 +- html/user/create_account_form.php | 2 +- html/user/create_profile.php | 9 +- 16 files changed, 901 insertions(+), 166 deletions(-) create mode 100644 html/inc/ReCaptcha/ReCaptcha.php create mode 100644 html/inc/ReCaptcha/RequestMethod.php create mode 100644 html/inc/ReCaptcha/RequestMethod/Curl.php create mode 100644 html/inc/ReCaptcha/RequestMethod/CurlPost.php create mode 100644 html/inc/ReCaptcha/RequestMethod/Post.php create mode 100644 html/inc/ReCaptcha/RequestMethod/Socket.php create mode 100644 html/inc/ReCaptcha/RequestMethod/SocketPost.php create mode 100644 html/inc/ReCaptcha/RequestParameters.php create mode 100644 html/inc/ReCaptcha/Response.php create mode 100644 html/inc/recaptcha_loader.php diff --git a/html/inc/ReCaptcha/ReCaptcha.php b/html/inc/ReCaptcha/ReCaptcha.php new file mode 100644 index 0000000000..7139fae37b --- /dev/null +++ b/html/inc/ReCaptcha/ReCaptcha.php @@ -0,0 +1,97 @@ +secret = $secret; + + if (!is_null($requestMethod)) { + $this->requestMethod = $requestMethod; + } else { + $this->requestMethod = new RequestMethod\Post(); + } + } + + /** + * Calls the reCAPTCHA siteverify API to verify whether the user passes + * CAPTCHA test. + * + * @param string $response The value of 'g-recaptcha-response' in the submitted form. + * @param string $remoteIp The end user's IP address. + * @return Response Response from the service. + */ + public function verify($response, $remoteIp = null) + { + // Discard empty solution submissions + if (empty($response)) { + $recaptchaResponse = new Response(false, array('missing-input-response')); + return $recaptchaResponse; + } + + $params = new RequestParameters($this->secret, $response, $remoteIp, self::VERSION); + $rawResponse = $this->requestMethod->submit($params); + return Response::fromJson($rawResponse); + } +} diff --git a/html/inc/ReCaptcha/RequestMethod.php b/html/inc/ReCaptcha/RequestMethod.php new file mode 100644 index 0000000000..fc4dde59c0 --- /dev/null +++ b/html/inc/ReCaptcha/RequestMethod.php @@ -0,0 +1,42 @@ +curl = $curl; + } else { + $this->curl = new Curl(); + } + } + + /** + * Submit the cURL request with the specified parameters. + * + * @param RequestParameters $params Request parameters + * @return string Body of the reCAPTCHA response + */ + public function submit(RequestParameters $params) + { + $handle = $this->curl->init(self::SITE_VERIFY_URL); + + $options = array( + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $params->toQueryString(), + CURLOPT_HTTPHEADER => array( + 'Content-Type: application/x-www-form-urlencoded' + ), + CURLINFO_HEADER_OUT => false, + CURLOPT_HEADER => false, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => true + ); + $this->curl->setoptArray($handle, $options); + + $response = $this->curl->exec($handle); + $this->curl->close($handle); + + return $response; + } +} diff --git a/html/inc/ReCaptcha/RequestMethod/Post.php b/html/inc/ReCaptcha/RequestMethod/Post.php new file mode 100644 index 0000000000..7770d90814 --- /dev/null +++ b/html/inc/ReCaptcha/RequestMethod/Post.php @@ -0,0 +1,70 @@ + array( + 'header' => "Content-type: application/x-www-form-urlencoded\r\n", + 'method' => 'POST', + 'content' => $params->toQueryString(), + // Force the peer to validate (not needed in 5.6.0+, but still works + 'verify_peer' => true, + // Force the peer validation to use www.google.com + $peer_key => 'www.google.com', + ), + ); + $context = stream_context_create($options); + return file_get_contents(self::SITE_VERIFY_URL, false, $context); + } +} diff --git a/html/inc/ReCaptcha/RequestMethod/Socket.php b/html/inc/ReCaptcha/RequestMethod/Socket.php new file mode 100644 index 0000000000..f51f1239a9 --- /dev/null +++ b/html/inc/ReCaptcha/RequestMethod/Socket.php @@ -0,0 +1,104 @@ +handle = fsockopen($hostname, $port, $errno, $errstr, (is_null($timeout) ? ini_get("default_socket_timeout") : $timeout)); + + if ($this->handle != false && $errno === 0 && $errstr === '') { + return $this->handle; + } + return false; + } + + /** + * fwrite + * + * @see http://php.net/fwrite + * @param string $string + * @param int $length + * @return int | bool + */ + public function fwrite($string, $length = null) + { + return fwrite($this->handle, $string, (is_null($length) ? strlen($string) : $length)); + } + + /** + * fgets + * + * @see http://php.net/fgets + * @param int $length + * @return string + */ + public function fgets($length = null) + { + return fgets($this->handle, $length); + } + + /** + * feof + * + * @see http://php.net/feof + * @return bool + */ + public function feof() + { + return feof($this->handle); + } + + /** + * fclose + * + * @see http://php.net/fclose + * @return bool + */ + public function fclose() + { + return fclose($this->handle); + } +} diff --git a/html/inc/ReCaptcha/RequestMethod/SocketPost.php b/html/inc/ReCaptcha/RequestMethod/SocketPost.php new file mode 100644 index 0000000000..47541215f0 --- /dev/null +++ b/html/inc/ReCaptcha/RequestMethod/SocketPost.php @@ -0,0 +1,121 @@ +socket = $socket; + } else { + $this->socket = new Socket(); + } + } + + /** + * Submit the POST request with the specified parameters. + * + * @param RequestParameters $params Request parameters + * @return string Body of the reCAPTCHA response + */ + public function submit(RequestParameters $params) + { + $errno = 0; + $errstr = ''; + + if (false === $this->socket->fsockopen('ssl://' . self::RECAPTCHA_HOST, 443, $errno, $errstr, 30)) { + return self::BAD_REQUEST; + } + + $content = $params->toQueryString(); + + $request = "POST " . self::SITE_VERIFY_PATH . " HTTP/1.1\r\n"; + $request .= "Host: " . self::RECAPTCHA_HOST . "\r\n"; + $request .= "Content-Type: application/x-www-form-urlencoded\r\n"; + $request .= "Content-length: " . strlen($content) . "\r\n"; + $request .= "Connection: close\r\n\r\n"; + $request .= $content . "\r\n\r\n"; + + $this->socket->fwrite($request); + $response = ''; + + while (!$this->socket->feof()) { + $response .= $this->socket->fgets(4096); + } + + $this->socket->fclose(); + + if (0 !== strpos($response, 'HTTP/1.1 200 OK')) { + return self::BAD_RESPONSE; + } + + $parts = preg_split("#\n\s*\n#Uis", $response); + + return $parts[1]; + } +} diff --git a/html/inc/ReCaptcha/RequestParameters.php b/html/inc/ReCaptcha/RequestParameters.php new file mode 100644 index 0000000000..cb66f26cf4 --- /dev/null +++ b/html/inc/ReCaptcha/RequestParameters.php @@ -0,0 +1,103 @@ +secret = $secret; + $this->response = $response; + $this->remoteIp = $remoteIp; + $this->version = $version; + } + + /** + * Array representation. + * + * @return array Array formatted parameters. + */ + public function toArray() + { + $params = array('secret' => $this->secret, 'response' => $this->response); + + if (!is_null($this->remoteIp)) { + $params['remoteip'] = $this->remoteIp; + } + + if (!is_null($this->version)) { + $params['version'] = $this->version; + } + + return $params; + } + + /** + * Query string representation for HTTP request. + * + * @return string Query string formatted parameters. + */ + public function toQueryString() + { + return http_build_query($this->toArray(), '', '&'); + } +} diff --git a/html/inc/ReCaptcha/Response.php b/html/inc/ReCaptcha/Response.php new file mode 100644 index 0000000000..d2d8a8bf77 --- /dev/null +++ b/html/inc/ReCaptcha/Response.php @@ -0,0 +1,102 @@ +success = $success; + $this->errorCodes = $errorCodes; + } + + /** + * Is success? + * + * @return boolean + */ + public function isSuccess() + { + return $this->success; + } + + /** + * Get error codes. + * + * @return array + */ + public function getErrorCodes() + { + return $this->errorCodes; + } +} diff --git a/html/inc/profile.inc b/html/inc/profile.inc index 9f924e0b6c..5f9f39ce69 100644 --- a/html/inc/profile.inc +++ b/html/inc/profile.inc @@ -24,7 +24,6 @@ require_once("../inc/user.inc"); require_once("../inc/translation.inc"); require_once("../inc/text_transform.inc"); require_once("../inc/forum.inc"); -require_once("../inc/recaptchalib.php"); define('SMALL_IMG_WIDTH', 64); define('SMALL_IMG_HEIGHT', 64); diff --git a/html/inc/recaptcha_loader.php b/html/inc/recaptcha_loader.php new file mode 100644 index 0000000000..b7f475fdd7 --- /dev/null +++ b/html/inc/recaptcha_loader.php @@ -0,0 +1,42 @@ +. -/** - * A ReCaptchaResponse is returned from checkAnswer(). - */ -class ReCaptchaResponse -{ - public $success; - public $errorCodes; -} +// recaptcha utilities -class ReCaptcha -{ - 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"; +// do not include the loader from somewhere else +require('../inc/recaptcha_loader.php'); - /** - * 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 . ""); - } - $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)) . '&'; - } - - // 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; - } - - $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 $recaptchaResponse; +function boinc_recaptcha_get_head_extra() { + // are we using recaptcha? + $publickey = parse_config(get_config(), ""); + if ($publickey) { + // the meta tag must be included + // for Recaptcha to work with some IE browsers + return ' + '; + } else { + return null; } } -?> +function boinc_recaptcha_get_html($publickey) { + if ($publickey) { + return '
'; + } else { + return ''; + } +} + +// wrapper for ReCaptcha implementation +// returns true if the captcha was correct or no $privatekey was supplied +// everything else means there was an error verifying the captcha +// +function boinc_recaptcha_isValidated($privatekey) { + if ($privatekey) { + // tells ReCaptcha to use fsockopen() instead of get_file_contents() + $recaptcha = new \ReCaptcha\ReCaptcha($privatekey, new \ReCaptcha\RequestMethod\SocketPost()); + $resp = $recaptcha->verify($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']); + return $resp->isSuccess(); + } + return true; +} + +?> \ No newline at end of file diff --git a/html/inc/util.inc b/html/inc/util.inc index 9e5d55abaa..aaebf28d04 100644 --- a/html/inc/util.inc +++ b/html/inc/util.inc @@ -987,29 +987,6 @@ function version_string_maj_min_rel($v) { return sprintf("%d.%d.%d", $maj, $min, $v); } -// recaptcha utilities - -function recaptcha_get_head_extra() { - // are we using recaptcha? - $publickey = parse_config(get_config(), ""); - if ($publickey) { - // the meta tag must be included - // for Recaptcha to work with some IE browsers - return ' - '; - } else { - return null; - } -} - -function boinc_recaptcha_get_html($publickey) { - if ($publickey) { - return '
'; - } else { - return ''; - } -} - $cvs_version_tracker[]="\$Id$"; //Generated automatically - do not edit ?> diff --git a/html/user/create_account_action.php b/html/user/create_account_action.php index 88ea6e6df7..e103979f60 100644 --- a/html/user/create_account_action.php +++ b/html/user/create_account_action.php @@ -40,9 +40,7 @@ if (parse_bool($config, "disable_account_creation") $privatekey = parse_config($config, ""); if ($privatekey) { - $recaptcha = new ReCaptcha($privatekey); - $resp = $recaptcha->verifyResponse($_SERVER["REMOTE_ADDR"], $_POST["g-recaptcha-response"]); - if (!$resp->success) { + if (!boinc_recaptcha_isValidated($privatekey)) { show_error(tra("Your reCAPTCHA response was not correct. Please try again.")); } } diff --git a/html/user/create_account_form.php b/html/user/create_account_form.php index 630f630c28..467aa39084 100644 --- a/html/user/create_account_form.php +++ b/html/user/create_account_form.php @@ -38,7 +38,7 @@ if (parse_bool($config, "no_web_account_creation")) { error_page("This project has disabled Web account creation"); } -page_head(tra("Create an account"), null, null, null, recaptcha_get_head_extra()); +page_head(tra("Create an account"), null, null, null, boinc_recaptcha_get_head_extra()); if (!no_computing()) { echo "

diff --git a/html/user/create_profile.php b/html/user/create_profile.php index 9d2d4d69f4..7b549a7853 100644 --- a/html/user/create_profile.php +++ b/html/user/create_profile.php @@ -20,6 +20,7 @@ require_once("../inc/profile.inc"); require_once("../inc/akismet.inc"); +require_once("../inc/recaptchalib.php"); if (DISABLE_PROFILES) error_page("Profiles are disabled"); @@ -200,9 +201,7 @@ function process_create_profile($user, $profile) { $privatekey = parse_config($config, ""); if ($privatekey) { - $recaptcha = new ReCaptcha($privatekey); - $resp = $recaptcha->verifyResponse($_SERVER["REMOTE_ADDR"], $_POST["g-recaptcha-response"]); - if (!$resp->success) { + if (!boinc_recaptcha_isValidated($privatekey)) { $profile->response1 = $response1; $profile->response2 = $response2; show_profile_form($profile, @@ -314,9 +313,9 @@ function process_create_profile($user, $profile) { function show_profile_form($profile, $warning=null) { if ($profile) { - page_head(tra("Edit your profile"), null, null, null, recaptcha_get_head_extra()); + page_head(tra("Edit your profile"), null, null, null, boinc_recaptcha_get_head_extra()); } else { - page_head(tra("Create a profile"), null, null, null, recaptcha_get_head_extra()); + page_head(tra("Create a profile"), null, null, null, boinc_recaptcha_get_head_extra()); } if ($warning) { From 1920a1b5942576325da85c59a013507e980812a1 Mon Sep 17 00:00:00 2001 From: Christian Beer Date: Thu, 19 Nov 2015 17:23:42 +0100 Subject: [PATCH 2/2] Web: update the upgrade script to install ReCaptcha files --- py/Boinc/setup_project.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/py/Boinc/setup_project.py b/py/Boinc/setup_project.py index 0cefe9d27d..ea0a7a9ad6 100644 --- a/py/Boinc/setup_project.py +++ b/py/Boinc/setup_project.py @@ -263,6 +263,8 @@ def create_project_dirs(dest_dir): 'html', 'html/cache', 'html/inc', + 'html/inc/ReCaptcha', + 'html/inc/ReCaptcha/RequestMethod', 'html/languages', 'html/languages/compiled', 'html/languages/translations', @@ -314,6 +316,8 @@ def install_boinc_files(dest_dir, install_web_files, install_server_files): if install_web_files: install_glob(srcdir('html/inc/*.inc'), dir('html/inc/')) install_glob(srcdir('html/inc/*.php'), dir('html/inc/')) + install_glob(srcdir('html/inc/ReCaptcha/*.php'), dir('html/inc/ReCaptcha/')) + install_glob(srcdir('html/inc/ReCaptcha/RequestMethod/*.php'), dir('html/inc/ReCaptcha/RequestMethod')) install_glob(srcdir('html/inc/*.dat'), dir('html/inc/')) install_glob(srcdir('html/ops/*.css'), dir('html/ops/')) install_glob(srcdir('html/ops/ffmail/sample*'), dir('html/ops/ffmail/'))