From 6606688bf3845fec0745a059494f9f11aad15bae Mon Sep 17 00:00:00 2001 From: tux3 Date: Thu, 10 Dec 2015 18:44:45 +0100 Subject: [PATCH] New Windows updater code We don't support XP anymore to simplify the logic with regards to UAC shenanigans. --- updater/main.cpp | 73 +++++++++++++++++++ updater/settingsDir.cpp | 55 --------------- updater/settingsDir.h | 30 -------- updater/update.cpp | 30 +++++++- updater/update.h | 4 +- updater/updater.pro | 6 +- updater/widget.cpp | 150 +++++++++++++++++++++++++++++++++++----- updater/widget.h | 10 +++ 8 files changed, 251 insertions(+), 107 deletions(-) delete mode 100644 updater/settingsDir.cpp delete mode 100644 updater/settingsDir.h diff --git a/updater/main.cpp b/updater/main.cpp index d2fd9b4e5..6688f98c1 100644 --- a/updater/main.cpp +++ b/updater/main.cpp @@ -20,10 +20,83 @@ #include "widget.h" #include +#include +#include +#include +#include +#include +#include +#include +#include + +static std::unique_ptr logFileStream {nullptr}; +static std::unique_ptr logFileFile {nullptr}; +static QMutex mutex; + +void logMessageHandler(QtMsgType type, const QMessageLogContext& ctxt, const QString& msg) +{ + // Silence qWarning spam due to bug in QTextBrowser (trying to open a file for base64 images) + if (ctxt.function == QString("virtual bool QFSFileEngine::open(QIODevice::OpenMode)") + && msg == QString("QFSFileEngine::open: No file name specified")) + return; + + QString LogMsg = QString("[%1] %2:%3 : ") + .arg(QTime::currentTime().toString("HH:mm:ss.zzz")).arg(ctxt.file).arg(ctxt.line); + switch (type) + { + case QtDebugMsg: + LogMsg += "Debug"; + break; + case QtWarningMsg: + LogMsg += "Warning"; + break; + case QtCriticalMsg: + LogMsg += "Critical"; + break; + case QtFatalMsg: + LogMsg += "Fatal"; + break; + default: + break; + } + + LogMsg += ": " + msg + "\n"; + + QTextStream out(stderr, QIODevice::WriteOnly); + out << LogMsg; + + if (!logFileStream) + return; + + QMutexLocker locker(&mutex); + *logFileStream << LogMsg; + logFileStream->flush(); +} int main(int argc, char *argv[]) { + qInstallMessageHandler(logMessageHandler); QApplication a(argc, argv); + + logFileStream.reset(new QTextStream); + logFileFile.reset(new QFile(QDir::cleanPath(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + QDir::separator() + + "AppData" + QDir::separator() + "Roaming" + QDir::separator() + "tox")+QDir::separator()+"qtox.log")); + if (logFileFile->open(QIODevice::Append)) + { + logFileStream->setDevice(logFileFile.get()); + *logFileStream << QDateTime::currentDateTime().toString("\nyyyy-MM-dd HH:mm:ss' Updater file logger starting\n'"); + } + else + { + qWarning() << "Couldn't open log file!\n"; + logFileStream.release(); + } + + long unsigned int bufsize=100; + char buf[100]; + GetUserNameA(buf, &bufsize); + qDebug() << "Updater running as user" << buf; + Widget w; w.show(); diff --git a/updater/settingsDir.cpp b/updater/settingsDir.cpp deleted file mode 100644 index 149ae79ee..000000000 --- a/updater/settingsDir.cpp +++ /dev/null @@ -1,55 +0,0 @@ -/* - Copyright © 2014 by The qTox Project - - This file is part of qTox, a Qt-based graphical interface for Tox. - - qTox is libre software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - qTox is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with qTox. If not, see -*/ - - -#include "settingsDir.h" -#include -#include -#include -#include - -const QString FILENAME = "settings.ini"; - -QString getSettingsDirPath() -{ - if (isToxPortableEnabled()) - return "."; - -#ifdef Q_OS_WIN - return QDir::cleanPath(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) - + QDir::separator() + "AppData" + QDir::separator() + "Roaming" + QDir::separator() + "tox"); -#else - return QDir::cleanPath(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QDir::separator() + "tox"); -#endif -} - -bool isToxPortableEnabled() -{ - QFile portableSettings(FILENAME); - if (portableSettings.exists()) - { - QSettings ps(FILENAME, QSettings::IniFormat); - ps.beginGroup("General"); - return ps.value("makeToxPortable", false).toBool(); - } - else - { - return false; - } -} diff --git a/updater/settingsDir.h b/updater/settingsDir.h deleted file mode 100644 index 2e06c7f3e..000000000 --- a/updater/settingsDir.h +++ /dev/null @@ -1,30 +0,0 @@ -/* - Copyright © 2014 by The qTox Project - - This file is part of qTox, a Qt-based graphical interface for Tox. - - qTox is libre software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - qTox is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with qTox. If not, see -*/ - - -#ifndef SETTINGSDIR_H -#define SETTINGSDIR_H - -#include - -QString getSettingsDirPath(); - -bool isToxPortableEnabled(); - -#endif // SETTINGSDIR_H diff --git a/updater/update.cpp b/updater/update.cpp index 75a7e2c71..6e6e4d47b 100644 --- a/updater/update.cpp +++ b/updater/update.cpp @@ -20,9 +20,12 @@ #include "update.h" #include "serialize.h" +#include "widget.h" #include #include #include +#include +#include unsigned char key[crypto_sign_PUBLICKEYBYTES] = { @@ -47,14 +50,35 @@ QByteArray getLocalFlist() return flist; } -QList genUpdateDiff(QList updateFlist) +bool isUpToDate(UpdateFileMeta fileMeta) +{ + QString appDir = qApp->applicationDirPath(); + QFile file(appDir+QDir::separator()+fileMeta.installpath); + if (!file.open(QIODevice::ReadOnly)) + return false; + + // If the data we have is corrupted or old, mark it for update + QByteArray data = file.readAll(); + if (crypto_sign_verify_detached(fileMeta.sig, (unsigned char*)data.data(), data.size(), key) != 0) + return false; + + return true; +} + +QList genUpdateDiff(QList updateFlist, Widget* w) { QList diff; - QList localFlist = parseFlist(getLocalFlist()); + + float progressDiff = 45; + float progress = 5; for (UpdateFileMeta file : updateFlist) - if (!localFlist.contains(file)) + { + if (!isUpToDate(file)) diff += file; + progress += progressDiff / updateFlist.size(); + w->setProgress(progress); + } return diff; } diff --git a/updater/update.h b/updater/update.h index 71fb11910..13f8db17a 100644 --- a/updater/update.h +++ b/updater/update.h @@ -25,6 +25,8 @@ #include #include +class Widget; + struct UpdateFileMeta { unsigned char sig[crypto_sign_BYTES]; ///< Signature of the file (ed25519) @@ -51,7 +53,7 @@ QByteArray getLocalFlist(); /// Parses and validates a flist file. Returns an empty list on error QList parseFlist(QByteArray flistData); /// Generates a list of files we need to update -QList genUpdateDiff(QList updateFlist); +QList genUpdateDiff(QList updateFlist, Widget *w); extern unsigned char key[crypto_sign_PUBLICKEYBYTES]; diff --git a/updater/updater.pro b/updater/updater.pro index 582b0f918..5373d1a72 100644 --- a/updater/updater.pro +++ b/updater/updater.pro @@ -17,12 +17,10 @@ QMAKE_CXXFLAGS += -fno-exceptions SOURCES += main.cpp\ widget.cpp \ - settingsDir.cpp \ update.cpp \ serialize.cpp HEADERS += widget.h \ - settingsDir.h \ update.h \ serialize.h @@ -36,3 +34,7 @@ INCLUDEPATH += libs/include RC_FILE = windows/updater.rc LIBS += -L$$PWD/libs/lib/ -lsodium + +win32 { + LIBS += -lshell32 -luuid +} diff --git a/updater/widget.cpp b/updater/widget.cpp index 6e5a02798..0a141b520 100644 --- a/updater/widget.cpp +++ b/updater/widget.cpp @@ -26,17 +26,28 @@ #include #include #include +#include +#include -#include "settingsDir.h" #include "update.h" #ifdef Q_OS_WIN +#ifdef _WIN32_WINNT +#undef _WIN32_WINNT +#endif +#define _WIN32_WINNT 0x0600 // Vista for SHGetKnownFolderPath +#include +#include +#include +#include + const bool supported = true; const QString QTOX_PATH = "qtox.exe"; #else const bool supported = false; const QString QTOX_PATH; #endif +const QString SETTINGS_FILE = "settings.ini"; Widget::Widget(QWidget *parent) : QWidget(parent), @@ -48,11 +59,66 @@ Widget::Widget(QWidget *parent) : if (!supported) fatalError(tr("The qTox updater is not supported on this platform.")); +#ifdef Q_OS_WIN + // Get a primary unelevated token of the actual user + hPrimaryToken = nullptr; + HANDLE hShellProcess = nullptr, hShellProcessToken = nullptr; + const DWORD dwTokenRights = TOKEN_QUERY | TOKEN_IMPERSONATE | TOKEN_ASSIGN_PRIMARY + | TOKEN_DUPLICATE | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID; + DWORD dwPID = 0; + HWND hwnd = nullptr; + DWORD dwLastErr = 0; + + // Enable SeIncreaseQuotaPrivilege + HANDLE hProcessToken = NULL; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hProcessToken)) + goto unelevateFail; + TOKEN_PRIVILEGES tkp; + tkp.PrivilegeCount = 1; + LookupPrivilegeValueW(NULL, SE_INCREASE_QUOTA_NAME, &tkp.Privileges[0].Luid); + tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + AdjustTokenPrivileges(hProcessToken, FALSE, &tkp, 0, NULL, NULL); + dwLastErr = GetLastError(); + CloseHandle(hProcessToken); + if (ERROR_SUCCESS != dwLastErr) + goto unelevateFail; + + // Get a primary copy of the desktop shell's token, + // we're assuming the shell is running as the actual user + hwnd = GetShellWindow(); + if (!hwnd) + goto unelevateFail; + GetWindowThreadProcessId(hwnd, &dwPID); + if (!dwPID) + goto unelevateFail; + hShellProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwPID); + if (!hShellProcess) + goto unelevateFail; + if (!OpenProcessToken(hShellProcess, TOKEN_DUPLICATE, &hShellProcessToken)) + goto unelevateFail; + + // Duplicate the shell's process token to get a primary token. + // Based on experimentation, this is the minimal set of rights required for CreateProcessWithTokenW (contrary to current documentation). + if (!DuplicateTokenEx(hShellProcessToken, dwTokenRights, NULL, SecurityImpersonation, TokenPrimary, &hPrimaryToken)) + goto unelevateFail; + + qDebug() << "Unelevated primary access token acquired"; + goto unelevateCleanup; +unelevateFail: + qWarning() << "Unelevate failed, couldn't get access token"; +unelevateCleanup: + CloseHandle(hShellProcessToken); + CloseHandle(hShellProcess); +#endif + QMetaObject::invokeMethod(this, "update", Qt::QueuedConnection); } Widget::~Widget() { +#ifdef Q_OS_WIN + CloseHandle(hPrimaryToken); +#endif delete ui; } @@ -65,6 +131,7 @@ void Widget::setProgress(int value) void Widget::fatalError(QString message) { + qCritical() << "Update aborted with error:"< updateFlist = parseFlist(updateFlistData); + setProgress(5); - setProgress(2); - QList diff = genUpdateDiff(updateFlist); - setProgress(4); + /// 2. Generate a diff (5-50%) + QList diff = genUpdateDiff(updateFlist, this); for (UpdateFileMeta fileMeta : diff) if (!QFile::exists(updateDirStr+fileMeta.installpath)) fatalError(tr("The update is incomplete.")); if (diff.size() == 0) - fatalError(tr("The diff list is empty.")); + fatalError(tr("The update is empty!")); + setProgress(50); + qDebug() << "Diff generated,"< +#ifdef Q_OS_WIN +#include +#endif + namespace Ui { class Widget; } @@ -39,6 +43,8 @@ public: void deleteBackups(); void restoreBackups(); void setProgress(int value); + QString getSettingsDirPath(); + bool isToxPortableEnabled(); // Noreturn void fatalError(QString message); ///< Calls deleteUpdate and startQToxAndExit @@ -52,6 +58,10 @@ public slots: private: Ui::Widget *ui; QStringList backups; + +#ifdef Q_OS_WIN + HANDLE hPrimaryToken; +#endif }; #endif // WIDGET_H