From 0edda6b191f67ff7319ac80371fa02153e56e045 Mon Sep 17 00:00:00 2001 From: Demian Katz <demian.katz@villanova.edu> Date: Wed, 22 Jul 2020 12:13:30 -0400 Subject: [PATCH] Standardize user IP address retrieval. (#1681) - Adds setting to enable or disable processing of HTTP headers related to IP forwarding. - Increases test coverage. --- config/vufind/config.ini | 6 + module/VuFind/config/module.config.php | 1 + module/VuFind/src/VuFind/Log/Logger.php | 33 ++++- .../VuFind/src/VuFind/Log/LoggerFactory.php | 6 +- module/VuFind/src/VuFind/Net/UserIpReader.php | 94 +++++++++++++ .../src/VuFind/Net/UserIpReaderFactory.php | 73 ++++++++++ .../VuFind/src/VuFind/Resolver/Driver/Ezb.php | 28 +++- .../src/VuFind/Resolver/Driver/EzbFactory.php | 63 +++++++++ .../VuFind/Resolver/Driver/PluginManager.php | 2 +- .../Role/PermissionProvider/IpRange.php | 33 +++-- .../PermissionProvider/IpRangeFactory.php | 3 +- .../Role/PermissionProvider/IpRegEx.php | 25 +++- .../PermissionProvider/IpRegExFactory.php | 70 ++++++++++ .../Role/PermissionProvider/PluginManager.php | 2 +- .../src/VuFindTest/Net/UserIpReaderTest.php | 130 ++++++++++++++++++ .../VuFindTest/Resolver/Driver/EzbTest.php | 31 ++++- .../Role/PermissionProvider/IpRangeTest.php | 114 +++++++++++++++ .../Role/PermissionProvider/IpRegExTest.php | 91 ++++++++++++ 18 files changed, 772 insertions(+), 33 deletions(-) create mode 100644 module/VuFind/src/VuFind/Net/UserIpReader.php create mode 100644 module/VuFind/src/VuFind/Net/UserIpReaderFactory.php create mode 100644 module/VuFind/src/VuFind/Resolver/Driver/EzbFactory.php create mode 100644 module/VuFind/src/VuFind/Role/PermissionProvider/IpRegExFactory.php create mode 100644 module/VuFind/tests/unit-tests/src/VuFindTest/Net/UserIpReaderTest.php create mode 100644 module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/IpRangeTest.php create mode 100644 module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/IpRegExTest.php diff --git a/config/vufind/config.ini b/config/vufind/config.ini index fbfc1a2e11e..d234f9906c9 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -1329,6 +1329,12 @@ url = https://www.myendnoteweb.com/EndNoteWeb.html ;type = socks5 ;type = socks5_hostname +; If VuFind is running behind a proxy that uses X-Real-IP/X-Forwarded-For headers, +; you should turn this setting on so that VuFind reports correct user IP +; addresses, and sets permissions appropriately. If you are NOT behind a proxy, you +; should leave this off to prevent spoofing. +allow_forwarded_ips = false + ; Default HTTP settings can be loaded here. These values will be passed to ; the \Zend\Http\Client's setOptions method. [Http] diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php index 643692d96d9..f93e5666e98 100644 --- a/module/VuFind/config/module.config.php +++ b/module/VuFind/config/module.config.php @@ -397,6 +397,7 @@ $config = [ 'VuFind\Mailer\Mailer' => 'VuFind\Mailer\Factory', 'VuFind\MetadataVocabulary\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory', 'VuFind\Net\IpAddressUtils' => 'Laminas\ServiceManager\Factory\InvokableFactory', + 'VuFind\Net\UserIpReader' => 'VuFind\Net\UserIpReaderFactory', 'VuFind\OAI\Server' => 'VuFind\OAI\ServerFactory', 'VuFind\OAI\Server\Auth' => 'VuFind\OAI\ServerFactory', 'VuFind\QRCode\Loader' => 'VuFind\QRCode\LoaderFactory', diff --git a/module/VuFind/src/VuFind/Log/Logger.php b/module/VuFind/src/VuFind/Log/Logger.php index 1cd90f5f116..599e6545ef1 100644 --- a/module/VuFind/src/VuFind/Log/Logger.php +++ b/module/VuFind/src/VuFind/Log/Logger.php @@ -28,6 +28,8 @@ namespace VuFind\Log; use Laminas\Log\Logger as BaseLogger; +use Traversable; +use VuFind\Net\UserIpReader; /** * This class wraps the BaseLogger class to allow for log verbosity @@ -47,6 +49,32 @@ class Logger extends BaseLogger */ protected $debugNeeded = false; + /** + * User IP address reader + * + * @var UserIpReader + */ + protected $userIpReader; + + /** + * Constructor + * + * Set options for a logger. Accepted options are: + * - writers: array of writers to add to this logger + * - exceptionhandler: if true register this logger as exceptionhandler + * - errorhandler: if true register this logger as errorhandler + * - vufind_ip_reader: UserIpReader object to use for IP lookups + * + * @param array|Traversable $options Configuration options + * + * @throws \Laminas\Log\Exception\InvalidArgumentException + */ + public function __construct($options = null) + { + parent::__construct($options); + $this->userIpReader = $options['vufind_ip_reader'] ?? null; + } + /** * Is one of the log writers listening for debug messages? (This is useful to * know, since some code can save time that would be otherwise wasted generating @@ -132,9 +160,10 @@ class Logger extends BaseLogger $prev = $prev->getPrevious(); } $referer = $server->get('HTTP_REFERER', 'none'); - $ip = $server->get('HTTP_X_FORWARDED_FOR') ?? $server->get('REMOTE_ADDR'); + $ipAddr = $this->userIpReader !== null + ? $this->userIpReader->getUserIp() : $server->get('REMOTE_ADDR'); $basicServer - = '(Server: IP = ' . $ip . ', ' + = '(Server: IP = ' . $ipAddr . ', ' . 'Referer = ' . $referer . ', ' . 'User Agent = ' . $server->get('HTTP_USER_AGENT') . ', ' diff --git a/module/VuFind/src/VuFind/Log/LoggerFactory.php b/module/VuFind/src/VuFind/Log/LoggerFactory.php index 6cc2cc11a04..1a36d0e38d1 100644 --- a/module/VuFind/src/VuFind/Log/LoggerFactory.php +++ b/module/VuFind/src/VuFind/Log/LoggerFactory.php @@ -381,7 +381,11 @@ class LoggerFactory implements FactoryInterface $proxy->setProxyInitializer(null); // Now build the actual service: - $wrapped = new $requestedName(); + $loggerOptions = [ + 'vufind_ip_reader' => + $container->get(\VuFind\Net\UserIpReader::class) + ]; + $wrapped = new $requestedName($loggerOptions); $this->configureLogger($container, $wrapped); }; $cfg = $container->get(\ProxyManager\Configuration::class); diff --git a/module/VuFind/src/VuFind/Net/UserIpReader.php b/module/VuFind/src/VuFind/Net/UserIpReader.php new file mode 100644 index 00000000000..ae84e23b39e --- /dev/null +++ b/module/VuFind/src/VuFind/Net/UserIpReader.php @@ -0,0 +1,94 @@ +<?php +/** + * Service to retrieve user IP address. + * + * PHP version 7 + * + * Copyright (C) Villanova University 2020. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Net + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +namespace VuFind\Net; + +use Laminas\Stdlib\Parameters; + +/** + * Service to retrieve user IP address. + * + * @category VuFind + * @package Net + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class UserIpReader +{ + /** + * Server parameters + * + * @var Parameters + */ + protected $server; + + /** + * Should we respect the X-Forwarded-For header? + * + * @var bool + */ + protected $allowForwardedIps; + + /** + * Constructor + * + * @param Parameters $server Server parameters + * @param bool $allowForwardedIps Should we respect the X-Forwarded-For + * header? + */ + public function __construct(Parameters $server, $allowForwardedIps = false) + { + $this->server = $server; + $this->allowForwardedIps = $allowForwardedIps; + } + + /** + * Get the active user's IP address. Returns null if no address can be found. + * + * @return string + */ + public function getUserIp() + { + if ($this->allowForwardedIps) { + // First check X-Real-IP; this is most accurate when set... + $realIp = $this->server->get('HTTP_X_REAL_IP'); + if (!empty($realIp)) { + return $realIp; + } + // Next, try X-Forwarded-For; if it's a comma-separated list, use + // only the first part. + $forwarded = $this->server->get('HTTP_X_FORWARDED_FOR'); + if (!empty($forwarded)) { + $parts = explode(',', $forwarded); + return trim($parts[0]); + } + } + // Default case: use REMOTE_ADDR directly. + return $this->server->get('REMOTE_ADDR'); + } +} diff --git a/module/VuFind/src/VuFind/Net/UserIpReaderFactory.php b/module/VuFind/src/VuFind/Net/UserIpReaderFactory.php new file mode 100644 index 00000000000..02dc9bc3564 --- /dev/null +++ b/module/VuFind/src/VuFind/Net/UserIpReaderFactory.php @@ -0,0 +1,73 @@ +<?php +/** + * Factory for instantiating UserIpReader. + * + * PHP version 7 + * + * Copyright (C) Villanova University 2020. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Net + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\Net; + +use Interop\Container\ContainerInterface; + +/** + * Factory for instantiating UserIpReader. + * + * @category VuFind + * @package Net + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class UserIpReaderFactory implements \Laminas\ServiceManager\Factory\FactoryInterface +{ + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + * + * @throws ServiceNotFoundException if unable to resolve the service. + * @throws ServiceNotCreatedException if an exception is raised when + * creating a service. + * @throws ContainerException if any other error occurs + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(ContainerInterface $container, $requestedName, + array $options = null + ) { + if (!empty($options)) { + throw new \Exception('Unexpected options passed to factory.'); + } + $config = $container->get(\VuFind\Config\PluginManager::class) + ->get('config'); + $allowForwardedIps = $config->Proxy->allow_forwarded_ips ?? false; + return new $requestedName( + $container->get('Request')->getServer(), + $allowForwardedIps + ); + } +} diff --git a/module/VuFind/src/VuFind/Resolver/Driver/Ezb.php b/module/VuFind/src/VuFind/Resolver/Driver/Ezb.php index 9dcbe6b3be3..5e5eadcd93b 100644 --- a/module/VuFind/src/VuFind/Resolver/Driver/Ezb.php +++ b/module/VuFind/src/VuFind/Resolver/Driver/Ezb.php @@ -38,6 +38,7 @@ namespace VuFind\Resolver\Driver; use DOMDocument; use DOMXpath; +use VuFind\Net\UserIpReader; /** * EZB Link Resolver Driver @@ -58,16 +59,26 @@ class Ezb extends AbstractBase */ protected $httpClient; + /** + * User IP address reader + * + * @var UserIpReader + */ + protected $userIpReader; + /** * Constructor * - * @param string $baseUrl Base URL for link resolver - * @param \Laminas\Http\Client $httpClient HTTP client + * @param string $baseUrl Base URL for link resolver + * @param \Laminas\Http\Client $httpClient HTTP client + * @param UserIpReader $userIpReader User IP address reader */ - public function __construct($baseUrl, \Laminas\Http\Client $httpClient) - { + public function __construct($baseUrl, \Laminas\Http\Client $httpClient, + UserIpReader $userIpReader = null + ) { parent::__construct($baseUrl); $this->httpClient = $httpClient; + $this->userIpReader = $userIpReader; } /** @@ -146,17 +157,20 @@ class Ezb extends AbstractBase foreach ($tmp as $current) { $tmp2 = explode('=', $current, 2); - $parsed[$tmp2[0]] = $tmp2[1]; + $parsed[$tmp2[0]] = $tmp2[1] ?? null; } // Downgrade 1.0 to 0.1 - if ($parsed['ctx_ver'] == 'Z39.88-2004') { + if ($parsed['ctx_ver'] ?? null == 'Z39.88-2004') { $openURL = $this->downgradeOpenUrl($parsed); } // make the request IP-based to allow automatic // indication on institution level - $openURL .= '&pid=client_ip%3D' . $_SERVER['REMOTE_ADDR']; + $ipAddr = $this->userIpReader !== null + ? $this->userIpReader->getUserIp() + : $_SERVER['REMOTE_ADDR']; + $openURL .= '&pid=client_ip%3D' . urlencode($ipAddr); // Make the call to the EZB and load results $url = $this->baseUrl . '?' . $openURL; diff --git a/module/VuFind/src/VuFind/Resolver/Driver/EzbFactory.php b/module/VuFind/src/VuFind/Resolver/Driver/EzbFactory.php new file mode 100644 index 00000000000..4c34431c82e --- /dev/null +++ b/module/VuFind/src/VuFind/Resolver/Driver/EzbFactory.php @@ -0,0 +1,63 @@ +<?php +/** + * Factory for EZB resolver driver. + * + * PHP version 7 + * + * Copyright (C) Villanova University 2020. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Resolver_Drivers + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\Resolver\Driver; + +use Interop\Container\ContainerInterface; + +/** + * Factory for EZB resolver driver. + * + * @category VuFind + * @package Resolver_Drivers + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class EzbFactory extends DriverWithHttpClientFactory +{ + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + * + * @throws ServiceNotFoundException if unable to resolve the service. + * @throws ServiceNotCreatedException if an exception is raised when + * creating a service. + * @throws ContainerException if any other error occurs + */ + public function __invoke(ContainerInterface $container, $requestedName, + array $options = null + ) { + $options = [$container->get(\VuFind\Net\UserIpReader::class)]; + return parent::__invoke($container, $requestedName, $options); + } +} diff --git a/module/VuFind/src/VuFind/Resolver/Driver/PluginManager.php b/module/VuFind/src/VuFind/Resolver/Driver/PluginManager.php index eb88efd26e9..08a677c9200 100644 --- a/module/VuFind/src/VuFind/Resolver/Driver/PluginManager.php +++ b/module/VuFind/src/VuFind/Resolver/Driver/PluginManager.php @@ -66,7 +66,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager Alma::class => DriverWithHttpClientFactory::class, Threesixtylink::class => DriverWithHttpClientFactory::class, Demo::class => InvokableFactory::class, - Ezb::class => DriverWithHttpClientFactory::class, + Ezb::class => EzbFactory::class, Sfx::class => DriverWithHttpClientFactory::class, Redi::class => DriverWithHttpClientFactory::class, Generic::class => AbstractBaseFactory::class, diff --git a/module/VuFind/src/VuFind/Role/PermissionProvider/IpRange.php b/module/VuFind/src/VuFind/Role/PermissionProvider/IpRange.php index 5cd0094c126..8791ef2ed38 100644 --- a/module/VuFind/src/VuFind/Role/PermissionProvider/IpRange.php +++ b/module/VuFind/src/VuFind/Role/PermissionProvider/IpRange.php @@ -32,6 +32,7 @@ namespace VuFind\Role\PermissionProvider; use Laminas\Stdlib\RequestInterface; use VuFind\Net\IpAddressUtils; +use VuFind\Net\UserIpReader; /** * IpRange permission provider for VuFind. @@ -60,16 +61,26 @@ class IpRange implements PermissionProviderInterface */ protected $ipAddressUtils; + /** + * User IP address reader + * + * @var UserIpReader + */ + protected $userIpReader; + /** * Constructor * - * @param RequestInterface $request Request object - * @param IpAddressUtils $ipUtils IpAddressUtils object + * @param RequestInterface $request Request object + * @param IpAddressUtils $ipUtils IpAddressUtils object + * @param UserIpReader $userIpReader User IP address reader */ - public function __construct(RequestInterface $request, IpAddressUtils $ipUtils) - { + public function __construct(RequestInterface $request, IpAddressUtils $ipUtils, + UserIpReader $userIpReader = null + ) { $this->request = $request; $this->ipAddressUtils = $ipUtils; + $this->userIpReader = $userIpReader; } /** @@ -82,13 +93,15 @@ class IpRange implements PermissionProviderInterface */ public function getPermissions($options) { - if (PHP_SAPI == 'cli') { - return []; - } // Check if any regex matches.... - $ip = $this->request->getServer()->get('HTTP_X_FORWARDED_FOR') - ?? $this->request->getServer()->get('REMOTE_ADDR'); - if ($this->ipAddressUtils->isInRange($ip, (array)$options)) { + if ($this->userIpReader !== null) { + $ipAddr = $this->userIpReader->getUserIp(); + } elseif (PHP_SAPI == 'cli') { + $ipAddr = null; + } else { + $ipAddr = $this->request->getServer()->get('REMOTE_ADDR'); + } + if ($this->ipAddressUtils->isInRange($ipAddr, (array)$options)) { // Match? Grant to all users (guest or logged in). return ['guest', 'loggedin']; } diff --git a/module/VuFind/src/VuFind/Role/PermissionProvider/IpRangeFactory.php b/module/VuFind/src/VuFind/Role/PermissionProvider/IpRangeFactory.php index d863110274d..5f697fbb8dd 100644 --- a/module/VuFind/src/VuFind/Role/PermissionProvider/IpRangeFactory.php +++ b/module/VuFind/src/VuFind/Role/PermissionProvider/IpRangeFactory.php @@ -64,7 +64,8 @@ class IpRangeFactory implements \Laminas\ServiceManager\Factory\FactoryInterface } return new $requestedName( $container->get('Request'), - $container->get(\VuFind\Net\IpAddressUtils::class) + $container->get(\VuFind\Net\IpAddressUtils::class), + $container->get(\VuFind\Net\UserIpReader::class) ); } } diff --git a/module/VuFind/src/VuFind/Role/PermissionProvider/IpRegEx.php b/module/VuFind/src/VuFind/Role/PermissionProvider/IpRegEx.php index 70249137f22..d86f3bc0fdd 100644 --- a/module/VuFind/src/VuFind/Role/PermissionProvider/IpRegEx.php +++ b/module/VuFind/src/VuFind/Role/PermissionProvider/IpRegEx.php @@ -28,6 +28,7 @@ namespace VuFind\Role\PermissionProvider; use Laminas\Http\PhpEnvironment\Request; +use VuFind\Net\UserIpReader; /** * IpRegEx permission provider for VuFind. @@ -47,14 +48,23 @@ class IpRegEx implements PermissionProviderInterface */ protected $request; + /** + * User IP address reader + * + * @var UserIpReader + */ + protected $userIpReader; + /** * Constructor * - * @param Request $request Request object + * @param Request $request Request object + * @param UserIpReader $userIpReader User IP address reader */ - public function __construct(Request $request) + public function __construct(Request $request, UserIpReader $userIpReader = null) { $this->request = $request; + $this->userIpReader = $userIpReader; } /** @@ -68,10 +78,15 @@ class IpRegEx implements PermissionProviderInterface public function getPermissions($options) { // Check if any regex matches.... - $ip = $this->request->getServer()->get('HTTP_X_FORWARDED_FOR') - ?? $this->request->getServer()->get('REMOTE_ADDR'); + if ($this->userIpReader !== null) { + $ipAddr = $this->userIpReader->getUserIp(); + } elseif (PHP_SAPI == 'cli') { + $ipAddr = null; + } else { + $ipAddr = $this->request->getServer()->get('REMOTE_ADDR'); + } foreach ((array)$options as $current) { - if (preg_match($current, $ip)) { + if (preg_match($current, $ipAddr)) { // Match? Grant to all users (guest or logged in). return ['guest', 'loggedin']; } diff --git a/module/VuFind/src/VuFind/Role/PermissionProvider/IpRegExFactory.php b/module/VuFind/src/VuFind/Role/PermissionProvider/IpRegExFactory.php new file mode 100644 index 00000000000..545d6b91d3c --- /dev/null +++ b/module/VuFind/src/VuFind/Role/PermissionProvider/IpRegExFactory.php @@ -0,0 +1,70 @@ +<?php +/** + * Factory for instantiating IpRegEx permission provider. + * + * PHP version 7 + * + * Copyright (C) Villanova University 2020. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Authorization + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\Role\PermissionProvider; + +use Interop\Container\ContainerInterface; + +/** + * Factory for instantiating IpRegEx permission provider. + * + * @category VuFind + * @package Authorization + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class IpRegExFactory implements \Laminas\ServiceManager\Factory\FactoryInterface +{ + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + * + * @throws ServiceNotFoundException if unable to resolve the service. + * @throws ServiceNotCreatedException if an exception is raised when + * creating a service. + * @throws ContainerException if any other error occurs + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(ContainerInterface $container, $requestedName, + array $options = null + ) { + if (!empty($options)) { + throw new \Exception('Unexpected options passed to factory.'); + } + return new $requestedName( + $container->get('Request'), + $container->get(\VuFind\Net\UserIpReader::class) + ); + } +} diff --git a/module/VuFind/src/VuFind/Role/PermissionProvider/PluginManager.php b/module/VuFind/src/VuFind/Role/PermissionProvider/PluginManager.php index 35786501578..fd89480367c 100644 --- a/module/VuFind/src/VuFind/Role/PermissionProvider/PluginManager.php +++ b/module/VuFind/src/VuFind/Role/PermissionProvider/PluginManager.php @@ -60,7 +60,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager */ protected $factories = [ IpRange::class => IpRangeFactory::class, - IpRegEx::class => InjectRequestFactory::class, + IpRegEx::class => IpRegExFactory::class, Role::class => \Laminas\ServiceManager\Factory\InvokableFactory::class, ServerParam::class => InjectRequestFactory::class, Shibboleth::class => ShibbolethFactory::class, diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Net/UserIpReaderTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Net/UserIpReaderTest.php new file mode 100644 index 00000000000..ff0ecb5a818 --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Net/UserIpReaderTest.php @@ -0,0 +1,130 @@ +<?php +/** + * UserIpReader Test Class + * + * PHP version 7 + * + * Copyright (C) Villanova University 2020. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Tests + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ +namespace VuFindTest\Net; + +use Laminas\Stdlib\Parameters; +use VuFind\Net\UserIpReader; + +/** + * UserIpReader Test Class + * + * @category VuFind + * @package Tests + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ +class UserIpReaderTest extends \VuFindTest\Unit\TestCase +{ + /** + * Test X-Real-IP; it should take priority over all other settings when + * forwarding is allowed. + * + * @return void + */ + public function testXRealIp() + { + $params = new Parameters( + [ + 'HTTP_X_REAL_IP' => '1.2.3.4', + 'HTTP_X_FORWARDED_FOR' => '5.6.7.8', + 'REMOTE_ADDR' => '127.0.0.1', + ] + ); + // Test appropriate behavior with forwarding enabled: + $reader1 = new UserIpReader($params, true); + $this->assertEquals('1.2.3.4', $reader1->getUserIp()); + // Test appropriate behavior with forwarding disabled: + $reader2 = new UserIpReader($params, false); + $this->assertEquals('127.0.0.1', $reader2->getUserIp()); + } + + /** + * Test X-Forwarded-For (single value); it should take priority over REMOTE_ADDR + * when forwarding is allowed. + * + * @return void + */ + public function testXForwardedForSingle() + { + $params = new Parameters( + [ + 'HTTP_X_FORWARDED_FOR' => '5.6.7.8', + 'REMOTE_ADDR' => '127.0.0.1', + ] + ); + // Test appropriate behavior with forwarding enabled: + $reader1 = new UserIpReader($params, true); + $this->assertEquals('5.6.7.8', $reader1->getUserIp()); + // Test appropriate behavior with forwarding disabled: + $reader2 = new UserIpReader($params, false); + $this->assertEquals('127.0.0.1', $reader2->getUserIp()); + } + + /** + * Test X-Forwarded-For (multi-value); the leftmost IP should take priority over + * REMOTE_ADDR when forwarding is allowed. + * + * @return void + */ + public function testXForwardedForMultiValued() + { + $params = new Parameters( + [ + 'HTTP_X_FORWARDED_FOR' => '5.6.7.8, 9.10.11.12', + 'REMOTE_ADDR' => '127.0.0.1', + ] + ); + // Test appropriate behavior with forwarding enabled: + $reader1 = new UserIpReader($params, true); + $this->assertEquals('5.6.7.8', $reader1->getUserIp()); + // Test appropriate behavior with forwarding disabled: + $reader2 = new UserIpReader($params, false); + $this->assertEquals('127.0.0.1', $reader2->getUserIp()); + } + + /** + * Test what happens when only REMOTE_ADDR is provided. + * + * @return void + */ + public function testXForwardedForSimple() + { + $params = new Parameters( + [ + 'REMOTE_ADDR' => '127.0.0.1', + ] + ); + // Test appropriate behavior with forwarding enabled: + $reader1 = new UserIpReader($params, true); + $this->assertEquals('127.0.0.1', $reader1->getUserIp()); + // Test appropriate behavior with forwarding disabled: + $reader2 = new UserIpReader($params, false); + $this->assertEquals('127.0.0.1', $reader2->getUserIp()); + } +} diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Resolver/Driver/EzbTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Resolver/Driver/EzbTest.php index 79a0fe13646..5bcccd54feb 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Resolver/Driver/EzbTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Resolver/Driver/EzbTest.php @@ -67,7 +67,7 @@ class EzbTest extends \VuFindTest\Unit\TestCase ]; /** - * Test + * Test link parsing * * @return void */ @@ -132,16 +132,34 @@ class EzbTest extends \VuFindTest\Unit\TestCase $this->assertEquals($result, $testResult); } + /** + * Test URL generation + * + * @return void + */ + public function testGetResolverUrl() + { + $ipAddr = '1.2.3.4'; + $connector = $this->createConnector(null, $ipAddr); + $expected = 'http://services.d-nb.de/fize-service/gvr/full.xml?' + . 'foo=bar&pid=client_ip%3D' . $ipAddr; + $this->assertEquals( + $expected, + $connector->getResolverUrl('foo=bar') + ); + } + /** * Create connector with fixture file. * * @param string $fixture Fixture file + * @param string $ipAddr Source IP address to simulate * * @return Connector * * @throws InvalidArgumentException Fixture file does not exist */ - protected function createConnector($fixture = null) + protected function createConnector($fixture = null, $ipAddr = '127.0.0.1') { $adapter = new TestAdapter(); if ($fixture) { @@ -158,12 +176,15 @@ class EzbTest extends \VuFindTest\Unit\TestCase $responseObj = HttpResponse::fromString($response); $adapter->setResponse($responseObj); } - $_SERVER['REMOTE_ADDR'] = "127.0.0.1"; - $client = new \Laminas\Http\Client(); $client->setAdapter($adapter); - $conn = new Ezb($this->openUrlConfig['OpenURL']['url'], $client); + $ipReader = $this->getMockBuilder(\VuFind\Net\UserIpReader::class) + ->disableOriginalConstructor() + ->getMock(); + $ipReader->expects($this->once())->method('getUserIp') + ->will($this->returnValue($ipAddr)); + $conn = new Ezb($this->openUrlConfig['OpenURL']['url'], $client, $ipReader); return $conn; } } diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/IpRangeTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/IpRangeTest.php new file mode 100644 index 00000000000..a4cb6d1a652 --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/IpRangeTest.php @@ -0,0 +1,114 @@ +<?php +/** + * IpRange ServerParam Test Class + * + * PHP version 7 + * + * Copyright (C) Villanova University 2020. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Tests + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ +namespace VuFindTest\Role\PermissionProvider; + +use VuFind\Net\IpAddressUtils; +use VuFind\Role\PermissionProvider\IpRange; + +/** + * IpRange ServerParam Test Class + * + * @category VuFind + * @package Tests + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ +class IpRangeTest extends \VuFindTest\Unit\TestCase +{ + /** + * Get a permission provider with the specified IP assigned. + * + * @param string $ipAddr IP address to send to provider. + * @param IpAddressUtils $utils IP address utils to use + * + * @return IpRegEx + */ + protected function getPermissionProvider($ipAddr, IpAddressUtils $utils) + { + $mockRequestClass = $this->getMockClass( + \Laminas\Http\PhpEnvironment\Request::class + ); + $mockIpReader = $this->getMockBuilder(\VuFind\Net\UserIpReader::class) + ->disableOriginalConstructor() + ->getMock(); + $mockIpReader->expects($this->once())->method('getUserIp') + ->will($this->returnValue($ipAddr)); + return new IpRange(new $mockRequestClass, $utils, $mockIpReader); + } + + /** + * Test a matching range. + * + * @return void + */ + public function testMatchingRange() + { + // In this example, we'll pass the IP address as the options to the provider. + // Note that we're not actually testing the range checking itself, because + // we're mocking out the IpAddressUtils; we're just confirming that the parts + // fit together correctly. + $ipAddr = '123.124.125.126'; + $utils = $this->getMockBuilder(IpAddressUtils::class) + ->disableOriginalConstructor() + ->getMock(); + $utils->expects($this->once())->method('isInRange') + ->with($this->equalTo($ipAddr), $this->equalTo([$ipAddr])) + ->will($this->returnValue(true)); + $provider = $this->getPermissionProvider($ipAddr, $utils); + $this->assertEquals( + ['guest', 'loggedin'], $provider->getPermissions($ipAddr) + ); + } + + /** + * Test an array of non-matching ranges. + * + * @return void + */ + public function testNonMatchingRegExArray() + { + // In this example, we'll pass the IP address as the options to the provider. + // Note that we're not actually testing the range checking itself, because + // we're mocking out the IpAddressUtils; we're just confirming that the parts + // fit together correctly. + $ipAddr = '123.124.125.126'; + $options = [ + '1.2.3.4-1.2.3.7', + '2.3.4.5', + ]; + $utils = $this->getMockBuilder(IpAddressUtils::class) + ->disableOriginalConstructor() + ->getMock(); + $utils->expects($this->once())->method('isInRange') + ->with($this->equalTo($ipAddr), $this->equalTo($options)) + ->will($this->returnValue(false)); + $provider = $this->getPermissionProvider($ipAddr, $utils); + $this->assertEquals([], $provider->getPermissions($options)); + } +} diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/IpRegExTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/IpRegExTest.php new file mode 100644 index 00000000000..eeb00f81df7 --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/IpRegExTest.php @@ -0,0 +1,91 @@ +<?php +/** + * IpRegEx ServerParam Test Class + * + * PHP version 7 + * + * Copyright (C) Villanova University 2020. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Tests + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ +namespace VuFindTest\Role\PermissionProvider; + +use VuFind\Role\PermissionProvider\IpRegEx; + +/** + * IpRegEx ServerParam Test Class + * + * @category VuFind + * @package Tests + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ +class IpRegExTest extends \VuFindTest\Unit\TestCase +{ + /** + * Get a permission provider with the specified IP assigned. + * + * @param string $ipAddr IP address to send to provider. + * + * @return IpRegEx + */ + protected function getPermissionProvider($ipAddr) + { + $mockRequestClass = $this->getMockClass( + \Laminas\Http\PhpEnvironment\Request::class + ); + $mockIpReader = $this->getMockBuilder(\VuFind\Net\UserIpReader::class) + ->disableOriginalConstructor() + ->getMock(); + $mockIpReader->expects($this->once())->method('getUserIp') + ->will($this->returnValue($ipAddr)); + return new IpRegEx(new $mockRequestClass, $mockIpReader); + } + + /** + * Test a matching regular expression. + * + * @return void + */ + public function testMatchingRegEx() + { + $regEx = '/123\.124\..*/'; + $provider = $this->getPermissionProvider('123.124.125.126'); + $this->assertEquals( + ['guest', 'loggedin'], $provider->getPermissions($regEx) + ); + } + + /** + * Test an array of non-matching regular expressions. + * + * @return void + */ + public function testNonMatchingRegExArray() + { + $regEx = [ + '/123\.124\..*/', + '/125\.126\..*/', + ]; + $provider = $this->getPermissionProvider('129.124.125.126'); + $this->assertEquals([], $provider->getPermissions($regEx)); + } +} -- GitLab