changeset 15:95e1b6edf2fc

Implement more downloader functionality for Windows
author Andre Heinecke <aheinecke@intevation.de>
date Wed, 19 Feb 2014 10:45:06 +0000
parents 58d51a91d448
children 225a5ec20dad
files ui/downloader.cpp ui/downloader.h ui/downloader_linux.cpp ui/downloader_win.cpp
diffstat 4 files changed, 452 insertions(+), 111 deletions(-) [+]
line wrap: on
line diff
--- 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 <QFile>
+#include <QDir>
 #include <QDebug>
+#include <QStandardPaths>
 
-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();
 }
-
--- 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 <QThread>
 #include <QString>
 #include <QByteArray>
+#include <QDateTime>
+
+#ifdef Q_OS_WIN
+#include <windows.h>
+#include <winhttp.h>
+#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);
 };
--- 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
--- 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 <winhttp.h>
 
 #include <QDebug>
+#include <QDateTime>
+#include <QSaveFile>
 
 #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) {

http://wald.intevation.org/projects/trustbridge/