From 886bda5f81d52ba4443094e4c2fffac33c27bc4b Mon Sep 17 00:00:00 2001 From: Lukas Reschke Date: Tue, 10 Feb 2015 13:02:48 +0100 Subject: Refactor OC_Request into TrustedDomainHelper and IRequest This changeset removes the static class `OC_Request` and moves the functions either into `IRequest` which is accessible via `\OC::$server::->getRequest()` or into a separated `TrustedDomainHelper` class for some helper methods which should not be publicly exposed. This changes only internal methods and nothing on the public API. Some public functions in `util.php` have been deprecated though in favour of the new non-static functions. Unfortunately some part of this code uses things like `__DIR__` and thus is not completely unit-testable. Where tests where possible they ahve been added though. Fixes https://github.com/owncloud/core/issues/13976 which was requested in https://github.com/owncloud/core/pull/13973#issuecomment-73492969 --- lib/private/appframework/http/request.php | 288 +++++++++++++++++++++++++++++- 1 file changed, 282 insertions(+), 6 deletions(-) (limited to 'lib/private/appframework') diff --git a/lib/private/appframework/http/request.php b/lib/private/appframework/http/request.php index 4902671d4b8..f11d189962d 100644 --- a/lib/private/appframework/http/request.php +++ b/lib/private/appframework/http/request.php @@ -24,6 +24,8 @@ namespace OC\AppFramework\Http; +use OC\Security\TrustedDomainHelper; +use OCP\IConfig; use OCP\IRequest; use OCP\Security\ISecureRandom; @@ -31,9 +33,14 @@ use OCP\Security\ISecureRandom; * Class for accessing variables in the request. * This class provides an immutable object with request variables. */ - class Request implements \ArrayAccess, \Countable, IRequest { + const USER_AGENT_IE = '/MSIE/'; + // Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent + const USER_AGENT_ANDROID_MOBILE_CHROME = '#Android.*Chrome/[.0-9]*#'; + const USER_AGENT_FREEBOX = '#^Mozilla/5\.0$#'; + const REGEX_LOCALHOST = '/^(127\.0\.0\.1|localhost)$/'; + protected $inputStream; protected $content; protected $items = array(); @@ -51,6 +58,8 @@ class Request implements \ArrayAccess, \Countable, IRequest { ); /** @var ISecureRandom */ protected $secureRandom; + /** @var IConfig */ + protected $config; /** @var string */ protected $requestId = ''; @@ -66,15 +75,18 @@ class Request implements \ArrayAccess, \Countable, IRequest { * - string 'method' the request method (GET, POST etc) * - string|false 'requesttoken' the requesttoken or false when not available * @param ISecureRandom $secureRandom + * @param IConfig $config * @param string $stream * @see http://www.php.net/manual/en/reserved.variables.php */ public function __construct(array $vars=array(), ISecureRandom $secureRandom, + IConfig $config, $stream='php://input') { $this->inputStream = $stream; $this->items['params'] = array(); $this->secureRandom = $secureRandom; + $this->config = $config; if(!array_key_exists('method', $vars)) { $vars['method'] = 'GET'; @@ -115,7 +127,9 @@ class Request implements \ArrayAccess, \Countable, IRequest { ); } - + /** + * @param $parameters + */ public function setUrlParameters($parameters) { $this->items['urlParams'] = $parameters; $this->items['parameters'] = array_merge( @@ -124,7 +138,10 @@ class Request implements \ArrayAccess, \Countable, IRequest { ); } - // Countable method. + /** + * Countable method + * @return int + */ public function count() { return count(array_keys($this->items['parameters'])); } @@ -176,7 +193,11 @@ class Request implements \ArrayAccess, \Countable, IRequest { throw new \RuntimeException('You cannot change the contents of the request object'); } - // Magic property accessors + /** + * Magic property accessors + * @param $name + * @param $value + */ public function __set($name, $value) { throw new \RuntimeException('You cannot change the contents of the request object'); } @@ -231,12 +252,17 @@ class Request implements \ArrayAccess, \Countable, IRequest { } } - + /** + * @param $name + * @return bool + */ public function __isset($name) { return isset($this->items['parameters'][$name]); } - + /** + * @param $id + */ public function __unset($id) { throw new \RunTimeException('You cannot change the contents of the request object'); } @@ -412,4 +438,254 @@ class Request implements \ArrayAccess, \Countable, IRequest { return $this->requestId; } + /** + * Returns the remote address, if the connection came from a trusted proxy + * and `forwarded_for_headers` has been configured then the IP address + * specified in this header will be returned instead. + * Do always use this instead of $_SERVER['REMOTE_ADDR'] + * @return string IP address + */ + public function getRemoteAddress() { + $remoteAddress = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : ''; + $trustedProxies = $this->config->getSystemValue('trusted_proxies', []); + + if(is_array($trustedProxies) && in_array($remoteAddress, $trustedProxies)) { + $forwardedForHeaders = $this->config->getSystemValue('forwarded_for_headers', []); + + foreach($forwardedForHeaders as $header) { + if(isset($this->server[$header])) { + foreach(explode(',', $this->server[$header]) as $IP) { + $IP = trim($IP); + if (filter_var($IP, FILTER_VALIDATE_IP) !== false) { + return $IP; + } + } + } + } + } + + return $remoteAddress; + } + + /** + * Check overwrite condition + * @param string $type + * @return bool + */ + private function isOverwriteCondition($type = '') { + $regex = '/' . $this->config->getSystemValue('overwritecondaddr', '') . '/'; + return $regex === '//' || preg_match($regex, $this->server['REMOTE_ADDR']) === 1 + || ($type !== 'protocol' && $this->config->getSystemValue('forcessl', false)); + } + + /** + * Returns the server protocol. It respects reverse proxy servers and load + * balancers. + * @return string Server protocol (http or https) + */ + public function getServerProtocol() { + if($this->config->getSystemValue('overwriteprotocol') !== '' + && $this->isOverwriteCondition('protocol')) { + return $this->config->getSystemValue('overwriteprotocol'); + } + + if (isset($this->server['HTTP_X_FORWARDED_PROTO'])) { + $proto = strtolower($this->server['HTTP_X_FORWARDED_PROTO']); + // Verify that the protocol is always HTTP or HTTPS + // default to http if an invalid value is provided + return $proto === 'https' ? 'https' : 'http'; + } + + if (isset($this->server['HTTPS']) + && $this->server['HTTPS'] !== null + && $this->server['HTTPS'] !== 'off') { + return 'https'; + } + + return 'http'; + } + + /** + * Returns the request uri, even if the website uses one or more + * reverse proxies + * @return string + */ + public function getRequestUri() { + $uri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : ''; + if($this->config->getSystemValue('overwritewebroot') !== '' && $this->isOverwriteCondition()) { + $uri = $this->getScriptName() . substr($uri, strlen($this->server['SCRIPT_NAME'])); + } + return $uri; + } + + /** + * Get raw PathInfo from request (not urldecoded) + * @throws \Exception + * @return string|false Path info or false when not found + */ + public function getRawPathInfo() { + $requestUri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : ''; + // remove too many leading slashes - can be caused by reverse proxy configuration + if (strpos($requestUri, '/') === 0) { + $requestUri = '/' . ltrim($requestUri, '/'); + } + + $requestUri = preg_replace('%/{2,}%', '/', $requestUri); + + // Remove the query string from REQUEST_URI + if ($pos = strpos($requestUri, '?')) { + $requestUri = substr($requestUri, 0, $pos); + } + + $scriptName = $this->server['SCRIPT_NAME']; + $pathInfo = $requestUri; + + // strip off the script name's dir and file name + // FIXME: Sabre does not really belong here + list($path, $name) = \Sabre\DAV\URLUtil::splitPath($scriptName); + if (!empty($path)) { + if($path === $pathInfo || strpos($pathInfo, $path.'/') === 0) { + $pathInfo = substr($pathInfo, strlen($path)); + } else { + throw new \Exception("The requested uri($requestUri) cannot be processed by the script '$scriptName')"); + } + } + if (strpos($pathInfo, '/'.$name) === 0) { + $pathInfo = substr($pathInfo, strlen($name) + 1); + } + if (strpos($pathInfo, $name) === 0) { + $pathInfo = substr($pathInfo, strlen($name)); + } + if($pathInfo === '/'){ + return ''; + } else { + return $pathInfo; + } + } + + /** + * Get PathInfo from request + * @throws \Exception + * @return string|false Path info or false when not found + */ + public function getPathInfo() { + if(isset($this->server['PATH_INFO'])) { + return $this->server['PATH_INFO']; + } + + $pathInfo = $this->getRawPathInfo(); + // following is taken from \Sabre\DAV\URLUtil::decodePathSegment + $pathInfo = rawurldecode($pathInfo); + $encoding = mb_detect_encoding($pathInfo, ['UTF-8', 'ISO-8859-1']); + + switch($encoding) { + case 'ISO-8859-1' : + $pathInfo = utf8_encode($pathInfo); + } + // end copy + + return $pathInfo; + } + + /** + * Returns the script name, even if the website uses one or more + * reverse proxies + * @return string the script name + */ + public function getScriptName() { + $name = $this->server['SCRIPT_NAME']; + $overwriteWebRoot = $this->config->getSystemValue('overwritewebroot'); + if ($overwriteWebRoot !== '' && $this->isOverwriteCondition()) { + // FIXME: This code is untestable due to ___DIR__ + $serverRoot = str_replace('\\', '/', substr(__DIR__, 0, -strlen('lib/private/'))); + $suburi = str_replace('\\', '/', substr(realpath($this->server['SCRIPT_FILENAME']), strlen($serverRoot))); + $name = '/' . ltrim($overwriteWebRoot . $suburi, '/'); + } + return $name; + } + + /** + * Checks whether the user agent matches a given regex + * @param array $agent array of agent names + * @return bool true if at least one of the given agent matches, false otherwise + */ + public function isUserAgent(array $agent) { + foreach ($agent as $regex) { + if (preg_match($regex, $this->server['HTTP_USER_AGENT'])) { + return true; + } + } + return false; + } + + /** + * Returns the unverified server host from the headers without checking + * whether it is a trusted domain + * @return string Server host + */ + public function getInsecureServerHost() { + $host = null; + if (isset($this->server['HTTP_X_FORWARDED_HOST'])) { + if (strpos($this->server['HTTP_X_FORWARDED_HOST'], ',') !== false) { + $parts = explode(',', $this->server['HTTP_X_FORWARDED_HOST']); + $host = trim(current($parts)); + } else { + $host = $this->server['HTTP_X_FORWARDED_HOST']; + } + } else { + if (isset($this->server['HTTP_HOST'])) { + $host = $this->server['HTTP_HOST']; + } else if (isset($this->server['SERVER_NAME'])) { + $host = $this->server['SERVER_NAME']; + } + } + return $host; + } + + + /** + * Returns the server host from the headers, or the first configured + * trusted domain if the host isn't in the trusted list + * @return string Server host + */ + public function getServerHost() { + // FIXME: Ugly workaround that we need to get rid of + if (\OC::$CLI && defined('PHPUNIT_RUN')) { + return 'localhost'; + } + + // overwritehost is always trusted + $host = $this->getOverwriteHost(); + if ($host !== null) { + return $host; + } + + // get the host from the headers + $host = $this->getInsecureServerHost(); + + // Verify that the host is a trusted domain if the trusted domains + // are defined + // If no trusted domain is provided the first trusted domain is returned + $trustedDomainHelper = new TrustedDomainHelper($this->config); + if ($trustedDomainHelper->isTrustedDomain($host)) { + return $host; + } else { + $trustedList = $this->config->getSystemValue('trusted_domains', []); + return $trustedList[0]; + } + } + + /** + * Returns the overwritehost setting from the config if set and + * if the overwrite condition is met + * @return string|null overwritehost value or null if not defined or the defined condition + * isn't met + */ + private function getOverwriteHost() { + if($this->config->getSystemValue('overwritehost') !== '' && $this->isOverwriteCondition()) { + return $this->config->getSystemValue('overwritehost'); + } + return null; + } + } -- cgit v1.2.3 From 9f91d64918f70002ec5043613f83694137c2d9ee Mon Sep 17 00:00:00 2001 From: Lukas Reschke Date: Mon, 16 Feb 2015 14:01:15 +0100 Subject: Make scrutinizer happy --- lib/private/appframework/http/request.php | 2 +- lib/private/security/trusteddomainhelper.php | 2 +- lib/public/irequest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/private/appframework') diff --git a/lib/private/appframework/http/request.php b/lib/private/appframework/http/request.php index f11d189962d..97e9809c1fb 100644 --- a/lib/private/appframework/http/request.php +++ b/lib/private/appframework/http/request.php @@ -521,7 +521,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { /** * Get raw PathInfo from request (not urldecoded) * @throws \Exception - * @return string|false Path info or false when not found + * @return string Path info */ public function getRawPathInfo() { $requestUri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : ''; diff --git a/lib/private/security/trusteddomainhelper.php b/lib/private/security/trusteddomainhelper.php index 593263897be..da5e0ff0a12 100644 --- a/lib/private/security/trusteddomainhelper.php +++ b/lib/private/security/trusteddomainhelper.php @@ -28,7 +28,7 @@ class TrustedDomainHelper { /** * Strips a potential port from a domain (in format domain:port) - * @param $host + * @param string $host * @return string $host without appended port */ private function getDomainWithoutPort($host) { diff --git a/lib/public/irequest.php b/lib/public/irequest.php index 814fc0251cf..025c8367d41 100644 --- a/lib/public/irequest.php +++ b/lib/public/irequest.php @@ -161,7 +161,7 @@ interface IRequest { /** * Get raw PathInfo from request (not urldecoded) * @throws \Exception - * @return string|false Path info or false when not found + * @return string Path info */ public function getRawPathInfo(); -- cgit v1.2.3 From 992164446cf7da39078d879300475b6353e1d40f Mon Sep 17 00:00:00 2001 From: Lukas Reschke Date: Mon, 16 Feb 2015 16:26:02 +0100 Subject: Add blackmagic due to cyclic dependency :see_no_evil: --- lib/base.php | 9 ++++++++- lib/private/appframework/http/request.php | 6 +++--- 2 files changed, 11 insertions(+), 4 deletions(-) (limited to 'lib/private/appframework') diff --git a/lib/base.php b/lib/base.php index 51d59d130aa..5905aa0406f 100644 --- a/lib/base.php +++ b/lib/base.php @@ -104,7 +104,14 @@ class OC { * FIXME: The following line is required because of a cyclic dependency * on IRequest. */ - $scriptName = $_SERVER['SCRIPT_NAME']; + $params = [ + 'server' => [ + 'SCRIPT_NAME' => $_SERVER['SCRIPT_NAME'], + 'SCRIPT_FILENAME' => $_SERVER['SCRIPT_FILENAME'], + ], + ]; + $fakeRequest = new \OC\AppFramework\Http\Request($params, null, new \OC\AllConfig(new \OC\SystemConfig())); + $scriptName = $fakeRequest->getScriptName(); if (substr($scriptName, -1) == '/') { $scriptName .= 'index.php'; //make sure suburi follows the same rules as scriptName diff --git a/lib/private/appframework/http/request.php b/lib/private/appframework/http/request.php index 97e9809c1fb..5cffbccb623 100644 --- a/lib/private/appframework/http/request.php +++ b/lib/private/appframework/http/request.php @@ -80,7 +80,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { * @see http://www.php.net/manual/en/reserved.variables.php */ public function __construct(array $vars=array(), - ISecureRandom $secureRandom, + ISecureRandom $secureRandom = null, IConfig $config, $stream='php://input') { $this->inputStream = $stream; @@ -596,8 +596,8 @@ class Request implements \ArrayAccess, \Countable, IRequest { $name = $this->server['SCRIPT_NAME']; $overwriteWebRoot = $this->config->getSystemValue('overwritewebroot'); if ($overwriteWebRoot !== '' && $this->isOverwriteCondition()) { - // FIXME: This code is untestable due to ___DIR__ - $serverRoot = str_replace('\\', '/', substr(__DIR__, 0, -strlen('lib/private/'))); + // FIXME: This code is untestable due to __DIR__, also that hardcoded path is really dangerous + $serverRoot = str_replace('\\', '/', substr(__DIR__, 0, -strlen('lib/private/appframework/http/'))); $suburi = str_replace('\\', '/', substr(realpath($this->server['SCRIPT_FILENAME']), strlen($serverRoot))); $name = '/' . ltrim($overwriteWebRoot . $suburi, '/'); } -- cgit v1.2.3 From cebf9f6a5a2d75ea682f109486ada3d5558fb6a2 Mon Sep 17 00:00:00 2001 From: Lukas Reschke Date: Mon, 16 Feb 2015 22:12:47 +0100 Subject: Incorporate review changes --- lib/base.php | 3 +-- lib/private/appframework/http/request.php | 12 ++++++------ lib/private/server.php | 14 +++++++++++++- 3 files changed, 20 insertions(+), 9 deletions(-) (limited to 'lib/private/appframework') diff --git a/lib/base.php b/lib/base.php index 5905aa0406f..cdc662c28d9 100644 --- a/lib/base.php +++ b/lib/base.php @@ -626,7 +626,6 @@ class OC { return; } - $trustedDomainHelper = new \OC\Security\TrustedDomainHelper(\OC::$server->getConfig()); $request = \OC::$server->getRequest(); $host = $request->getInsecureServerHost(); /** @@ -637,7 +636,7 @@ class OC { // overwritehost is always trusted, workaround to not have to make // \OC\AppFramework\Http\Request::getOverwriteHost public && self::$server->getConfig()->getSystemValue('overwritehost') === '' - && !$trustedDomainHelper->isTrustedDomain($host) + && !\OC::$server->getTrustedDomainHelper()->isTrustedDomain($host) ) { header('HTTP/1.1 400 Bad Request'); header('Status: 400 Bad Request'); diff --git a/lib/private/appframework/http/request.php b/lib/private/appframework/http/request.php index 5cffbccb623..d85bfd4f30a 100644 --- a/lib/private/appframework/http/request.php +++ b/lib/private/appframework/http/request.php @@ -128,9 +128,9 @@ class Request implements \ArrayAccess, \Countable, IRequest { } /** - * @param $parameters + * @param array $parameters */ - public function setUrlParameters($parameters) { + public function setUrlParameters(array $parameters) { $this->items['urlParams'] = $parameters; $this->items['parameters'] = array_merge( $this->items['parameters'], @@ -195,8 +195,8 @@ class Request implements \ArrayAccess, \Countable, IRequest { /** * Magic property accessors - * @param $name - * @param $value + * @param string $name + * @param mixed $value */ public function __set($name, $value) { throw new \RuntimeException('You cannot change the contents of the request object'); @@ -253,7 +253,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { } /** - * @param $name + * @param string $name * @return bool */ public function __isset($name) { @@ -261,7 +261,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { } /** - * @param $id + * @param string $id */ public function __unset($id) { throw new \RunTimeException('You cannot change the contents of the request object'); diff --git a/lib/private/server.php b/lib/private/server.php index 9422f332eb1..7c7f3c25cc8 100644 --- a/lib/private/server.php +++ b/lib/private/server.php @@ -17,6 +17,7 @@ use OC\Security\Crypto; use OC\Security\Hasher; use OC\Security\SecureRandom; use OC\Diagnostics\NullEventLogger; +use OC\Security\TrustedDomainHelper; use OCP\IServerContainer; use OCP\ISession; use OC\Tagging\TagMapper; @@ -260,6 +261,9 @@ class Server extends SimpleContainer implements IServerContainer { $this->registerService('IniWrapper', function ($c) { return new IniGetWrapper(); }); + $this->registerService('TrustedDomainHelper', function ($c) { + return new TrustedDomainHelper($this->getConfig()); + }); } /** @@ -324,7 +328,6 @@ class Server extends SimpleContainer implements IServerContainer { ); } - /** * Returns the preview manager which can create preview images for a given file * @@ -743,4 +746,13 @@ class Server extends SimpleContainer implements IServerContainer { public function getIniWrapper() { return $this->query('IniWrapper'); } + + /** + * Get the trusted domain helper + * + * @return TrustedDomainHelper + */ + public function getTrustedDomainHelper() { + return $this->query('TrustedDomainHelper'); + } } -- cgit v1.2.3