From 3ab05a1654e07e3b24659049318e4c911a7e5cba Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 26 Mar 2024 00:13:35 -0500 Subject: [PATCH] Verify downloaded file against content-md5 response header --- .../scwx/qt/manager/download_manager.cpp | 74 +++++++++++++++-- .../scwx/qt/request/download_request.hpp | 3 +- wxdata/include/scwx/util/digest.hpp | 18 ++++ wxdata/source/scwx/util/digest.cpp | 82 +++++++++++++++++++ wxdata/wxdata.cmake | 6 +- 5 files changed, 171 insertions(+), 12 deletions(-) create mode 100644 wxdata/include/scwx/util/digest.hpp create mode 100644 wxdata/source/scwx/util/digest.cpp diff --git a/scwx-qt/source/scwx/qt/manager/download_manager.cpp b/scwx-qt/source/scwx/qt/manager/download_manager.cpp index b5d4f30f..4724424d 100644 --- a/scwx-qt/source/scwx/qt/manager/download_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/download_manager.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -128,15 +129,9 @@ void DownloadManager::Download( bool ofsGood = ofs.good(); ofs.close(); - // Handle response - if (response.error.code == cpr::ErrorCode::OK && - !request->IsCanceled() && ofsGood) - { - logger_->info("Download complete: \"{}\"", request->url()); - Q_EMIT request->RequestComplete( - request::DownloadRequest::CompleteReason::OK); - } - else + // Handle error response + if (response.error.code != cpr::ErrorCode::OK || + request->IsCanceled() || !ofsGood) { request::DownloadRequest::CompleteReason reason = request::DownloadRequest::CompleteReason::IOError; @@ -173,7 +168,68 @@ void DownloadManager::Download( } Q_EMIT request->RequestComplete(reason); + + return; } + + // Handle response + const auto contentMd5 = response.header.find("content-md5"); + if (contentMd5 != response.header.cend() && + !contentMd5->second.empty()) + { + // Open file for reading + std::ifstream is {destinationPath, + std::ios_base::in | std::ios_base::binary}; + if (!is.is_open() || !is.good()) + { + logger_->error( + "Unable to open destination file for reading: \"{}\"", + destinationPath.string()); + + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::IOError); + + return; + } + + // Compute MD5 + std::vector digest {}; + if (!util::ComputeDigest(EVP_md5(), is, digest)) + { + logger_->error("Failed to compute MD5: \"{}\"", + destinationPath.string()); + + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::IOError); + + return; + } + + // Compare calculated MD5 with digest in response header + QByteArray expectedDigestArray = + QByteArray::fromBase64(contentMd5->second.c_str()); + std::vector expectedDigest( + expectedDigestArray.cbegin(), expectedDigestArray.cend()); + + if (digest != expectedDigest) + { + QByteArray calculatedDigest( + reinterpret_cast(digest.data()), digest.size()); + + logger_->error("Digest mismatch: {} != {}", + calculatedDigest.toBase64().toStdString(), + contentMd5->second); + + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::DigestError); + + return; + } + } + + logger_->info("Download complete: \"{}\"", request->url()); + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::OK); }); } diff --git a/scwx-qt/source/scwx/qt/request/download_request.hpp b/scwx-qt/source/scwx/qt/request/download_request.hpp index 4b461b8e..aa73bd17 100644 --- a/scwx-qt/source/scwx/qt/request/download_request.hpp +++ b/scwx-qt/source/scwx/qt/request/download_request.hpp @@ -22,7 +22,8 @@ public: OK, Canceled, IOError, - RemoteError + RemoteError, + DigestError }; explicit DownloadRequest(const std::string& url, diff --git a/wxdata/include/scwx/util/digest.hpp b/wxdata/include/scwx/util/digest.hpp new file mode 100644 index 00000000..e0bbc3c9 --- /dev/null +++ b/wxdata/include/scwx/util/digest.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +#include + +namespace scwx +{ +namespace util +{ + +bool ComputeDigest(const EVP_MD* mdtype, + std::istream& is, + std::vector& digest); + +} // namespace util +} // namespace scwx diff --git a/wxdata/source/scwx/util/digest.cpp b/wxdata/source/scwx/util/digest.cpp new file mode 100644 index 00000000..fe0c055d --- /dev/null +++ b/wxdata/source/scwx/util/digest.cpp @@ -0,0 +1,82 @@ +#include +#include + +namespace scwx +{ +namespace util +{ + +static const std::string logPrefix_ = "scwx::util::digest"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +bool ComputeDigest(const EVP_MD* mdtype, + std::istream& is, + std::vector& digest) +{ + int mdsize; + EVP_MD_CTX* mdctx = nullptr; + + digest.clear(); + + if ((mdsize = EVP_MD_get_size(mdtype)) < 1) + { + logger_->error("Invalid digest"); + return false; + } + + if ((mdctx = EVP_MD_CTX_new()) == nullptr) + { + logger_->error("Error allocating a digest context"); + return false; + } + + if (!EVP_DigestInit_ex(mdctx, mdtype, nullptr)) + { + logger_->error("Message digest initialization failed"); + EVP_MD_CTX_free(mdctx); + return false; + } + + is.seekg(0, std::ios_base::end); + const std::size_t streamSize = is.tellg(); + is.seekg(0, std::ios_base::beg); + + std::size_t bytesRead = 0; + std::size_t chunkSize = 4096; + std::string fileData; + fileData.resize(chunkSize); + + while (bytesRead < streamSize) + { + const std::size_t bytesRemaining = streamSize - bytesRead; + const std::size_t readSize = std::min(chunkSize, bytesRemaining); + + is.read(fileData.data(), readSize); + + if (!is.good() || !EVP_DigestUpdate(mdctx, fileData.data(), readSize)) + { + logger_->error("Message digest update failed"); + EVP_MD_CTX_free(mdctx); + return false; + } + + bytesRead += readSize; + } + + digest.resize(mdsize); + + if (!EVP_DigestFinal_ex(mdctx, digest.data(), nullptr)) + { + logger_->error("Message digest finalization failed"); + EVP_MD_CTX_free(mdctx); + digest.clear(); + return false; + } + + EVP_MD_CTX_free(mdctx); + + return true; +} + +} // namespace util +} // namespace scwx diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 122786fd..0ce08cbb 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -67,7 +67,8 @@ set(SRC_PROVIDER source/scwx/provider/aws_level2_data_provider.cpp source/scwx/provider/nexrad_data_provider.cpp source/scwx/provider/nexrad_data_provider_factory.cpp source/scwx/provider/warnings_provider.cpp) -set(HDR_UTIL include/scwx/util/enum.hpp +set(HDR_UTIL include/scwx/util/digest.hpp + include/scwx/util/enum.hpp include/scwx/util/environment.hpp include/scwx/util/float.hpp include/scwx/util/hash.hpp @@ -80,7 +81,8 @@ set(HDR_UTIL include/scwx/util/enum.hpp include/scwx/util/threads.hpp include/scwx/util/time.hpp include/scwx/util/vectorbuf.hpp) -set(SRC_UTIL source/scwx/util/environment.cpp +set(SRC_UTIL source/scwx/util/digest.cpp + source/scwx/util/environment.cpp source/scwx/util/float.cpp source/scwx/util/hash.cpp source/scwx/util/logger.cpp