diff --git a/lib/apereo/phpcas/source/CAS/ServiceBaseUrl/AllowedListDiscovery.php b/lib/apereo/phpcas/source/CAS/ServiceBaseUrl/AllowedListDiscovery.php new file mode 100644 index 000000000..39d269c05 --- /dev/null +++ b/lib/apereo/phpcas/source/CAS/ServiceBaseUrl/AllowedListDiscovery.php @@ -0,0 +1,152 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 + * @link https://wiki.jasig.org/display/CASC/phpCAS + */ + + +/** + * Class that gets the service base URL of the PHP server by HTTP header + * discovery and allowlist check. This is used to generate service URL + * and PGT callback URL. + * + * @class CAS_ServiceBaseUrl_AllowedListDiscovery + * @category Authentication + * @package PhpCAS + * @author Henry Pan + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 + * @link https://wiki.jasig.org/display/CASC/phpCAS + */ + +class CAS_ServiceBaseUrl_AllowedListDiscovery +extends CAS_ServiceBaseUrl_Base +{ + private $_list = array(); + + public function __construct($list) { + if (is_array($list)) { + if (count($list) === 0) { + throw new CAS_InvalidArgumentException('$list should not be empty'); + } + foreach ($list as $value) { + $this->allow($value); + } + } else { + throw new CAS_TypeMismatchException($list, '$list', 'array'); + } + } + + /** + * Add a base URL to the allowed list. + * + * @param $url protocol, host name and port to add to the allowed list + * + * @return void + */ + public function allow($url) + { + $this->_list[] = $this->removeStandardPort($url); + } + + /** + * Check if the server name is allowed by configuration. + * + * @param $name server name to check + * + * @return bool whether the allowed list contains the server name + */ + protected function isAllowed($name) + { + return in_array($name, $this->_list); + } + + /** + * Discover the server name through HTTP headers. + * + * We read: + * - HTTP header X-Forwarded-Host + * - HTTP header X-Forwarded-Server and X-Forwarded-Port + * - HTTP header Host and SERVER_PORT + * - PHP SERVER_NAME (which can change based on the HTTP server used) + * + * The standard port will be omitted (80 for HTTP, 443 for HTTPS). + * + * @return string the discovered, unsanitized server protocol, hostname and port + */ + protected function discover() + { + $isHttps = $this->isHttps(); + $protocol = $isHttps ? 'https' : 'http'; + $protocol .= '://'; + if (!empty($_SERVER['HTTP_X_FORWARDED_HOST'])) { + // explode the host list separated by comma and use the first host + $hosts = explode(',', $_SERVER['HTTP_X_FORWARDED_HOST']); + // see rfc7239#5.3 and rfc7230#2.7.1: port is in HTTP_X_FORWARDED_HOST if non default + return $protocol . $hosts[0]; + } else if (!empty($_SERVER['HTTP_X_FORWARDED_SERVER'])) { + $server_url = $_SERVER['HTTP_X_FORWARDED_SERVER']; + } else { + if (empty($_SERVER['SERVER_NAME'])) { + $server_url = $_SERVER['HTTP_HOST']; + } else { + $server_url = $_SERVER['SERVER_NAME']; + } + } + if (!strpos($server_url, ':')) { + if (empty($_SERVER['HTTP_X_FORWARDED_PORT'])) { + $server_port = $_SERVER['SERVER_PORT']; + } else { + $ports = explode(',', $_SERVER['HTTP_X_FORWARDED_PORT']); + $server_port = $ports[0]; + } + + $server_url .= ':'; + $server_url .= $server_port; + } + return $protocol . $server_url; + } + + /** + * Get PHP server base URL. + * + * @return string the server protocol, hostname and port + */ + public function get() + { + phpCAS::traceBegin(); + $result = $this->removeStandardPort($this->discover()); + phpCAS::trace("Discovered server base URL: " . $result); + if ($this->isAllowed($result)) { + phpCAS::trace("Server base URL is allowed"); + phpCAS::traceEnd(true); + } else { + $result = $this->_list[0]; + phpCAS::trace("Server base URL is not allowed, using default: " . $result); + phpCAS::traceEnd(false); + } + return $result; + } +} diff --git a/lib/apereo/phpcas/source/CAS/ServiceBaseUrl/Base.php b/lib/apereo/phpcas/source/CAS/ServiceBaseUrl/Base.php new file mode 100644 index 000000000..6b4d3f306 --- /dev/null +++ b/lib/apereo/phpcas/source/CAS/ServiceBaseUrl/Base.php @@ -0,0 +1,98 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 + * @link https://wiki.jasig.org/display/CASC/phpCAS + */ + +/** + * Base class of CAS/ServiceBaseUrl that implements isHTTPS method. + * + * @class CAS_ServiceBaseUrl_Base + * @category Authentication + * @package PhpCAS + * @author Henry Pan + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 + * @link https://wiki.jasig.org/display/CASC/phpCAS + */ +abstract class CAS_ServiceBaseUrl_Base +implements CAS_ServiceBaseUrl_Interface +{ + + /** + * Get PHP server name. + * + * @return string the server hostname and port of the server + */ + abstract public function get(); + + /** + * Check whether HTTPS is used. + * + * This is used to construct the protocol in the URL. + * + * @return bool true if HTTPS is used + */ + public function isHttps() { + if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) { + return ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'); + } elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTOCOL'])) { + return ($_SERVER['HTTP_X_FORWARDED_PROTOCOL'] === 'https'); + } elseif ( isset($_SERVER['HTTPS']) + && !empty($_SERVER['HTTPS']) + && strcasecmp($_SERVER['HTTPS'], 'off') !== 0 + ) { + return true; + } + return false; + } + + /** + * Remove standard HTTP and HTTPS port for discovery and allowlist input. + * + * @param $url URL as https://domain:port without trailing slash + * @return standardized URL, or the original URL + * @throws CAS_InvalidArgumentException if the URL does not include the protocol + */ + protected function removeStandardPort($url) { + if (strpos($url, "://") === false) { + throw new CAS_InvalidArgumentException( + "Configured base URL should include the protocol string: " . $url); + } + + $url = rtrim($url, '/'); + + if (strpos($url, "https://") === 0 && substr_compare($url, ':443', -4) === 0) { + return substr($url, 0, -4); + } + + if (strpos($url, "http://") === 0 && substr_compare($url, ':80', -3) === 0) { + return substr($url, 0, -3); + } + + return $url; + } + +} diff --git a/lib/apereo/phpcas/source/CAS/ServiceBaseUrl/Interface.php b/lib/apereo/phpcas/source/CAS/ServiceBaseUrl/Interface.php new file mode 100644 index 000000000..77cb2bdc0 --- /dev/null +++ b/lib/apereo/phpcas/source/CAS/ServiceBaseUrl/Interface.php @@ -0,0 +1,61 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 + * @link https://wiki.jasig.org/display/CASC/phpCAS + */ + +/** + * An interface for classes that gets the server name of the PHP server. + * This is used to generate service URL and PGT callback URL. + * + * @class CAS_ServiceBaseUrl_Interface + * @category Authentication + * @package PhpCAS + * @author Henry Pan + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 + * @link https://wiki.jasig.org/display/CASC/phpCAS + */ +interface CAS_ServiceBaseUrl_Interface +{ + + /** + * Get PHP HTTP protocol and server name. + * + * @return string protocol, server hostname, and optionally port, + * without trailing slash (https://localhost:8443) + */ + public function get(); + + /** + * Check whether HTTPS is used. + * + * This is used to construct the protocol in the URL. + * + * @return bool true if HTTPS is used + */ + public function isHttps(); + +} diff --git a/lib/apereo/phpcas/source/CAS/ServiceBaseUrl/Static.php b/lib/apereo/phpcas/source/CAS/ServiceBaseUrl/Static.php new file mode 100644 index 000000000..577ecb979 --- /dev/null +++ b/lib/apereo/phpcas/source/CAS/ServiceBaseUrl/Static.php @@ -0,0 +1,69 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 + * @link https://wiki.jasig.org/display/CASC/phpCAS + */ + + +/** + * Class that gets the server name of the PHP server by statically set + * hostname and port. This is used to generate service URL and PGT + * callback URL. + * + * @class CAS_ServiceBaseUrl_Static + * @category Authentication + * @package PhpCAS + * @author Henry Pan + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 + * @link https://wiki.jasig.org/display/CASC/phpCAS + */ + +class CAS_ServiceBaseUrl_Static +extends CAS_ServiceBaseUrl_Base +{ + private $_name = null; + + public function __construct($name) { + if (is_string($name)) { + $this->_name = $this->removeStandardPort($name); + } else { + throw new CAS_TypeMismatchException($name, '$name', 'string'); + } + } + + /** + * Get the server name through static config. + * + * @return string the server hostname and port of the server configured + */ + public function get() + { + phpCAS::traceBegin(); + phpCAS::trace("Returning static server name: " . $this->_name); + phpCAS::traceEnd(true); + return $this->_name; + } +} \ No newline at end of file diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index e4bf469ec..cb832eea6 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -145,6 +145,10 @@ return array( 'CAS_Request_Exception' => $vendorDir . '/apereo/phpcas/source/CAS/Request/Exception.php', 'CAS_Request_MultiRequestInterface' => $vendorDir . '/apereo/phpcas/source/CAS/Request/MultiRequestInterface.php', 'CAS_Request_RequestInterface' => $vendorDir . '/apereo/phpcas/source/CAS/Request/RequestInterface.php', + 'CAS_ServiceBaseUrl_AllowedListDiscovery' => $vendorDir . '/apereo/phpcas/source/CAS/ServiceBaseUrl/AllowedListDiscovery.php', + 'CAS_ServiceBaseUrl_Base' => $vendorDir . '/apereo/phpcas/source/CAS/ServiceBaseUrl/Base.php', + 'CAS_ServiceBaseUrl_Interface' => $vendorDir . '/apereo/phpcas/source/CAS/ServiceBaseUrl/Interface.php', + 'CAS_ServiceBaseUrl_Static' => $vendorDir . '/apereo/phpcas/source/CAS/ServiceBaseUrl/Static.php', 'CAS_Session_PhpSession' => $vendorDir . '/apereo/phpcas/source/CAS/Session/PhpSession.php', 'CAS_TypeMismatchException' => $vendorDir . '/apereo/phpcas/source/CAS/TypeMismatchException.php', 'CLILikeWebPage' => $baseDir . '/sources/Application/WebPage/CLILikeWebPage.php', @@ -1635,8 +1639,6 @@ return array( 'Symfony\\Bridge\\Twig\\Node\\StopwatchNode' => $vendorDir . '/symfony/twig-bridge/Node/StopwatchNode.php', 'Symfony\\Bridge\\Twig\\Node\\TransDefaultDomainNode' => $vendorDir . '/symfony/twig-bridge/Node/TransDefaultDomainNode.php', 'Symfony\\Bridge\\Twig\\Node\\TransNode' => $vendorDir . '/symfony/twig-bridge/Node/TransNode.php', - 'Symfony\\Bridge\\Twig\\Test\\FormLayoutTestCase' => $vendorDir . '/symfony/twig-bridge/Test/FormLayoutTestCase.php', - 'Symfony\\Bridge\\Twig\\Test\\Traits\\RuntimeLoaderProvider' => $vendorDir . '/symfony/twig-bridge/Test/Traits/RuntimeLoaderProvider.php', 'Symfony\\Bridge\\Twig\\TokenParser\\DumpTokenParser' => $vendorDir . '/symfony/twig-bridge/TokenParser/DumpTokenParser.php', 'Symfony\\Bridge\\Twig\\TokenParser\\FormThemeTokenParser' => $vendorDir . '/symfony/twig-bridge/TokenParser/FormThemeTokenParser.php', 'Symfony\\Bridge\\Twig\\TokenParser\\StopwatchTokenParser' => $vendorDir . '/symfony/twig-bridge/TokenParser/StopwatchTokenParser.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index e8b0f9a93..da00cbf5b 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -503,6 +503,10 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'CAS_Request_Exception' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Request/Exception.php', 'CAS_Request_MultiRequestInterface' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Request/MultiRequestInterface.php', 'CAS_Request_RequestInterface' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Request/RequestInterface.php', + 'CAS_ServiceBaseUrl_AllowedListDiscovery' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ServiceBaseUrl/AllowedListDiscovery.php', + 'CAS_ServiceBaseUrl_Base' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ServiceBaseUrl/Base.php', + 'CAS_ServiceBaseUrl_Interface' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ServiceBaseUrl/Interface.php', + 'CAS_ServiceBaseUrl_Static' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ServiceBaseUrl/Static.php', 'CAS_Session_PhpSession' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Session/PhpSession.php', 'CAS_TypeMismatchException' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/TypeMismatchException.php', 'CLILikeWebPage' => __DIR__ . '/../..' . '/sources/Application/WebPage/CLILikeWebPage.php', @@ -1993,8 +1997,6 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Symfony\\Bridge\\Twig\\Node\\StopwatchNode' => __DIR__ . '/..' . '/symfony/twig-bridge/Node/StopwatchNode.php', 'Symfony\\Bridge\\Twig\\Node\\TransDefaultDomainNode' => __DIR__ . '/..' . '/symfony/twig-bridge/Node/TransDefaultDomainNode.php', 'Symfony\\Bridge\\Twig\\Node\\TransNode' => __DIR__ . '/..' . '/symfony/twig-bridge/Node/TransNode.php', - 'Symfony\\Bridge\\Twig\\Test\\FormLayoutTestCase' => __DIR__ . '/..' . '/symfony/twig-bridge/Test/FormLayoutTestCase.php', - 'Symfony\\Bridge\\Twig\\Test\\Traits\\RuntimeLoaderProvider' => __DIR__ . '/..' . '/symfony/twig-bridge/Test/Traits/RuntimeLoaderProvider.php', 'Symfony\\Bridge\\Twig\\TokenParser\\DumpTokenParser' => __DIR__ . '/..' . '/symfony/twig-bridge/TokenParser/DumpTokenParser.php', 'Symfony\\Bridge\\Twig\\TokenParser\\FormThemeTokenParser' => __DIR__ . '/..' . '/symfony/twig-bridge/TokenParser/FormThemeTokenParser.php', 'Symfony\\Bridge\\Twig\\TokenParser\\StopwatchTokenParser' => __DIR__ . '/..' . '/symfony/twig-bridge/TokenParser/StopwatchTokenParser.php', diff --git a/lib/composer/installed.php b/lib/composer/installed.php index 0e22c3555..dce3a9bdb 100644 --- a/lib/composer/installed.php +++ b/lib/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'combodo/itop', 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => '4fb581c784061ae348ea6d19e70248ec1bfeb310', + 'reference' => '7d3ff60eb26aec3f18c9cd88dc7e7ebcd1f93a52', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -22,7 +22,7 @@ 'combodo/itop' => array( 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => '4fb581c784061ae348ea6d19e70248ec1bfeb310', + 'reference' => '7d3ff60eb26aec3f18c9cd88dc7e7ebcd1f93a52', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(),