From c2677682c4a3886f8316bfae120dc1b2fbde39ac Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 7 Feb 2020 14:22:12 +0100 Subject: [PATCH] Return hashes of uploaded content for dav uploads hashes are set in "X-Hash-MD5", "X-Hash-SHA1" and "X-Hash-SHA256" headers. these headers are set for file uploads and the MOVE request at the end of a multipart upload. Signed-off-by: Robin Appelman --- apps/dav/lib/Connector/Sabre/File.php | 27 +++++--- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + lib/private/Files/Stream/HashWrapper.php | 72 +++++++++++++++++++++ tests/lib/Files/Stream/HashWrapperTest.php | 55 ++++++++++++++++ 5 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 lib/private/Files/Stream/HashWrapper.php create mode 100644 tests/lib/Files/Stream/HashWrapperTest.php diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php index 2c108819e9..a7a2470b4f 100644 --- a/apps/dav/lib/Connector/Sabre/File.php +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -41,6 +41,7 @@ namespace OCA\DAV\Connector\Sabre; use Icewind\Streams\CallbackWrapper; use OC\AppFramework\Http\Request; use OC\Files\Filesystem; +use OC\Files\Stream\HashWrapper; use OC\Files\View; use OCA\DAV\Connector\Sabre\Exception\EntityTooLarge; use OCA\DAV\Connector\Sabre\Exception\FileLocked; @@ -173,16 +174,26 @@ class File extends Node implements IFile { $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE); } - if ($partStorage->instanceOfStorage(Storage\IWriteStreamStorage::class)) { - if (!is_resource($data)) { - $tmpData = fopen('php://temp', 'r+'); - if ($data !== null) { - fwrite($tmpData, $data); - rewind($tmpData); - } - $data = $tmpData; + if (!is_resource($data)) { + $tmpData = fopen('php://temp', 'r+'); + if ($data !== null) { + fwrite($tmpData, $data); + rewind($tmpData); } + $data = $tmpData; + } + $data = HashWrapper::wrap($data, 'md5', function ($hash) { + $this->header('X-Hash-MD5: ' . $hash); + }); + $data = HashWrapper::wrap($data, 'sha1', function ($hash) { + $this->header('X-Hash-SHA1: ' . $hash); + }); + $data = HashWrapper::wrap($data, 'sha256', function ($hash) { + $this->header('X-Hash-SHA256: ' . $hash); + }); + + if ($partStorage->instanceOfStorage(Storage\IWriteStreamStorage::class)) { $isEOF = false; $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF) { $isEOF = feof($stream); diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index c600c15068..3b709fe8e9 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1024,6 +1024,7 @@ return array( 'OC\\Files\\Storage\\Wrapper\\Quota' => $baseDir . '/lib/private/Files/Storage/Wrapper/Quota.php', 'OC\\Files\\Storage\\Wrapper\\Wrapper' => $baseDir . '/lib/private/Files/Storage/Wrapper/Wrapper.php', 'OC\\Files\\Stream\\Encryption' => $baseDir . '/lib/private/Files/Stream/Encryption.php', + 'OC\\Files\\Stream\\HashWrapper' => $baseDir . '/lib/private/Files/Stream/HashWrapper.php', 'OC\\Files\\Stream\\Quota' => $baseDir . '/lib/private/Files/Stream/Quota.php', 'OC\\Files\\Stream\\SeekableHttpStream' => $baseDir . '/lib/private/Files/Stream/SeekableHttpStream.php', 'OC\\Files\\Type\\Detection' => $baseDir . '/lib/private/Files/Type/Detection.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 87a9460f77..1c3d23011c 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1053,6 +1053,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Files\\Storage\\Wrapper\\Quota' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/Quota.php', 'OC\\Files\\Storage\\Wrapper\\Wrapper' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/Wrapper.php', 'OC\\Files\\Stream\\Encryption' => __DIR__ . '/../../..' . '/lib/private/Files/Stream/Encryption.php', + 'OC\\Files\\Stream\\HashWrapper' => __DIR__ . '/../../..' . '/lib/private/Files/Stream/HashWrapper.php', 'OC\\Files\\Stream\\Quota' => __DIR__ . '/../../..' . '/lib/private/Files/Stream/Quota.php', 'OC\\Files\\Stream\\SeekableHttpStream' => __DIR__ . '/../../..' . '/lib/private/Files/Stream/SeekableHttpStream.php', 'OC\\Files\\Type\\Detection' => __DIR__ . '/../../..' . '/lib/private/Files/Type/Detection.php', diff --git a/lib/private/Files/Stream/HashWrapper.php b/lib/private/Files/Stream/HashWrapper.php new file mode 100644 index 0000000000..048e50794e --- /dev/null +++ b/lib/private/Files/Stream/HashWrapper.php @@ -0,0 +1,72 @@ + + * + * @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 . + * + */ + +namespace OC\Files\Stream; + +use Icewind\Streams\Wrapper; + +class HashWrapper extends Wrapper { + protected $callback; + protected $hash; + + public static function wrap($source, string $algo, callable $callback) { + $hash = hash_init($algo); + $context = stream_context_create([ + 'hash' => [ + 'source' => $source, + 'callback' => $callback, + 'hash' => $hash, + ], + ]); + return Wrapper::wrapSource($source, $context, 'hash', self::class); + } + + protected function open() { + $context = $this->loadContext('hash'); + + $this->callback = $context['callback']; + $this->hash = $context['hash']; + return true; + } + + public function dir_opendir($path, $options) { + return $this->open(); + } + + public function stream_open($path, $mode, $options, &$opened_path) { + return $this->open(); + } + + public function stream_read($count) { + $result = parent::stream_read($count); + hash_update($this->hash, $result); + return $result; + } + + public function stream_close() { + if (is_callable($this->callback)) { + call_user_func($this->callback, hash_final($this->hash)); + // prevent further calls by potential PHP 7 GC ghosts + $this->callback = null; + } + return parent::stream_close(); + } +} diff --git a/tests/lib/Files/Stream/HashWrapperTest.php b/tests/lib/Files/Stream/HashWrapperTest.php new file mode 100644 index 0000000000..7066bf857a --- /dev/null +++ b/tests/lib/Files/Stream/HashWrapperTest.php @@ -0,0 +1,55 @@ + + * + * @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 . + * + */ + +namespace Test\Files\Stream; + +use OC\Files\Stream\HashWrapper; +use Test\TestCase; + +class HashWrapperTest extends TestCase { + /** + * @dataProvider hashProvider + */ + public function testHashStream($data, string $algo, string $hash) { + if (!is_resource($data)) { + $tmpData = fopen('php://temp', 'r+'); + if ($data !== null) { + fwrite($tmpData, $data); + rewind($tmpData); + } + $data = $tmpData; + } + + $wrapper = HashWrapper::wrap($data, $algo, function ($result) use ($hash) { + $this->assertEquals($hash, $result); + }); + stream_get_contents($wrapper); + } + + public function hashProvider() { + return [ + ['foo', 'md5', 'acbd18db4cc2f85cedef654fccc4a4d8'], + ['foo', 'sha1', '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'], + ['foo', 'sha256', '2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae'], + [str_repeat('foo', 8192), 'md5', '96684d2b796a2c54a026b5d60f9de819'], + ]; + } +}