# HG changeset patch # User Andre Heinecke # Date 1392806706 0 # Node ID 95e1b6edf2fcee6f2259d1e0cd69f9b734065f24 # Parent 58d51a91d44865071881d5a96b9f68cc5dfdc17c Implement more downloader functionality for Windows diff -r 58d51a91d448 -r 95e1b6edf2fc ui/downloader.cpp --- a/ui/downloader.cpp Wed Feb 19 10:44:40 2014 +0000 +++ b/ui/downloader.cpp Wed Feb 19 10:45:06 2014 +0000 @@ -8,24 +8,44 @@ #endif #include +#include #include +#include -Downloader::Downloader(QObject* parent, const QString& url): +Downloader::Downloader(QObject* parent, const QString& url, + const QByteArray& certificate, + const QDateTime& newestSW, + const QDateTime& newestList): QThread(parent), - mUrl(url) + mUrl(url), + mLastModSW(newestSW), + mLastModList(newestList) { - QFile certResource(":certificates/https"); - certResource.open(QFile::ReadOnly); - mCert = certResource.readAll(); - certResource.close(); + if (certificate.isEmpty()) { + QFile certResource(":certificates/https"); + certResource.open(QFile::ReadOnly); + mCert = certResource.readAll(); + certResource.close(); + } } -Downloader::Downloader(QObject* parent, const QString& url, - const QByteArray& certificate): - QThread(parent), - mUrl(url), - mCert(certificate) +QString Downloader::getDataDirectory() { + QString candidate = + QStandardPaths::writableLocation(QStandardPaths::DataLocation); + if (candidate.isEmpty()) { + qDebug() << "Could not find writeable locaction for me"; + return QString(); + } + + QDir cDir(candidate); + + if (!cDir.exists()) { + if (!cDir.mkpath(candidate)) { + qDebug() << "Could not create path to: " << candidate; + return QString(); + } + } + return cDir.absolutePath(); } - diff -r 58d51a91d448 -r 95e1b6edf2fc ui/downloader.h --- a/ui/downloader.h Wed Feb 19 10:44:40 2014 +0000 +++ b/ui/downloader.h Wed Feb 19 10:45:06 2014 +0000 @@ -9,6 +9,13 @@ #include #include #include +#include + +#ifdef Q_OS_WIN +#include +#include +#endif + class Downloader: public QThread { @@ -16,47 +23,46 @@ public: /** - * @brief Construct a downloader to download data from url - * - * Takes the builtin certificate for https verification. + * @brief Construct a downloader with a specific certificate * - * @param[in] parent the parent object. - * @param[in] url the Url to download data from. - */ - Downloader(QObject* parent, const QString& url); - - /** - * @brief Construct a downloader with a specific certificate + * The downloader will check the last-modified date of the + * certificate list / sw on the server at the specified url + * and download those accordingly. If newestSW or newestList is + * are valid datetimes only files modified after the respective date + * are downloaded. + * + * Downloaded files are placed in QStandardPaths::DataLocation * * @param[in] parent the parent object. * @param[in] url the Url to download data from - * @param[in] certificate to accept https connection from + * @param[in] certificate optional certificate to validate https connection + * @param[in] newestSW datetime after which software should be downloaded + * @param[in] newestList datetime after which the list should be downloaded */ Downloader(QObject* parent, const QString& url, - const QByteArray& certificate); + const QByteArray& certificate = QByteArray(), + const QDateTime& newestSW = QDateTime(), + const QDateTime& newestList = QDateTime()); - enum Status { - NewSoftwareAvailable, // A Software Update has been downloaded - NewListAvailable, // A certificate list has been downloaded - UpToDate, // Nothing changed - Error // An error happened - }; enum ErrorCode { NoConnection, InvalidCertificate, ConnectionLost, Timeout, - Unknown + ErrUnknown }; /** - * @brief Construct a downloader with a specific certificate + * @brief get the directory where the downloader saves data * - * @param[in] url the Url to download data from - * @param[in] certificate to accept https connection from - */ - QString getDownloadedFileName(); + * If the directory does not exist this function ensures that it + * is created. + * + * @returns The directory in which downloaded files are placed. + **/ + QString getDataDirectory(); + protected: void run(); @@ -65,8 +71,63 @@ QString mUrl; QByteArray mCert; + QDateTime mLastModSW; + QDateTime mLastModList; + +#ifdef Q_OS_WIN + /** @brief Download a file from the Internet + * + * @param[in] HSession the session to work in. + * @param[in] HConnect the connection to use. + * @param[in] resource the resource to download. + * @param[in] filename where the file should be saved. + * @param[in] maxSize maximum amount of bytes to download + * + * @returns True if the download was successful. + */ + bool downloadFile(HINTERNET hSession, HINTERNET hConnect, + LPCWSTR resource, const QString &filename, DWORD maxSize); + + /** @brief get the last modified header of a resource. + * + * On error call getLastError to get extended error information. + * This function still does not do any networking but only initializes + * it. + * + * @param[in] HSession the session to work in. + * @param[in] HConnect the connection to use. + * @param[in] resource the resource to check the last-modified date on + * + * @returns the last modified date or a null datetime in case of errors + */ + QDateTime getLastModifiedHeader(HINTERNET hSession, + HINTERNET hConnect, LPCWSTR resource); + + /** @brief verify that the certificate of the request matches + * + * Validates the certificate against the member variable certificate + * + * @param[in] hRequest: The request from which to get the certificate + * + * @returns True if the certificate exactly matches the one in hRequest + */ + + bool verifyCertificate(HINTERNET hRequest); +#endif + Q_SIGNALS: /** + * @brief software update is available + */ + void newSoftwareAvailable(const QString &fileName, + const QDateTime &lastMod); + + /** + * @brief new certificate list available + */ + void newListAvailable(const QString &fileName, const QDateTime &lastMod); + + /** * @brief Some progress has been made. * * @param[out] message: A message to show. Can be empty. @@ -80,7 +141,6 @@ * * @param[out] message: A message to show. Can be empty. * @param[out] errorCode: ErrorCode of this error. - * @param[out] total: Total value of possible progress. */ void error(const QString &message, ErrorCode error); }; diff -r 58d51a91d448 -r 95e1b6edf2fc ui/downloader_linux.cpp --- a/ui/downloader_linux.cpp Wed Feb 19 10:44:40 2014 +0000 +++ b/ui/downloader_linux.cpp Wed Feb 19 10:45:06 2014 +0000 @@ -4,9 +4,9 @@ */ -#include "certificatelist.h" +#include "downloader.h" +#ifdef Q_OS_LINUX -#ifdef Q_OS_LINUX void Downloader::run() { } #endif diff -r 58d51a91d448 -r 95e1b6edf2fc ui/downloader_win.cpp --- a/ui/downloader_win.cpp Wed Feb 19 10:44:40 2014 +0000 +++ b/ui/downloader_win.cpp Wed Feb 19 10:45:06 2014 +0000 @@ -19,9 +19,18 @@ #include #include +#include +#include #define DEBUG if (1) qDebug() << __PRETTY_FUNCTION__ +#define MAX_SW_SIZE 10485760 +#define MAX_LIST_SIZE 1048576 + +/** @brief Qt wrapper around FormatMessage + * + * @returns The error message of the error that occurred + */ const QString getLastErrorMsg() { LPWSTR bufPtr = NULL; DWORD err = GetLastError(); @@ -29,6 +38,16 @@ FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, err, 0, (LPWSTR)&bufPtr, 0, NULL); + if (!bufPtr) { + HMODULE hWinhttp = GetModuleHandleW(L"winhttp"); + if (hWinhttp) { + FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_HMODULE | + FORMAT_MESSAGE_IGNORE_INSERTS, + hWinhttp, HRESULT_CODE(err), 0, + (LPWSTR)&bufPtr, 0, NULL); + } + } const QString result = (bufPtr) ? QString::fromUtf16((const ushort*)bufPtr).trimmed() : QString("Unknown Error %1").arg(err); @@ -36,7 +55,8 @@ return result; } -/** @brief open a session with appropiate proxy settings + +/** @brief open a session with appropriate proxy settings * * @param[inout] *pHSession pointer to a HInternet structure * @@ -56,7 +76,6 @@ return false; } - qDebug() << "2"; memset(&proxyConfig, 0, sizeof (WINHTTP_CURRENT_USER_IE_PROXY_CONFIG)); if (WinHttpGetIEProxyConfigForCurrentUser(&proxyConfig)) { @@ -144,6 +163,7 @@ bool createRequest(HINTERNET hSession, HINTERNET hConnect, HINTERNET *pHRequest, LPCWSTR requestType, LPCWSTR resource) { + DWORD dwSSLFlag; DEBUG; if (!hSession || !hConnect || !pHRequest) { SetLastError(ERROR_INVALID_PARAMETER); @@ -154,101 +174,342 @@ NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE); + + dwSSLFlag = SECURITY_FLAG_IGNORE_UNKNOWN_CA; + dwSSLFlag |= SECURITY_FLAG_IGNORE_CERT_DATE_INVALID; + dwSSLFlag |= SECURITY_FLAG_IGNORE_CERT_CN_INVALID; + dwSSLFlag |= SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE; + + WinHttpSetOption(*pHRequest, WINHTTP_OPTION_SECURITY_FLAGS, + &dwSSLFlag, sizeof(dwSSLFlag)); + return *pHRequest; } -void Downloader::run() { - BOOL bResults = FALSE; - HINTERNET hSession = NULL, - hConnect = NULL, - hRequest = NULL; +bool Downloader::verifyCertificate(HINTERNET hRequest) +{ + CERT_CONTEXT *certContext = NULL; + DWORD certContextLen = sizeof(CERT_CONTEXT); + bool retval = false; - SYSTEMTIME lastModified; - DWORD sizeOfSystemtime = sizeof (SYSTEMTIME); - - memset(&lastModified, 0, sizeof (SYSTEMTIME)); - - if (!openSession(&hSession)) { - DEBUG << "Failed to open session: " << getLastErrorMsg(); - return; + if (!WinHttpQueryOption(hRequest, + WINHTTP_OPTION_SERVER_CERT_CONTEXT, + &certContext, + &certContextLen)) { + DEBUG << "Unable to get server certificate"; + return false; } - if (!initializeConnection(hSession, &hConnect, L"www.intevation.de")) { - DEBUG << "Failed to initialize connection: " << getLastErrorMsg(); + QByteArray serverCert ((const char *) certContext->pbCertEncoded, + certContext->cbCertEncoded); + + retval = (serverCert == mCert); + + if (!retval) { + DEBUG << "Certificate is not the same as the pinned one!" + << "Base64 cert: " << serverCert.toBase64(); + } + + CertFreeCertificateContext(certContext); + return retval; +} + +QDateTime Downloader::getLastModifiedHeader(HINTERNET hSession, + HINTERNET hConnect, LPCWSTR resource) +{ + HINTERNET hRequest = NULL; + SYSTEMTIME lMod; + DWORD sizeOfSystemtime = sizeof (SYSTEMTIME); + QDateTime retval; + DWORD err = 0; + + memset(&lMod, 0, sizeof (SYSTEMTIME)); + + if (!hSession || !hConnect || !resource) { + SetLastError(ERROR_INVALID_PARAMETER); + return retval; + } + + if (!createRequest(hSession, hConnect, &hRequest, L"HEAD", + resource)) { + err = GetLastError(); goto cleanup; } - if (!createRequest(hSession, hConnect, &hRequest, L"GET", L"/")) { - DEBUG << "Failed to create the request: " << getLastErrorMsg(); + if (!WinHttpSendRequest(hRequest, + WINHTTP_NO_ADDITIONAL_HEADERS, + 0, WINHTTP_NO_REQUEST_DATA, 0, + 0, 0)) { + err = GetLastError(); goto cleanup; } - if (hRequest) { - DEBUG << "Doing Request"; - bResults = WinHttpSendRequest(hRequest, - WINHTTP_NO_ADDITIONAL_HEADERS, - 0, WINHTTP_NO_REQUEST_DATA, 0, - 0, 0); - } else { - DEBUG << "Error: " << GetLastError(); - } - - if (bResults) { - DEBUG << "Recieving Response"; - bResults = WinHttpReceiveResponse(hRequest, NULL); - } else { - DEBUG << "Error: " << GetLastError(); + if (!WinHttpReceiveResponse(hRequest, NULL)) { + err = GetLastError(); + goto cleanup; } - if (bResults) { - DEBUG << "Querying Headers"; - bResults = WinHttpQueryHeaders(hRequest, - WINHTTP_QUERY_LAST_MODIFIED | - WINHTTP_QUERY_FLAG_SYSTEMTIME, - NULL, - &lastModified, - &sizeOfSystemtime, - WINHTTP_NO_HEADER_INDEX); - } else { - DWORD errCode = GetLastError(); - switch (errCode) { - case ERROR_WINHTTP_HEADER_NOT_FOUND: - DEBUG << "Header not found"; - break; - case ERROR_WINHTTP_INCORRECT_HANDLE_STATE: - DEBUG << "Incorrect handle state"; - break; - case ERROR_WINHTTP_INCORRECT_HANDLE_TYPE: - DEBUG << "Incorrect handle type"; - break; - case ERROR_WINHTTP_INTERNAL_ERROR: - DEBUG << "Internal error"; - break; - case ERROR_NOT_ENOUGH_MEMORY: - DEBUG << "OOM"; - break; - default: - DEBUG << "Error: " << getLastErrorMsg(); - } + if (!verifyCertificate(hRequest)) { + DEBUG << "Certificate verification failed"; + // TODO error out } - DEBUG << "Last modified year: " << lastModified.wYear; - + if (!(WinHttpQueryHeaders(hRequest, + WINHTTP_QUERY_LAST_MODIFIED | + WINHTTP_QUERY_FLAG_SYSTEMTIME, + NULL, + &lMod, + &sizeOfSystemtime, + WINHTTP_NO_HEADER_INDEX))) { + err = GetLastError(); + goto cleanup; + } - if (!bResults) { - // Report any errors. - DEBUG << "Error" << GetLastError(); - emit error(tr("Unknown Problem when connecting"), Unknown); - } + retval = QDateTime(QDate(lMod.wYear, lMod.wMonth, lMod.wDay), + QTime(lMod.wHour, lMod.wMinute, lMod.wSecond, + lMod.wMilliseconds), + Qt::UTC); cleanup: if (hRequest) { WinHttpCloseHandle(hRequest); } + // Close handle might overwrite the last error. + SetLastError(err); + return retval; +} + +bool Downloader::downloadFile(HINTERNET hSession, HINTERNET hConnect, + LPCWSTR resource, const QString &filename, DWORD maxSize) +{ + HINTERNET hRequest = NULL; + bool retval = false; + DWORD bytesAvailable = 0, + err = 0, + bytesRead = 0, + totalDownloaded = 0; + + QSaveFile outputFile(filename); + + if (!hSession || !hConnect || !resource) { + SetLastError(ERROR_INVALID_PARAMETER); + return retval; + } + + if (!createRequest(hSession, hConnect, &hRequest, L"GET", + resource)) { + err = GetLastError(); + goto cleanup; + } + + if (!WinHttpSendRequest(hRequest, + WINHTTP_NO_ADDITIONAL_HEADERS, + 0, WINHTTP_NO_REQUEST_DATA, 0, + 0, 0)) { + err = GetLastError(); + goto cleanup; + } + + + if (!WinHttpReceiveResponse(hRequest, NULL)) { + err = GetLastError(); + goto cleanup; + } + + if (!verifyCertificate(hRequest)) { + DEBUG << "Certificate verification failed"; + // TODO error out + } + + // Open / Create the file to write to. + if (!outputFile.open(QIODevice::WriteOnly)) { + DEBUG << "Failed to open file"; + err = GetLastError(); + goto cleanup; + } + + do + { + char outBuf[8192]; // 8KB is the internal buffer size of winhttp + memset(outBuf, 0, sizeof(outBuf)); + bytesRead = 0; + + if (!WinHttpQueryDataAvailable(hRequest, &bytesAvailable)) { + DEBUG << "Querying for available data failed"; + retval = false; + err = GetLastError(); + break; + } + + if (!bytesAvailable) { + // Might indicate that we are done. + break; + } + + if (bytesAvailable > maxSize) { + DEBUG << "File to large"; + retval = false; + break; + } + + if (!WinHttpReadData(hRequest, (LPVOID)outBuf, + sizeof(outBuf), &bytesRead)) { + DEBUG << "Error reading data"; + err = GetLastError(); + break; + } else { + if (bytesRead) { + DEBUG << "Downloaded: " << bytesRead << "B"; + + // Write data to file. + if (outputFile.write(outBuf, bytesRead) != + bytesRead) { + err = GetLastError(); + DEBUG << "Error writing to file."; + retval = false; + } + // Completed a read / write cycle. If not error follows + // the download was successful. + retval = true; + } else { + // Should not happen as we queried for available + // bytes before and the function did not return an + // error. + DEBUG << "Unable to read available data"; + retval = false; + break; + } + } + totalDownloaded += bytesRead; + + if (totalDownloaded > maxSize) { + DEBUG << "Downloaded too much data. Breaking."; + retval = false; + break; + } + } while (bytesAvailable > 0); + +cleanup: + + if (retval) { + // Actually save the file to disk / move to homedir + retval = outputFile.commit(); + } + + if (hRequest) { + WinHttpCloseHandle(hRequest); + } + + // Close handle might overwrite the last error. + SetLastError(err); + return retval; +} + +void Downloader::run() { + bool results = false; + HINTERNET hSession = NULL, + hConnect = NULL; + wchar_t wUrl[mUrl.size() + 1]; + QDateTime lastModifiedSoftware; + QDateTime lastModifiedList; + + int rc = 0; + + memset(wUrl, 0, sizeof (wchar_t) * (mUrl.size() + 1)); + + rc = mUrl.toWCharArray(wUrl); + + if (rc != mUrl.size()) { + DEBUG << "Problem converting to wchar array"; + return; + } + + // Should not be necessary because we initialized the memory + wUrl[rc] = '\0'; + + // Initialize connection + if (!openSession(&hSession)) { + DEBUG << "Failed to open session: " << getLastErrorMsg(); + return; + } + if (!initializeConnection(hSession, &hConnect, wUrl)) { + DEBUG << "Failed to initialize connection: " << getLastErrorMsg(); + goto cleanup; + } + + + lastModifiedSoftware = getLastModifiedHeader(hSession, hConnect, + L"/incoming/aheinecke/test"); + + lastModifiedList = getLastModifiedHeader(hSession, hConnect, + L"/incoming/aheinecke/test"); + + if (!lastModifiedList.isValid() || !lastModifiedSoftware.isValid()) { + DEBUG << "Could not read headers: " << getLastErrorMsg(); + goto cleanup; + } + + if (!mLastModSW.isValid() || lastModifiedSoftware > mLastModSW) { + QString dataDirectory = getDataDirectory(); + + if (dataDirectory.isEmpty()) { + DEBUG << "Failed to get data directory"; + goto cleanup; + } + + QString fileName = dataDirectory.append("/SW-") + .append(lastModifiedSoftware.toString("yyyymmddHHmmss")) + .append(".exe"); + + DEBUG << "Filename: " << fileName; + + if (!downloadFile(hSession, hConnect, L"/incoming/aheinecke/test", + fileName, MAX_SW_SIZE)) { + DEBUG << "Error downloading File: " << getLastErrorMsg(); + goto cleanup; + } + + emit newSoftwareAvailable(fileName, lastModifiedSoftware); + } else if (!mLastModList.isValid() || lastModifiedList > mLastModList) { + QString dataDirectory = getDataDirectory(); + + if (dataDirectory.isEmpty()) { + DEBUG << "Failed to get data directory"; + goto cleanup; + } + + QString fileName = dataDirectory.append("/list-") + .append(lastModifiedSoftware.toString("yyyymmddHHmmss")) + .append(".txt"); + + DEBUG << "Filename: " << fileName; + + if (!downloadFile(hSession, hConnect, L"/incoming/aheinecke/test", + fileName, MAX_LIST_SIZE)) { + DEBUG << "Error downloading File: " << getLastErrorMsg(); + goto cleanup; + } + + emit newListAvailable(fileName, lastModifiedList); + } + + DEBUG << "SW date: " << lastModifiedSoftware; + DEBUG << "List date: " << lastModifiedList; + + /*if (!WinHttpQueryDataAvailable(hRequest, &dataAvaiable)) { + DEBUG << "Failed to query data Available: " << getLastErrorMsg(); + goto cleanup; + }*/ + + if (!results) { + // Report any errors. + DEBUG << "Error" << GetLastError(); + emit error(tr("Unknown Problem when connecting"), ErrUnknown); + } +cleanup: if (hConnect) { WinHttpCloseHandle(hConnect); - } if (hSession) {