<?php namespace SimpleSAML\Test\Utils; use PHPUnit\Framework\TestCase; use SimpleSAML\Test\Utils\ClearStateTestCase; use SimpleSAML\Utils\HTTP; use SimpleSAML\Configuration; class HTTPTest extends ClearStateTestCase { /** * Set up the environment ($_SERVER) populating the typical variables from a given URL. * * @param string $url The URL to use as the current one. * @return void */ private function setupEnvFromURL($url) { $addr = parse_url($url); $_SERVER['HTTP_HOST'] = $addr['host']; $_SERVER['SERVER_NAME'] = $addr['host']; if ($addr['scheme'] === 'https') { $_SERVER['HTTPS'] = 'on'; $default_port = '443'; } else { unset($_SERVER['HTTPS']); $default_port = '80'; } $_SERVER['SERVER_PORT'] = $default_port; if (isset($addr['port']) && strval($addr['port']) !== $default_port) { $_SERVER['SERVER_PORT'] = strval($addr['port']); } $_SERVER['REQUEST_URI'] = $addr['path'] . '?' . $addr['query']; } /** * Test SimpleSAML\Utils\HTTP::addURLParameters(). * @return void * @psalm-suppress InvalidArgument * @deprecated Can be removed in 2.0 when codebase if fully typehinted */ public function testAddURLParametersInvalidURL() { $this->expectException(\InvalidArgumentException::class); HTTP::addURLParameters([], []); } /** * Test SimpleSAML\Utils\HTTP::addURLParameters(). * @return void * @psalm-suppress InvalidArgument * @deprecated Can be removed in 2.0 when codebase if fully typehinted */ public function testAddURLParametersInvalidParameters() { $this->expectException(\InvalidArgumentException::class); HTTP::addURLParameters('string', 'string'); } /** * Test SimpleSAML\Utils\HTTP::addURLParameters(). * @return void */ public function testAddURLParameters() { $url = 'http://example.com/'; $params = [ 'foo' => 'bar', 'bar' => 'foo', ]; $this->assertEquals($url . '?foo=bar&bar=foo', HTTP::addURLParameters($url, $params)); $url = 'http://example.com/?'; $params = [ 'foo' => 'bar', 'bar' => 'foo', ]; $this->assertEquals($url . 'foo=bar&bar=foo', HTTP::addURLParameters($url, $params)); $url = 'http://example.com/?foo=bar'; $params = [ 'bar' => 'foo', ]; $this->assertEquals($url . '&bar=foo', HTTP::addURLParameters($url, $params)); } /** * Test SimpleSAML\Utils\HTTP::guessBasePath(). * @return void */ public function testGuessBasePath() { $original = $_SERVER; $_SERVER['REQUEST_URI'] = '/simplesaml/module.php'; $_SERVER['SCRIPT_FILENAME'] = '/some/path/simplesamlphp/www/module.php'; $this->assertEquals('/simplesaml/', HTTP::guessBasePath()); $_SERVER['REQUEST_URI'] = '/simplesaml/module.php/some/path/to/other/script.php'; $_SERVER['SCRIPT_FILENAME'] = '/some/path/simplesamlphp/www/module.php'; $this->assertEquals('/simplesaml/', HTTP::guessBasePath()); $_SERVER['REQUEST_URI'] = '/module.php'; $_SERVER['SCRIPT_FILENAME'] = '/some/path/simplesamlphp/www/module.php'; $this->assertEquals('/', HTTP::guessBasePath()); $_SERVER['REQUEST_URI'] = '/module.php/some/path/to/other/script.php'; $_SERVER['SCRIPT_FILENAME'] = '/some/path/simplesamlphp/www/module.php'; $this->assertEquals('/', HTTP::guessBasePath()); $_SERVER['REQUEST_URI'] = '/some/path/module.php'; $_SERVER['SCRIPT_FILENAME'] = '/some/path/simplesamlphp/www/module.php'; $this->assertEquals('/some/path/', HTTP::guessBasePath()); $_SERVER['REQUEST_URI'] = '/some/path/module.php/some/path/to/other/script.php'; $_SERVER['SCRIPT_FILENAME'] = '/some/path/simplesamlphp/www/module.php'; $this->assertEquals('/some/path/', HTTP::guessBasePath()); $_SERVER['REQUEST_URI'] = '/some/dir/in/www/script.php'; $_SERVER['SCRIPT_FILENAME'] = '/some/path/simplesamlphp/www/some/dir/in/www/script.php'; $this->assertEquals('/', HTTP::guessBasePath()); $_SERVER['REQUEST_URI'] = '/simplesaml/some/dir/in/www/script.php'; $_SERVER['SCRIPT_FILENAME'] = '/some/path/simplesamlphp/www/some/dir/in/www/script.php'; $this->assertEquals('/simplesaml/', HTTP::guessBasePath()); $_SERVER = $original; } /** * Test SimpleSAML\Utils\HTTP::getSelfHost() with and without custom port. * @return void */ public function testGetSelfHost() { $original = $_SERVER; Configuration::loadFromArray([ 'baseurlpath' => '', ], '[ARRAY]', 'simplesaml'); $_SERVER['SERVER_PORT'] = '80'; $this->assertEquals('localhost', HTTP::getSelfHost()); $_SERVER['SERVER_PORT'] = '3030'; $this->assertEquals('localhost', HTTP::getSelfHost()); $_SERVER = $original; } /** * Test SimpleSAML\Utils\HTTP::getSelfHostWithPort(), with and without custom port. * @return void */ public function testGetSelfHostWithPort() { $original = $_SERVER; Configuration::loadFromArray([ 'baseurlpath' => '', ], '[ARRAY]', 'simplesaml'); // standard port for HTTP $_SERVER['SERVER_PORT'] = '80'; $this->assertEquals('localhost', HTTP::getSelfHostWithNonStandardPort()); // non-standard port $_SERVER['SERVER_PORT'] = '3030'; $this->assertEquals('localhost:3030', HTTP::getSelfHostWithNonStandardPort()); // standard port for HTTPS $_SERVER['HTTPS'] = 'on'; $_SERVER['SERVER_PORT'] = '443'; $this->assertEquals('localhost', HTTP::getSelfHostWithNonStandardPort()); $_SERVER = $original; } /** * Test SimpleSAML\Utils\HTTP::getSelfURL(). * @return void */ public function testGetSelfURLMethods() { $original = $_SERVER; /* * Test a URL pointing to a script that's not part of the public interface. This allows us to test calls to * getSelfURL() from scripts outside of SimpleSAMLphp */ Configuration::loadFromArray([ 'baseurlpath' => 'http://example.com/simplesaml/', ], '[ARRAY]', 'simplesaml'); $url = 'https://example.com/app/script.php/some/path?foo=bar'; $this->setupEnvFromURL($url); $_SERVER['SCRIPT_FILENAME'] = '/var/www/app/script.php'; $this->assertEquals($url, HTTP::getSelfURL()); $this->assertEquals('https://example.com', HTTP::getSelfURLHost()); $this->assertEquals('https://example.com/app/script.php/some/path', HTTP::getSelfURLNoQuery()); $this->assertTrue(HTTP::isHTTPS()); $this->assertEquals('https://' . HTTP::getSelfHostWithNonStandardPort(), HTTP::getSelfURLHost()); // test a request URI that doesn't match the current script $cfg = Configuration::loadFromArray([ 'baseurlpath' => 'https://example.org/simplesaml/', ], '[ARRAY]', 'simplesaml'); $baseDir = $cfg->getBaseDir(); $_SERVER['SCRIPT_FILENAME'] = $baseDir . 'www/module.php'; $this->setupEnvFromURL('http://www.example.com/protected/resource.asp?foo=bar'); $this->assertEquals('http://www.example.com/protected/resource.asp?foo=bar', HTTP::getSelfURL()); $this->assertEquals('http://www.example.com', HTTP::getSelfURLHost()); $this->assertEquals('http://www.example.com/protected/resource.asp', HTTP::getSelfURLNoQuery()); $this->assertFalse(HTTP::isHTTPS()); $this->assertEquals('example.org', HTTP::getSelfHostWithNonStandardPort()); $this->assertEquals('http://www.example.com', HTTP::getSelfURLHost()); // test a valid, full URL, based on a full URL in the configuration Configuration::loadFromArray([ 'baseurlpath' => 'https://example.com/simplesaml/', ], '[ARRAY]', 'simplesaml'); $this->setupEnvFromURL('http://www.example.org/module.php/module/file.php?foo=bar'); $this->assertEquals( 'https://example.com/simplesaml/module.php/module/file.php?foo=bar', HTTP::getSelfURL() ); $this->assertEquals('https://example.com', HTTP::getSelfURLHost()); $this->assertEquals('https://example.com/simplesaml/module.php/module/file.php', HTTP::getSelfURLNoQuery()); $this->assertTrue(HTTP::isHTTPS()); $this->assertEquals('https://' . HTTP::getSelfHostWithNonStandardPort(), HTTP::getSelfURLHost()); // test a valid, full URL, based on a full URL *without* a trailing slash in the configuration Configuration::loadFromArray([ 'baseurlpath' => 'https://example.com/simplesaml', ], '[ARRAY]', 'simplesaml'); $this->assertEquals( 'https://example.com/simplesaml/module.php/module/file.php?foo=bar', HTTP::getSelfURL() ); $this->assertEquals('https://example.com', HTTP::getSelfURLHost()); $this->assertEquals('https://example.com/simplesaml/module.php/module/file.php', HTTP::getSelfURLNoQuery()); $this->assertTrue(HTTP::isHTTPS()); $this->assertEquals('https://' . HTTP::getSelfHostWithNonStandardPort(), HTTP::getSelfURLHost()); // test a valid, full URL, based on a full URL *without* a path in the configuration Configuration::loadFromArray([ 'baseurlpath' => 'https://example.com', ], '[ARRAY]', 'simplesaml'); $this->assertEquals( 'https://example.com/module.php/module/file.php?foo=bar', HTTP::getSelfURL() ); $this->assertEquals('https://example.com', HTTP::getSelfURLHost()); $this->assertEquals('https://example.com/module.php/module/file.php', HTTP::getSelfURLNoQuery()); $this->assertTrue(HTTP::isHTTPS()); $this->assertEquals('https://' . HTTP::getSelfHostWithNonStandardPort(), HTTP::getSelfURLHost()); // test a valid, full URL, based on a relative path in the configuration Configuration::loadFromArray([ 'baseurlpath' => '/simplesaml/', ], '[ARRAY]', 'simplesaml'); $this->setupEnvFromURL('http://www.example.org/simplesaml/module.php/module/file.php?foo=bar'); $this->assertEquals( 'http://www.example.org/simplesaml/module.php/module/file.php?foo=bar', HTTP::getSelfURL() ); $this->assertEquals('http://www.example.org', HTTP::getSelfURLHost()); $this->assertEquals('http://www.example.org/simplesaml/module.php/module/file.php', HTTP::getSelfURLNoQuery()); $this->assertFalse(HTTP::isHTTPS()); $this->assertEquals('http://' . HTTP::getSelfHostWithNonStandardPort(), HTTP::getSelfURLHost()); // test a valid, full URL, based on a relative path in the configuration and a non standard port Configuration::loadFromArray([ 'baseurlpath' => '/simplesaml/', ], '[ARRAY]', 'simplesaml'); $this->setupEnvFromURL('http://example.org:8080/simplesaml/module.php/module/file.php?foo=bar'); $this->assertEquals( 'http://example.org:8080/simplesaml/module.php/module/file.php?foo=bar', HTTP::getSelfURL() ); $this->assertEquals('http://example.org:8080', HTTP::getSelfURLHost()); $this->assertEquals('http://example.org:8080/simplesaml/module.php/module/file.php', HTTP::getSelfURLNoQuery()); $this->assertFalse(HTTP::isHTTPS()); $this->assertEquals('http://' . HTTP::getSelfHostWithNonStandardPort(), HTTP::getSelfURLHost()); // test a valid, full URL, based on a relative path in the configuration, a non standard port and HTTPS Configuration::loadFromArray([ 'baseurlpath' => '/simplesaml/', ], '[ARRAY]', 'simplesaml'); $this->setupEnvFromURL('https://example.org:8080/simplesaml/module.php/module/file.php?foo=bar'); $this->assertEquals( 'https://example.org:8080/simplesaml/module.php/module/file.php?foo=bar', HTTP::getSelfURL() ); $this->assertEquals('https://example.org:8080', HTTP::getSelfURLHost()); $this->assertEquals( 'https://example.org:8080/simplesaml/module.php/module/file.php', HTTP::getSelfURLNoQuery() ); $this->assertTrue(HTTP::isHTTPS()); $this->assertEquals('https://' . HTTP::getSelfHostWithNonStandardPort(), HTTP::getSelfURLHost()); $_SERVER = $original; } /** * Test SimpleSAML\Utils\HTTP::checkURLAllowed(), without regex. * @return void */ public function testCheckURLAllowedWithoutRegex() { $original = $_SERVER; Configuration::loadFromArray([ 'trusted.url.domains' => ['sp.example.com', 'app.example.com'], 'trusted.url.regex' => false, ], '[ARRAY]', 'simplesaml'); $_SERVER['REQUEST_URI'] = '/module.php'; $allowed = [ 'https://sp.example.com/', 'http://sp.example.com/', 'https://app.example.com/', 'http://app.example.com/', ]; foreach ($allowed as $url) { $this->assertEquals(HTTP::checkURLAllowed($url), $url); } $this->expectException(\SimpleSAML\Error\Exception::class); HTTP::checkURLAllowed('https://evil.com'); $_SERVER = $original; } /** * Test SimpleSAML\Utils\HTTP::checkURLAllowed(), with regex. * @return void */ public function testCheckURLAllowedWithRegex() { $original = $_SERVER; Configuration::loadFromArray([ 'trusted.url.domains' => ['.*\.example\.com'], 'trusted.url.regex' => true, ], '[ARRAY]', 'simplesaml'); $_SERVER['REQUEST_URI'] = '/module.php'; $allowed = [ 'https://sp.example.com/', 'http://sp.example.com/', 'https://app1.example.com/', 'http://app1.example.com/', 'https://app2.example.com/', 'http://app2.example.com/', ]; foreach ($allowed as $url) { $this->assertEquals(HTTP::checkURLAllowed($url), $url); } $this->expectException(\SimpleSAML\Error\Exception::class); HTTP::checkURLAllowed('https://evil.com'); $_SERVER = $original; } /** * Test SimpleSAML\Utils\HTTP::getServerPort(). * @return void */ public function testGetServerPort() { $original = $_SERVER; // Test HTTP + non-standard port $_SERVER['HTTPS'] = 'off'; $_SERVER['SERVER_PORT'] = '3030'; $this->assertEquals(HTTP::getServerPort(), ':3030'); // Test HTTP + standard port $_SERVER['SERVER_PORT'] = '80'; $this->assertEquals(HTTP::getServerPort(), ''); // Test HTTP + standard integer port $_SERVER['SERVER_PORT'] = 80; $this->assertEquals(HTTP::getServerPort(), ''); // Test HTTP + without port unset($_SERVER['SERVER_PORT']); $this->assertEquals(HTTP::getServerPort(), ''); // Test HTTPS + non-standard port $_SERVER['HTTPS'] = 'on'; $_SERVER['SERVER_PORT'] = '3030'; $this->assertEquals(HTTP::getServerPort(), ':3030'); // Test HTTPS + non-standard integer port $_SERVER['SERVER_PORT'] = 3030; $this->assertEquals(HTTP::getServerPort(), ':3030'); // Test HTTPS + standard port $_SERVER['SERVER_PORT'] = '443'; $this->assertEquals(HTTP::getServerPort(), ''); // Test HTTPS + without port unset($_SERVER['SERVER_PORT']); $this->assertEquals(HTTP::getServerPort(), ''); $_SERVER = $original; } /** * Test SimpleSAML\Utils\HTTP::checkURLAllowed(), with the regex as a * subdomain of an evil domain. * @return void */ public function testCheckURLAllowedWithRegexWithoutDelimiters() { $original = $_SERVER; Configuration::loadFromArray([ 'trusted.url.domains' => ['app\.example\.com'], 'trusted.url.regex' => true, ], '[ARRAY]', 'simplesaml'); $_SERVER['REQUEST_URI'] = '/module.php'; $this->expectException(\SimpleSAML\Error\Exception::class); HTTP::checkURLAllowed('https://app.example.com.evil.com'); $_SERVER = $original; } /** * @covers SimpleSAML\Utils\HTTP::getFirstPathElement() * @return void */ public function testGetFirstPathElement() { $original = $_SERVER; $_SERVER['SCRIPT_NAME'] = '/test/tmp.php'; $this->assertEquals(HTTP::getFirstPathElement(), '/test'); $this->assertEquals(HTTP::getFirstPathElement(false), 'test'); $_SERVER = $original; } /** * @covers SimpleSAML\Utils\HTTP::setCookie() * @runInSeparateProcess * @requires extension xdebug * @return void */ public function testSetCookie() { $original = $_SERVER; Configuration::loadFromArray([ 'baseurlpath' => 'https://example.com/simplesaml/', ], '[ARRAY]', 'simplesaml'); $url = 'https://example.com/a?b=c'; $this->setupEnvFromURL($url); HTTP::setCookie( 'TestCookie', 'value%20', [ 'expire' => 2147483640, 'path' => '/ourPath', 'domain' => 'example.com', 'secure' => true, 'httponly' => true ] ); HTTP::setCookie( 'RawCookie', 'value%20', [ 'lifetime' => 100, 'path' => '/ourPath', 'domain' => 'example.com', 'secure' => true, 'httponly' => true, 'raw' => true ] ); $headers = xdebug_get_headers(); $this->assertContains('TestCookie=value%2520;', $headers[0]); $this->assertRegExp('/\b[Ee]xpires=[Tt]ue/', $headers[0]); $this->assertRegExp('/\b[Pp]ath=\/ourPath(;|$)/', $headers[0]); $this->assertRegExp('/\b[Dd]omain=example.com(;|$)/', $headers[0]); $this->assertRegExp('/\b[Ss]ecure(;|$)/', $headers[0]); $this->assertRegExp('/\b[Hh]ttp[Oo]nly(;|$)/', $headers[0]); $this->assertContains('RawCookie=value%20;', $headers[1]); $this->assertRegExp('/\b[Ee]xpires=([Mm]on|[Tt]ue|[Ww]ed|[Tt]hu|[Ff]ri|[Ss]at|[Ss]un)/', $headers[1]); $this->assertRegExp('/\b[Pp]ath=\/ourPath(;|$)/', $headers[1]); $this->assertRegExp('/\b[Dd]omain=example.com(;|$)/', $headers[1]); $this->assertRegExp('/\b[Ss]ecure(;|$)/', $headers[1]); $this->assertRegExp('/\b[Hh]ttp[Oo]nly(;|$)/', $headers[1]); $_SERVER = $original; } /** * @covers SimpleSAML\Utils\HTTP::setCookie() * @return void */ public function testSetCookieInsecure() { $this->expectException(\SimpleSAML\Error\CannotSetCookie::class); $original = $_SERVER; Configuration::loadFromArray([ 'baseurlpath' => 'http://example.com/simplesaml/', ], '[ARRAY]', 'simplesaml'); $url = 'http://example.com/a?b=c'; $this->setupEnvFromURL($url); HTTP::setCookie('testCookie', 'value', ['secure' => true], true); $_SERVER = $original; } /** * @covers SimpleSAML\Utils\HTTP::setCookie() * @runInSeparateProcess * @requires extension xdebug * @return void */ public function testSetCookieSameSite() { HTTP::setCookie('SSNull', 'value', ['samesite' => null]); HTTP::setCookie('SSNone', 'value', ['samesite' => 'None']); HTTP::setCookie('SSLax', 'value', ['samesite' => 'Lax']); HTTP::setCookie('SSStrict', 'value', ['samesite' => 'Strict']); $headers = xdebug_get_headers(); $this->assertNotRegExp('/\b[Ss]ame[Ss]ite=/', $headers[0]); $this->assertRegExp('/\b[Ss]ame[Ss]ite=None(;|$)/', $headers[1]); $this->assertRegExp('/\b[Ss]ame[Ss]ite=Lax(;|$)/', $headers[2]); $this->assertRegExp('/\b[Ss]ame[Ss]ite=Strict(;|$)/', $headers[3]); } }