-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Server side checksum verification #26655 #27097
Conversation
@IljaN, thanks for your PR! By analyzing the history of the files in this pull request, we identified @DeepDiver1975, @PVince81 and @butonic to be potential reviewers. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great progress.
Some additional things came to mind while reviewing.
*/ | ||
public function fopen($path, $mode) { | ||
$stream = $this->getWrapperStorage()->fopen($path, $mode); | ||
if (!self::requiresChecksum($path)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you also don't need to compute the checksum if there is already a checksum value in oc_filecache and $mode
is a read operation. Could save a bit of computing power on download.
$memoryStream = fopen('php://memory', 'r+'); | ||
$checksumStream = \OC\Files\Stream\Checksum::wrap($memoryStream, $path); | ||
|
||
fwrite($checksumStream, $data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does that really work, does it write the whole data ?
needs to be tested with some external storages, I suspect that there are cases where fwrite
would write only 8k blocks or so but I might be wrong
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the fwrite docs it will write the whole string. You can limit it by a optional length parameter. Which external storages should I test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Best would be to test with
- SFTP
- SMB
- one of GDrive or Dropbox
In the past we had issues with GDrive because the fread
from GDrive didn't always return 8k per read and it messed up the encryption wrapper which was expecting 8k blocks...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
according to the docblocks file_put_contents of the Wrapper interface (unlike the native version) only expects strings so this shouldn`t be a Problem, because we will always get a full string. ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good
return null; | ||
} | ||
|
||
return self::$checksums[$path]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now thinking of it, something should probably clear all the checksum values from that static array.
In the case of single PHP requests this isn't a problem. But if someone runs an occ command that reads a million files (maybe something like transfer ownership or decrypt-all), it could happen that old files still have their checksum stored in there.
One way to compensate for this is to use our CappedMemoryCache
class which will auto-remove entries when full.
lib/private/legacy/util.php
Outdated
@@ -175,6 +175,12 @@ public static function setupFS($user = '') { | |||
return $storage; | |||
}); | |||
|
|||
// install storage checksum wrapper | |||
\OC\Files\Filesystem::addStorageWrapper('oc_checksum', function ($mountPoint, $storage) { | |||
return new \OC\Files\Storage\Wrapper\Checksum(['storage' => $storage]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we shouldn't install the checksum wrapper if $storage
is a shared storage. See below for a check to exclude it.
The reason is that the SharedStorage for a share recipient is itself is just a wrapper around the original Storage of the owner, which itself will already have a checksum storage applied.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is a !$storage->isLocal() check also required? (like in the other wrapper registrations)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. The isLocal
was specific to the encoding wrapper which only makes sense for external storages.
In the case of checksums, we want it both on local and external storages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great stuff!
$expectedChecksum = trim($request->server['HTTP_OC_CHECKSUM']); | ||
$computedChecksums = $meta['checksum']; | ||
|
||
return strpos($computedChecksums, $expectedChecksum) !== false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why no direct equality ?
} | ||
$this->fileView->putFileInfo( | ||
$this->path, | ||
['checksum' => $partStorage->getMetaData($internalPartPath)['checksum']] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a mechanism to disable checksums ?
Maybe better be safe and make sure getMetaData
actually does contain a checksum before putting it ?
|
||
foreach ($checksums as $checksum) { | ||
// starts with $algo | ||
if (substr($checksum, 0, strlen($algo)) === $algo) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we had another separator on which we could explode here ?
What if the checksum had both algos "ABC" and "ABC1" ?
When querying "ABC" it might match "ABC1" too and deliver a piece of the wrong checksum.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would make it more complicated to follow the code we do explode again on ":" I will just check until the ":" to be save.
@@ -335,7 +335,7 @@ public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) | |||
}); | |||
|
|||
$propFind->handle(self::CHECKSUMS_PROPERTYNAME, function() use ($node) { | |||
$checksum = $node->getChecksum(); | |||
$checksum = $node->getChecksum('SHA1'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"sha1" up there and "SHA1" here ? maybe decide on one casing.
By the way: are checksum algo names automatically capitalized ? If not then you likely have a bug here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@PVince81 PHP uses lowercase for algo everywhere but our protocol uses uppercase, the getChecksum method internally uppercases every input anyway to do the internal comparisons.
@@ -268,6 +226,52 @@ public function testExpireOldFilesShared() { | |||
} | |||
|
|||
/** | |||
* test expiration of files older then the max storage time defined for the trash | |||
*/ | |||
public function testExpireOldFiles() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What a weird coincidence. Some of my test was affected by this one in another location.
The reason was that a test disabled the trashbin permanently which made other tests fail. Ref: #27042 (comment)
Anyway, I'm fine leaving this as is to not waste more time with old cruddy tests.
Given user "user0" exists | ||
And file "/chksumtst.txt" does not exist for user "user0" | ||
And user "user0" uploads file with checksum "SHA1:f005ba11" and content "Some Text" to "/chksumtst.txt" | ||
Then the HTTP status code should be "400" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also need such tests with chunking.
Pick one of the chunking tests and add the header.
* @return bool | ||
*/ | ||
private static function requiresChecksum($path, $mode) { | ||
return substr($path, 0, 6) === 'files/' && $mode !== 'r'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
&& mode !== 'rb'
(if any app is still using that...)
$memoryStream = fopen('php://memory', 'r+'); | ||
$checksumStream = \OC\Files\Stream\Checksum::wrap($memoryStream, $path); | ||
|
||
fwrite($checksumStream, $data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good
private static $checksums; | ||
|
||
|
||
public function __construct(array $algos = ['sha1', 'md5', 'adler32']) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Weird, this shows up red. Does PHP 5.6 support this syntax ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it does: https://3v4l.org/cJnUO, apparently it is a github bug
class Checksum extends Wrapper { | ||
|
||
/** @var resource[] */ | ||
private $hashingContexts; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add PHPDoc, what's a hashing context here ? (I know, but explain to others)
we also need a test for the checksum from ext storage. Integration tests have a local storage but it contains no default data. Might need to change the setup a bit, can you help here @SergioBertolinSG ? Have an example existing file on local_storage at start, or better: a scenario method to put a file directly there bypassing OC, will be useful for future update detection tests. |
@PVince81 It is not already available to do this? Using upload function like here: But using /local_storage/ |
@@ -112,10 +110,9 @@ private function getChecksumRequirement($path, $mode) { | |||
*/ | |||
public function onClose() { | |||
$cache = $this->getCache(); | |||
foreach ($this->pathsInCacheWithoutChecksum as $path) { | |||
$entry = $cache->get($path); | |||
foreach ($this->pathsInCacheWithoutChecksum as $cacheId => $path) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@PVince81 I removed all checks here, because then we can save a query when getting the cache entry. If we have a race condition the worst thing that will happen is a update which will affect 0 rows.
@SergioBertolinSG no, because using the Webdav upload will already write a checksum into the table. |
@IljaN please rebase then use #27227 (comment) to create a file directly on the local_storage, that file will not have a checksum since we bypassed ownCloud. |
e4bb97d
to
7734f0b
Compare
Bad luck or real error ? (sqlite)
|
eb22802
to
b5617a3
Compare
@PVince81 that one seems to be a real error. |
One last rebase to get past the JS bug ? Then this is good for merge 👍 |
e2c80e9
to
ff6b908
Compare
@PVince81 I think i need to look at is lock_error, it seems to happen at every jenkins run, so probably no bad luck ... |
|
Seems you have a stray lock after fopen which is not supposed to be there. |
Disabling all tests except the So one of the 3 data provider cases before are causing a side effect. |
Nope. I disabled the wrong fopen one. Having only dataset 4 enabled fails too. At least no cross-test side effects. |
Removing the checksum stream wrapper solves the issue. Diving deeper... |
* @return false|resource | ||
*/ | ||
public function fopen($path, $mode) { | ||
$stream = $this->getWrapperStorage()->fopen($path, $mode); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please check if $stream
is a resource, else return it.
add a unit test for the case where fopen failed. Maybe simply $view->fopen()
of a non-existing file and check result, that might be enough to cover all possible registered wrappers already.
@@ -55,6 +55,11 @@ class Checksum extends Wrapper { | |||
*/ | |||
public function fopen($path, $mode) { | |||
$stream = $this->getWrapperStorage()->fopen($path, $mode); | |||
if (!is_resource($stream)) { | |||
// don't wrap on error | |||
return $stream; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should fail more loudly here (Exception?) Or else we might have silent bugs, what do you think?
The rest looks good to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No... silent bugs is the "norm" so far for these APIs. If we throw exceptions it risks breaking other things. One day we'll replace all these PHP-like APIs with proper APIs that throw exceptions: #13792
The checksum behat tests all pass locally :-/ |
…eOldFiles. If testExpireOldFiles runs before testExpireOldFilesShared, the second test fails randomly. #26655
…me weird sideeffects if other tests are running before it, couldn`t find out the root cause, test runs ok in isolation.
9c4a065
to
eeb8f07
Compare
Rebased onto master. Bring a bag full of luck ! |
@IljaN @SergioBertolinSG legit failures:
|
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Description
Related Issue
Motivation and Context
How Has This Been Tested?
Screenshots (if appropriate):
Types of changes
Checklist: