Mercurial > trustbridge
view ui/downloader_win.cpp @ 1088:508c96e72f62
(issue124) Switch server URL and remove some RELEASE_BUILD options
As the test server speaks ECDSA we do not need so much
#ifndef RELEASE_BUILD options anymore.
author | Andre Heinecke <andre.heinecke@intevation.de> |
---|---|
date | Fri, 12 Sep 2014 15:38:42 +0200 |
parents | 09bb19e5e369 |
children | 82fab0c689bf |
line wrap: on
line source
/* Copyright (C) 2014 by Bundesamt für Sicherheit in der Informationstechnik * Software engineering by Intevation GmbH * * This file is Free Software under the GNU GPL (v>=2) * and comes with ABSOLUTELY NO WARRANTY! * See LICENSE.txt for details. */ /** * @file downloader_win.cpp * @brief Downloader implementation for Windows * * We use Windows API here instead of Qt because we want to avoid * QtNetworks SSL stack which is based on OpenSSL and so * we might be incompatible with GPL code. Also using the * native API means that the security of the SSL implementation * is tied to the security of the system. * */ #include "downloader.h" #ifdef Q_OS_WIN #ifndef MYVERSION #define MYVERSION "1" #endif #include <windows.h> #include <winhttp.h> #include <QDebug> #include <QDateTime> #include <QSaveFile> #include <QFileInfo> #define DEBUG if (1) qDebug() << __PRETTY_FUNCTION__ #define MAX_SW_SIZE 10485760 #define MAX_LIST_SIZE 1048576 /** @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); #define LIST_RESOURCE "/incoming/aheinecke/test" #define SW_RESOURCE "/incoming/aheinecke/test" /** @brief A wrapper around a HINTERNET structure that handles closing * * Holds a HINTERNET structure and closes it if necessary on destruction. * */ class SmartHINTERNET { public: SmartHINTERNET() : handle(NULL) {} ~SmartHINTERNET() { if (handle) { WinHttpCloseHandle(handle); } } HINTERNET handle; }; /** @brief Qt wrapper around FormatMessage * * @returns The error message of the error that occurred */ const QString getLastErrorMsg() { LPWSTR bufPtr = NULL; DWORD err = GetLastError(); FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | 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); LocalFree(bufPtr); return result; } /** @brief open a session with appropriate proxy settings * * @param[inout] *pHSession pointer to a HInternet structure * * On error call getLastError to get extended error information. * * @returns True on success, false on error. */ bool openSession(HINTERNET *pHSession) { WINHTTP_CURRENT_USER_IE_PROXY_CONFIG proxyConfig; DEBUG; if (!pHSession) { SetLastError(ERROR_INVALID_PARAMETER); return false; } memset(&proxyConfig, 0, sizeof (WINHTTP_CURRENT_USER_IE_PROXY_CONFIG)); if (WinHttpGetIEProxyConfigForCurrentUser(&proxyConfig)) { if (proxyConfig.fAutoDetect) { // TODO Handle this qDebug() << "Autodetect is set"; } if (proxyConfig.lpszProxy || proxyConfig.lpszProxyBypass) { DEBUG << "Using proxies."; } if (proxyConfig.lpszProxy) { *pHSession = WinHttpOpen(L"TrustBridge "MYVERSION, WINHTTP_ACCESS_TYPE_NAMED_PROXY, proxyConfig.lpszProxy, proxyConfig.lpszProxyBypass, 0); } } if (!*pHSession) { DEBUG << "No IE Proxy falling back to default proxy"; *pHSession = WinHttpOpen(L"TrustBridge "MYVERSION, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); } // Cleanup if (proxyConfig.lpszAutoConfigUrl) { GlobalFree(proxyConfig.lpszAutoConfigUrl); } if (proxyConfig.lpszProxy) { GlobalFree(proxyConfig.lpszProxy); } if (proxyConfig.lpszProxyBypass) { GlobalFree(proxyConfig.lpszProxyBypass); } return *pHSession; } /** @brief initialize a connection in the session * * @param[in] HSession the session to work in. * @param[inout] *pHConnect pointer to the connection. * @param[in] url pointer to the URL in wchar representation. * * On error call getLastError to get extended error information. * * @returns True on success, false on error. */ bool initializeConnection(HINTERNET hSession, HINTERNET *pHConnect, LPCWSTR url) { DEBUG; if (!hSession || !pHConnect) { SetLastError(ERROR_INVALID_PARAMETER); return false; } // Initialize connection. No request is done here. *pHConnect = WinHttpConnect(hSession, url, INTERNET_DEFAULT_HTTPS_PORT, 0); return *pHConnect; } /** @brief Create a request * * @param[in] HSession the session to work in. * @param[in] HConnect the connection to use. * @param[inout] *pHRequest pointer to the request structure to be filled. * @param[in] requestType the HTTP request to be made default is GET * @param[in] resource pointer to the resource to request in wchar * representation. * * On error call getLastError to get extended error information. * This function still does not do any networking but only initializes * it. * * @returns True on success, false on error. */ bool createRequest(HINTERNET hSession, HINTERNET hConnect, HINTERNET *pHRequest, LPCWSTR requestType, LPCWSTR resource) { DWORD dwSSLFlag; DEBUG; if (!hSession || !hConnect || !pHRequest) { SetLastError(ERROR_INVALID_PARAMETER); return false; } *pHRequest = WinHttpOpenRequest(hConnect, requestType, resource, 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; } bool Downloader::verifyCertificate(HINTERNET hRequest) { CERT_CONTEXT *certContext = NULL; DWORD certContextLen = sizeof(CERT_CONTEXT); bool retval = false; if (!WinHttpQueryOption(hRequest, WINHTTP_OPTION_SERVER_CERT_CONTEXT, &certContext, &certContextLen)) { DEBUG << "Unable to get server certificate"; return false; } 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(); emit error("Invalid certificate", InvalidCertificate); } CertFreeCertificateContext(certContext); return retval; } QDateTime Downloader::getLastModifiedHeader(HINTERNET hSession, HINTERNET hConnect, LPCWSTR resource) { SmartHINTERNET sHRequest; SYSTEMTIME lMod; DWORD sizeOfSystemtime = sizeof (SYSTEMTIME); QDateTime retval; memset(&lMod, 0, sizeof (SYSTEMTIME)); if (!hSession || !hConnect || !resource) { SetLastError(ERROR_INVALID_PARAMETER); return retval; } if (!createRequest(hSession, hConnect, &sHRequest.handle, L"HEAD", resource)) { return retval; } if (!WinHttpSendRequest(sHRequest.handle, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) { return retval; } if (!WinHttpReceiveResponse(sHRequest.handle, NULL)) { return retval; } if (!verifyCertificate(sHRequest.handle)) { DEBUG << "Certificate verification failed"; return retval; } if (!(WinHttpQueryHeaders(sHRequest.handle, WINHTTP_QUERY_LAST_MODIFIED | WINHTTP_QUERY_FLAG_SYSTEMTIME, NULL, &lMod, &sizeOfSystemtime, WINHTTP_NO_HEADER_INDEX))) { return retval; } retval = QDateTime(QDate(lMod.wYear, lMod.wMonth, lMod.wDay), QTime(lMod.wHour, lMod.wMinute, lMod.wSecond, lMod.wMilliseconds), Qt::UTC); return retval; } bool Downloader::downloadFile(HINTERNET hSession, HINTERNET hConnect, LPCWSTR resource, const QString &fileName, DWORD maxSize) { SmartHINTERNET sHRequest; bool retval = false; DWORD bytesAvailable = 0, bytesRead = 0, totalDownloaded = 0, contentLength = 0, sizeOfDWORD = sizeof (DWORD); QSaveFile outputFile(fileName); if (!hSession || !hConnect || !resource) { SetLastError(ERROR_INVALID_PARAMETER); return retval; } if (!createRequest(hSession, hConnect, &sHRequest.handle, L"GET", resource)) { return retval; } if (!WinHttpSendRequest(sHRequest.handle, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) { return retval; } if (!WinHttpReceiveResponse(sHRequest.handle, NULL)) { return retval; } if (!verifyCertificate(sHRequest.handle)) { DEBUG << "Certificate verification failed"; return retval; } if (!(WinHttpQueryHeaders(sHRequest.handle, WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER, NULL, &contentLength, &sizeOfDWORD, WINHTTP_NO_HEADER_INDEX))) { // Continue anyway as we later really check how // much we download. DEBUG << "No content-length"; } if (contentLength > maxSize) { return retval; } if (contentLength) { QFileInfo finf(fileName); if (finf.exists() && finf.isReadable() && finf.size() == contentLength) { // We already have data of the same size // No need to waste bandwidth. DEBUG << "Skipping download because file exists"; retval = true; return retval; } } // Open / Create the file to write to. if (!outputFile.open(QIODevice::WriteOnly)) { DEBUG << "Failed to open file"; return retval; } DEBUG << "output file size: " << outputFile.size(); do { char outBuf[8192]; // 8KB is the internal buffer size of winhttp memset(outBuf, 0, sizeof(outBuf)); bytesRead = 0; if (!WinHttpQueryDataAvailable(sHRequest.handle, &bytesAvailable)) { DEBUG << "Querying for available data failed"; retval = false; break; } if (!bytesAvailable) { // Might indicate that we are done. break; } if (bytesAvailable > maxSize) { DEBUG << "File to large"; retval = false; SetLastError(ERROR_INVALID_DATA); break; } if (!WinHttpReadData(sHRequest.handle, (LPVOID)outBuf, sizeof(outBuf), &bytesRead)) { DEBUG << "Error reading data"; break; } else { if (bytesRead) { // Write data to file. if (outputFile.write(outBuf, bytesRead) != bytesRead) { 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); if (retval && outputFile.isOpen()) { // Actually save the file to disk / move to homedir retval = outputFile.commit(); } return retval; } void Downloader::run() { bool results = false; SmartHINTERNET sHSession; SmartHINTERNET sHConnect; 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(&sHConnect.handle)) { DEBUG << "Failed to open session: " << getLastErrorMsg(); return; } if (!initializeConnection(sHConnect.handle, &sHConnect.handle, wUrl)) { DEBUG << "Failed to initialize connection: " << getLastErrorMsg(); return; } lastModifiedSoftware = getLastModifiedHeader(sHConnect.handle, sHConnect.handle, L""SW_RESOURCE); lastModifiedList = getLastModifiedHeader(sHConnect.handle, sHConnect.handle, L""LIST_RESOURCE); if (!lastModifiedList.isValid() || !lastModifiedSoftware.isValid()) { DEBUG << "Could not read headers: " << getLastErrorMsg(); return; } if (!mLastModSW.isValid() || lastModifiedSoftware > mLastModSW) { QString dataDirectory = getDataDirectory(); if (dataDirectory.isEmpty()) { DEBUG << "Failed to get data directory"; return; } QString fileName = dataDirectory.append("/SW-") .append(lastModifiedSoftware.toString("yyyymmddHHmmss")) .append(".exe"); DEBUG << "fileName: " << fileName; if (!downloadFile(sHConnect.handle, sHConnect.handle, L""SW_RESOURCE, fileName, MAX_SW_SIZE)) { DEBUG << "Error downloading File: " << getLastErrorMsg(); return; } emit newSoftwareAvailable(fileName, lastModifiedSoftware); } else if (!mLastModList.isValid() || lastModifiedList > mLastModList) { QString dataDirectory = getDataDirectory(); if (dataDirectory.isEmpty()) { DEBUG << "Failed to get data directory"; return; } QString fileName = dataDirectory.append("/list-") .append(lastModifiedSoftware.toString("yyyymmddHHmmss")) .append(".txt"); DEBUG << "fileName: " << fileName; if (!downloadFile(sHConnect.handle, sHConnect.handle, L""LIST_RESOURCE, fileName, MAX_LIST_SIZE)) { DEBUG << "Error downloading File: " << getLastErrorMsg(); return; } emit newListAvailable(fileName, lastModifiedList); } DEBUG << "SW date: " << lastModifiedSoftware; DEBUG << "List date: " << lastModifiedList; if (!results) { // Report any errors. DEBUG << "Error" << GetLastError(); emit error(tr("Unknown Problem when connecting"), ErrUnknown); } return; } #endif