diff options
Diffstat (limited to 'lib/private/Files/Stream')
-rw-r--r-- | lib/private/Files/Stream/Encryption.php | 190 | ||||
-rw-r--r-- | lib/private/Files/Stream/HashWrapper.php | 33 | ||||
-rw-r--r-- | lib/private/Files/Stream/Quota.php | 29 | ||||
-rw-r--r-- | lib/private/Files/Stream/SeekableHttpStream.php | 136 |
4 files changed, 178 insertions, 210 deletions
diff --git a/lib/private/Files/Stream/Encryption.php b/lib/private/Files/Stream/Encryption.php index 16d2ca3ce97..ef147ec421f 100644 --- a/lib/private/Files/Stream/Encryption.php +++ b/lib/private/Files/Stream/Encryption.php @@ -1,115 +1,52 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author jknockaert <jasper@knockaert.nl> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author martink-p <47943787+martink-p@users.noreply.github.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.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\Files\Stream; use Icewind\Streams\Wrapper; use OC\Encryption\Exceptions\EncryptionHeaderKeyExistsException; +use OC\Encryption\File; +use OC\Encryption\Util; +use OC\Files\Storage\Storage; +use OCP\Encryption\IEncryptionModule; use function is_array; use function stream_context_create; class Encryption extends Wrapper { - - /** @var \OC\Encryption\Util */ - protected $util; - - /** @var \OC\Encryption\File */ - protected $file; - - /** @var \OCP\Encryption\IEncryptionModule */ - protected $encryptionModule; - - /** @var \OC\Files\Storage\Storage */ - protected $storage; - - /** @var \OC\Files\Storage\Wrapper\Encryption */ - protected $encryptionStorage; - - /** @var string */ - protected $internalPath; - - /** @var string */ - protected $cache; - - /** @var integer */ - protected $size; - - /** @var integer */ - protected $position; - - /** @var integer */ - protected $unencryptedSize; - - /** @var integer */ - protected $headerSize; - - /** @var integer */ - protected $unencryptedBlockSize; - - /** @var array */ - protected $header; - - /** @var string */ - protected $fullPath; - - /** @var bool */ - protected $signed; - + protected Util $util; + protected File $file; + protected IEncryptionModule $encryptionModule; + protected Storage $storage; + protected \OC\Files\Storage\Wrapper\Encryption $encryptionStorage; + protected string $internalPath; + protected string $cache; + protected ?int $size = null; + protected int $position; + protected ?int $unencryptedSize = null; + protected int $headerSize; + protected int $unencryptedBlockSize; + protected array $header; + protected string $fullPath; + protected bool $signed; /** * header data returned by the encryption module, will be written to the file * in case of a write operation - * - * @var array */ - protected $newHeader; - + protected array $newHeader; /** * user who perform the read/write operation null for public access - * - * @var string */ - protected $uid; - - /** @var bool */ - protected $readOnly; - - /** @var bool */ - protected $writeFlag; - - /** @var array */ - protected $expectedContextProperties; - - /** @var bool */ - protected $fileUpdated; + protected ?string $uid; + protected bool $readOnly; + protected bool $writeFlag; + protected array $expectedContextProperties; + protected bool $fileUpdated; public function __construct() { $this->expectedContextProperties = [ @@ -139,14 +76,14 @@ class Encryption extends Wrapper { * @param string $fullPath relative to data/ * @param array $header * @param string $uid - * @param \OCP\Encryption\IEncryptionModule $encryptionModule - * @param \OC\Files\Storage\Storage $storage + * @param IEncryptionModule $encryptionModule + * @param Storage $storage * @param \OC\Files\Storage\Wrapper\Encryption $encStorage - * @param \OC\Encryption\Util $util - * @param \OC\Encryption\File $file + * @param Util $util + * @param File $file * @param string $mode - * @param int $size - * @param int $unencryptedSize + * @param int|float $size + * @param int|float $unencryptedSize * @param int $headerSize * @param bool $signed * @param string $wrapper stream wrapper class @@ -154,19 +91,24 @@ class Encryption extends Wrapper { * * @throws \BadMethodCallException */ - public static function wrap($source, $internalPath, $fullPath, array $header, - $uid, - \OCP\Encryption\IEncryptionModule $encryptionModule, - \OC\Files\Storage\Storage $storage, - \OC\Files\Storage\Wrapper\Encryption $encStorage, - \OC\Encryption\Util $util, - \OC\Encryption\File $file, - $mode, - $size, - $unencryptedSize, - $headerSize, - $signed, - $wrapper = Encryption::class) { + public static function wrap( + $source, + $internalPath, + $fullPath, + array $header, + $uid, + IEncryptionModule $encryptionModule, + Storage $storage, + \OC\Files\Storage\Wrapper\Encryption $encStorage, + Util $util, + File $file, + $mode, + $size, + $unencryptedSize, + $headerSize, + $signed, + $wrapper = Encryption::class, + ) { $context = stream_context_create([ 'ocencryption' => [ 'source' => $source, @@ -213,7 +155,7 @@ class Encryption extends Wrapper { } else { $wrapped = fopen($protocol . '://', $mode, false, $context); } - } catch (\BadMethodCallException $e) { + } catch (\Exception $e) { stream_wrapper_unregister($protocol); throw $e; } @@ -260,7 +202,6 @@ class Encryption extends Wrapper { $this->cache = ''; $this->writeFlag = false; $this->fileUpdated = false; - $this->unencryptedBlockSize = $this->encryptionModule->getUnencryptedBlockSize($this->signed); if ( $mode === 'w' @@ -285,6 +226,7 @@ class Encryption extends Wrapper { $accessList = $this->file->getAccessList($sharePath); } $this->newHeader = $this->encryptionModule->begin($this->fullPath, $this->uid, $mode, $this->header, $accessList); + $this->unencryptedBlockSize = $this->encryptionModule->getUnencryptedBlockSize($this->signed); if ( $mode === 'w' @@ -323,7 +265,7 @@ class Encryption extends Wrapper { $result .= substr($this->cache, $blockPosition, $remainingLength); $this->position += $remainingLength; $count = 0; - // otherwise remainder of current block is fetched, the block is flushed and the position updated + // otherwise remainder of current block is fetched, the block is flushed and the position updated } else { $result .= substr($this->cache, $blockPosition); $this->flush(); @@ -370,13 +312,12 @@ class Encryption extends Wrapper { // for seekable streams the pointer is moved back to the beginning of the encrypted block // flush will start writing there when the position moves to another block - $positionInFile = (int)floor($this->position / $this->unencryptedBlockSize) * - $this->util->getBlockSize() + $this->headerSize; + $positionInFile = (int)floor($this->position / $this->unencryptedBlockSize) + * $this->util->getBlockSize() + $this->headerSize; $resultFseek = $this->parentStreamSeek($positionInFile); // only allow writes on seekable streams, or at the end of the encrypted stream if (!$this->readOnly && ($resultFseek || $positionInFile === $this->size)) { - // switch the writeFlag so flush() will write the block $this->writeFlag = true; $this->fileUpdated = true; @@ -392,11 +333,11 @@ class Encryption extends Wrapper { $this->position += $remainingLength; $length += $remainingLength; $data = ''; - // if $data doesn't fit the current block, the fill the current block and reiterate + // if $data doesn't fit the current block, the fill the current block and reiterate // after the block is filled, it is flushed and $data is updatedxxx } else { - $this->cache = substr($this->cache, 0, $blockPosition) . - substr($data, 0, $this->unencryptedBlockSize - $blockPosition); + $this->cache = substr($this->cache, 0, $blockPosition) + . substr($data, 0, $this->unencryptedBlockSize - $blockPosition); $this->flush(); $this->position += ($this->unencryptedBlockSize - $blockPosition); $length += ($this->unencryptedBlockSize - $blockPosition); @@ -466,7 +407,7 @@ class Encryption extends Wrapper { $cacheEntry = $cache->get($this->internalPath); if ($cacheEntry) { $version = $cacheEntry['encryptedVersion'] + 1; - $cache->update($cacheEntry->getId(), ['encrypted' => $version, 'encryptedVersion' => $version]); + $cache->update($cacheEntry->getId(), ['encrypted' => $version, 'encryptedVersion' => $version, 'unencrypted_size' => $this->unencryptedSize]); } } @@ -524,11 +465,12 @@ class Encryption extends Wrapper { /** * write header at beginning of encrypted file * - * @return integer + * @return int|false * @throws EncryptionHeaderKeyExistsException if header key is already in use */ protected function writeHeader() { $header = $this->util->createHeader($this->newHeader, $this->encryptionModule); + $this->fileUpdated = true; return parent::stream_write($header); } diff --git a/lib/private/Files/Stream/HashWrapper.php b/lib/private/Files/Stream/HashWrapper.php index 059dd117555..5956ad92549 100644 --- a/lib/private/Files/Stream/HashWrapper.php +++ b/lib/private/Files/Stream/HashWrapper.php @@ -3,27 +3,9 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl> - * - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace OC\Files\Stream; use Icewind\Streams\Wrapper; @@ -68,7 +50,16 @@ class HashWrapper extends Wrapper { public function stream_close() { if (is_callable($this->callback)) { - call_user_func($this->callback, hash_final($this->hash)); + // if the stream is closed as a result of the end-of-request GC, the hash context might be cleaned up before this stream + if ($this->hash instanceof \HashContext) { + try { + $hash = @hash_final($this->hash); + if ($hash) { + call_user_func($this->callback, $hash); + } + } catch (\Throwable $e) { + } + } // prevent further calls by potential PHP 7 GC ghosts $this->callback = null; } diff --git a/lib/private/Files/Stream/Quota.php b/lib/private/Files/Stream/Quota.php index f5db662a0ae..cc737910fd8 100644 --- a/lib/private/Files/Stream/Quota.php +++ b/lib/private/Files/Stream/Quota.php @@ -1,29 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Vincent Petry <vincent@nextcloud.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\Files\Stream; use Icewind\Streams\Wrapper; @@ -42,7 +23,7 @@ class Quota extends Wrapper { /** * @param resource $stream * @param int $limit - * @return bool|resource + * @return resource|false */ public static function wrap($stream, $limit) { $context = stream_context_create([ diff --git a/lib/private/Files/Stream/SeekableHttpStream.php b/lib/private/Files/Stream/SeekableHttpStream.php index efbfd293ad5..6ce0a880e8d 100644 --- a/lib/private/Files/Stream/SeekableHttpStream.php +++ b/lib/private/Files/Stream/SeekableHttpStream.php @@ -1,30 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2020, Lukas Stabe (lukas@stabe.de) - * - * @author Lukas Stabe <lukas@stabe.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace OC\Files\Stream; use Icewind\Streams\File; +use Icewind\Streams\Wrapper; /** * A stream wrapper that uses http range requests to provide a seekable stream for http reading @@ -32,7 +15,7 @@ use Icewind\Streams\File; class SeekableHttpStream implements File { private const PROTOCOL = 'httpseek'; - private static $registered = false; + private static bool $registered = false; /** * Registers the stream wrapper using the `httpseek://` url scheme @@ -73,28 +56,49 @@ class SeekableHttpStream implements File { /** @var callable */ private $openCallback; - /** @var resource */ + /** @var ?resource|closed-resource */ private $current; - /** @var int */ - private $offset = 0; - - private function reconnect(int $start) { + /** @var int $offset offset of the current chunk */ + private int $offset = 0; + /** @var int $length length of the current chunk */ + private int $length = 0; + /** @var int $totalSize size of the full stream */ + private int $totalSize = 0; + private bool $needReconnect = false; + + private function reconnect(int $start): bool { + $this->needReconnect = false; $range = $start . '-'; - if ($this->current != null) { + if ($this->hasOpenStream()) { fclose($this->current); } - $this->current = ($this->openCallback)($range); + $stream = ($this->openCallback)($range); - if ($this->current === false) { + if ($stream === false) { + $this->current = null; return false; } + $this->current = $stream; $responseHead = stream_get_meta_data($this->current)['wrapper_data']; + + while ($responseHead instanceof Wrapper) { + $wrapperOptions = stream_context_get_options($responseHead->context); + foreach ($wrapperOptions as $options) { + if (isset($options['source']) && is_resource($options['source'])) { + $responseHead = stream_get_meta_data($options['source'])['wrapper_data']; + continue 2; + } + } + throw new \Exception('Failed to get source stream from stream wrapper of ' . get_class($responseHead)); + } + $rangeHeaders = array_values(array_filter($responseHead, function ($v) { return preg_match('#^content-range:#i', $v) === 1; })); if (!$rangeHeaders) { + $this->current = null; return false; } $contentRange = $rangeHeaders[0]; @@ -102,16 +106,44 @@ class SeekableHttpStream implements File { $content = trim(explode(':', $contentRange)[1]); $range = trim(explode(' ', $content)[1]); $begin = intval(explode('-', $range)[0]); + $length = intval(explode('/', $range)[1]); if ($begin !== $start) { + $this->current = null; return false; } $this->offset = $begin; + $this->length = $length; + if ($start === 0) { + $this->totalSize = $length; + } return true; } + /** + * @return ?resource + */ + private function getCurrent() { + if ($this->needReconnect) { + $this->reconnect($this->offset); + } + if (is_resource($this->current)) { + return $this->current; + } else { + return null; + } + } + + /** + * @return bool + * @psalm-assert-if-true resource $this->current + */ + private function hasOpenStream(): bool { + return is_resource($this->current); + } + public function stream_open($path, $mode, $options, &$opened_path) { $options = stream_context_get_options($this->context)[self::PROTOCOL]; $this->openCallback = $options['callback']; @@ -120,10 +152,10 @@ class SeekableHttpStream implements File { } public function stream_read($count) { - if (!$this->current) { + if (!$this->getCurrent()) { return false; } - $ret = fread($this->current, $count); + $ret = fread($this->getCurrent(), $count); $this->offset += strlen($ret); return $ret; } @@ -133,17 +165,34 @@ class SeekableHttpStream implements File { case SEEK_SET: if ($offset === $this->offset) { return true; + } else { + $this->offset = $offset; } - return $this->reconnect($offset); + break; case SEEK_CUR: if ($offset === 0) { return true; + } else { + $this->offset += $offset; } - return $this->reconnect($this->offset + $offset); + break; case SEEK_END: - return false; + if ($this->length === 0) { + return false; + } elseif ($this->length + $offset === $this->offset) { + return true; + } else { + $this->offset = $this->length + $offset; + } + break; } - return false; + + if ($this->hasOpenStream()) { + fclose($this->current); + } + $this->current = null; + $this->needReconnect = true; + return true; } public function stream_tell() { @@ -151,25 +200,30 @@ class SeekableHttpStream implements File { } public function stream_stat() { - if (is_resource($this->current)) { - return fstat($this->current); + if ($this->getCurrent()) { + $stat = fstat($this->getCurrent()); + if ($stat) { + $stat['size'] = $this->totalSize; + } + return $stat; } else { return false; } } public function stream_eof() { - if (is_resource($this->current)) { - return feof($this->current); + if ($this->getCurrent()) { + return feof($this->getCurrent()); } else { return true; } } public function stream_close() { - if (is_resource($this->current)) { + if ($this->hasOpenStream()) { fclose($this->current); } + $this->current = null; } public function stream_write($data) { |