diff options
Diffstat (limited to 'lib/private/AppFramework/Http')
-rw-r--r-- | lib/private/AppFramework/Http/Dispatcher.php | 102 | ||||
-rw-r--r-- | lib/private/AppFramework/Http/Output.php | 38 | ||||
-rw-r--r-- | lib/private/AppFramework/Http/Request.php | 260 | ||||
-rw-r--r-- | lib/private/AppFramework/Http/RequestId.php | 22 |
4 files changed, 195 insertions, 227 deletions
diff --git a/lib/private/AppFramework/Http/Dispatcher.php b/lib/private/AppFramework/Http/Dispatcher.php index b4b03574d56..8d91ddf7502 100644 --- a/lib/private/AppFramework/Http/Dispatcher.php +++ b/lib/private/AppFramework/Http/Dispatcher.php @@ -1,34 +1,10 @@ <?php declare(strict_types=1); - /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bernhard Posselt <dev@bernhard-posselt.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Thomas Tanghus <thomas@tanghus.net> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\AppFramework\Http; @@ -38,6 +14,7 @@ use OC\AppFramework\Utility\ControllerMethodReflector; use OC\DB\ConnectionAdapter; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\ParameterOutOfRangeException; use OCP\AppFramework\Http\Response; use OCP\Diagnostics\IEventLogger; use OCP\IConfig; @@ -78,24 +55,26 @@ class Dispatcher { /** * @param Http $protocol the http protocol with contains all status headers * @param MiddlewareDispatcher $middlewareDispatcher the dispatcher which - * runs the middleware + * runs the middleware * @param ControllerMethodReflector $reflector the reflector that is used to inject - * the arguments for the controller + * the arguments for the controller * @param IRequest $request the incoming request * @param IConfig $config * @param ConnectionAdapter $connection * @param LoggerInterface $logger * @param IEventLogger $eventLogger */ - public function __construct(Http $protocol, - MiddlewareDispatcher $middlewareDispatcher, - ControllerMethodReflector $reflector, - IRequest $request, - IConfig $config, - ConnectionAdapter $connection, - LoggerInterface $logger, - IEventLogger $eventLogger, - ContainerInterface $appContainer) { + public function __construct( + Http $protocol, + MiddlewareDispatcher $middlewareDispatcher, + ControllerMethodReflector $reflector, + IRequest $request, + IConfig $config, + ConnectionAdapter $connection, + LoggerInterface $logger, + IEventLogger $eventLogger, + ContainerInterface $appContainer, + ) { $this->protocol = $protocol; $this->middlewareDispatcher = $middlewareDispatcher; $this->reflector = $reflector; @@ -112,10 +91,12 @@ class Dispatcher { * Handles a request and calls the dispatcher on the controller * @param Controller $controller the controller which will be called * @param string $methodName the method name which will be called on - * the controller - * @return array $array[0] contains a string with the http main header, - * $array[1] contains headers in the form: $key => value, $array[2] contains - * the response output + * the controller + * @return array $array[0] contains the http status header as a string, + * $array[1] contains response headers as an array, + * $array[2] contains response cookies as an array, + * $array[3] contains the response output as a string, + * $array[4] contains the response object * @throws \Exception */ public function dispatch(Controller $controller, string $methodName): array { @@ -197,7 +178,7 @@ class Dispatcher { private function executeController(Controller $controller, string $methodName): Response { $arguments = []; - // valid types that will be casted + // valid types that will be cast $types = ['int', 'integer', 'bool', 'boolean', 'float', 'double']; foreach ($this->reflector->getParameters() as $param => $default) { @@ -206,19 +187,12 @@ class Dispatcher { $value = $this->request->getParam($param, $default); $type = $this->reflector->getType($param); - // if this is submitted using GET or a POST form, 'false' should be - // converted to false - if (($type === 'bool' || $type === 'boolean') && - $value === 'false' && - ( - $this->request->method === 'GET' || - strpos($this->request->getHeader('Content-Type'), - 'application/x-www-form-urlencoded') !== false - ) - ) { + // Converted the string `'false'` to false when the controller wants a boolean + if ($value === 'false' && ($type === 'bool' || $type === 'boolean')) { $value = false; } elseif ($value !== null && \in_array($type, $types, true)) { settype($value, $type); + $this->ensureParameterValueSatisfiesRange($param, $value); } elseif ($value === null && $type !== null && $this->appContainer->has($type)) { $value = $this->appContainer->get($type); } @@ -230,6 +204,10 @@ class Dispatcher { $response = \call_user_func_array([$controller, $methodName], $arguments); $this->eventLogger->end('controller:' . get_class($controller) . '::' . $methodName); + if (!($response instanceof Response)) { + $this->logger->debug($controller::class . '::' . $methodName . ' returned raw data. Please wrap it in a Response or one of it\'s inheritors.'); + } + // format response if ($response instanceof DataResponse || !($response instanceof Response)) { // get format from the url format or request format parameter @@ -250,4 +228,22 @@ class Dispatcher { return $response; } + + /** + * @psalm-param mixed $value + * @throws ParameterOutOfRangeException + */ + private function ensureParameterValueSatisfiesRange(string $param, $value): void { + $rangeInfo = $this->reflector->getRange($param); + if ($rangeInfo) { + if ($value < $rangeInfo['min'] || $value > $rangeInfo['max']) { + throw new ParameterOutOfRangeException( + $param, + $value, + $rangeInfo['min'], + $rangeInfo['max'], + ); + } + } + } } diff --git a/lib/private/AppFramework/Http/Output.php b/lib/private/AppFramework/Http/Output.php index 963e01456e0..b4a8672fdc7 100644 --- a/lib/private/AppFramework/Http/Output.php +++ b/lib/private/AppFramework/Http/Output.php @@ -1,28 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bernhard Posselt <dev@bernhard-posselt.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Stefan Weil <sw@weilnetz.de> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\AppFramework\Http; @@ -32,14 +13,9 @@ use OCP\AppFramework\Http\IOutput; * Very thin wrapper class to make output testable */ class Output implements IOutput { - /** @var string */ - private $webRoot; - - /** - * @param $webRoot - */ - public function __construct($webRoot) { - $this->webRoot = $webRoot; + public function __construct( + private string $webRoot, + ) { } /** diff --git a/lib/private/AppFramework/Http/Request.php b/lib/private/AppFramework/Http/Request.php index ac162f6565e..7cc7467675c 100644 --- a/lib/private/AppFramework/Http/Request.php +++ b/lib/private/AppFramework/Http/Request.php @@ -1,47 +1,10 @@ <?php declare(strict_types=1); - /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author b108@volgograd "b108@volgograd" - * @author Bart Visscher <bartv@thisnet.nl> - * @author Bernhard Posselt <dev@bernhard-posselt.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author J0WI <J0WI@users.noreply.github.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Juan Pablo Villafáñez <jvillafanez@solidgear.es> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Mitar <mitar.git@tnode.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Oliver Wegner <void1976@gmail.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Thomas Tanghus <thomas@tanghus.net> - * @author Vincent Petry <vincent@nextcloud.com> - * @author Simon Leiner <simon@leiner.me> - * @author Stanimir Bozhilov <stanimir@audriga.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\AppFramework\Http; @@ -51,36 +14,39 @@ use OC\Security\TrustedDomainHelper; use OCP\IConfig; use OCP\IRequest; use OCP\IRequestId; +use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\IpUtils; /** * Class for accessing variables in the request. * This class provides an immutable object with request variables. * - * @property mixed[] cookies - * @property mixed[] env - * @property mixed[] files - * @property string method - * @property mixed[] parameters - * @property mixed[] server + * @property mixed[] $cookies + * @property mixed[] $env + * @property mixed[] $files + * @property string $method + * @property mixed[] $parameters + * @property mixed[] $server + * @template-implements \ArrayAccess<string,mixed> */ class Request implements \ArrayAccess, \Countable, IRequest { public const USER_AGENT_IE = '/(MSIE)|(Trident)/'; // Microsoft Edge User Agent from https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx - public const USER_AGENT_MS_EDGE = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+ Edge\/[0-9.]+$/'; + public const USER_AGENT_MS_EDGE = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+ Edge?\/[0-9.]+$/'; // Firefox User Agent from https://developer.mozilla.org/en-US/docs/Web/HTTP/Gecko_user_agent_string_reference public const USER_AGENT_FIREFOX = '/^Mozilla\/5\.0 \([^)]+\) Gecko\/[0-9.]+ Firefox\/[0-9.]+$/'; // Chrome User Agent from https://developer.chrome.com/multidevice/user-agent public const USER_AGENT_CHROME = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\)( Ubuntu Chromium\/[0-9.]+|) Chrome\/[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+( (Vivaldi|Brave|OPR)\/[0-9.]+|)$/'; // Safari User Agent from http://www.useragentstring.com/pages/Safari/ public const USER_AGENT_SAFARI = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/[0-9.]+ Safari\/[0-9.A-Z]+$/'; + public const USER_AGENT_SAFARI_MOBILE = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/[0-9.]+ (Mobile\/[0-9.A-Z]+) Safari\/[0-9.A-Z]+$/'; // Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent public const USER_AGENT_ANDROID_MOBILE_CHROME = '#Android.*Chrome/[.0-9]*#'; public const USER_AGENT_FREEBOX = '#^Mozilla/5\.0$#'; public const REGEX_LOCALHOST = '/^(127\.0\.0\.1|localhost|\[::1\])$/'; protected string $inputStream; - protected $content; + private bool $isPutStreamContentAlreadySent = false; protected array $items = []; protected array $allowedKeys = [ 'get', @@ -99,18 +65,19 @@ class Request implements \ArrayAccess, \Countable, IRequest { protected ?CsrfTokenManager $csrfTokenManager; protected bool $contentDecoded = false; + private ?\JsonException $decodingException = null; /** * @param array $vars An associative array with the following optional values: - * - array 'urlParams' the parameters which were matched from the URL - * - array 'get' the $_GET array - * - array|string 'post' the $_POST array or JSON string - * - array 'files' the $_FILES array - * - array 'server' the $_SERVER array - * - array 'env' the $_ENV array - * - array 'cookies' the $_COOKIE array - * - string 'method' the request method (GET, POST etc) - * - string|false 'requesttoken' the requesttoken or false when not available + * - array 'urlParams' the parameters which were matched from the URL + * - array 'get' the $_GET array + * - array|string 'post' the $_POST array or JSON string + * - array 'files' the $_FILES array + * - array 'server' the $_SERVER array + * - array 'env' the $_ENV array + * - array 'cookies' the $_COOKIE array + * - string 'method' the request method (GET, POST etc) + * - string|false 'requesttoken' the requesttoken or false when not available * @param IRequestId $requestId * @param IConfig $config * @param CsrfTokenManager|null $csrfTokenManager @@ -118,10 +85,10 @@ class Request implements \ArrayAccess, \Countable, IRequest { * @see https://www.php.net/manual/en/reserved.variables.php */ public function __construct(array $vars, - IRequestId $requestId, - IConfig $config, - CsrfTokenManager $csrfTokenManager = null, - string $stream = 'php://input') { + IRequestId $requestId, + IConfig $config, + ?CsrfTokenManager $csrfTokenManager = null, + string $stream = 'php://input') { $this->inputStream = $stream; $this->items['params'] = []; $this->requestId = $requestId; @@ -193,9 +160,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { */ #[\ReturnTypeWillChange] public function offsetGet($offset) { - return isset($this->items['parameters'][$offset]) - ? $this->items['parameters'][$offset] - : null; + return $this->items['parameters'][$offset] ?? null; } /** @@ -255,11 +220,12 @@ class Request implements \ArrayAccess, \Countable, IRequest { case 'cookies': case 'urlParams': case 'method': - return isset($this->items[$name]) - ? $this->items[$name] - : null; + return $this->items[$name] ?? null; case 'parameters': case 'params': + if ($this->isPutStreamContent()) { + return $this->items['parameters']; + } return $this->getContent(); default: return isset($this[$name]) @@ -320,11 +286,11 @@ class Request implements \ArrayAccess, \Countable, IRequest { * In case of json requests the encoded json body is accessed * * @param string $key the key which you want to access in the URL Parameter - * placeholder, $_POST or $_GET array. - * The priority how they're returned is the following: - * 1. URL parameters - * 2. POST parameters - * 3. GET parameters + * placeholder, $_POST or $_GET array. + * The priority how they're returned is the following: + * 1. URL parameters + * 2. POST parameters + * 3. GET parameters * @param mixed $default If the key is not found, this value will be returned * @return mixed the content of the array */ @@ -391,19 +357,14 @@ class Request implements \ArrayAccess, \Countable, IRequest { */ protected function getContent() { // If the content can't be parsed into an array then return a stream resource. - if ($this->method === 'PUT' - && $this->getHeader('Content-Length') !== '0' - && $this->getHeader('Content-Length') !== '' - && strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') === false - && strpos($this->getHeader('Content-Type'), 'application/json') === false - ) { - if ($this->content === false) { + if ($this->isPutStreamContent()) { + if ($this->isPutStreamContentAlreadySent) { throw new \LogicException( '"put" can only be accessed once if not ' . 'application/x-www-form-urlencoded or application/json.' ); } - $this->content = false; + $this->isPutStreamContentAlreadySent = true; return fopen($this->inputStream, 'rb'); } else { $this->decodeContent(); @@ -411,6 +372,14 @@ class Request implements \ArrayAccess, \Countable, IRequest { } } + private function isPutStreamContent(): bool { + return $this->method === 'PUT' + && $this->getHeader('Content-Length') !== '0' + && $this->getHeader('Content-Length') !== '' + && !str_contains($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') + && !str_contains($this->getHeader('Content-Type'), 'application/json'); + } + /** * Attempt to decode the content and populate parameters */ @@ -422,18 +391,25 @@ class Request implements \ArrayAccess, \Countable, IRequest { // 'application/json' and other JSON-related content types must be decoded manually. if (preg_match(self::JSON_CONTENT_TYPE_REGEX, $this->getHeader('Content-Type')) === 1) { - $params = json_decode(file_get_contents($this->inputStream), true); + $content = file_get_contents($this->inputStream); + if ($content !== '') { + try { + $params = json_decode($content, true, flags:JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + $this->decodingException = $e; + } + } if (\is_array($params) && \count($params) > 0) { $this->items['params'] = $params; if ($this->method === 'POST') { $this->items['post'] = $params; } } - // Handle application/x-www-form-urlencoded for methods other than GET - // or post correctly + // Handle application/x-www-form-urlencoded for methods other than GET + // or post correctly } elseif ($this->method !== 'GET' && $this->method !== 'POST' - && strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') !== false) { + && str_contains($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded')) { parse_str(file_get_contents($this->inputStream), $params); if (\is_array($params)) { $this->items['params'] = $params; @@ -446,6 +422,12 @@ class Request implements \ArrayAccess, \Countable, IRequest { $this->contentDecoded = true; } + public function throwDecodingExceptionIfAny(): void { + if ($this->decodingException !== null) { + throw $this->decodingException; + } + } + /** * Checks if the CSRF check was correct @@ -460,6 +442,10 @@ class Request implements \ArrayAccess, \Countable, IRequest { return false; } + if ($this->getHeader('OCS-APIRequest') !== '') { + return true; + } + if (isset($this->items['get']['requesttoken'])) { $token = $this->items['get']['requesttoken']; } elseif (isset($this->items['post']['requesttoken'])) { @@ -513,7 +499,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { $prefix = '__Host-'; } - return $prefix.$name; + return $prefix . $name; } /** @@ -571,7 +557,14 @@ class Request implements \ArrayAccess, \Countable, IRequest { * @return boolean true if $remoteAddress matches any entry in $trustedProxies, false otherwise */ protected function isTrustedProxy($trustedProxies, $remoteAddress) { - return IpUtils::checkIp($remoteAddress, $trustedProxies); + try { + return IpUtils::checkIp($remoteAddress, $trustedProxies); + } catch (\Throwable) { + // We can not log to our log here as the logger is using `getRemoteAddress` which uses the function, so we would have a cyclic dependency + // Reaching this line means `trustedProxies` is in invalid format. + error_log('Nextcloud trustedProxies has malformed entries'); + return false; + } } /** @@ -591,14 +584,25 @@ class Request implements \ArrayAccess, \Countable, IRequest { // only have one default, so we cannot ship an insecure product out of the box ]); - foreach ($forwardedForHeaders as $header) { + // Read the x-forwarded-for headers and values in reverse order as per + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#selecting_an_ip_address + foreach (array_reverse($forwardedForHeaders) as $header) { if (isset($this->server[$header])) { - foreach (explode(',', $this->server[$header]) as $IP) { + foreach (array_reverse(explode(',', $this->server[$header])) as $IP) { $IP = trim($IP); + $colons = substr_count($IP, ':'); + if ($colons > 1) { + // Extract IP from string with brackets and optional port + if (preg_match('/^\[(.+?)\](?::\d+)?$/', $IP, $matches) && isset($matches[1])) { + $IP = $matches[1]; + } + } elseif ($colons === 1) { + // IPv4 with port + $IP = substr($IP, 0, strpos($IP, ':')); + } - // remove brackets from IPv6 addresses - if (strpos($IP, '[') === 0 && substr($IP, -1) === ']') { - $IP = substr($IP, 1, -1); + if ($this->isTrustedProxy($trustedProxies, $IP)) { + continue; } if (filter_var($IP, FILTER_VALIDATE_IP) !== false) { @@ -614,48 +618,56 @@ class Request implements \ArrayAccess, \Countable, IRequest { /** * Check overwrite condition - * @param string $type * @return bool */ - private function isOverwriteCondition(string $type = ''): bool { - $regex = '/' . $this->config->getSystemValue('overwritecondaddr', '') . '/'; + private function isOverwriteCondition(): bool { + $regex = '/' . $this->config->getSystemValueString('overwritecondaddr', '') . '/'; $remoteAddr = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : ''; - return $regex === '//' || preg_match($regex, $remoteAddr) === 1 - || $type !== 'protocol'; + return $regex === '//' || preg_match($regex, $remoteAddr) === 1; } /** * Returns the server protocol. It respects one or more reverse proxies servers - * and load balancers + * and load balancers. Precedence: + * 1. `overwriteprotocol` config value + * 2. `X-Forwarded-Proto` header value + * 3. $_SERVER['HTTPS'] value + * If an invalid protocol is provided, defaults to http, continues, but logs as an error. + * * @return string Server protocol (http or https) */ public function getServerProtocol(): string { - if ($this->config->getSystemValue('overwriteprotocol') !== '' - && $this->isOverwriteCondition('protocol')) { - return $this->config->getSystemValue('overwriteprotocol'); - } + $proto = 'http'; - if ($this->fromTrustedProxy() && isset($this->server['HTTP_X_FORWARDED_PROTO'])) { - if (strpos($this->server['HTTP_X_FORWARDED_PROTO'], ',') !== false) { + if ($this->config->getSystemValueString('overwriteprotocol') !== '' + && $this->isOverwriteCondition() + ) { + $proto = strtolower($this->config->getSystemValueString('overwriteprotocol')); + } elseif ($this->fromTrustedProxy() + && isset($this->server['HTTP_X_FORWARDED_PROTO']) + ) { + if (str_contains($this->server['HTTP_X_FORWARDED_PROTO'], ',')) { $parts = explode(',', $this->server['HTTP_X_FORWARDED_PROTO']); $proto = strtolower(trim($parts[0])); } else { $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'; + } elseif (!empty($this->server['HTTPS']) + && $this->server['HTTPS'] !== 'off' + ) { + $proto = 'https'; } - if (isset($this->server['HTTPS']) - && $this->server['HTTPS'] !== null - && $this->server['HTTPS'] !== 'off' - && $this->server['HTTPS'] !== '') { - return 'https'; + if ($proto !== 'https' && $proto !== 'http') { + // log unrecognized value so admin has a chance to fix it + \OCP\Server::get(LoggerInterface::class)->critical( + 'Server protocol is malformed [falling back to http] (check overwriteprotocol and/or X-Forwarded-Proto to remedy): ' . $proto, + ['app' => 'core'] + ); } - return 'http'; + // default to http if provided an invalid value + return $proto === 'https' ? 'https' : 'http'; } /** @@ -690,7 +702,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { */ public function getRequestUri(): string { $uri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : ''; - if ($this->config->getSystemValue('overwritewebroot') !== '' && $this->isOverwriteCondition()) { + if ($this->config->getSystemValueString('overwritewebroot') !== '' && $this->isOverwriteCondition()) { $uri = $this->getScriptName() . substr($uri, \strlen($this->server['SCRIPT_NAME'])); } return $uri; @@ -718,7 +730,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { // FIXME: Sabre does not really belong here [$path, $name] = \Sabre\Uri\split($scriptName); if (!empty($path)) { - if ($path === $pathInfo || strpos($pathInfo, $path.'/') === 0) { + if ($path === $pathInfo || str_starts_with($pathInfo, $path . '/')) { $pathInfo = substr($pathInfo, \strlen($path)); } else { throw new \Exception("The requested uri($requestUri) cannot be processed by the script '$scriptName')"); @@ -728,10 +740,10 @@ class Request implements \ArrayAccess, \Countable, IRequest { $name = ''; } - if (strpos($pathInfo, '/'.$name) === 0) { + if (str_starts_with($pathInfo, '/' . $name)) { $pathInfo = substr($pathInfo, \strlen($name) + 1); } - if ($name !== '' && strpos($pathInfo, $name) === 0) { + if ($name !== '' && str_starts_with($pathInfo, $name)) { $pathInfo = substr($pathInfo, \strlen($name)); } if ($pathInfo === false || $pathInfo === '/') { @@ -742,11 +754,11 @@ class Request implements \ArrayAccess, \Countable, IRequest { } /** - * Get PathInfo from request + * Get PathInfo from request (rawurldecoded) * @throws \Exception * @return string|false Path info or false when not found */ - public function getPathInfo() { + public function getPathInfo(): string|false { $pathInfo = $this->getRawPathInfo(); return \Sabre\HTTP\decodePath($pathInfo); } @@ -758,7 +770,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { */ public function getScriptName(): string { $name = $this->server['SCRIPT_NAME']; - $overwriteWebRoot = $this->config->getSystemValue('overwritewebroot'); + $overwriteWebRoot = $this->config->getSystemValueString('overwritewebroot'); if ($overwriteWebRoot !== '' && $this->isOverwriteCondition()) { // 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/'))); @@ -797,7 +809,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { $host = 'localhost'; if ($this->fromTrustedProxy() && isset($this->server['HTTP_X_FORWARDED_HOST'])) { - if (strpos($this->server['HTTP_X_FORWARDED_HOST'], ',') !== false) { + if (str_contains($this->server['HTTP_X_FORWARDED_HOST'], ',')) { $parts = explode(',', $this->server['HTTP_X_FORWARDED_HOST']); $host = trim(current($parts)); } else { @@ -850,11 +862,11 @@ class Request implements \ArrayAccess, \Countable, IRequest { * 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 + * isn't met */ private function getOverwriteHost() { - if ($this->config->getSystemValue('overwritehost') !== '' && $this->isOverwriteCondition()) { - return $this->config->getSystemValue('overwritehost'); + if ($this->config->getSystemValueString('overwritehost') !== '' && $this->isOverwriteCondition()) { + return $this->config->getSystemValueString('overwritehost'); } return null; } diff --git a/lib/private/AppFramework/Http/RequestId.php b/lib/private/AppFramework/Http/RequestId.php index 70032873a75..c3a99c93591 100644 --- a/lib/private/AppFramework/Http/RequestId.php +++ b/lib/private/AppFramework/Http/RequestId.php @@ -2,24 +2,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022, Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\AppFramework\Http; @@ -31,7 +15,7 @@ class RequestId implements IRequestId { protected string $requestId; public function __construct(string $uniqueId, - ISecureRandom $secureRandom) { + ISecureRandom $secureRandom) { $this->requestId = $uniqueId; $this->secureRandom = $secureRandom; } |