Checksum validation PropagateDownload unit tests.

Signed-off-by: alex-z <blackslayer4@gmail.com>
This commit is contained in:
alex-z 2021-12-20 14:59:08 +02:00 committed by allexzander (Rebase PR Action)
parent 5b0e2d8ed0
commit 190d278fd4
10 changed files with 86 additions and 27 deletions

View file

@ -337,7 +337,7 @@ ComputeChecksum *ValidateChecksumHeader::prepareStart(const QByteArray &checksum
if (!parseChecksumHeader(checksumHeader, &_expectedChecksumType, &_expectedChecksum)) { if (!parseChecksumHeader(checksumHeader, &_expectedChecksumType, &_expectedChecksum)) {
qCWarning(lcChecksums) << "Checksum header malformed:" << checksumHeader; qCWarning(lcChecksums) << "Checksum header malformed:" << checksumHeader;
emit validationFailed(tr("The checksum header is malformed."), ChecksumHeaderMalformed); emit validationFailed(tr("The checksum header is malformed."), _calculatedChecksumType, _calculatedChecksum, ChecksumHeaderMalformed);
return nullptr; return nullptr;
} }
@ -377,11 +377,13 @@ void ValidateChecksumHeader::slotChecksumCalculated(const QByteArray &checksumTy
_calculatedChecksum = checksum; _calculatedChecksum = checksum;
if (checksumType != _expectedChecksumType) { if (checksumType != _expectedChecksumType) {
emit validationFailed(tr("The checksum header contained an unknown checksum type \"%1\"").arg(QString::fromLatin1(_expectedChecksumType)), ChecksumTypeUnknown); emit validationFailed(tr("The checksum header contained an unknown checksum type \"%1\"").arg(QString::fromLatin1(_expectedChecksumType)),
_calculatedChecksumType, _calculatedChecksum, ChecksumTypeUnknown);
return; return;
} }
if (checksum != _expectedChecksum) { if (checksum != _expectedChecksum) {
emit validationFailed(tr(R"(The downloaded file does not match the checksum, it will be resumed. "%1" != "%2")").arg(QString::fromUtf8(_expectedChecksum), QString::fromUtf8(checksum)), ChecksumMismatch); emit validationFailed(tr(R"(The downloaded file does not match the checksum, it will be resumed. "%1" != "%2")").arg(QString::fromUtf8(_expectedChecksum), QString::fromUtf8(checksum)),
_calculatedChecksumType, _calculatedChecksum, ChecksumMismatch);
return; return;
} }
emit validated(checksumType, checksum); emit validated(checksumType, checksum);

View file

@ -146,6 +146,8 @@ public:
ChecksumTypeUnknown, ChecksumTypeUnknown,
ChecksumMismatch, ChecksumMismatch,
}; };
Q_ENUM(FailureReason)
explicit ValidateChecksumHeader(QObject *parent = nullptr); explicit ValidateChecksumHeader(QObject *parent = nullptr);
/** /**
@ -172,7 +174,8 @@ public:
signals: signals:
void validated(const QByteArray &checksumType, const QByteArray &checksum); void validated(const QByteArray &checksumType, const QByteArray &checksum);
void validationFailed(const QString &errMsg, FailureReason reason); void validationFailed(const QString &errMsg, const QByteArray &calculatedChecksumType,
const QByteArray &calculatedChecksum, ValidateChecksumHeader::FailureReason reason);
private slots: private slots:
void slotChecksumCalculated(const QByteArray &checksumType, const QByteArray &checksum); void slotChecksumCalculated(const QByteArray &checksumType, const QByteArray &checksum);

View file

@ -641,6 +641,11 @@ bool Account::isChecksumRecalculateRequestSupported() const
return serverVersionInt() >= makeServerVersion(checksumRecalculateRequestServerVersionMinSupportedMajor, 0, 0); return serverVersionInt() >= makeServerVersion(checksumRecalculateRequestServerVersionMinSupportedMajor, 0, 0);
} }
int Account::checksumRecalculateServerVersionMinSupportedMajor() const
{
return checksumRecalculateRequestServerVersionMinSupportedMajor;
}
void Account::setServerVersion(const QString &version) void Account::setServerVersion(const QString &version)
{ {
if (version == _serverVersion) { if (version == _serverVersion) {

View file

@ -234,6 +234,8 @@ public:
bool isChecksumRecalculateRequestSupported() const; bool isChecksumRecalculateRequestSupported() const;
int checksumRecalculateServerVersionMinSupportedMajor() const;
/** True when the server connection is using HTTP2 */ /** True when the server connection is using HTTP2 */
bool isHttp2Supported() { return _http2Supported; } bool isHttp2Supported() { return _http2Supported; }
void setHttp2Supported(bool value) { _http2Supported = value; } void setHttp2Supported(bool value) { _http2Supported = value; }

View file

@ -1084,12 +1084,12 @@ bool SimpleNetworkJob::finished()
return true; return true;
} }
SimpleFileManipulationNetworkJob::SimpleFileManipulationNetworkJob(AccountPtr account, const QString &filePath, QObject *parent) SimpleFileJob::SimpleFileJob(AccountPtr account, const QString &filePath, QObject *parent)
: AbstractNetworkJob(account, filePath, parent) : AbstractNetworkJob(account, filePath, parent)
{ {
} }
QNetworkReply *SimpleFileManipulationNetworkJob::startRequest( QNetworkReply *SimpleFileJob::startRequest(
const QByteArray &verb, QNetworkRequest req, QIODevice *requestBody) const QByteArray &verb, QNetworkRequest req, QIODevice *requestBody)
{ {
const auto davUrlString = makeDavUrl(path()).toString(); const auto davUrlString = makeDavUrl(path()).toString();
@ -1098,7 +1098,7 @@ QNetworkReply *SimpleFileManipulationNetworkJob::startRequest(
return reply; return reply;
} }
bool SimpleFileManipulationNetworkJob::finished() bool SimpleFileJob::finished()
{ {
emit finishedSignal(reply()); emit finishedSignal(reply());
return true; return true;

View file

@ -502,11 +502,11 @@ private slots:
* @brief A basic file manipulation job * @brief A basic file manipulation job
* @ingroup libsync * @ingroup libsync
*/ */
class OWNCLOUDSYNC_EXPORT SimpleFileManipulationNetworkJob : public AbstractNetworkJob class OWNCLOUDSYNC_EXPORT SimpleFileJob : public AbstractNetworkJob
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit SimpleFileManipulationNetworkJob(AccountPtr account, const QString &filePath, QObject *parent = nullptr); explicit SimpleFileJob(AccountPtr account, const QString &filePath, QObject *parent = nullptr);
QNetworkReply *startRequest(const QByteArray &verb, QNetworkRequest req = QNetworkRequest(), QIODevice *requestBody = nullptr); QNetworkReply *startRequest(const QByteArray &verb, QNetworkRequest req = QNetworkRequest(), QIODevice *requestBody = nullptr);

View file

@ -922,7 +922,8 @@ void PropagateDownloadFile::slotGetFinished()
validator->start(_tmpFile.fileName(), checksumHeader); validator->start(_tmpFile.fileName(), checksumHeader);
} }
void PropagateDownloadFile::slotChecksumFail(const QString &errMsg, ValidateChecksumHeader::FailureReason reason) void PropagateDownloadFile::slotChecksumFail(const QString &errMsg,
const QByteArray &calculatedChecksumType, const QByteArray &calculatedChecksum, ValidateChecksumHeader::FailureReason reason)
{ {
const auto processChecksumFailure = [this, errMsg]() { const auto processChecksumFailure = [this, errMsg]() {
FileSystem::remove(_tmpFile.fileName()); FileSystem::remove(_tmpFile.fileName());
@ -932,24 +933,23 @@ void PropagateDownloadFile::slotChecksumFail(const QString &errMsg, ValidateChec
if (reason == ValidateChecksumHeader::FailureReason::ChecksumMismatch if (reason == ValidateChecksumHeader::FailureReason::ChecksumMismatch
&& propagator()->account()->isChecksumRecalculateRequestSupported()) { && propagator()->account()->isChecksumRecalculateRequestSupported()) {
if (const auto validator = qobject_cast<ValidateChecksumHeader *>(sender())) { const QByteArray calculatedChecksumHeader(calculatedChecksumType + ':' + calculatedChecksum);
const QByteArray calculatedChecksum(validator->calculatedChecksumType() + ':' + validator->calculatedChecksum());
const QString fullRemotePathForFile(propagator()->fullRemotePath(_isEncrypted ? _item->_encryptedFileName : _item->_file)); const QString fullRemotePathForFile(propagator()->fullRemotePath(_isEncrypted ? _item->_encryptedFileName : _item->_file));
auto *job = new SimpleFileManipulationNetworkJob(propagator()->account(), fullRemotePathForFile); auto *job = new SimpleFileJob(propagator()->account(), fullRemotePathForFile);
QObject::connect(job, &SimpleFileManipulationNetworkJob::finishedSignal, this, [this, calculatedChecksum, processChecksumFailure](QNetworkReply *reply) { QObject::connect(job, &SimpleFileJob::finishedSignal, this, [this, calculatedChecksumHeader, processChecksumFailure](QNetworkReply *reply) {
if (reply->error() == QNetworkReply::NoError) { if (reply->error() == QNetworkReply::NoError) {
const auto newChecksumFromServer = reply->rawHeader(checkSumHeaderC); const auto newChecksumHeaderFromServer = reply->rawHeader(checkSumHeaderC);
if (newChecksumFromServer == calculatedChecksum) { if (newChecksumHeaderFromServer == calculatedChecksumHeader) {
const auto newChecksumFromServerSplit = newChecksumFromServer.split(':'); const auto newChecksumHeaderFromServerSplit = newChecksumHeaderFromServer.split(':');
if (newChecksumFromServerSplit.size() > 1) { if (newChecksumHeaderFromServerSplit.size() > 1) {
transmissionChecksumValidated( transmissionChecksumValidated(
newChecksumFromServerSplit.first(), newChecksumFromServerSplit.last()); newChecksumHeaderFromServerSplit.first(), newChecksumHeaderFromServerSplit.last());
return; return;
} }
} }
qCCritical(lcPropagateDownload) << "Checksum recalculation has failed for file:" << reply->url() qCCritical(lcPropagateDownload) << "Checksum recalculation has failed for file:" << reply->url()
<< " " << checkSumHeaderC << " received is:" << newChecksumFromServer; << " " << checkSumHeaderC << " received is:" << newChecksumHeaderFromServer;
} }
if (reply->error() != QNetworkReply::NoError) { if (reply->error() != QNetworkReply::NoError) {
@ -964,10 +964,9 @@ void PropagateDownloadFile::slotChecksumFail(const QString &errMsg, ValidateChec
qCWarning(lcPropagateDownload) << "Checksum validation has failed for file:" << fullRemotePathForFile qCWarning(lcPropagateDownload) << "Checksum validation has failed for file:" << fullRemotePathForFile
<< " Requesting checksum recalculation on the server..."; << " Requesting checksum recalculation on the server...";
QNetworkRequest req; QNetworkRequest req;
req.setRawHeader(checksumRecalculateOnServer, validator->calculatedChecksumType()); req.setRawHeader(checksumRecalculateOnServer, calculatedChecksumType);
job->startRequest(QByteArrayLiteral("PATCH"), req); job->startRequest(QByteArrayLiteral("PATCH"), req);
return; return;
}
} }
processChecksumFailure(); processChecksumFailure();

View file

@ -236,7 +236,8 @@ private slots:
void abort(PropagatorJob::AbortType abortType) override; void abort(PropagatorJob::AbortType abortType) override;
void slotDownloadProgress(qint64, qint64); void slotDownloadProgress(qint64, qint64);
void slotChecksumFail(const QString &errMsg, ValidateChecksumHeader::FailureReason reason); void slotChecksumFail(const QString &errMsg, const QByteArray &calculatedChecksumType,
const QByteArray &calculatedChecksum, ValidateChecksumHeader::FailureReason reason);
private: private:
void startAfterIsEncryptedIsChecked(); void startAfterIsEncryptedIsChecked();

View file

@ -43,8 +43,11 @@ using namespace OCC::Utility;
_successDown = true; _successDown = true;
} }
void slotDownError(const QString &errMsg, ValidateChecksumHeader::FailureReason reason) void slotDownError(const QString &errMsg, const QByteArray &calculatedChecksumType,
const QByteArray &calculatedChecksum, ValidateChecksumHeader::FailureReason reason)
{ {
Q_UNUSED(calculatedChecksumType);
Q_UNUSED(calculatedChecksum);
QCOMPARE(_expectedError, errMsg); QCOMPARE(_expectedError, errMsg);
QCOMPARE(_expectedFailureReason, reason); QCOMPARE(_expectedFailureReason, reason);
_errorSeen = true; _errorSeen = true;

View file

@ -8,6 +8,7 @@
#include <QtTest> #include <QtTest>
#include "syncenginetestutils.h" #include "syncenginetestutils.h"
#include <syncengine.h> #include <syncengine.h>
#include <propagatorjobs.h>
using namespace OCC; using namespace OCC;
@ -551,16 +552,27 @@ private slots:
QObject parent; QObject parent;
QByteArray checksumValue; QByteArray checksumValue;
QByteArray checksumValueRecalculated;
QByteArray contentMd5Value; QByteArray contentMd5Value;
bool isChecksumRecalculateSupported = false;
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * { fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
if (op == QNetworkAccessManager::GetOperation) { if (op == QNetworkAccessManager::GetOperation) {
auto reply = new FakeGetReply(fakeFolder.remoteModifier(), op, request, &parent); auto reply = new FakeGetReply(fakeFolder.remoteModifier(), op, request, &parent);
if (!checksumValue.isNull()) if (!checksumValue.isNull())
reply->setRawHeader("OC-Checksum", checksumValue); reply->setRawHeader(OCC::checkSumHeaderC, checksumValue);
if (!contentMd5Value.isNull()) if (!contentMd5Value.isNull())
reply->setRawHeader("Content-MD5", contentMd5Value); reply->setRawHeader(OCC::contentMd5HeaderC, contentMd5Value);
return reply; return reply;
} else if (op == QNetworkAccessManager::CustomOperation) {
if (request.hasRawHeader(OCC::checksumRecalculateOnServer)) {
if (!isChecksumRecalculateSupported) {
return new FakeErrorReply(op, request, &parent, 402);
}
auto reply = new FakeGetReply(fakeFolder.remoteModifier(), op, request, &parent);
reply->setRawHeader(OCC::checkSumHeaderC, checksumValueRecalculated);
return reply;
}
} }
return nullptr; return nullptr;
}); });
@ -575,8 +587,11 @@ private slots:
fakeFolder.remoteModifier().create("A/a4", 16, 'A'); fakeFolder.remoteModifier().create("A/a4", 16, 'A');
QVERIFY(!fakeFolder.syncOnce()); QVERIFY(!fakeFolder.syncOnce());
const QByteArray matchedSha1Checksum(QByteArrayLiteral("SHA1:19b1928d58a2030d08023f3d7054516dbc186f20"));
const QByteArray mismatchedSha1Checksum(matchedSha1Checksum.chopped(1));
// Good OC-Checksum // Good OC-Checksum
checksumValue = "SHA1:19b1928d58a2030d08023f3d7054516dbc186f20"; // printf 'A%.0s' {1..16} | sha1sum - checksumValue = matchedSha1Checksum; // printf 'A%.0s' {1..16} | sha1sum -
QVERIFY(fakeFolder.syncOnce()); QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
checksumValue = QByteArray(); checksumValue = QByteArray();
@ -610,6 +625,35 @@ private slots:
checksumValue = "Unsupported:XXXX SHA1:19b1928d58a2030d08023f3d7054516dbc186f20 Invalid:XxX"; checksumValue = "Unsupported:XXXX SHA1:19b1928d58a2030d08023f3d7054516dbc186f20 Invalid:XxX";
QVERIFY(fakeFolder.syncOnce()); // The supported SHA1 checksum is valid now, so the file are downloaded QVERIFY(fakeFolder.syncOnce()); // The supported SHA1 checksum is valid now, so the file are downloaded
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
// Begin Test mismatch recalculation---------------------------------------------------------------------------------
const auto prevServerVersion = fakeFolder.account()->serverVersion();
fakeFolder.account()->setServerVersion(QString("%1.0.0").arg(fakeFolder.account()->checksumRecalculateServerVersionMinSupportedMajor()));
// Mismatched OC-Checksum and X-Recalculate-Hash is not supported -> sync must fail
isChecksumRecalculateSupported = false;
checksumValue = mismatchedSha1Checksum;
checksumValueRecalculated = matchedSha1Checksum;
fakeFolder.remoteModifier().create("A/a9", 16, 'A');
QVERIFY(!fakeFolder.syncOnce());
// Mismatched OC-Checksum and X-Recalculate-Hash is supported, but, recalculated checksum is again mismatched -> sync must fail
isChecksumRecalculateSupported = true;
checksumValue = mismatchedSha1Checksum;
checksumValueRecalculated = mismatchedSha1Checksum;
QVERIFY(!fakeFolder.syncOnce());
// Mismatched OC-Checksum and X-Recalculate-Hash is supported, and, recalculated checksum is a match -> sync must succeed
isChecksumRecalculateSupported = true;
checksumValue = mismatchedSha1Checksum;
checksumValueRecalculated = matchedSha1Checksum;
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
checksumValue = QByteArray();
fakeFolder.account()->setServerVersion(prevServerVersion);
// End Test mismatch recalculation-----------------------------------------------------------------------------------
} }
// Tests the behavior of invalid filename detection // Tests the behavior of invalid filename detection