diff --git a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in index e55e856e4..7fff3d62e 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in +++ b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in @@ -357,6 +357,60 @@ our @options = type => $types{boolean}, category => "system", }, + # PP - Google reCaptcha settings + { + name => "ZM_OPT_USE_GOOG_RECAPTCHA", + default => "no", + description => "Add Google reCaptcha to login page", + help => qqq(" + This option allows you to include a google + reCaptcha validation at login. This means in addition to providing + a valid usernane and password, you will also have to + pass the reCaptcha test. Please note that enabling this + option results in the zoneminder login page reach out + to google servers for captcha validation. Also please note + that enabling this option will break 3rd party clients + like zmNinja and zmView as they also need to login to ZoneMinder + and they will fail the reCaptcha test. + "), + requires => [ + {name=>"ZM_OPT_USE_AUTH", value=>"yes"} + ], + type => $types {boolean}, + category => "system", + }, + + { + name => "ZM_OPT_GOOG_RECAPTCHA_SITEKEY", + default => "...Insert your recaptcha site-key here...", + description => "Your recaptcha site-key", + help => qqq("You need to generate your keys from + the Google reCaptcha website. + Please refer to https://www.google.com/recaptcha/ + for more details. + "), + requires => [ + {name=>"ZM_OPT_USE_GOOG_RECAPTCHA", value=>"yes"} + ], + type => $types {string}, + category => "system", + }, + { + name => "ZM_OPT_GOOG_RECAPTCHA_SECRETKEY", + default => "...Insert your recaptcha secret-key here...", + description => "Your recaptcha secret-key", + help => qqq("You need to generate your keys from + the Google reCaptcha website. + Please refer to https://www.google.com/recaptcha/ + for more details. + "), + requires => [ + {name=>"ZM_OPT_USE_GOOG_RECAPTCHA", value=>"yes"} + ], + type => $types {string}, + category => "system", + }, + { name => "ZM_DIR_EVENTS", default => "events", diff --git a/web/includes/actions.php b/web/includes/actions.php index 3ddf1bb40..d3a4c4121 100644 --- a/web/includes/actions.php +++ b/web/includes/actions.php @@ -42,6 +42,22 @@ if ( ZM_OPT_USE_AUTH && ZM_AUTH_HASH_LOGINS && empty($user) && !empty($_REQUEST[ if ( !empty($action) ) { + // PP - lets validate reCaptcha if it exists + if (ZM_OPT_USE_GOOG_RECAPTCHA) + { + require_once( 'recaptcha/src/autoload.php' ); + $secret = ZM_OPT_GOOG_RECAPTCHA_SECRETKEY; + $gRecaptchaResponse = $_REQUEST['g-recaptcha-response']; + $remoteIp = $_SERVER['REMOTE_ADDR']; + $recaptcha = new \ReCaptcha\ReCaptcha($secret); + $resp = $recaptcha->verify($gRecaptchaResponse, $remoteIp); + if (!$resp->isSuccess()) { + userLogout(); + $view='login'; + $refreshParent = true; + } + } + // General scope actions if ( $action == "login" && isset($_REQUEST['username']) && ( ZM_AUTH_TYPE == "remote" || isset($_REQUEST['password']) ) ) { diff --git a/web/includes/recaptcha/.gitignore b/web/includes/recaptcha/.gitignore new file mode 100644 index 000000000..436384ab7 --- /dev/null +++ b/web/includes/recaptcha/.gitignore @@ -0,0 +1,3 @@ +/composer.lock +/nbproject/private/ +/vendor/ diff --git a/web/includes/recaptcha/.travis.yml b/web/includes/recaptcha/.travis.yml new file mode 100644 index 000000000..fb25bad2d --- /dev/null +++ b/web/includes/recaptcha/.travis.yml @@ -0,0 +1,18 @@ +language: php + +sudo: false + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - hhvm + +before_script: + - composer install + - phpenv version-name | grep ^5.[34] && echo "extension=apc.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini ; true + - phpenv version-name | grep ^5.[34] && echo "apc.enable_cli=1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini ; true + +script: + - vendor/bin/phpunit diff --git a/web/includes/recaptcha/CONTRIBUTING.md b/web/includes/recaptcha/CONTRIBUTING.md new file mode 100644 index 000000000..1ba853922 --- /dev/null +++ b/web/includes/recaptcha/CONTRIBUTING.md @@ -0,0 +1,24 @@ +Want to contribute? Great! First, read this page (including the small print at the end). + +### Before you contribute +Before we can use your code, you must sign the +[Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual?csw=1) +(CLA), which you can do online. The CLA is necessary mainly because you own the +copyright to your changes, even after your contribution becomes part of our +codebase, so we need your permission to use and distribute your code. We also +need to be sure of various other things—for instance that you'll tell us if you +know that your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. + +### Code reviews +All submissions, including submissions by project members, require review. We +use Github pull requests for this purpose. + +### The small print +Contributions made by corporations are covered by a different agreement than +the one above, the Software Grant and Corporate Contributor License Agreement. diff --git a/web/includes/recaptcha/LICENSE b/web/includes/recaptcha/LICENSE new file mode 100644 index 000000000..f6412328f --- /dev/null +++ b/web/includes/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/web/includes/recaptcha/README.md b/web/includes/recaptcha/README.md new file mode 100644 index 000000000..c3364caab --- /dev/null +++ b/web/includes/recaptcha/README.md @@ -0,0 +1,113 @@ +# reCAPTCHA PHP client library + +[![Build Status](https://travis-ci.org/google/recaptcha.svg)](https://travis-ci.org/google/recaptcha) +[![Latest Stable Version](https://poser.pugx.org/google/recaptcha/v/stable.svg)](https://packagist.org/packages/google/recaptcha) +[![Total Downloads](https://poser.pugx.org/google/recaptcha/downloads.svg)](https://packagist.org/packages/google/recaptcha) + +* Project page: http://www.google.com/recaptcha/ +* Repository: https://github.com/google/recaptcha +* Version: 1.1.1 +* License: BSD, see [LICENSE](LICENSE) + +## Description + +reCAPTCHA is a free CAPTCHA service that protect websites from spam and abuse. +This is Google authored code that provides plugins for third-party integration +with reCAPTCHA. + +## Installation + +### Composer (Recommended) + +[Composer](https://getcomposer.org/) is a widely used dependency manager for PHP +packages. This reCAPTCHA client is available on Packagist as +[`google/recaptcha`](https://packagist.org/packages/google/recaptcha) and can be +installed either by running the `composer require` command or adding the library +to your `composer.json`. To enable Composer for you project, refer to the +project's [Getting Started](https://getcomposer.org/doc/00-intro.md) +documentation. + +To add this dependency using the command, run the following from within your +project directory: +``` +composer require google/recaptcha "~1.1" +``` + +Alternatively, add the dependency directly to your `composer.json` file: +```json +"require": { + "google/recaptcha": "~1.1" +} +``` + +### Direct download (no Composer) + +If you wish to install the library manually (i.e. without Composer), then you +can use the links on the main project page to either clone the repo or download +the [ZIP file](https://github.com/google/recaptcha/archive/master.zip). For +convenience, an autoloader script is provided in `src/autoload.php` which you +can require into your script instead of Composer's `vendor/autoload.php`. For +example: + +```php +require('/path/to/recaptcha/src/autoload.php'); +$recaptcha = new \ReCaptcha\ReCaptcha($secret); +``` + +The classes in the project are structured according to the +[PSR-4](http://www.php-fig.org/psr/psr-4/) standard, so you may of course also +use your own autoloader or require the needed files directly in your code. + +### Development install + +If you would like to contribute to this project or run the unit tests on within +your own environment you will need to install the development dependencies, in +this case that means [PHPUnit](https://phpunit.de/). If you clone the repo and +run `composer install` from within the repo, this will also grab PHPUnit and all +its dependencies for you. If you only need the autoloader installed, then you +can always specify to Composer not to run in development mode, e.g. `composer +install --no-dev`. + +*Note:* These dependencies are only required for development, there's no +requirement for them to be included in your production code. + +## Usage + +First, register keys for your site at https://www.google.com/recaptcha/admin + +When your app receives a form submission containing the `g-recaptcha-response` +field, you can verify it using: +```php +verify($gRecaptchaResponse, $remoteIp); +if ($resp->isSuccess()) { + // verified! +} else { + $errors = $resp->getErrorCodes(); +} +``` + +You can see an end-to-end working example in +[examples/example-captcha.php](examples/example-captcha.php) + +## Upgrading + +### From 1.0.0 + +The previous version of this client is still available on the `1.0.0` tag [in +this repo](https://github.com/google/recaptcha/tree/1.0.0) but it is purely for +reference and will not receive any updates. + +The major changes in 1.1.0 are: +* installation now via Composer; +* class loading also via Composer; +* classes now namespaced; +* old method call was `$rc->verifyResponse($remoteIp, $response)`, new call is + `$rc->verify($response, $remoteIp)` + +## Contributing + +We accept contributions via GitHub Pull Requests, but all contributors need to +be covered by the standard Google Contributor License Agreement. You can find +instructions for this in [CONTRIBUTING](CONTRIBUTING.md) diff --git a/web/includes/recaptcha/src/ReCaptcha/ReCaptcha.php b/web/includes/recaptcha/src/ReCaptcha/ReCaptcha.php new file mode 100644 index 000000000..523c4aae4 --- /dev/null +++ b/web/includes/recaptcha/src/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/web/includes/recaptcha/src/ReCaptcha/RequestMethod.php b/web/includes/recaptcha/src/ReCaptcha/RequestMethod.php new file mode 100644 index 000000000..fc4dde59c --- /dev/null +++ b/web/includes/recaptcha/src/ReCaptcha/RequestMethod.php @@ -0,0 +1,42 @@ + 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 + ); + curl_setopt_array($handle, $options); + + $response = curl_exec($handle); + curl_close($handle); + + return $response; + } +} diff --git a/web/includes/recaptcha/src/ReCaptcha/RequestMethod/Post.php b/web/includes/recaptcha/src/ReCaptcha/RequestMethod/Post.php new file mode 100644 index 000000000..7770d9081 --- /dev/null +++ b/web/includes/recaptcha/src/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/web/includes/recaptcha/src/ReCaptcha/RequestMethod/Socket.php b/web/includes/recaptcha/src/ReCaptcha/RequestMethod/Socket.php new file mode 100644 index 000000000..e74fc49d6 --- /dev/null +++ b/web/includes/recaptcha/src/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; + } else { + 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 + */ + 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/web/includes/recaptcha/src/ReCaptcha/RequestMethod/SocketPost.php b/web/includes/recaptcha/src/ReCaptcha/RequestMethod/SocketPost.php new file mode 100644 index 000000000..47541215f --- /dev/null +++ b/web/includes/recaptcha/src/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/web/includes/recaptcha/src/ReCaptcha/RequestParameters.php b/web/includes/recaptcha/src/ReCaptcha/RequestParameters.php new file mode 100644 index 000000000..cb66f26cf --- /dev/null +++ b/web/includes/recaptcha/src/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/web/includes/recaptcha/src/ReCaptcha/Response.php b/web/includes/recaptcha/src/ReCaptcha/Response.php new file mode 100644 index 000000000..d2d8a8bf7 --- /dev/null +++ b/web/includes/recaptcha/src/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/web/includes/recaptcha/src/autoload.php b/web/includes/recaptcha/src/autoload.php new file mode 100644 index 000000000..a53cbd78b --- /dev/null +++ b/web/includes/recaptcha/src/autoload.php @@ -0,0 +1,38 @@ + + + "; + } +?>