From 340f4eae9672e123cd920494cc8374c9b3bfa082 Mon Sep 17 00:00:00 2001 From: Christian Kuhn Date: Sun, 11 Feb 2018 10:41:20 +0100 Subject: [PATCH] [FEATURE] Add normalized server parameters to PSR-7 requests To slowly substitute GeneralUtility::getIndpEnv() with a better API, a new class is introduced that calculates all normalized server parameters. The object is added as PSR-7 request attribute in a frontend and backend middleware. For a transition phase, the request is made available as $GLOBALS['TYPO3_REQUEST'] until enough core code has been refactored to get rid of this again. Resolves: #83736 Releases: master Change-Id: I96c8cb6dda4cc38bbb51b64439b8e81f2c00d7ac Reviewed-on: https://review.typo3.org/55506 Tested-by: TYPO3com Reviewed-by: Georg Ringer Reviewed-by: Benjamin Franzke Tested-by: Benjamin Franzke Reviewed-by: Christian Kuhn Tested-by: Christian Kuhn Reviewed-by: Stefan Neufeind Reviewed-by: Benni Mack Tested-by: Benni Mack --- .../Configuration/RequestMiddlewares.php | 8 +- .../core/Classes/Http/NormalizedParams.php | 810 ++++++++++++ .../Middleware/NormalizedParamsAttribute.php | 51 + ...n-83736-DeprecatedGlobalsTYPO3_REQUEST.rst | 38 + ...PSR-7RequestsWithTYPO3ServerParameters.rst | 72 ++ .../Tests/Unit/Http/NormalizedParamsTest.php | 1100 +++++++++++++++++ .../Tests/Unit/Utility/GeneralUtilityTest.php | 2 +- .../Configuration/RequestMiddlewares.php | 8 +- .../Php/ArrayGlobalMatcher.php | 5 + 9 files changed, 2091 insertions(+), 3 deletions(-) create mode 100644 typo3/sysext/core/Classes/Http/NormalizedParams.php create mode 100644 typo3/sysext/core/Classes/Middleware/NormalizedParamsAttribute.php create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Deprecation-83736-DeprecatedGlobalsTYPO3_REQUEST.rst create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-83736-ExtendedPSR-7RequestsWithTYPO3ServerParameters.rst create mode 100644 typo3/sysext/core/Tests/Unit/Http/NormalizedParamsTest.php diff --git a/typo3/sysext/backend/Configuration/RequestMiddlewares.php b/typo3/sysext/backend/Configuration/RequestMiddlewares.php index 401cd94cb524..62b3e6acfea1 100644 --- a/typo3/sysext/backend/Configuration/RequestMiddlewares.php +++ b/typo3/sysext/backend/Configuration/RequestMiddlewares.php @@ -14,10 +14,16 @@ 'typo3/cms-core/legacy-request-handler-dispatcher' => [ 'target' => \TYPO3\CMS\Core\Middleware\LegacyRequestHandlerDispatcher::class, ], + 'typo3/cms-core/normalized-params-attribute' => [ + 'target' => \TYPO3\CMS\Core\Middleware\NormalizedParamsAttribute::class, + 'after' => [ + 'typo3/cms-core/legacy-request-handler-dispatcher', + ], + ], 'typo3/cms-backend/locked-backend' => [ 'target' => \TYPO3\CMS\Backend\Middleware\LockedBackendGuard::class, 'after' => [ - 'typo3/cms-core/legacy-request-handler-dispatcher' + 'typo3/cms-core/normalized-params-attribute' ], ], 'typo3/cms-backend/https-redirector' => [ diff --git a/typo3/sysext/core/Classes/Http/NormalizedParams.php b/typo3/sysext/core/Classes/Http/NormalizedParams.php new file mode 100644 index 000000000000..63af1d859f6f --- /dev/null +++ b/typo3/sysext/core/Classes/Http/NormalizedParams.php @@ -0,0 +1,810 @@ +getServerParams() / $GLOBALS['_SERVER']). + * + * An instance of this class is available as PSR-7 ServerRequestInterface attribute: + * $normalizedParams = $request->getAttribute('normalizedParams') + * + * This class substitutes the old GeneralUtility::getIndpEnv() method. + */ +class NormalizedParams +{ + /** + * Sanitized HTTP_HOST value + * + * host[:port] + * + * - www.domain.com + * - www.domain.com:443 + * - 192.168.1.42:80 + * + * @var string + */ + protected $httpHost = ''; + + /** + * @var bool True if request has been done via HTTPS + */ + protected $isHttps = false; + + /** + * Sanitized HTTP_HOST with protocol + * + * scheme://host[:port] + * + * - https://www.domain.com + * + * @var string + */ + protected $requestHost = ''; + + /** + * Host / domain part of HTTP_HOST, no port, no protocol + * + * - www.domain.com + * - 192.168.1.42 + * + * @var string + */ + protected $requestHostOnly = ''; + + /** + * Port of HTTP_HOST if given + * + * @var int + */ + protected $requestPort = 0; + + /** + * Entry script path of URI, without domain and without query parameters, with leading / + * + * [path_script] + * + * - /typo3/index.php + * + * @var string + */ + protected $scriptName = ''; + + /** + * REQUEST URI without domain and scheme, with trailing slash + * + * [path][?[query]] + * + * - /index.php + * - /typo3/index.php/arg1/arg2/?arg1,arg2&p1=parameter1&p2[key]=value + * + * @var string + */ + protected $requestUri = ''; + + /** + * REQUEST URI with scheme, host, port, path and query + * + * scheme://host[:[port]][path][?[query]] + * + * - http://www.domain.com/typo3/index.php?route=foo/bar&id=42 + * + * @var string + */ + protected $requestUrl = ''; + + /** + * REQUEST URI with scheme, host, port and path, but *without* query part + * + * scheme://host[:[port]][path_script] + * + * - http://www.domain.com/typo3/index.php + * + * @var string + */ + protected $requestScript = ''; + + /** + * Full Uri with path, but without script name and query parts + * + * scheme://host[:[port]][path_dir] + * + * - http://www.domain.com/typo3/ + * + * @var string + */ + protected $requestDir = ''; + + /** + * True if request via a reverse proxy is detected + * + * @var bool + */ + protected $isBehindReverseProxy = false; + + /** + * IPv4 or IPv6 address of remote client with resolved proxy setup + * + * @var string + */ + protected $remoteAddress = ''; + + /** + * Absolute server path to entry script on server filesystem + * + * - /var/www/typo3/index.php + * + * @var string + */ + protected $scriptFilename = ''; + + /** + * Absolute server path to web document root without trailing slash + * + * - /var/www/typo3 + * + * @var string + */ + protected $documentRoot = ''; + + /** + * Website frontend URL. + * Note this is note "safe" if called from Backend since sys_domain and + * other factors are not taken into account. + * + * scheme://host[:[port]]/[path_dir] + * + * - https://www.domain.com/ + * - https://www.domain.com/some/sub/dir/ + * + * @var string + */ + protected $siteUrl = ''; + + /** + * Path part to frontend, no domain, no protocol + * + * - / + * - /some/sub/dir/ + * + * @var string + */ + protected $sitePath = ''; + + /** + * Path to script, without sub path if TYPO3 is running in sub directory, without trailing slash + * + * - typo/index.php?id=42 + * - index.php?id=42 + * + * @var string + */ + protected $siteScript = ''; + + /** + * Entry script path of URI, without domain and without query parameters, with leading / + * This is often not set at all. + * Will be deprecated later, use $scriptName instead as more reliable solution. + * + * [path_script] + * + * - /typo3/index.php + * + * @var string + */ + protected $pathInfo = ''; + + /** + * HTTP_REFERER + * Will be deprecated later, use $request->getServerParams()['HTTP_REFERER'] instead + * + * scheme://host[:[port]][path] + * + * - https://www.domain.com/typo3/index.php?id=42 + * + * @var string + */ + protected $httpReferer = ''; + + /** + * HTTP_USER_AGENT + * Will be deprecated later, use $request->getServerParams()['HTTP_USER_AGENT'] instead + * + * - Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 + * + * @var string + */ + protected $httpUserAgent = ''; + + /** + * HTTP_ACCEPT_ENCODING + * Will be deprecated later, use $request->getServerParams()['HTTP_ACCEPT_ENCODING'] instead + * + * - gzip, deflate + * + * @var string + */ + protected $httpAcceptEncoding = ''; + + /** + * HTTP_ACCEPT_LANGUAGE + * Will be deprecated later, use $request->getServerParams()['HTTP_ACCEPT_LANGUAGE'] instead + * + * - de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 + * + * @var string + */ + protected $httpAcceptLanguage = ''; + + /** + * REMOTE_HOST Resolved host name of REMOTE_ADDR if configured in web server + * Will be deprecated later, use $request->getServerParams()['REMOTE_HOST'] instead + * + * - www.clientDomain.com + * + * @var string + */ + protected $remoteHost = ''; + + /** + * QUERY_STRING + * Will be deprecated later, use $request->getServerParams()['QUERY_STRING'] instead + * + * [query] + * + * - id=42&foo=bar + * + * @var string + */ + protected $queryString = ''; + + /** + * Constructor calculates all values by incoming variables. + * + * This object is immutable. + * + * All determine*() "detail worker methods" in this class retrieve their dependencies + * to other properties as method arguments, they are static, stateless and have no + * dependency to $this. This ensures the chain of inter-property dependencies + * is visible by only looking at the construct() method. + * + * @param ServerRequestInterface $serverRequest Used to access $_SERVER + * @param array $typo3ConfVars $GLOBALS['TYPO3_CONF_VARS'] + * @param string $pathThisScript Absolute server entry script path, constant PATH_thisScript + * @param string $pathSite Absolute server path to document root, constant PATH_site + */ + public function __construct(ServerRequestInterface $serverRequest, array $typo3ConfVars, string $pathThisScript, string $pathSite) + { + $serverParams = $serverRequest->getServerParams(); + $isBehindReverseProxy = $this->isBehindReverseProxy = self::determineIsBehindReverseProxy($serverParams, $typo3ConfVars); + $httpHost = $this->httpHost = self::determineHttpHost($serverParams, $typo3ConfVars, $isBehindReverseProxy); + $isHttps = $this->isHttps = self::determineHttps($serverParams, $typo3ConfVars); + $requestHost = $this->requestHost = ($isHttps ? 'https://' : 'http://') . $httpHost; + $requestHostOnly = $this->requestHostOnly = self::determineRequestHostOnly($httpHost); + $this->requestPort = self::determineRequestPort($httpHost, $requestHostOnly); + $scriptName = $this->scriptName = self::determineScriptName($serverParams, $typo3ConfVars, $isHttps, $isBehindReverseProxy); + $requestUri = $this->requestUri = self::determineRequestUri($serverParams, $typo3ConfVars, $isHttps, $scriptName, $isBehindReverseProxy); + $requestUrl = $this->requestUrl = $requestHost . $requestUri; + $this->requestScript = $requestHost . $scriptName; + $requestDir = $this->requestDir = $requestHost . GeneralUtility::dirname($scriptName) . '/'; + $this->remoteAddress = self::determineRemoteAddress($serverParams, $typo3ConfVars, $isBehindReverseProxy); + $scriptFilename = $this->scriptFilename = $pathThisScript; + $this->documentRoot = self::determineDocumentRoot($scriptName, $scriptFilename); + $siteUrl = $this->siteUrl = self::determineSiteUrl($requestDir, $pathThisScript, $pathSite); + $this->sitePath = self::determineSitePath($requestHost, $siteUrl); + $this->siteScript = self::determineSiteScript($requestUrl, $siteUrl); + + // @deprecated Below variables can be fully deprecated as soon as core does not use them anymore + $this->pathInfo = $serverParams['PATH_INFO'] ?? ''; + $this->httpReferer = $serverParams['HTTP_REFERER'] ?? ''; + $this->httpUserAgent = $serverParams['HTTP_USER_AGENT'] ?? ''; + $this->httpAcceptEncoding = $serverParams['HTTP_ACCEPT_ENCODING'] ?? ''; + $this->httpAcceptLanguage = $serverParams['HTTP_ACCEPT_LANGUAGE'] ?? ''; + $this->remoteHost = $serverParams['REMOTE_HOST'] ?? ''; + $this->queryString = $serverParams['QUERY_STRING'] ?? ''; + } + + /** + * @return string Sanitized HTTP_HOST value host[:port] + */ + public function getHttpHost(): string + { + return $this->httpHost; + } + + /** + * @return bool True if client request has been done using HTTPS + */ + public function isHttps(): bool + { + return $this->isHttps; + } + + /** + * @return string Sanitized HTTP_HOST with protocol scheme://host[:port], eg. https://www.domain.com/ + */ + public function getRequestHost(): string + { + return $this->requestHost; + } + + /** + * @return string Host / domain /IP only, eg. www.domain.com + */ + public function getRequestHostOnly(): string + { + return $this->requestHostOnly; + } + + /** + * @return int Requested port if given, eg. 8080 - often not explicitly given, then 0 + */ + public function getRequestPort(): int + { + return $this->requestPort; + } + + /** + * @return string Script path part of URI, eg. 'typo3/index.php' + */ + public function getScriptName(): string + { + return $this->scriptName; + } + + /** + * @return string Request Uri without domain and protocol, eg. /index.php?id=42 + */ + public function getRequestUri(): string + { + return $this->requestUri; + } + + /** + * @return string Full REQUEST_URI, eg. http://www.domain.com/typo3/index.php?route=foo/bar&id=42 + */ + public function getRequestUrl(): string + { + return $this->requestUrl; + } + + /** + * @return string REQUEST URI without query part, eg. http://www.domain.com/typo3/index.php + */ + public function getRequestScript(): string + { + return $this->requestScript; + } + + /** + * @return string REQUEST URI without script file name and query parts, eg. http://www.domain.com/typo3/ + */ + public function getRequestDir(): string + { + return $this->requestDir; + } + + /** + * @return bool True if request comes from a configured reverse proxy + */ + public function isBehindReverseProxy(): bool + { + return $this->isBehindReverseProxy; + } + + /** + * @return string Client IP + */ + public function getRemoteAddress(): string + { + return $this->remoteAddress; + } + + /** + * @return string Absolute entry script path on server, eg. /var/www/typo3/index.php + */ + public function getScriptFilename(): string + { + return $this->scriptFilename; + } + + /** + * @return string Absolute path to web document root, eg. /var/www/typo3 + */ + public function getDocumentRoot(): string + { + return $this->documentRoot; + } + + /** + * @return string Website frontend url, eg. https://www.domain.com/some/sub/dir/ + */ + public function getSiteUrl(): string + { + return $this->siteUrl; + } + + /** + * @return string Path part to frontend, eg. /some/sub/dir/ + */ + public function getSitePath(): string + { + return $this->sitePath; + } + + /** + * @return string Path part to entry script with parameters, without sub dir, eg 'typo3/index.php?id=42' + */ + public function getSiteScript(): string + { + return $this->siteScript; + } + + /** + * Will be deprecated later, use getScriptName() as reliable solution instead + * + * @return string Script path part of URI, eg. 'typo3/index.php' + */ + public function getPathInfo(): string + { + return $this->pathInfo; + } + + /** + * Will be deprecated later, use $request->getServerParams()['HTTP_REFERER'] instead + * + * @return string HTTP_REFERER, eg. 'https://www.domain.com/typo3/index.php?id=42' + */ + public function getHttpReferer(): string + { + return $this->httpReferer; + } + + /** + * Will be deprecated later, use $request->getServerParams()['HTTP_USER_AGENT'] instead + * + * @return string HTTP_USER_AGENT identifier + */ + public function getHttpUserAgent(): string + { + return $this->httpUserAgent; + } + + /** + * Will be deprecated later, use $request->getServerParams()['HTTP_ACCEPT_ENCODING'] instead + * + * @return string HTTP_ACCEPT_ENCODING, eg. 'gzip, deflate' + */ + public function getHttpAcceptEncoding(): string + { + return $this->httpAcceptEncoding; + } + + /** + * Will be deprecated later, use $request->getServerParams()['HTTP_ACCEPT_LANGUAGE'] instead + * + * @return string HTTP_ACCEPT_LANGUAGE, eg. 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7' + */ + public function getHttpAcceptLanguage(): string + { + return $this->httpAcceptLanguage; + } + + /** + * Will be deprecated later, use $request->getServerParams()['REMOTE_HOST'] instead + * + * @return string REMOTE_HOST if configured in web server, eg. 'www.clientDomain.com' + */ + public function getRemoteHost(): string + { + return $this->remoteHost; + } + + /** + * Will be deprecated later, use $request->getServerParams()['QUERY_STRING'] instead + * + * @return string QUERY_STRING, eg 'id=42&foo=bar' + */ + public function getQueryString(): string + { + return $this->queryString; + } + + /** + * Sanitize HTTP_HOST, take proxy configuration into account and + * verify allowed hosts with configured trusted hosts pattern. + * + * @param array $serverParams Basically the $_SERVER, but from $request object + * @param array $typo3ConfVars TYPO3_CONF_VARS array + * @param bool $isBehindReverseProxy True if reverse proxy setup is detected + * @return string Sanitized HTTP_HOST + */ + protected static function determineHttpHost(array $serverParams, array $typo3ConfVars, bool $isBehindReverseProxy): string + { + $httpHost = $serverParams['HTTP_HOST'] ?? ''; + if ($isBehindReverseProxy) { + // If the request comes from a configured proxy which has set HTTP_X_FORWARDED_HOST, then + // evaluate reverseProxyHeaderMultiValue and + $xForwardedHostArray = GeneralUtility::trimExplode(',', $serverParams['HTTP_X_FORWARDED_HOST'] ?? '', true); + $xForwardedHost = ''; + // Choose which host in list to use + if (!empty($xForwardedHostArray)) { + $configuredReverseProxyHeaderMultiValue = trim($typo3ConfVars['SYS']['reverseProxyHeaderMultiValue'] ?? ''); + // Default if reverseProxyHeaderMultiValue is not set or set to 'none', instead of 'first' / 'last' is to + // ignore $serverParams['HTTP_X_FORWARDED_HOST'] + // @todo: Maybe this default is stupid: Both SYS/reverseProxyIP hand SYS/reverseProxyHeaderMultiValue have to + // @todo: be configured for a working setup. It would be easier to only configure SYS/reverseProxyIP and fall + // @todo: back to "first" if SYS/reverseProxyHeaderMultiValue is not set. + if ($configuredReverseProxyHeaderMultiValue === 'last') { + $xForwardedHost = array_pop($xForwardedHostArray); + } elseif ($configuredReverseProxyHeaderMultiValue === 'first') { + $xForwardedHost = array_shift($xForwardedHostArray); + } + } + if ($xForwardedHost) { + $httpHost = $xForwardedHost; + } + } + if (!GeneralUtility::isAllowedHostHeaderValue($httpHost)) { + throw new \UnexpectedValueException( + 'The current host header value does not match the configured trusted hosts pattern!' + . ' Check the pattern defined in $GLOBALS[\'TYPO3_CONF_VARS\'][\'SYS\'][\'trustedHostsPattern\']' + . ' and adapt it, if you want to allow the current host header \'' . $httpHost . '\' for your installation.', + 1396795886 + ); + } + return $httpHost; + } + + /** + * Determine if the client called via HTTPS. Takes proxy ssl terminator + * configurations into account. + * + * @param array $serverParams Basically the $_SERVER, but from $request object + * @param array $typo3ConfVars TYPO3_CONF_VARS array + * @return bool True if request has been done via HTTPS + */ + protected static function determineHttps(array $serverParams, array $typo3ConfVars): bool + { + $isHttps = false; + $configuredProxySSL = trim($typo3ConfVars['SYS']['reverseProxySSL'] ?? ''); + if ($configuredProxySSL === '*') { + $configuredProxySSL = trim($typo3ConfVars['SYS']['reverseProxyIP'] ?? ''); + } + if (GeneralUtility::cmpIP(trim($serverParams['REMOTE_ADDR'] ?? ''), $configuredProxySSL) + || ($serverParams['SSL_SESSION_ID'] ?? '') + || strtolower($serverParams['HTTPS'] ?? '') === 'on' + || (string)($serverParams['HTTPS'] ?? '') === '1' + ) { + $isHttps = true; + } + return $isHttps; + } + + /** + * Determine script name and path + * + * @param array $serverParams Basically the $_SERVER, but from $request object + * @param array $typo3ConfVars TYPO3_CONF_VARS array + * @param bool $isHttps True if used protocol is HTTPS + * @param bool $isBehindReverseProxy True if reverse proxy setup is detected + * @return string Sanitized script name + */ + protected static function determineScriptName(array $serverParams, array $typo3ConfVars, bool $isHttps, bool $isBehindReverseProxy): string + { + $scriptName = $serverParams['ORIG_PATH_INFO'] ?? + $serverParams['PATH_INFO'] ?? + $serverParams['ORIG_SCRIPT_NAME'] ?? + $serverParams['SCRIPT_NAME'] ?? + ''; + if ($isBehindReverseProxy) { + // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix + if ($isHttps && !empty($typo3ConfVars['SYS']['reverseProxyPrefixSSL'])) { + $scriptName = $typo3ConfVars['SYS']['reverseProxyPrefixSSL'] . $scriptName; + } elseif (!empty($typo3ConfVars['SYS']['reverseProxyPrefix'])) { + $scriptName = $typo3ConfVars['SYS']['reverseProxyPrefix'] . $scriptName; + } + } + return $scriptName; + } + + /** + * Determine REQUEST_URI, taking proxy configuration and various web server + * specifics into account. + * + * @param array $serverParams Basically the $_SERVER, but from $request object + * @param array $typo3ConfVars TYPO3_CONF_VARS array + * @param bool $isHttps True if used protocol is HTTPS + * @param string $scriptName Script name + * @param bool $isBehindReverseProxy True if reverse proxy setup is detected + * @return string Sanitized REQUEST_URI + */ + protected static function determineRequestUri(array $serverParams, array $typo3ConfVars, bool $isHttps, string $scriptName, bool $isBehindReverseProxy): string + { + $proxyPrefixApplied = false; + if (!empty($typo3ConfVars['SYS']['requestURIvar'])) { + // This is for URL rewriter that store the original URI in a server + // variable (e.g. ISAPI Rewriter for IIS: HTTP_X_REWRITE_URL), a config then looks like: + // requestURIvar = '_SERVER|HTTP_X_REWRITE_URL' which will access $GLOBALS['_SERVER']['HTTP_X_REWRITE_URL'] + list($firstLevel, $secondLevel) = GeneralUtility::trimExplode('|', $typo3ConfVars['SYS']['requestURIvar'], true); + $requestUri = $GLOBALS[$firstLevel][$secondLevel]; + } elseif (empty($serverParams['REQUEST_URI'])) { + // This is for ISS/CGI which does not have the REQUEST_URI available. + $queryString = !empty($serverParams['QUERY_STRING']) ? '?' . $serverParams['QUERY_STRING'] : ''; + // script name already had the proxy prefix handling, we must not add it a second time + $proxyPrefixApplied = true; + $requestUri = '/' . ltrim($scriptName, '/') . $queryString; + } else { + $requestUri = '/' . ltrim($serverParams['REQUEST_URI'], '/'); + } + if (!$proxyPrefixApplied && $isBehindReverseProxy) { + // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix + if ($isHttps && !empty($typo3ConfVars['SYS']['reverseProxyPrefixSSL'])) { + $requestUri = $typo3ConfVars['SYS']['reverseProxyPrefixSSL'] . $requestUri; + } elseif (!empty($typo3ConfVars['SYS']['reverseProxyPrefix'])) { + $requestUri = $typo3ConfVars['SYS']['reverseProxyPrefix'] . $requestUri; + } + } + return $requestUri; + } + + /** + * Determine clients REMOTE_ADDR, even if there is a reverse proxy in between. + * + * @param array $serverParams Basically the $_SERVER, but from $request object + * @param array $typo3ConfVars TYPO3_CONF_VARS array + * @param bool $isBehindReverseProxy True if reverse proxy setup is detected + * @return string Resolved REMOTE_ADDR + */ + protected static function determineRemoteAddress(array $serverParams, array $typo3ConfVars, bool $isBehindReverseProxy): string + { + $remoteAddress = trim($serverParams['REMOTE_ADDR'] ?? ''); + if ($isBehindReverseProxy) { + $ip = GeneralUtility::trimExplode(',', $serverParams['HTTP_X_FORWARDED_FOR'] ?? '', true); + // Choose which IP in list to use + $configuredReverseProxyHeaderMultiValue = trim($typo3ConfVars['SYS']['reverseProxyHeaderMultiValue'] ?? ''); + if (!empty($ip) && $configuredReverseProxyHeaderMultiValue === 'last') { + $ip = array_pop($ip); + } elseif (!empty($ip) && $configuredReverseProxyHeaderMultiValue === 'first') { + $ip = array_shift($ip); + } else { + $ip = ''; + } + if (GeneralUtility::validIP($ip)) { + $remoteAddress = $ip; + } + } + return $remoteAddress; + } + + /** + * Check if a configured reverse proxy setup is detected. + * + * @param array $serverParams Basically the $_SERVER, but from $request object + * @param array $typo3ConfVars TYPO3_CONF_VARS array + * @return bool True if TYPO3 is behind a reverse proxy + */ + protected static function determineIsBehindReverseProxy($serverParams, $typo3ConfVars): bool + { + return GeneralUtility::cmpIP(trim($serverParams['REMOTE_ADDR'] ?? ''), trim($typo3ConfVars['SYS']['reverseProxyIP'] ?? '')); + } + + /** + * HTTP_HOST without port + * + * @param string $httpHost host[:[port]] + * @return string Resolved host + */ + protected static function determineRequestHostOnly(string $httpHost): string + { + $httpHostBracketPosition = strpos($httpHost, ']'); + $httpHostParts = explode(':', $httpHost); + return $httpHostBracketPosition !== false ? substr($httpHost, 0, $httpHostBracketPosition + 1) : array_shift($httpHostParts); + } + + /** + * Requested port if given + * + * @param string $httpHost host[:[port]] + * @param string $httpHostOnly host + * @return int Resolved port if given, else 0 + */ + protected static function determineRequestPort(string $httpHost, string $httpHostOnly): int + { + return strlen($httpHost) > strlen($httpHostOnly) ? (int)substr($httpHost, strlen($httpHostOnly) + 1) : 0; + } + + /** + * Calculate absolute path to web document root + * + * @param string $scriptName Entry script path of URI, without domain and without query parameters, with leading / + * @param string $scriptFilename Absolute path to entry script on server filesystem + * @return string Path to document root with trailing slash + */ + protected static function determineDocumentRoot(string $scriptName, string $scriptFilename): string + { + // Get the web root (it is not the root of the TYPO3 installation) + // Some CGI-versions (LA13CGI) and mod-rewrite rules on MODULE versions will deliver a 'wrong' + // DOCUMENT_ROOT (according to our description). Further various aliases/mod_rewrite rules can + // disturb this as well. Therefore the DOCUMENT_ROOT is always calculated as the SCRIPT_FILENAME + // minus the end part shared with SCRIPT_NAME. + $webDocRoot = ''; + $scriptNameArray = explode('/', strrev($scriptName)); + $scriptFilenameArray = explode('/', strrev($scriptFilename)); + $path = []; + foreach ($scriptNameArray as $segmentNumber => $segment) { + if ((string)$scriptFilenameArray[$segmentNumber] === (string)$segment) { + $path[] = $segment; + } else { + break; + } + } + $commonEnd = strrev(implode('/', $path)); + if ((string)$commonEnd !== '') { + $webDocRoot = substr($scriptFilename, 0, -(strlen($commonEnd) + 1)); + } + return $webDocRoot; + } + + /** + * Determine frontend url + * + * @param string $requestDir Full Uri with path, but without script name and query parts + * @param string $pathThisScript Absolute path to entry script on server filesystem + * @param string $pathSite Absolute server path to document root + * @return string Calculated Frontend Url + */ + protected static function determineSiteUrl(string $requestDir, string $pathThisScript, string $pathSite): string + { + if (defined('TYPO3_PATH_WEB')) { + // This can only be set by external entry scripts + $siteUrl = $requestDir; + } else { + $pathThisScriptDir = substr(dirname($pathThisScript), strlen($pathSite)) . '/'; + $siteUrl = substr($requestDir, 0, -strlen($pathThisScriptDir)); + $siteUrl = rtrim($siteUrl, '/') . '/'; + } + return $siteUrl; + } + + /** + * Determine site path + * + * @param string $requestHost scheme://host[:port] + * @param string $siteUrl Full Frontend Url + * @return string + */ + protected static function determineSitePath(string $requestHost, string $siteUrl): string + { + return (string)substr($siteUrl, strlen($requestHost)); + } + + /** + * Determine site script + * + * @param string $requestUrl + * @param string $siteUrl + * @return string + */ + protected static function determineSiteScript(string $requestUrl, string $siteUrl): string + { + return substr($requestUrl, strlen($siteUrl)); + } +} diff --git a/typo3/sysext/core/Classes/Middleware/NormalizedParamsAttribute.php b/typo3/sysext/core/Classes/Middleware/NormalizedParamsAttribute.php new file mode 100644 index 000000000000..2a4eb5db61ac --- /dev/null +++ b/typo3/sysext/core/Classes/Middleware/NormalizedParamsAttribute.php @@ -0,0 +1,51 @@ +withAttribute('normalizedParams', new NormalizedParams($request, $GLOBALS['TYPO3_CONF_VARS'], PATH_thisScript, PATH_site)); + + // Set $request as global variable. This is needed in a transition phase until core code has been + // refactored to have ServerRequest object available where it is needed. This global will be + // deprecated then and removed. + $GLOBALS['TYPO3_REQUEST'] = $request; + + return $handler->handle($request); + } +} diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-83736-DeprecatedGlobalsTYPO3_REQUEST.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-83736-DeprecatedGlobalsTYPO3_REQUEST.rst new file mode 100644 index 000000000000..0321e6bd8481 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-83736-DeprecatedGlobalsTYPO3_REQUEST.rst @@ -0,0 +1,38 @@ +.. include:: ../../Includes.txt + +====================================================== +Deprecation: #83736 - Deprecated globals TYPO3_REQUEST +====================================================== + +See :issue:`83736` + +Description +=========== + +The :php:`ServerRequestInterface $request` is available as :php:`$GLOBALS['TYPO3_REQUEST']` +in HTTP requests. This global is available in a transition phase only and will be removed later. + +Extension authors are discouraged to use that global and the extension scanner marks any +usage as deprecated. + + +Impact +====== + +Accessing :php:`$GLOBALS['TYPO3_REQUEST']` is discouraged. + + +Affected Installations +====================== + +Instances with extensions using :php:`$GLOBALS['TYPO3_REQUEST']`. + + +Migration +========= + +Controller classes for HTTP requests retrieve the request object. Access should either be +done from within controllers or by passing :php:`$request` to service classes that +need to access values from :php:`$request`. + +.. index:: PHP-API, FullyScanned diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-83736-ExtendedPSR-7RequestsWithTYPO3ServerParameters.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-83736-ExtendedPSR-7RequestsWithTYPO3ServerParameters.rst new file mode 100644 index 000000000000..6942b059fd53 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-83736-ExtendedPSR-7RequestsWithTYPO3ServerParameters.rst @@ -0,0 +1,72 @@ +.. include:: ../../Includes.txt + +================================================================================= +Feature: #83736 - Extended PSR-7 requests with TYPO3 normalized server parameters +================================================================================= + +See :issue:`83736` + +Description +=========== + +The PSR-7 based ServerRequest objects created by TYPO3 now contain a TYPO3-specific +attribute object for normalized server parameters that for instance resolves variables +if the instance is behind a reverse proxy. This substitutes :php:`GeneralUtility::getIndpEnv()`. + +The object is **for now** available from :php:`ServerRequestInterface $request` objects as +attribute. The request object is given to controllers, example: + +.. code-block:: php + + /** @var NormalizedParams $normalizedParams */ + $normalizedParams = $request->getAttribute('normalizedParams'); + $requestPort = $normalizedParams->getRequestPort(); + +The request object is also available as a global variable in :php:`$GLOBALS['TYPO3_REQUEST']`. +This is a workaround for the core which has to access the server parameters at places where +$request is not available. So, while this object is globally available during any HTTP request, +it is considered bad practice to use it, and the extension scanner will mark an access to this +global variable as deprecated. The global object will vanish later if the core code has been +refactored enough to not rely on it anymore. + +For now, class :php:`NormalizedParams` is a one-to-one transition of :php:`GeneralUtility::getIndpEnv()`, +the old arguments can be substituted with these calls: + +- :php:`SCRIPT_NAME` is now :php:`->getScriptName()` +- :php:`SCRIPT_FILENAME` is now :php:`->getScriptFilename()` +- :php:`REQUEST_URI` is now :php:`->getRequestUri()` +- :php:`TYPO3_REV_PROXY` is now :php:`->isBehindReverseProxy()` +- :php:`REMOTE_ADDR` is now :php:`->getRemoteAddress()` +- :php:`HTTP_HOST` is now :php:`->getHttpHost()` +- :php:`TYPO3_DOCUMENT_ROOT` is now :php:`->getDocumentRoot()` +- :php:`TYPO3_HOST_ONLY` is now :php:`->getRequestHostOnly()` +- :php:`TYPO3_PORT` is now :php:`->getRequestPort()` +- :php:`TYPO3_REQUEST_HOST` is now :php:`->getRequestHost()` +- :php:`TYPO3_REQUEST_URL` is now :php:`->getRequestUrl()` +- :php:`TYPO3_REQUEST_SCRIPT` is now :php:`->getRequestScript()` +- :php:`TYPO3_REQUEST_DIR` is now :php:`->getRequestDir()` +- :php:`TYPO3_SITE_URL` is now :php:`->getSiteUrl()` +- :php:`TYPO3_SITE_PATH` is now :php:`->getSitePath()` +- :php:`TYPO3_SITE_SCRIPT` is now :php:`->getSiteScript()` +- :php:`TYPO3_SSL` is now :php:`->isHttps()` + +Some further old :php:`getIndpEnv()` arguments directly access :php:`$request->serverParams()` and +do not apply any normalization. These have been transferred to the new class, too, but will be +deprecated later if the core does not use these anymore: + +- :php:`PATH_INFO` is now :php:`->getPathInfo()`, but better use :php:`->getScriptPath()` instead +- :php:`HTTP_REFERER` is now :php:`->getHttpReferer()`, but better use :php:`$request->getServerParams()['HTTP_REFERER']` instead +- :php:`HTTP_USER_AGENT` is now :php:`->getHttpUserAgent()`, but better use :php:`$request->getServerParams()['HTTP_USER_AGENT']` instead +- :php:`HTTP_ACCEPT_ENCODING` is now :php:`->getHttpAcceptEncoding()`, but better use :php:`$request->getServerParams()['HTTP_ACCEPT_ENCODING']` instead +- :php:`HTTP_ACCEPT_LANGUAGE` is now :php:`->getHttpAcceptLanguage()`, but better use :php:`$request->getServerParams()['HTTP_ACCEPT_LANGUAGE']` instead +- :php:`REMOTE_HOST` is now :php:`->getRemoteHost()`, but better use :php:`$request->getServerParams()['REMOTE_HOST']` instead +- :php:`QUERY_STRING` is now :php:`->getQueryString()`, but better use :php:`$request->getServerParams()['QUERY_STRING']` instead + + +Impact +====== + +The PSR-7 request objects created by TYPO3 now contain an instance of :php:`NormalizedParams` which can +be used instead of :php:`GeneralUtility::getIndpEnv()` to access normalized server params. + +.. index:: PHP-API \ No newline at end of file diff --git a/typo3/sysext/core/Tests/Unit/Http/NormalizedParamsTest.php b/typo3/sysext/core/Tests/Unit/Http/NormalizedParamsTest.php new file mode 100644 index 000000000000..72e6e22276dd --- /dev/null +++ b/typo3/sysext/core/Tests/Unit/Http/NormalizedParamsTest.php @@ -0,0 +1,1100 @@ + [ + [ + 'HTTP_HOST' => 'www.domain.com' + ], + [], + 'www.domain.com' + ], + 'first HTTP_X_FORWARDED_HOST from configured proxy' => [ + [ + 'HTTP_HOST' => '', + 'REMOTE_ADDR' => '123.123.123.123', + 'HTTP_X_FORWARDED_HOST' => 'www.domain1.com, www.domain2.com,' + ], + [ + 'SYS' => [ + 'reverseProxyIP' => ' 123.123.123.123', + 'reverseProxyHeaderMultiValue' => 'first', + ] + ], + 'www.domain1.com', + ], + 'last HTTP_X_FORWARDED_HOST from configured proxy' => [ + [ + 'HTTP_HOST' => '', + 'REMOTE_ADDR' => '123.123.123.123', + 'HTTP_X_FORWARDED_HOST' => 'www.domain1.com, www.domain2.com,' + ], + [ + 'SYS' => [ + 'reverseProxyIP' => '123.123.123.123', + 'reverseProxyHeaderMultiValue' => 'last', + ] + ], + 'www.domain2.com', + ], + 'simple HTTP_HOST if reverseProxyHeaderMultiValue is not configured' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'REMOTE_ADDR' => '123.123.123.123', + 'HTTP_X_FORWARDED_HOST' => 'www.domain1.com' + ], + [ + 'SYS' => [ + 'reverseProxyIP' => '123.123.123.123', + ] + ], + 'www.domain.com', + ], + 'simple HTTP_HOST if proxy IP does not match' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'REMOTE_ADDR' => '123.123.123.123', + 'HTTP_X_FORWARDED_HOST' => 'www.domain1.com' + ], + [ + 'SYS' => [ + 'reverseProxyIP' => '234.234.234.234', + 'reverseProxyHeaderMultiValue' => 'last', + ] + ], + 'www.domain.com', + ], + 'simple HTTP_HOST if REMOTE_ADDR misses' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'HTTP_X_FORWARDED_HOST' => 'www.domain1.com' + ], + [ + 'SYS' => [ + 'reverseProxyIP' => '234.234.234.234', + 'reverseProxyHeaderMultiValue' => 'last', + ] + ], + 'www.domain.com', + ], + 'simple HTTP_HOST if HTTP_X_FORWARDED_HOST is empty' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'REMOTE_ADDR' => '123.123.123.123', + 'HTTP_X_FORWARDED_HOST' => '' + ], + [ + 'SYS' => [ + 'reverseProxyIP' => '123.123.123.123', + 'reverseProxyHeaderMultiValue' => 'last', + ] + ], + 'www.domain.com', + ], + ]; + } + + /** + * @test + * @dataProvider getHttpHostReturnsSanitizedValueDataProvider + * @param array $serverParams + * @param array $typo3ConfVars + * @param string $expected + */ + public function getHttpHostReturnsSanitizedValue(array $serverParams, array $typo3ConfVars, string $expected) + { + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), $typo3ConfVars, '', ''); + $this->assertSame($expected, $serverRequestParameters->getHttpHost()); + } + + /** + * @return array[] + */ + public function isHttpsReturnSanitizedValueDataProvider(): array + { + return [ + 'false if nothing special is set' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + ], + [], + false + ], + 'true if SSL_SESSION_ID is set' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'SSL_SESSION_ID' => 'foo', + ], + [], + true + ], + 'false if SSL_SESSION_ID is empty' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'SSL_SESSION_ID' => '', + ], + [], + false + ], + 'true if HTTPS is "ON"' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'HTTPS' => 'ON', + ], + [], + true, + ], + 'true if HTTPS is "on"' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'HTTPS' => 'on', + ], + [], + true, + ], + 'true if HTTPS is "1"' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'HTTPS' => '1', + ], + [], + true, + ], + 'false if HTTPS is "0"' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'HTTPS' => '0', + ], + [], + false, + ], + 'false if HTTPS is not on' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'HTTPS' => 'off', + ], + [], + false, + ], + 'false if HTTPS is empty' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'HTTPS' => '', + ], + [], + false, + ], + 'true if ssl proxy IP matches REMOTE_ADDR' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'REMOTE_ADDR' => '123.123.123.123 ', + ], + [ + 'SYS' => [ + 'reverseProxySSL' => ' 123.123.123.123', + ], + ], + true + ], + 'false if ssl proxy IP does not match REMOTE_ADDR' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'REMOTE_ADDR' => '123.123.123.123', + ], + [ + 'SYS' => [ + 'reverseProxySSL' => '234.234.234.234', + ], + ], + false + ], + 'true if SSL proxy is * and reverse proxy IP matches REMOTE_ADDR' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'REMOTE_ADDR' => '123.123.123.123', + ], + [ + 'SYS' => [ + 'reverseProxySSL' => '*', + 'reverseProxyIP' => '123.123.123.123', + ], + ], + true + ], + 'false if SSL proxy is * and reverse proxy IP does not match REMOTE_ADDR' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'REMOTE_ADDR' => '123.123.123.123', + ], + [ + 'SYS' => [ + 'reverseProxySSL' => '*', + 'reverseProxyIP' => '234.234.234.234', + ], + ], + false + ] + ]; + } + + /** + * @test + * @dataProvider isHttpsReturnSanitizedValueDataProvider + * @param array $serverParams + * @param array $typo3ConfVars + * @param bool $expected + */ + public function isHttpsReturnSanitizedValue(array $serverParams, array $typo3ConfVars, bool $expected) + { + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), $typo3ConfVars, '', ''); + $this->assertSame($expected, $serverRequestParameters->isHttps()); + } + + /** + * @test + */ + public function getRequestHostReturnsRequestHost() + { + $serverParams = [ + 'HTTP_HOST' => 'www.domain.com', + 'HTTPS' => 'on', + ]; + $expected = 'https://www.domain.com'; + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], '', ''); + $this->assertSame($expected, $serverRequestParameters->getRequestHost()); + } + + /** + * @return array[] + */ + public function getScriptNameReturnsExpectedValueDataProvider(): array + { + return [ + 'empty string if nothing is set' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + ], + [], + '' + ], + 'use ORIG_PATH_INFO' => [ + [ + 'ORIG_PATH_INFO' => '/orig/path/info.php', + 'PATH_INFO' => '/path/info.php', + 'ORIG_SCRIPT_NAME' => '/orig/script/name.php', + 'SCRIPT_NAME' => '/script/name.php', + ], + [], + '/orig/path/info.php', + ], + 'use PATH_INFO' => [ + [ + 'PATH_INFO' => '/path/info.php', + 'ORIG_SCRIPT_NAME' => '/orig/script/name.php', + 'SCRIPT_NAME' => '/script/name.php', + ], + [], + '/path/info.php', + ], + 'use ORIG_SCRIPT_NAME' => [ + [ + 'ORIG_SCRIPT_NAME' => '/orig/script/name.php', + 'SCRIPT_NAME' => '/script/name.php', + ], + [], + '/orig/script/name.php', + ], + 'use SCRIPT_NAME' => [ + [ + 'SCRIPT_NAME' => '/script/name.php', + ], + [], + '/script/name.php', + ], + 'add proxy ssl prefix' => [ + [ + 'REMOTE_ADDR' => '123.123.123.123', + 'HTTPS' => 'on', + 'PATH_INFO' => '/path/info.php', + ], + [ + 'SYS' => [ + 'reverseProxyIP' => '123.123.123.123', + 'reverseProxyPrefixSSL' => '/proxyPrefixSSL', + ], + ], + '/proxyPrefixSSL/path/info.php', + ], + 'add proxy prefix' => [ + [ + 'REMOTE_ADDR' => '123.123.123.123', + 'PATH_INFO' => '/path/info.php', + ], + [ + 'SYS' => [ + 'reverseProxyIP' => '123.123.123.123', + 'reverseProxyPrefix' => '/proxyPrefix', + ], + ], + '/proxyPrefix/path/info.php', + ], + ]; + } + + /** + * @test + * @dataProvider getScriptNameReturnsExpectedValueDataProvider + * @param array $serverParams + * @param array $typo3ConfVars + * @param string $expected + */ + public function getScriptNameReturnsExpectedValue(array $serverParams, array $typo3ConfVars, string $expected) + { + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), $typo3ConfVars, '', ''); + $this->assertSame($expected, $serverRequestParameters->getScriptName()); + } + + /** + * @return array[] + */ + public function getRequestUriReturnsExpectedValueDataProvider(): array + { + return [ + 'slash if nothing is set' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + ], + [], + '/' + ], + 'use REQUEST_URI' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'REQUEST_URI' => 'typo3/index.php?route=foo/bar&id=42', + ], + [], + '/typo3/index.php?route=foo/bar&id=42', + ], + 'use query string and script name if REQUEST_URI is not set' => [ + [ + 'QUERY_STRING' => 'route=foo/bar&id=42', + 'SCRIPT_NAME' => '/typo3/index.php', + ], + [], + '/typo3/index.php?route=foo/bar&id=42', + ], + 'prefix with proxy prefix with ssl if using REQUEST_URI' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'REMOTE_ADDR' => '123.123.123.123', + 'HTTPS' => 'on', + 'REQUEST_URI' => 'typo3/index.php?route=foo/bar&id=42', + ], + [ + 'SYS' => [ + 'reverseProxyIP' => '123.123.123.123', + 'reverseProxyPrefixSSL' => '/proxyPrefixSSL', + ], + ], + '/proxyPrefixSSL/typo3/index.php?route=foo/bar&id=42', + ], + 'prefix with proxy prefix if using REQUEST_URI' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'REMOTE_ADDR' => '123.123.123.123', + 'REQUEST_URI' => 'typo3/index.php?route=foo/bar&id=42', + ], + [ + 'SYS' => [ + 'reverseProxyIP' => '123.123.123.123', + 'reverseProxyPrefix' => '/proxyPrefix', + ], + ], + '/proxyPrefix/typo3/index.php?route=foo/bar&id=42', + ], + 'prefix with proxy prefix with ssl if using query string and script name' => [ + [ + 'REMOTE_ADDR' => '123.123.123.123', + 'HTTPS' => 'on', + 'QUERY_STRING' => 'route=foo/bar&id=42', + 'SCRIPT_NAME' => '/typo3/index.php', + ], + [ + 'SYS' => [ + 'reverseProxyIP' => '123.123.123.123', + 'reverseProxyPrefixSSL' => '/proxyPrefixSSL', + ], + ], + '/proxyPrefixSSL/typo3/index.php?route=foo/bar&id=42', + ], + 'prefix with proxy prefix if using query string and script name' => [ + [ + 'REMOTE_ADDR' => '123.123.123.123', + 'HTTPS' => 'on', + 'QUERY_STRING' => 'route=foo/bar&id=42', + 'SCRIPT_NAME' => '/typo3/index.php', + ], + [ + 'SYS' => [ + 'reverseProxyIP' => '123.123.123.123', + 'reverseProxyPrefix' => '/proxyPrefix', + ], + ], + '/proxyPrefix/typo3/index.php?route=foo/bar&id=42', + ], + ]; + } + + /** + * @test + * @dataProvider getRequestUriReturnsExpectedValueDataProvider + * @param array $serverParams + * @param array $typo3ConfVars + * @param string $expected + */ + public function getRequestUriReturnsExpectedValue(array $serverParams, array $typo3ConfVars, string $expected) + { + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), $typo3ConfVars, '', ''); + $this->assertSame($expected, $serverRequestParameters->getRequestUri()); + } + + /** + * @test + */ + public function getRequestUriFetchesFromConfiguredRequestUriVar() + { + $GLOBALS['foo']['bar'] = '/foo/bar.php'; + $serverParams = [ + 'HTTP_HOST' => 'www.domain.com', + ]; + $typo3ConfVars = [ + 'SYS' => [ + 'requestURIvar' => 'foo|bar', + ], + ]; + $expected = '/foo/bar.php'; + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), $typo3ConfVars, '', ''); + $this->assertSame($expected, $serverRequestParameters->getRequestUri()); + } + + /** + * @test + */ + public function getRequestUrlReturnsExpectedValue() + { + $serverParams = [ + 'HTTP_HOST' => 'www.domain.com', + 'REQUEST_URI' => 'typo3/index.php?route=foo/bar&id=42', + ]; + $expected = 'http://www.domain.com/typo3/index.php?route=foo/bar&id=42'; + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], '', ''); + $this->assertSame($expected, $serverRequestParameters->getRequestUrl()); + } + + /** + * @test + */ + public function getRequestScriptReturnsExpectedValue() + { + $serverParams = [ + 'HTTP_HOST' => 'www.domain.com', + 'PATH_INFO' => '/typo3/index.php', + ]; + $expected = 'http://www.domain.com/typo3/index.php'; + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], '', ''); + $this->assertSame($expected, $serverRequestParameters->getRequestScript()); + } + + /** + * @test + */ + public function getRequestDirReturnsExpectedValue() + { + $serverParams = [ + 'HTTP_HOST' => 'www.domain.com', + 'PATH_INFO' => '/typo3/index.php', + ]; + $expected = 'http://www.domain.com/typo3/'; + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], '', ''); + $this->assertSame($expected, $serverRequestParameters->getRequestDir()); + } + + /** + * @return array[] + */ + public function isBehindReverseProxyReturnsExpectedValueDataProvider(): array + { + return [ + 'false with empty data' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + ], + [], + false + ], + 'false if REMOTE_ADDR and reverseProxyIP do not match' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'REMOTE_ADDR' => '100.100.100.100', + ], + [ + 'SYS' => [ + 'reverseProxyIP' => '200.200.200.200', + ], + ], + false + ], + 'true if REMOTE_ADDR matches configured reverseProxyIP' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'REMOTE_ADDR' => '100.100.100.100', + ], + [ + 'SYS' => [ + 'reverseProxyIP' => '100.100.100.100', + ], + ], + true + ], + 'true if trimmed REMOTE_ADDR matches configured trimmed reverseProxyIP' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'REMOTE_ADDR' => ' 100.100.100.100 ', + ], + [ + 'SYS' => [ + 'reverseProxyIP' => ' 100.100.100.100 ', + ], + ], + true + ] + ]; + } + + /** + * @test + * @dataProvider isBehindReverseProxyReturnsExpectedValueDataProvider + * @param array $serverParams + * @param array $typo3ConfVars + * @param bool $expected + */ + public function isBehindReverseProxyReturnsExpectedValue(array $serverParams, array $typo3ConfVars, bool $expected) + { + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), $typo3ConfVars, '', ''); + $this->assertSame($expected, $serverRequestParameters->isBehindReverseProxy()); + } + + /** + * @return array[] + */ + public function getRemoteAddressReturnsExpectedValueDataProvider(): array + { + return [ + 'simple REMOTE_ADDR' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'REMOTE_ADDR' => ' 123.123.123.123 ', + ], + [], + '123.123.123.123' + ], + 'reverse proxy with last HTTP_X_FORWARDED_FOR' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'REMOTE_ADDR' => ' 123.123.123.123 ', + 'HTTP_X_FORWARDED_FOR' => ' 234.234.234.234, 235.235.235.235,', + ], + [ + 'SYS' => [ + 'reverseProxyIP' => '123.123.123.123', + 'reverseProxyHeaderMultiValue' => ' last ', + ] + ], + '235.235.235.235' + ], + 'reverse proxy with first HTTP_X_FORWARDED_FOR' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'REMOTE_ADDR' => ' 123.123.123.123 ', + 'HTTP_X_FORWARDED_FOR' => ' 234.234.234.234, 235.235.235.235,', + ], + [ + 'SYS' => [ + 'reverseProxyIP' => '123.123.123.123 ', + 'reverseProxyHeaderMultiValue' => ' first ', + ] + ], + '234.234.234.234' + ], + 'reverse proxy with broken reverseProxyHeaderMultiValue returns REMOTE_ADDR' => [ + [ + 'HTTP_HOST' => 'www.domain.com', + 'REMOTE_ADDR' => ' 123.123.123.123 ', + 'HTTP_X_FORWARDED_FOR' => ' 234.234.234.234, 235.235.235.235,', + ], + [ + 'SYS' => [ + 'reverseProxyIP' => '123.123.123.123 ', + 'reverseProxyHeaderMultiValue' => ' foo ', + ] + ], + '123.123.123.123' + ], + ]; + } + + /** + * @test + * @dataProvider getRemoteAddressReturnsExpectedValueDataProvider + * @param array $serverParams + * @param array $typo3ConfVars + * @param string $expected + */ + public function getRemoteAddressReturnsExpectedValue(array $serverParams, array $typo3ConfVars, string $expected) + { + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), $typo3ConfVars, '', ''); + $this->assertSame($expected, $serverRequestParameters->getRemoteAddress()); + } + + /** + * @return array + */ + public static function getRequestHostOnlyReturnsExpectedValueDataProvider(): array + { + return [ + 'localhost ipv4 without port' => [ + [ + 'HTTP_HOST' => '127.0.0.1', + ], + '127.0.0.1' + ], + 'localhost ipv4 with port' => [ + [ + 'HTTP_HOST' => '127.0.0.1:81', + ], + '127.0.0.1' + ], + 'localhost ipv6 without port' => [ + [ + 'HTTP_HOST' => '[::1]', + ], + '[::1]' + ], + 'localhost ipv6 with port' => [ + [ + 'HTTP_HOST' => '[::1]:81', + ], + '[::1]' + ], + 'ipv6 without port' => [ + [ + 'HTTP_HOST' => '[2001:DB8::1]', + ], + '[2001:DB8::1]' + ], + 'ipv6 with port' => [ + [ + 'HTTP_HOST' => '[2001:DB8::1]:81', + ], + '[2001:DB8::1]' + ], + 'hostname without port' => [ + [ + 'HTTP_HOST' => 'lolli.did.this', + ], + 'lolli.did.this' + ], + 'hostname with port' => [ + [ + 'HTTP_HOST' => 'lolli.did.this:42', + ], + 'lolli.did.this' + ], + ]; + } + + /** + * @test + * @dataProvider getRequestHostOnlyReturnsExpectedValueDataProvider + * @param array $serverParams + * @param string $expected + */ + public function getRequestHostOnlyReturnsExpectedValue(array $serverParams, string $expected) + { + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], '', ''); + $this->assertSame($expected, $serverRequestParameters->getRequestHostOnly()); + } + + /** + * @return array + */ + public static function getRequestPortOnlyReturnsExpectedValueDataProvider(): array + { + return [ + 'localhost ipv4 without port' => [ + [ + 'HTTP_HOST' => '127.0.0.1', + ], + 0 + ], + 'localhost ipv4 with port' => [ + [ + 'HTTP_HOST' => '127.0.0.1:81', + ], + 81 + ], + 'localhost ipv6 without port' => [ + [ + 'HTTP_HOST' => '[::1]', + ], + 0 + ], + 'localhost ipv6 with port' => [ + [ + 'HTTP_HOST' => '[::1]:81', + ], + 81 + ], + 'ipv6 without port' => [ + [ + 'HTTP_HOST' => '[2001:DB8::1]', + ], + 0 + ], + 'ipv6 with port' => [ + [ + 'HTTP_HOST' => '[2001:DB8::1]:81', + ], + 81 + ], + 'hostname without port' => [ + [ + 'HTTP_HOST' => 'lolli.did.this', + ], + 0 + ], + 'hostname with port' => [ + [ + 'HTTP_HOST' => 'lolli.did.this:42', + ], + 42 + ], + ]; + } + + /** + * @test + * @dataProvider getRequestPortOnlyReturnsExpectedValueDataProvider + * @param array $serverParams + * @param int $expected + */ + public function getRequestPortReturnsExpectedValue(array $serverParams, int $expected) + { + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], '', ''); + $this->assertSame($expected, $serverRequestParameters->getRequestPort()); + } + + /** + * @test + */ + public function getScriptFilenameReturnsThirdConstructorArgument() + { + $serverParams = [ + 'HTTP_HOST' => 'www.domain.com', + 'SCRIPT_NAME' => '/typo3/index.php', + ]; + $pathSite = '/var/www/'; + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], '/var/www/typo3/index.php', $pathSite); + $this->assertSame('/var/www/typo3/index.php', $serverRequestParameters->getScriptFilename()); + } + + /** + * @test + */ + public function getDocumentRootReturnsExpectedPath() + { + $serverParams = [ + 'HTTP_HOST' => 'www.domain.com', + 'SCRIPT_NAME' => '/typo3/index.php', + ]; + $pathThisScript = '/var/www/myInstance/Web/typo3/index.php'; + $pathSite = '/var/www/myInstance/Web/'; + $expected = '/var/www/myInstance/Web'; + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], $pathThisScript, $pathSite); + $this->assertSame($expected, $serverRequestParameters->getDocumentRoot()); + } + + /** + * @test + */ + public function getSiteUrlReturnsExpectedUrl() + { + $serverParams = [ + 'SCRIPT_NAME' => '/typo3/index.php', + 'HTTP_HOST' => 'www.domain.com', + 'PATH_INFO' => '/typo3/index.php', + ]; + $pathThisScript = '/var/www/myInstance/Web/typo3/index.php'; + $pathSite = '/var/www/myInstance/Web/'; + $expected = 'http://www.domain.com/'; + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], $pathThisScript, $pathSite); + $this->assertSame($expected, $serverRequestParameters->getSiteUrl()); + } + + /** + * @return array[] + */ + public function getSitePathReturnsExpectedPathDataProvider() + { + return [ + 'empty config' => [ + [], + '', + '', + '' + ], + 'not in a sub directory' => [ + [ + 'SCRIPT_NAME' => '/typo3/index.php', + 'HTTP_HOST' => 'www.domain.com', + ], + '/var/www/myInstance/Web/typo3/index.php', + '/var/www/myInstance/Web/', + '/' + ], + 'in a sub directory' => [ + [ + 'SCRIPT_NAME' => '/some/sub/dir/typo3/index.php', + 'HTTP_HOST' => 'www.domain.com', + ], + '/var/www/myInstance/Web/typo3/index.php', + '/var/www/myInstance/Web/', + '/some/sub/dir/' + ], + ]; + } + + /** + * @test + * @dataProvider getSitePathReturnsExpectedPathDataProvider + * @param array $serverParams + * @param string $pathThisScript + * @param string $pathSite + * @param string $expected + */ + public function getSitePathReturnsExpectedPath(array $serverParams, string $pathThisScript, string $pathSite, string $expected) + { + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], $pathThisScript, $pathSite); + $this->assertSame($expected, $serverRequestParameters->getSitePath()); + } + + /** + * @return array[] + */ + public function getSiteScriptReturnsExpectedPathDataProvider() + { + return [ + 'not in a sub directory' => [ + [ + 'SCRIPT_NAME' => '/typo3/index.php?id=42&foo=bar', + 'HTTP_HOST' => 'www.domain.com', + ], + '/var/www/myInstance/Web/typo3/index.php', + '/var/www/myInstance/Web/', + 'typo3/index.php?id=42&foo=bar' + ], + 'in a sub directory' => [ + [ + 'SCRIPT_NAME' => '/some/sub/dir/typo3/index.php?id=42&foo=bar', + 'HTTP_HOST' => 'www.domain.com', + ], + '/var/www/myInstance/Web/typo3/index.php', + '/var/www/myInstance/Web/', + 'typo3/index.php?id=42&foo=bar' + ], + ]; + } + + /** + * @test + * @dataProvider getSiteScriptReturnsExpectedPathDataProvider + * @param array $serverParams + * @param string $pathThisScript + * @param string $pathSite + * @param string $expected + */ + public function getSiteScriptReturnsExpectedPath(array $serverParams, string $pathThisScript, string $pathSite, string $expected) + { + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], $pathThisScript, $pathSite); + $this->assertSame($expected, $serverRequestParameters->getSiteScript()); + } + + /** + * @test + */ + public function getPathInfoReturnsExpectedValue() + { + $serverParams = [ + 'PATH_INFO' => '/typo3/index.php', + ]; + $expected = '/typo3/index.php'; + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], '', ''); + $this->assertSame($expected, $serverRequestParameters->getPathInfo()); + } + + /** + * @test + */ + public function getHttpRefererReturnsExpectedValue() + { + $serverParams = [ + 'HTTP_REFERER' => 'https://www.domain.com/typo3/index.php?id=42', + ]; + $expected = 'https://www.domain.com/typo3/index.php?id=42'; + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], '', ''); + $this->assertSame($expected, $serverRequestParameters->getHttpReferer()); + } + + /** + * @test + */ + public function getHttpUserAgentReturnsExpectedValue() + { + $serverParams = [ + 'HTTP_USER_AGENT' => 'the client browser', + ]; + $expected = 'the client browser'; + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], '', ''); + $this->assertSame($expected, $serverRequestParameters->getHttpUserAgent()); + } + + /** + * @test + */ + public function getHttpAcceptEncodingReturnsExpectedValue() + { + $serverParams = [ + 'HTTP_ACCEPT_ENCODING' => 'gzip, deflate', + ]; + $expected = 'gzip, deflate'; + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], '', ''); + $this->assertSame($expected, $serverRequestParameters->getHttpAcceptEncoding()); + } + + /** + * @test + */ + public function getHttpAcceptLanguageReturnsExpectedValue() + { + $serverParams = [ + 'HTTP_ACCEPT_LANGUAGE' => 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7', + ]; + $expected = 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7'; + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], '', ''); + $this->assertSame($expected, $serverRequestParameters->getHttpAcceptLanguage()); + } + + /** + * @test + */ + public function getRemoteHostReturnsExpectedValue() + { + $serverParams = [ + 'REMOTE_HOST' => 'www.clientDomain.com', + ]; + $expected = 'www.clientDomain.com'; + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], '', ''); + $this->assertSame($expected, $serverRequestParameters->getRemoteHost()); + } + + /** + * @test + */ + public function getQueryStringReturnsExpectedValue() + { + $serverParams = [ + 'QUERY_STRING' => 'id=42&foo=bar', + ]; + $expected = 'id=42&foo=bar'; + $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class); + $serverRequestProphecy->getServerParams()->willReturn($serverParams); + $serverRequestParameters = new NormalizedParams($serverRequestProphecy->reveal(), [], '', ''); + $this->assertSame($expected, $serverRequestParameters->getQueryString()); + } +} diff --git a/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php b/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php index e88c2ee8d370..ea5e23cdceea 100644 --- a/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php +++ b/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php @@ -1949,7 +1949,7 @@ public function checkisOnCurrentHostInvalidHosts() 'localhost IP' => ['127.0.0.1'], 'relative path' => ['./relpath/file.txt'], 'absolute path' => ['/abspath/file.txt?arg=value'], - 'differnt host' => [GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST') . '.example.org'] + 'different host' => [GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST') . '.example.org'] ]; } diff --git a/typo3/sysext/frontend/Configuration/RequestMiddlewares.php b/typo3/sysext/frontend/Configuration/RequestMiddlewares.php index 1808342ba83c..537b568ec663 100644 --- a/typo3/sysext/frontend/Configuration/RequestMiddlewares.php +++ b/typo3/sysext/frontend/Configuration/RequestMiddlewares.php @@ -20,10 +20,16 @@ 'typo3/cms-core/legacy-request-handler-dispatcher' ], ], + 'typo3/cms-core/normalized-params-attribute' => [ + 'target' => \TYPO3\CMS\Core\Middleware\NormalizedParamsAttribute::class, + 'after' => [ + 'typo3/cms-frontend/timetracker', + ] + ], 'typo3/cms-frontend/preprocessing' => [ 'target' => \TYPO3\CMS\Frontend\Middleware\PreprocessRequestHook::class, 'after' => [ - 'typo3/cms-frontend/timetracker' + 'typo3/cms-core/normalized-params-attribute', ] ], 'typo3/cms-frontend/eid' => [ diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayGlobalMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayGlobalMatcher.php index 2409ec1ebadf..85f49dfc46f5 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayGlobalMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayGlobalMatcher.php @@ -15,4 +15,9 @@ 'Breaking-82893-RemoveGlobalVariablePARSETIME_START.rst' ], ], + '$GLOBALS[\'TYPO3_REQUEST\']' => [ + 'restFiles' => [ + 'Deprecation-83736-DeprecatedGlobalsTYPO3_REQUEST.rst', + ], + ], ];