From 64aa4a1917977c4d8a374a364dff5cc6acb1c1c0 Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Tue, 19 Jul 2016 14:14:54 -0700 Subject: [PATCH] Add reCAPTCHA support --- README.md | 2 +- imgboard.php | 8 +- inc/defines.php | 4 +- inc/functions.php | 40 ++++-- inc/html.php | 35 ++++- inc/recaptcha/LICENSE | 29 +++++ inc/recaptcha/ReCaptcha/ReCaptcha.php | 97 ++++++++++++++ inc/recaptcha/ReCaptcha/RequestMethod.php | 42 ++++++ .../ReCaptcha/RequestMethod/Curl.php | 74 +++++++++++ .../ReCaptcha/RequestMethod/CurlPost.php | 88 +++++++++++++ .../ReCaptcha/RequestMethod/Post.php | 70 ++++++++++ .../ReCaptcha/RequestMethod/Socket.php | 104 +++++++++++++++ .../ReCaptcha/RequestMethod/SocketPost.php | 121 ++++++++++++++++++ inc/recaptcha/ReCaptcha/RequestParameters.php | 103 +++++++++++++++ inc/recaptcha/ReCaptcha/Response.php | 102 +++++++++++++++ inc/recaptcha/autoload.php | 38 ++++++ settings.default.php | 42 +++--- 17 files changed, 962 insertions(+), 37 deletions(-) create mode 100644 inc/recaptcha/LICENSE create mode 100644 inc/recaptcha/ReCaptcha/ReCaptcha.php create mode 100644 inc/recaptcha/ReCaptcha/RequestMethod.php create mode 100644 inc/recaptcha/ReCaptcha/RequestMethod/Curl.php create mode 100644 inc/recaptcha/ReCaptcha/RequestMethod/CurlPost.php create mode 100644 inc/recaptcha/ReCaptcha/RequestMethod/Post.php create mode 100644 inc/recaptcha/ReCaptcha/RequestMethod/Socket.php create mode 100644 inc/recaptcha/ReCaptcha/RequestMethod/SocketPost.php create mode 100644 inc/recaptcha/ReCaptcha/RequestParameters.php create mode 100644 inc/recaptcha/ReCaptcha/Response.php create mode 100644 inc/recaptcha/autoload.php diff --git a/README.md b/README.md index f732322..0730132 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Features ------------ - GIF, JPG, PNG, SWF and WebA/WebM upload. - YouTube, Vimeo and SoundCloud embedding. - - CAPTCHA. + - CAPTCHA (A simple implementation is included, reCAPTCHA is also supported) - Reference links >>### - Delete post via password. - Management panel: diff --git a/imgboard.php b/imgboard.php index 8993b84..f121197 100644 --- a/imgboard.php +++ b/imgboard.php @@ -36,7 +36,11 @@ if (!file_exists('settings.php')) { require 'settings.php'; if (TINYIB_TRIPSEED == '' || TINYIB_ADMINPASS == '') { - fancyDie('TINYIB_TRIPSEED and TINYIB_ADMINPASS must be configured'); + fancyDie('TINYIB_TRIPSEED and TINYIB_ADMINPASS must be configured.'); +} + +if (TINYIB_CAPTCHA === 'recaptcha' && (TINYIB_RECAPTCHA_SITE == '' || TINYIB_RECAPTCHA_SECRET == '')) { + fancyDie('TINYIB_RECAPTCHA_SITE and TINYIB_RECAPTCHA_SECRET must be configured.'); } // Check directories are writable by the script @@ -54,7 +58,7 @@ $includes = array("inc/defines.php", "inc/functions.php", "inc/html.php"); if (in_array(TINYIB_DBMODE, array('flatfile', 'mysql', 'mysqli', 'sqlite', 'pdo'))) { $includes[] = 'inc/database_' . TINYIB_DBMODE . '.php'; } else { - fancyDie("Unknown database mode specified"); + fancyDie("Unknown database mode specified."); } foreach ($includes as $include) { diff --git a/inc/defines.php b/inc/defines.php index f19f2a2..74efadb 100644 --- a/inc/defines.php +++ b/inc/defines.php @@ -37,10 +37,10 @@ if (!defined('TINYIB_NOFILEOK')) { define('TINYIB_NOFILEOK', false); } if (!defined('TINYIB_CAPTCHA')) { - define('TINYIB_CAPTCHA', false); + define('TINYIB_CAPTCHA', ''); } if (!defined('TINYIB_REQMOD')) { - define('TINYIB_REQMOD', 'disable'); + define('TINYIB_REQMOD', ''); } if (!defined('TINYIB_DBMIGRATE')) { define('TINYIB_DBMIGRATE', false); diff --git a/inc/functions.php b/inc/functions.php index 71c9cca..60a72fc 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -222,17 +222,39 @@ function deletePostImages($post) { } function checkCAPTCHA() { - if (!TINYIB_CAPTCHA) { - return; // CAPTCHA is disabled - } + if (TINYIB_CAPTCHA === 'recaptcha') { + require_once 'inc/recaptcha/autoload.php'; - $captcha = isset($_POST['captcha']) ? strtolower(trim($_POST['captcha'])) : ''; - $captcha_solution = isset($_SESSION['tinyibcaptcha']) ? strtolower(trim($_SESSION['tinyibcaptcha'])) : ''; + $captcha = isset($_POST['g-recaptcha-response']) ? $_POST['g-recaptcha-response'] : ''; + $failed_captcha = true; - if ($captcha == '') { - fancyDie('Please enter the CAPTCHA text.'); - } else if ($captcha != $captcha_solution) { - fancyDie('Incorrect CAPTCHA text entered. Please try again.
Click the image to retrieve a new CAPTCHA.'); + $recaptcha = new \ReCaptcha\ReCaptcha(TINYIB_RECAPTCHA_SECRET); + $resp = $recaptcha->verify($captcha, $_SERVER['REMOTE_ADDR']); + if ($resp->isSuccess()) { + $failed_captcha = false; + } + + if ($failed_captcha) { + $captcha_error = 'Failed CAPTCHA.'; + if (count($resp->getErrorCodes()) == 1 && $resp->getErrorCodes()[0] == 'missing-input-response') { + $captcha_error .= ' Please click the checkbox labeled "I\'m not a robot".'; + } else { + $captcha_error .= ' Reason:'; + foreach ($resp->getErrorCodes() as $error) { + $captcha_error .= '
' . $error; + } + } + fancyDie($captcha_error); + } + } else if (!empty(TINYIB_CAPTCHA)) { // Simple CAPTCHA + $captcha = isset($_POST['captcha']) ? strtolower(trim($_POST['captcha'])) : ''; + $captcha_solution = isset($_SESSION['tinyibcaptcha']) ? strtolower(trim($_SESSION['tinyibcaptcha'])) : ''; + + if ($captcha == '') { + fancyDie('Please enter the CAPTCHA text.'); + } else if ($captcha != $captcha_solution) { + fancyDie('Incorrect CAPTCHA text entered. Please try again.
Click the image to retrieve a new CAPTCHA.'); + } } } diff --git a/inc/html.php b/inc/html.php index 040566a..8f1d5f8 100644 --- a/inc/html.php +++ b/inc/html.php @@ -4,6 +4,8 @@ if (!defined('TINYIB_BOARD')) { } function pageHeader() { + $js_captcha = TINYIB_CAPTCHA === 'recaptcha' ? '' : ''; + $return = << @@ -25,6 +27,7 @@ EOF; + $js_captcha EOF; return $return; @@ -274,15 +277,37 @@ EOF; $unique_posts_html = ''; $captcha_html = ''; - if (TINYIB_CAPTCHA) { + if (!empty(TINYIB_CAPTCHA)) { + if (TINYIB_CAPTCHA === 'recaptcha') { + $captcha_inner_html = ' +
+
+ +
'; + } else { // Simple CAPTCHA + $captcha_inner_html = ' +  (enter the text below)
+CAPTCHA'; + } + $captcha_html = << CAPTCHA -   (enter the text below)
- CAPTCHA + $captcha_inner_html EOF; @@ -321,7 +346,7 @@ EOF; EOF; } - if (TINYIB_REQMOD != 'disable') { + if (TINYIB_REQMOD == 'files' || TINYIB_REQMOD == 'all') { $reqmod_html = '
  • All posts' . (TINYIB_REQMOD == 'files' ? ' with a file attached' : '') . ' will be moderated before being shown.
  • '; } @@ -759,7 +784,7 @@ EOF; $reqmod_html = ''; - if (TINYIB_REQMOD != 'disable') { + if (TINYIB_REQMOD == 'files' || TINYIB_REQMOD == 'all') { $reqmod_post_html = ''; $reqmod_posts = latestPosts(false); diff --git a/inc/recaptcha/LICENSE b/inc/recaptcha/LICENSE new file mode 100644 index 0000000..f641232 --- /dev/null +++ b/inc/recaptcha/LICENSE @@ -0,0 +1,29 @@ +Copyright 2014, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/inc/recaptcha/ReCaptcha/ReCaptcha.php b/inc/recaptcha/ReCaptcha/ReCaptcha.php new file mode 100644 index 0000000..7139fae --- /dev/null +++ b/inc/recaptcha/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/inc/recaptcha/ReCaptcha/RequestMethod.php b/inc/recaptcha/ReCaptcha/RequestMethod.php new file mode 100644 index 0000000..fc4dde5 --- /dev/null +++ b/inc/recaptcha/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/inc/recaptcha/ReCaptcha/RequestMethod/Post.php b/inc/recaptcha/ReCaptcha/RequestMethod/Post.php new file mode 100644 index 0000000..7770d90 --- /dev/null +++ b/inc/recaptcha/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/inc/recaptcha/ReCaptcha/RequestMethod/Socket.php b/inc/recaptcha/ReCaptcha/RequestMethod/Socket.php new file mode 100644 index 0000000..f51f123 --- /dev/null +++ b/inc/recaptcha/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/inc/recaptcha/ReCaptcha/RequestMethod/SocketPost.php b/inc/recaptcha/ReCaptcha/RequestMethod/SocketPost.php new file mode 100644 index 0000000..4754121 --- /dev/null +++ b/inc/recaptcha/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/inc/recaptcha/ReCaptcha/RequestParameters.php b/inc/recaptcha/ReCaptcha/RequestParameters.php new file mode 100644 index 0000000..cb66f26 --- /dev/null +++ b/inc/recaptcha/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/inc/recaptcha/ReCaptcha/Response.php b/inc/recaptcha/ReCaptcha/Response.php new file mode 100644 index 0000000..d2d8a8b --- /dev/null +++ b/inc/recaptcha/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/inc/recaptcha/autoload.php b/inc/recaptcha/autoload.php new file mode 100644 index 0000000..5a7ee94 --- /dev/null +++ b/inc/recaptcha/autoload.php @@ -0,0 +1,38 @@ +