diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 59c8735c7..858817a49 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -344,12 +344,16 @@ if(OS_WINDOWS) obs PRIVATE obs.manifest platform-windows.cpp - win-update/update-window.cpp - win-update/update-window.hpp - win-update/win-update.cpp - win-update/win-update.hpp - win-update/win-update-helpers.cpp - win-update/win-update-helpers.hpp + update/update-window.cpp + update/update-window.hpp + update/win-update.cpp + update/win-update.hpp + update/shared-update.cpp + update/shared-update.hpp + update/update-helpers.cpp + update/update-helpers.hpp + update/crypto-helpers-mbedtls.cpp + update/crypto-helpers.hpp ${CMAKE_BINARY_DIR}/obs.rc) if(_QT_VERSION EQUAL 5) @@ -357,7 +361,8 @@ if(OS_WINDOWS) target_link_libraries(obs PRIVATE Qt::WinExtras) endif() - target_link_libraries(obs PRIVATE crypt32 OBS::blake2) + find_package(MbedTLS) + target_link_libraries(obs PRIVATE Mbedtls::Mbedtls OBS::blake2) target_compile_features(obs PRIVATE cxx_std_17) @@ -446,12 +451,9 @@ elseif(OS_MACOS) target_sources( obs - PRIVATE nix-update/crypto-helpers.hpp - nix-update/crypto-helpers-mac.mm - nix-update/nix-update.cpp - nix-update/nix-update.hpp - nix-update/nix-update-helpers.cpp - nix-update/nix-update-helpers.hpp) + PRIVATE update/crypto-helpers.hpp update/crypto-helpers-mac.mm + update/shared-update.cpp update/shared-update.hpp + update/update-helpers.cpp update/update-helpers.hpp) endif() set_source_files_properties(platform-osx.mm PROPERTIES COMPILE_FLAGS @@ -485,12 +487,9 @@ elseif(OS_POSIX) target_sources( obs - PRIVATE nix-update/crypto-helpers.hpp - nix-update/crypto-helpers-mbedtls.cpp - nix-update/nix-update.cpp - nix-update/nix-update.hpp - nix-update/nix-update-helpers.cpp - nix-update/nix-update-helpers.hpp) + PRIVATE update/crypto-helpers.hpp update/crypto-helpers-mbedtls.cpp + update/shared-update.cpp update/shared-update.hpp + update/update-helpers.cpp update/update-helpers.hpp) target_link_libraries(obs PRIVATE Mbedtls::Mbedtls OBS::blake2) endif() endif() diff --git a/UI/nix-update/crypto-helpers-mac.mm b/UI/update/crypto-helpers-mac.mm similarity index 100% rename from UI/nix-update/crypto-helpers-mac.mm rename to UI/update/crypto-helpers-mac.mm diff --git a/UI/nix-update/crypto-helpers-mbedtls.cpp b/UI/update/crypto-helpers-mbedtls.cpp similarity index 100% rename from UI/nix-update/crypto-helpers-mbedtls.cpp rename to UI/update/crypto-helpers-mbedtls.cpp diff --git a/UI/nix-update/crypto-helpers.hpp b/UI/update/crypto-helpers.hpp similarity index 100% rename from UI/nix-update/crypto-helpers.hpp rename to UI/update/crypto-helpers.hpp diff --git a/UI/nix-update/nix-update.cpp b/UI/update/shared-update.cpp similarity index 78% rename from UI/nix-update/nix-update.cpp rename to UI/update/shared-update.cpp index 64f580bd1..5c29dce37 100644 --- a/UI/nix-update/nix-update.cpp +++ b/UI/update/shared-update.cpp @@ -1,6 +1,6 @@ -#include "nix-update.hpp" +#include "shared-update.hpp" #include "crypto-helpers.hpp" -#include "nix-update-helpers.hpp" +#include "update-helpers.hpp" #include "obs-app.hpp" #include "remote-text.hpp" #include "platform.hpp" @@ -24,12 +24,18 @@ extern QCef *cef; #define MAC_WHATSNEW_URL "https://obsproject.com/update_studio/whatsnew.json" #endif +#ifndef WIN_WHATSNEW_URL +#define WIN_WHATSNEW_URL "https://obsproject.com/update_studio/whatsnew.json" +#endif + #ifndef LINUX_WHATSNEW_URL #define LINUX_WHATSNEW_URL "https://obsproject.com/update_studio/whatsnew.json" #endif #ifdef __APPLE__ #define WHATSNEW_URL MAC_WHATSNEW_URL +#elif defined(_WIN32) +#define WHATSNEW_URL WIN_WHATSNEW_URL #else #define WHATSNEW_URL LINUX_WHATSNEW_URL #endif @@ -194,28 +200,34 @@ try { /* ------------------------------------------------------------------------ */ -void WhatsNewInfoThread::run() -try { +bool FetchAndVerifyFile(const char *name, const char *file, const char *url, + std::string *out, + const std::vector &extraHeaders) +{ long responseCode; - std::vector extraHeaders; - std::string text; + std::vector headers; std::string error; std::string signature; - uint8_t whatsnewHash[BLAKE2_HASH_LENGTH]; + std::string data; + uint8_t fileHash[BLAKE2_HASH_LENGTH]; bool success; - BPtr whatsnewPath = - GetConfigPathPtr("obs-studio/updates/whatsnew.json"); + BPtr filePath = GetConfigPathPtr(file); + + if (!extraHeaders.empty()) { + headers.insert(headers.end(), extraHeaders.begin(), + extraHeaders.end()); + } /* ----------------------------------- * - * avoid downloading json again */ + * avoid downloading file again */ - if (CalculateFileHash(whatsnewPath, whatsnewHash)) { - auto hash = QByteArray::fromRawData((const char *)whatsnewHash, + if (CalculateFileHash(filePath, fileHash)) { + auto hash = QByteArray::fromRawData((const char *)fileHash, BLAKE2_HASH_LENGTH); QString header = "If-None-Match: " + hash.toHex(); - extraHeaders.push_back(move(header.toStdString())); + headers.push_back(move(header.toStdString())); } /* ----------------------------------- * @@ -225,20 +237,20 @@ try { if (!guid.empty()) { std::string header = "X-OBS2-GUID: " + guid; - extraHeaders.push_back(move(header)); + headers.push_back(move(header)); } /* ----------------------------------- * - * get json from server */ + * get file from server */ - success = GetRemoteFile(WHATSNEW_URL, text, error, &responseCode, - nullptr, "", nullptr, extraHeaders, &signature); + success = GetRemoteFile(url, data, error, &responseCode, nullptr, "", + nullptr, headers, &signature); if (!success || (responseCode != 200 && responseCode != 304)) { if (responseCode == 404) - return; + return false; - throw strprintf("Failed to fetch whatsnew file: %s", + throw strprintf("Failed to fetch %s file: %s", name, error.c_str()); } @@ -246,29 +258,40 @@ try { * verify file signature */ if (responseCode == 200) { - success = CheckDataSignature("whatsnew", text, signature); + success = CheckDataSignature(name, data, signature); if (!success) - throw std::string("Invalid whatsnew signature"); + throw strprintf("Invalid %s signature", name); } /* ----------------------------------- * - * write or load json */ + * write or load file */ if (responseCode == 200) { - if (!QuickWriteFile(whatsnewPath, text)) + if (!QuickWriteFile(filePath, data)) throw strprintf("Could not write file '%s'", - whatsnewPath.Get()); - } else { - if (!QuickReadFile(whatsnewPath, text)) + filePath.Get()); + } else if (out) { /* Only read file if caller wants data */ + if (!QuickReadFile(filePath, data)) throw strprintf("Could not read file '%s'", - whatsnewPath.Get()); + filePath.Get()); } + if (out) + *out = data; + /* ----------------------------------- * * success */ + return true; +} - emit Result(QString::fromStdString(text)); +void WhatsNewInfoThread::run() +try { + std::string text; + if (FetchAndVerifyFile("whatsnew", "obs-studio/updates/whatsnew.json", + WHATSNEW_URL, &text)) { + emit Result(QString::fromStdString(text)); + } } catch (std::string &text) { blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); } @@ -277,6 +300,8 @@ try { void WhatsNewBrowserInitThread::run() { +#ifdef BROWSER_AVAILABLE cef->wait_for_browser_init(); +#endif emit Result(url); } diff --git a/UI/nix-update/nix-update.hpp b/UI/update/shared-update.hpp similarity index 67% rename from UI/nix-update/nix-update.hpp rename to UI/update/shared-update.hpp index 56394930f..b1522947c 100644 --- a/UI/nix-update/nix-update.hpp +++ b/UI/update/shared-update.hpp @@ -3,6 +3,14 @@ #include #include +#include +#include + +bool FetchAndVerifyFile(const char *name, const char *file, const char *url, + std::string *out, + const std::vector &extraHeaders = + std::vector()); + class WhatsNewInfoThread : public QThread { Q_OBJECT diff --git a/UI/nix-update/nix-update-helpers.cpp b/UI/update/update-helpers.cpp similarity index 93% rename from UI/nix-update/nix-update-helpers.cpp rename to UI/update/update-helpers.cpp index 5a9004305..8d466e691 100644 --- a/UI/nix-update/nix-update-helpers.cpp +++ b/UI/update/update-helpers.cpp @@ -1,4 +1,4 @@ -#include "nix-update-helpers.hpp" +#include "update-helpers.hpp" #include diff --git a/UI/nix-update/nix-update-helpers.hpp b/UI/update/update-helpers.hpp similarity index 100% rename from UI/nix-update/nix-update-helpers.hpp rename to UI/update/update-helpers.hpp diff --git a/UI/win-update/update-window.cpp b/UI/update/update-window.cpp similarity index 100% rename from UI/win-update/update-window.cpp rename to UI/update/update-window.cpp diff --git a/UI/win-update/update-window.hpp b/UI/update/update-window.hpp similarity index 100% rename from UI/win-update/update-window.hpp rename to UI/update/update-window.hpp diff --git a/UI/update/win-update.cpp b/UI/update/win-update.cpp new file mode 100644 index 000000000..86203b7eb --- /dev/null +++ b/UI/update/win-update.cpp @@ -0,0 +1,398 @@ +#include "update-helpers.hpp" +#include "shared-update.hpp" +#include "update-window.hpp" +#include "remote-text.hpp" +#include "qt-wrappers.hpp" +#include "win-update.hpp" +#include "obs-app.hpp" + +#include + +#include +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include +#include +#include + +#ifdef BROWSER_AVAILABLE +#include +#endif + +using namespace std; +using namespace json11; + +/* ------------------------------------------------------------------------ */ + +#ifndef WIN_MANIFEST_URL +#define WIN_MANIFEST_URL "https://obsproject.com/update_studio/manifest.json" +#endif + +#ifndef WIN_MANIFEST_BASE_URL +#define WIN_MANIFEST_BASE_URL "https://obsproject.com/update_studio/" +#endif + +#ifndef WIN_BRANCHES_URL +#define WIN_BRANCHES_URL "https://obsproject.com/update_studio/branches.json" +#endif + +#ifndef WIN_DEFAULT_BRANCH +#define WIN_DEFAULT_BRANCH "stable" +#endif + +#ifndef WIN_UPDATER_URL +#define WIN_UPDATER_URL "https://obsproject.com/update_studio/updater.exe" +#endif + +/* ------------------------------------------------------------------------ */ + +#if defined(OBS_RELEASE_CANDIDATE) && OBS_RELEASE_CANDIDATE > 0 +#define CUR_VER \ + ((uint64_t)OBS_RELEASE_CANDIDATE_VER << 16ULL | OBS_RELEASE_CANDIDATE \ + << 8ULL) +#define PRE_RELEASE true +#elif OBS_BETA > 0 +#define CUR_VER ((uint64_t)OBS_BETA_VER << 16ULL | OBS_BETA) +#define PRE_RELEASE true +#elif defined(OBS_COMMIT) +#define CUR_VER 1 << 16ULL +#define CUR_COMMIT OBS_COMMIT +#define PRE_RELEASE true +#else +#define CUR_VER ((uint64_t)LIBOBS_API_VER << 16ULL) +#define PRE_RELEASE false +#endif + +#ifndef CUR_COMMIT +#define CUR_COMMIT "00000000" +#endif + +static bool ParseUpdateManifest(const char *manifest, bool *updatesAvailable, + string ¬es_str, uint64_t &updateVer, + string &branch) +try { + + string error; + Json root = Json::parse(manifest, error); + if (!error.empty()) + throw strprintf("Failed reading json string: %s", + error.c_str()); + + if (!root.is_object()) + throw string("Root of manifest is not an object"); + + int major = root["version_major"].int_value(); + int minor = root["version_minor"].int_value(); + int patch = root["version_patch"].int_value(); + int rc = root["rc"].int_value(); + int beta = root["beta"].int_value(); + string commit_hash = root["commit"].string_value(); + + if (major == 0 && commit_hash.empty()) + throw strprintf("Invalid version number: %d.%d.%d", major, + minor, patch); + + const Json ¬es = root["notes"]; + if (!notes.is_string()) + throw string("'notes' value invalid"); + + notes_str = notes.string_value(); + + const Json &packages = root["packages"]; + if (!packages.is_array()) + throw string("'packages' value invalid"); + + uint64_t cur_ver; + uint64_t new_ver; + + if (commit_hash.empty()) { + cur_ver = CUR_VER; + new_ver = MAKE_SEMANTIC_VERSION( + (uint64_t)major, (uint64_t)minor, (uint64_t)patch); + new_ver <<= 16; + /* RC builds are shifted so that rc1 and beta1 versions do not result + * in the same new_ver. */ + if (rc > 0) + new_ver |= (uint64_t)rc << 8; + else if (beta > 0) + new_ver |= (uint64_t)beta; + } else { + /* Test or nightly builds may not have a (valid) version number, + * so compare commit hashes instead. */ + cur_ver = stoul(CUR_COMMIT, nullptr, 16); + new_ver = stoul(commit_hash.substr(0, 8), nullptr, 16); + } + + updateVer = new_ver; + + /* When using a pre-release build or non-default branch we only check if + * the manifest version is different, so that it can be rolled-back. */ + if (branch != WIN_DEFAULT_BRANCH || PRE_RELEASE) + *updatesAvailable = new_ver != cur_ver; + else + *updatesAvailable = new_ver > cur_ver; + + return true; + +} catch (string &text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); + return false; +} + +#undef CUR_COMMIT +#undef CUR_VER +#undef PRE_RELEASE + +/* ------------------------------------------------------------------------ */ + +bool GetBranchAndUrl(string &selectedBranch, string &manifestUrl) +{ + const char *config_branch = + config_get_string(GetGlobalConfig(), "General", "UpdateBranch"); + if (!config_branch) + return true; + + bool found = false; + for (const UpdateBranch &branch : App()->GetBranches()) { + if (branch.name != config_branch) + continue; + /* A branch that is found but disabled will just silently fall back to + * the default. But if the branch was removed entirely, the user should + * be warned, so leave this false *only* if the branch was removed. */ + found = true; + + if (branch.is_enabled) { + selectedBranch = branch.name.toStdString(); + if (branch.name != WIN_DEFAULT_BRANCH) { + manifestUrl = WIN_MANIFEST_BASE_URL; + manifestUrl += "manifest_" + + branch.name.toStdString() + + ".json"; + } + } + break; + } + + return found; +} + +/* ------------------------------------------------------------------------ */ + +void AutoUpdateThread::infoMsg(const QString &title, const QString &text) +{ + OBSMessageBox::information(App()->GetMainWindow(), title, text); +} + +void AutoUpdateThread::info(const QString &title, const QString &text) +{ + QMetaObject::invokeMethod(this, "infoMsg", Qt::BlockingQueuedConnection, + Q_ARG(QString, title), Q_ARG(QString, text)); +} + +int AutoUpdateThread::queryUpdateSlot(bool localManualUpdate, + const QString &text) +{ + OBSUpdate updateDlg(App()->GetMainWindow(), localManualUpdate, text); + return updateDlg.exec(); +} + +int AutoUpdateThread::queryUpdate(bool localManualUpdate, const char *text_utf8) +{ + int ret = OBSUpdate::No; + QString text = text_utf8; + QMetaObject::invokeMethod(this, "queryUpdateSlot", + Qt::BlockingQueuedConnection, + Q_RETURN_ARG(int, ret), + Q_ARG(bool, localManualUpdate), + Q_ARG(QString, text)); + return ret; +} + +bool AutoUpdateThread::queryRepairSlot() +{ + QMessageBox::StandardButton res = OBSMessageBox::question( + App()->GetMainWindow(), QTStr("Updater.RepairConfirm.Title"), + QTStr("Updater.RepairConfirm.Text"), + QMessageBox::Yes | QMessageBox::Cancel); + + return res == QMessageBox::Yes; +} + +bool AutoUpdateThread::queryRepair() +{ + bool ret = false; + QMetaObject::invokeMethod(this, "queryRepairSlot", + Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, ret)); + return ret; +} + +void AutoUpdateThread::run() +try { + string text; + string branch = WIN_DEFAULT_BRANCH; + string manifestUrl = WIN_MANIFEST_URL; + vector extraHeaders; + bool updatesAvailable = false; + + struct FinishedTrigger { + inline ~FinishedTrigger() + { + QMetaObject::invokeMethod(App()->GetMainWindow(), + "updateCheckFinished"); + } + } finishedTrigger; + + /* ----------------------------------- * + * get branches from server */ + + if (FetchAndVerifyFile("branches", "obs-studio\\updates\\branches.json", + WIN_BRANCHES_URL, &text)) + App()->SetBranchData(text); + + /* ----------------------------------- * + * check branch and get manifest url */ + + if (!GetBranchAndUrl(branch, manifestUrl)) { + config_set_string(GetGlobalConfig(), "General", "UpdateBranch", + WIN_DEFAULT_BRANCH); + info(QTStr("Updater.BranchNotFound.Title"), + QTStr("Updater.BranchNotFound.Text")); + } + + /* allow server to know if this was a manual update check in case + * we want to allow people to bypass a configured rollout rate */ + if (manualUpdate) + extraHeaders.emplace_back("X-OBS2-ManualUpdate: 1"); + + /* ----------------------------------- * + * get manifest from server */ + + text.clear(); + if (!FetchAndVerifyFile("manifest", + "obs-studio\\updates\\manifest.json", + manifestUrl.c_str(), &text, extraHeaders)) + return; + + /* ----------------------------------- * + * check manifest for update */ + + string notes; + uint64_t updateVer = 0; + + if (!ParseUpdateManifest(text.c_str(), &updatesAvailable, notes, + updateVer, branch)) + throw string("Failed to parse manifest"); + + if (!updatesAvailable && !repairMode) { + if (manualUpdate) + info(QTStr("Updater.NoUpdatesAvailable.Title"), + QTStr("Updater.NoUpdatesAvailable.Text")); + return; + } else if (updatesAvailable && repairMode) { + info(QTStr("Updater.RepairButUpdatesAvailable.Title"), + QTStr("Updater.RepairButUpdatesAvailable.Text")); + return; + } + + /* ----------------------------------- * + * skip this version if set to skip */ + + uint64_t skipUpdateVer = config_get_uint(GetGlobalConfig(), "General", + "SkipUpdateVersion"); + if (!manualUpdate && updateVer == skipUpdateVer && !repairMode) + return; + + /* ----------------------------------- * + * fetch updater module */ + + if (!FetchAndVerifyFile("updater", "obs-studio\\updates\\updater.exe", + WIN_UPDATER_URL, nullptr)) + return; + + /* ----------------------------------- * + * query user for update */ + + if (repairMode) { + if (!queryRepair()) + return; + } else { + int queryResult = queryUpdate(manualUpdate, notes.c_str()); + + if (queryResult == OBSUpdate::No) { + if (!manualUpdate) { + long long t = (long long)time(nullptr); + config_set_int(GetGlobalConfig(), "General", + "LastUpdateCheck", t); + } + return; + + } else if (queryResult == OBSUpdate::Skip) { + config_set_uint(GetGlobalConfig(), "General", + "SkipUpdateVersion", updateVer); + return; + } + } + + /* ----------------------------------- * + * get working dir */ + + wchar_t cwd[MAX_PATH]; + GetModuleFileNameW(nullptr, cwd, _countof(cwd) - 1); + wchar_t *p = wcsrchr(cwd, '\\'); + if (p) + *p = 0; + + /* ----------------------------------- * + * execute updater */ + + BPtr updateFilePath = + GetConfigPathPtr("obs-studio\\updates\\updater.exe"); + BPtr wUpdateFilePath; + + size_t size = os_utf8_to_wcs_ptr(updateFilePath, 0, &wUpdateFilePath); + if (!size) + throw string("Could not convert updateFilePath to wide"); + + /* note, can't use CreateProcess to launch as admin. */ + SHELLEXECUTEINFO execInfo = {}; + + execInfo.cbSize = sizeof(execInfo); + execInfo.lpFile = wUpdateFilePath; + + string parameters = ""; + if (App()->IsPortableMode()) + parameters += "--portable"; + if (branch != WIN_DEFAULT_BRANCH) + parameters += "--branch=" + branch; + + BPtr lpParameters; + size = os_utf8_to_wcs_ptr(parameters.c_str(), 0, &lpParameters); + if (!size && !parameters.empty()) + throw string("Could not convert parameters to wide"); + + execInfo.lpParameters = lpParameters; + execInfo.lpDirectory = cwd; + execInfo.nShow = SW_SHOWNORMAL; + + if (!ShellExecuteEx(&execInfo)) { + QString msg = QTStr("Updater.FailedToLaunch"); + info(msg, msg); + throw strprintf("Can't launch updater '%s': %d", + updateFilePath.Get(), GetLastError()); + } + + /* force OBS to perform another update check immediately after updating + * in case of issues with the new version */ + config_set_int(GetGlobalConfig(), "General", "LastUpdateCheck", 0); + config_set_int(GetGlobalConfig(), "General", "SkipUpdateVersion", 0); + + QMetaObject::invokeMethod(App()->GetMainWindow(), "close"); + +} catch (string &text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); +} diff --git a/UI/win-update/win-update.hpp b/UI/update/win-update.hpp similarity index 61% rename from UI/win-update/win-update.hpp rename to UI/update/win-update.hpp index a9e944c59..a2b85bd7d 100644 --- a/UI/win-update/win-update.hpp +++ b/UI/update/win-update.hpp @@ -27,29 +27,3 @@ public: { } }; - -class WhatsNewInfoThread : public QThread { - Q_OBJECT - - virtual void run() override; - -signals: - void Result(const QString &text); - -public: - inline WhatsNewInfoThread() {} -}; - -class WhatsNewBrowserInitThread : public QThread { - Q_OBJECT - - QString url; - - virtual void run() override; - -signals: - void Result(const QString &url); - -public: - inline WhatsNewBrowserInitThread(const QString &url_) : url(url_) {} -}; diff --git a/UI/win-update/updater/CMakeLists.txt b/UI/win-update/updater/CMakeLists.txt index 50afe0bc0..64672fae3 100644 --- a/UI/win-update/updater/CMakeLists.txt +++ b/UI/win-update/updater/CMakeLists.txt @@ -25,14 +25,13 @@ target_sources( updater.rc init-hook-files.c updater.manifest - ${CMAKE_SOURCE_DIR}/UI/win-update/win-update-helpers.cpp - ${CMAKE_SOURCE_DIR}/UI/win-update/win-update-helpers.hpp + helpers.cpp + helpers.hpp ${CMAKE_SOURCE_DIR}/deps/json11/json11.hpp ${CMAKE_SOURCE_DIR}/deps/json11/json11.cpp) -target_include_directories( - updater PRIVATE ${CMAKE_SOURCE_DIR}/libobs ${CMAKE_SOURCE_DIR}/UI/win-update - ${CMAKE_SOURCE_DIR}/deps/json11) +target_include_directories(updater PRIVATE ${CMAKE_SOURCE_DIR}/libobs + ${CMAKE_SOURCE_DIR}/deps/json11) target_compile_definitions(updater PRIVATE NOMINMAX "PSAPI_VERSION=2") diff --git a/UI/win-update/updater/helpers.cpp b/UI/win-update/updater/helpers.cpp new file mode 100644 index 000000000..2d0b3d99e --- /dev/null +++ b/UI/win-update/updater/helpers.cpp @@ -0,0 +1,6 @@ +#include "helpers.hpp" + +void FreeProvider(HCRYPTPROV prov) +{ + CryptReleaseContext(prov, 0); +} diff --git a/UI/win-update/win-update-helpers.hpp b/UI/win-update/updater/helpers.hpp similarity index 52% rename from UI/win-update/win-update-helpers.hpp rename to UI/win-update/updater/helpers.hpp index 3f7040c87..2c172758e 100644 --- a/UI/win-update/win-update-helpers.hpp +++ b/UI/win-update/updater/helpers.hpp @@ -37,35 +37,5 @@ public: }; void FreeProvider(HCRYPTPROV prov); -void FreeHash(HCRYPTHASH hash); -void FreeKey(HCRYPTKEY key); using CryptProvider = CustomHandle; -using CryptHash = CustomHandle; -using CryptKey = CustomHandle; - -/* ------------------------------------------------------------------------ */ - -template class LocalPtr { - T *ptr = nullptr; - -public: - inline ~LocalPtr() - { - if (ptr) - LocalFree(ptr); - } - - inline T **operator&() { return &ptr; } - inline operator T() const { return ptr; } - inline T *get() const { return ptr; } - - inline bool operator!() const { return !ptr; } - - inline T *operator->() { return ptr; } -}; - -/* ------------------------------------------------------------------------ */ - -std::string vstrprintf(const char *format, va_list args); -std::string strprintf(const char *format, ...); diff --git a/UI/win-update/updater/updater.hpp b/UI/win-update/updater/updater.hpp index 02d99d2a6..8d1adaec1 100644 --- a/UI/win-update/updater/updater.hpp +++ b/UI/win-update/updater/updater.hpp @@ -40,7 +40,7 @@ #include -#include "../win-update-helpers.hpp" +#include "helpers.hpp" #define BLAKE2_HASH_LENGTH 20 #define BLAKE2_HASH_STR_LENGTH ((BLAKE2_HASH_LENGTH * 2) + 1) diff --git a/UI/win-update/win-update-helpers.cpp b/UI/win-update/win-update-helpers.cpp deleted file mode 100644 index 98cffbc2d..000000000 --- a/UI/win-update/win-update-helpers.cpp +++ /dev/null @@ -1,40 +0,0 @@ -#include "win-update-helpers.hpp" - -void FreeProvider(HCRYPTPROV prov) -{ - CryptReleaseContext(prov, 0); -} - -void FreeHash(HCRYPTHASH hash) -{ - CryptDestroyHash(hash); -} - -void FreeKey(HCRYPTKEY key) -{ - CryptDestroyKey(key); -} - -std::string vstrprintf(const char *format, va_list args) -{ - if (!format) - return std::string(); - - std::string str; - int size = (int)vsnprintf(nullptr, 0, format, args) + 1; - str.resize(size); - vsnprintf(&str[0], size, format, args); - return str; -} - -std::string strprintf(const char *format, ...) -{ - std::string str; - va_list args; - - va_start(args, format); - str = vstrprintf(format, args); - va_end(args); - - return str; -} diff --git a/UI/win-update/win-update.cpp b/UI/win-update/win-update.cpp deleted file mode 100644 index 0a63adf4b..000000000 --- a/UI/win-update/win-update.cpp +++ /dev/null @@ -1,926 +0,0 @@ -#include "win-update-helpers.hpp" -#include "update-window.hpp" -#include "remote-text.hpp" -#include "qt-wrappers.hpp" -#include "win-update.hpp" -#include "obs-app.hpp" - -#include - -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include - -#ifdef BROWSER_AVAILABLE -#include -#endif - -using namespace std; -using namespace json11; - -struct QCef; -extern QCef *cef; - -/* ------------------------------------------------------------------------ */ - -#ifndef WIN_MANIFEST_URL -#define WIN_MANIFEST_URL "https://obsproject.com/update_studio/manifest.json" -#endif - -#ifndef WIN_MANIFEST_BASE_URL -#define WIN_MANIFEST_BASE_URL "https://obsproject.com/update_studio/" -#endif - -#ifndef WIN_BRANCHES_URL -#define WIN_BRANCHES_URL "https://obsproject.com/update_studio/branches.json" -#endif - -#ifndef WIN_DEFAULT_BRANCH -#define WIN_DEFAULT_BRANCH "stable" -#endif - -#ifndef WIN_WHATSNEW_URL -#define WIN_WHATSNEW_URL "https://obsproject.com/update_studio/whatsnew.json" -#endif - -#ifndef WIN_UPDATER_URL -#define WIN_UPDATER_URL "https://obsproject.com/update_studio/updater.exe" -#endif - -static __declspec(thread) HCRYPTPROV provider = 0; - -#pragma pack(push, r1, 1) - -typedef struct { - BLOBHEADER blobheader; - RSAPUBKEY rsapubkey; -} PUBLICKEYHEADER; - -#pragma pack(pop, r1) - -#define BLAKE2_HASH_LENGTH 20 -#define BLAKE2_HASH_STR_LENGTH ((BLAKE2_HASH_LENGTH * 2) + 1) - -// Hard coded 4096 bit RSA public key for obsproject.com in PEM format -static const unsigned char obs_pub[] = { - 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, 0x20, 0x50, - 0x55, 0x42, 0x4c, 0x49, 0x43, 0x20, 0x4b, 0x45, 0x59, 0x2d, 0x2d, 0x2d, - 0x2d, 0x2d, 0x0a, 0x4d, 0x49, 0x49, 0x43, 0x49, 0x6a, 0x41, 0x4e, 0x42, - 0x67, 0x6b, 0x71, 0x68, 0x6b, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, - 0x51, 0x45, 0x46, 0x41, 0x41, 0x4f, 0x43, 0x41, 0x67, 0x38, 0x41, 0x4d, - 0x49, 0x49, 0x43, 0x43, 0x67, 0x4b, 0x43, 0x41, 0x67, 0x45, 0x41, 0x6c, - 0x33, 0x73, 0x76, 0x65, 0x72, 0x77, 0x39, 0x48, 0x51, 0x2b, 0x72, 0x59, - 0x51, 0x4e, 0x6e, 0x39, 0x43, 0x61, 0x37, 0x0a, 0x39, 0x4c, 0x55, 0x36, - 0x32, 0x6e, 0x47, 0x36, 0x4e, 0x6f, 0x7a, 0x45, 0x2f, 0x46, 0x73, 0x49, - 0x56, 0x4e, 0x65, 0x72, 0x2b, 0x57, 0x2f, 0x68, 0x75, 0x65, 0x45, 0x38, - 0x57, 0x51, 0x31, 0x6d, 0x72, 0x46, 0x50, 0x2b, 0x32, 0x79, 0x41, 0x2b, - 0x69, 0x59, 0x52, 0x75, 0x74, 0x59, 0x50, 0x65, 0x45, 0x67, 0x70, 0x78, - 0x74, 0x6f, 0x64, 0x48, 0x68, 0x67, 0x6b, 0x52, 0x34, 0x70, 0x45, 0x4b, - 0x0a, 0x56, 0x6e, 0x72, 0x72, 0x31, 0x38, 0x71, 0x34, 0x73, 0x7a, 0x6c, - 0x76, 0x38, 0x39, 0x51, 0x49, 0x37, 0x74, 0x38, 0x6c, 0x4d, 0x6f, 0x4c, - 0x54, 0x6c, 0x46, 0x2b, 0x74, 0x31, 0x49, 0x52, 0x30, 0x56, 0x34, 0x77, - 0x4a, 0x56, 0x33, 0x34, 0x49, 0x33, 0x43, 0x2b, 0x33, 0x35, 0x39, 0x4b, - 0x69, 0x78, 0x6e, 0x7a, 0x4c, 0x30, 0x42, 0x6c, 0x39, 0x61, 0x6a, 0x2f, - 0x7a, 0x44, 0x63, 0x72, 0x58, 0x0a, 0x57, 0x6c, 0x35, 0x70, 0x48, 0x54, - 0x69, 0x6f, 0x4a, 0x77, 0x59, 0x4f, 0x67, 0x4d, 0x69, 0x42, 0x47, 0x4c, - 0x79, 0x50, 0x65, 0x69, 0x74, 0x4d, 0x46, 0x64, 0x6a, 0x6a, 0x54, 0x49, - 0x70, 0x43, 0x4d, 0x2b, 0x6d, 0x78, 0x54, 0x57, 0x58, 0x43, 0x72, 0x5a, - 0x39, 0x64, 0x50, 0x55, 0x4b, 0x76, 0x5a, 0x74, 0x67, 0x7a, 0x6a, 0x64, - 0x2b, 0x49, 0x7a, 0x6c, 0x48, 0x69, 0x64, 0x48, 0x74, 0x4f, 0x0a, 0x4f, - 0x52, 0x42, 0x4e, 0x35, 0x6d, 0x52, 0x73, 0x38, 0x4c, 0x4e, 0x4f, 0x35, - 0x38, 0x6b, 0x37, 0x39, 0x72, 0x37, 0x37, 0x44, 0x63, 0x67, 0x51, 0x59, - 0x50, 0x4e, 0x69, 0x69, 0x43, 0x74, 0x57, 0x67, 0x43, 0x2b, 0x59, 0x34, - 0x4b, 0x37, 0x75, 0x53, 0x5a, 0x58, 0x33, 0x48, 0x76, 0x65, 0x6f, 0x6d, - 0x32, 0x74, 0x48, 0x62, 0x56, 0x58, 0x79, 0x30, 0x4c, 0x2f, 0x43, 0x6c, - 0x37, 0x66, 0x4d, 0x0a, 0x48, 0x4b, 0x71, 0x66, 0x63, 0x51, 0x47, 0x75, - 0x79, 0x72, 0x76, 0x75, 0x64, 0x34, 0x32, 0x4f, 0x72, 0x57, 0x61, 0x72, - 0x41, 0x73, 0x6e, 0x32, 0x70, 0x32, 0x45, 0x69, 0x36, 0x4b, 0x7a, 0x78, - 0x62, 0x33, 0x47, 0x36, 0x45, 0x53, 0x43, 0x77, 0x31, 0x35, 0x6e, 0x48, - 0x41, 0x67, 0x4c, 0x61, 0x6c, 0x38, 0x7a, 0x53, 0x71, 0x37, 0x2b, 0x72, - 0x61, 0x45, 0x2f, 0x78, 0x6b, 0x4c, 0x70, 0x43, 0x0a, 0x62, 0x59, 0x67, - 0x35, 0x67, 0x6d, 0x59, 0x36, 0x76, 0x62, 0x6d, 0x57, 0x6e, 0x71, 0x39, - 0x64, 0x71, 0x57, 0x72, 0x55, 0x7a, 0x61, 0x71, 0x4f, 0x66, 0x72, 0x5a, - 0x50, 0x67, 0x76, 0x67, 0x47, 0x30, 0x57, 0x76, 0x6b, 0x42, 0x53, 0x68, - 0x66, 0x61, 0x45, 0x4f, 0x42, 0x61, 0x49, 0x55, 0x78, 0x41, 0x33, 0x51, - 0x42, 0x67, 0x7a, 0x41, 0x5a, 0x68, 0x71, 0x65, 0x65, 0x64, 0x46, 0x39, - 0x68, 0x0a, 0x61, 0x66, 0x4d, 0x47, 0x4d, 0x4d, 0x39, 0x71, 0x56, 0x62, - 0x66, 0x77, 0x75, 0x75, 0x7a, 0x4a, 0x32, 0x75, 0x68, 0x2b, 0x49, 0x6e, - 0x61, 0x47, 0x61, 0x65, 0x48, 0x32, 0x63, 0x30, 0x34, 0x6f, 0x56, 0x63, - 0x44, 0x46, 0x66, 0x65, 0x4f, 0x61, 0x44, 0x75, 0x78, 0x52, 0x6a, 0x43, - 0x43, 0x62, 0x71, 0x72, 0x35, 0x73, 0x4c, 0x53, 0x6f, 0x31, 0x43, 0x57, - 0x6f, 0x6b, 0x79, 0x6e, 0x6a, 0x4e, 0x0a, 0x43, 0x42, 0x2b, 0x62, 0x32, - 0x72, 0x51, 0x46, 0x37, 0x44, 0x50, 0x50, 0x62, 0x44, 0x34, 0x73, 0x2f, - 0x6e, 0x54, 0x39, 0x4e, 0x73, 0x63, 0x6b, 0x2f, 0x4e, 0x46, 0x7a, 0x72, - 0x42, 0x58, 0x52, 0x4f, 0x2b, 0x64, 0x71, 0x6b, 0x65, 0x42, 0x77, 0x44, - 0x55, 0x43, 0x76, 0x37, 0x62, 0x5a, 0x67, 0x57, 0x37, 0x4f, 0x78, 0x75, - 0x4f, 0x58, 0x30, 0x37, 0x4c, 0x54, 0x71, 0x66, 0x70, 0x35, 0x73, 0x0a, - 0x4f, 0x65, 0x47, 0x67, 0x75, 0x62, 0x75, 0x62, 0x69, 0x77, 0x59, 0x33, - 0x55, 0x64, 0x48, 0x59, 0x71, 0x2b, 0x4c, 0x39, 0x4a, 0x71, 0x49, 0x53, - 0x47, 0x31, 0x74, 0x4d, 0x34, 0x48, 0x65, 0x4b, 0x6a, 0x61, 0x48, 0x6a, - 0x75, 0x31, 0x4d, 0x44, 0x6a, 0x76, 0x48, 0x5a, 0x32, 0x44, 0x62, 0x6d, - 0x4c, 0x77, 0x55, 0x78, 0x75, 0x59, 0x61, 0x36, 0x4a, 0x5a, 0x44, 0x4b, - 0x57, 0x73, 0x37, 0x72, 0x0a, 0x49, 0x72, 0x64, 0x44, 0x77, 0x78, 0x33, - 0x4a, 0x77, 0x61, 0x63, 0x46, 0x36, 0x36, 0x68, 0x33, 0x59, 0x55, 0x57, - 0x36, 0x74, 0x7a, 0x55, 0x5a, 0x68, 0x7a, 0x74, 0x63, 0x6d, 0x51, 0x65, - 0x70, 0x50, 0x2f, 0x75, 0x37, 0x42, 0x67, 0x47, 0x72, 0x6b, 0x4f, 0x50, - 0x50, 0x70, 0x59, 0x41, 0x30, 0x4e, 0x45, 0x4a, 0x38, 0x30, 0x53, 0x65, - 0x41, 0x78, 0x37, 0x68, 0x69, 0x4e, 0x34, 0x76, 0x61, 0x0a, 0x65, 0x45, - 0x51, 0x4b, 0x6e, 0x52, 0x6e, 0x2b, 0x45, 0x70, 0x42, 0x4e, 0x36, 0x55, - 0x42, 0x61, 0x35, 0x66, 0x37, 0x4c, 0x6f, 0x4b, 0x38, 0x43, 0x41, 0x77, - 0x45, 0x41, 0x41, 0x51, 0x3d, 0x3d, 0x0a, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, - 0x45, 0x4e, 0x44, 0x20, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x20, 0x4b, - 0x45, 0x59, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x0a}; -static const unsigned int obs_pub_len = 800; - -/* ------------------------------------------------------------------------ */ - -static bool QuickWriteFile(const char *file, const void *data, size_t size) -try { - BPtr w_file; - if (os_utf8_to_wcs_ptr(file, 0, &w_file) == 0) - return false; - - WinHandle handle = CreateFileW(w_file, GENERIC_WRITE, 0, nullptr, - CREATE_ALWAYS, FILE_FLAG_WRITE_THROUGH, - nullptr); - - if (handle == INVALID_HANDLE_VALUE) - throw strprintf("Failed to open file '%s': %lu", file, - GetLastError()); - - DWORD written; - if (!WriteFile(handle, data, (DWORD)size, &written, nullptr)) - throw strprintf("Failed to write file '%s': %lu", file, - GetLastError()); - - return true; - -} catch (string &text) { - blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); - return false; -} - -static bool QuickReadFile(const char *file, string &data) -try { - BPtr w_file; - if (os_utf8_to_wcs_ptr(file, 0, &w_file) == 0) - return false; - - WinHandle handle = CreateFileW(w_file, GENERIC_READ, FILE_SHARE_READ, - nullptr, OPEN_EXISTING, 0, nullptr); - - if (handle == INVALID_HANDLE_VALUE) - throw strprintf("Failed to open file '%s': %lu", file, - GetLastError()); - - DWORD size = GetFileSize(handle, nullptr); - data.resize(size); - - DWORD read; - if (!ReadFile(handle, &data[0], size, &read, nullptr)) - throw strprintf("Failed to write file '%s': %lu", file, - GetLastError()); - - return true; - -} catch (string &text) { - blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); - return false; -} - -static void HashToString(const uint8_t *in, char *out) -{ - const char alphabet[] = "0123456789abcdef"; - - for (int i = 0; i != BLAKE2_HASH_LENGTH; ++i) { - out[2 * i] = alphabet[in[i] / 16]; - out[2 * i + 1] = alphabet[in[i] % 16]; - } - - out[BLAKE2_HASH_LENGTH * 2] = 0; -} - -static bool CalculateFileHash(const char *path, uint8_t *hash) -try { - blake2b_state blake2; - if (blake2b_init(&blake2, BLAKE2_HASH_LENGTH) != 0) - return false; - - BPtr w_path; - if (os_utf8_to_wcs_ptr(path, 0, &w_path) == 0) - return false; - - WinHandle handle = CreateFileW(w_path, GENERIC_READ, FILE_SHARE_READ, - nullptr, OPEN_EXISTING, 0, nullptr); - if (handle == INVALID_HANDLE_VALUE) - throw strprintf("Failed to open file '%s': %lu", path, - GetLastError()); - - vector buf; - buf.resize(65536); - - for (;;) { - DWORD read = 0; - if (!ReadFile(handle, buf.data(), (DWORD)buf.size(), &read, - nullptr)) - throw strprintf("Failed to read file '%s': %lu", path, - GetLastError()); - - if (!read) - break; - - if (blake2b_update(&blake2, buf.data(), read) != 0) - return false; - } - - if (blake2b_final(&blake2, hash, BLAKE2_HASH_LENGTH) != 0) - return false; - - return true; - -} catch (string &text) { - blog(LOG_DEBUG, "%s: %s", __FUNCTION__, text.c_str()); - return false; -} - -/* ------------------------------------------------------------------------ */ - -static bool VerifyDigitalSignature(uint8_t *buf, size_t len, uint8_t *sig, - size_t sigLen) -{ - /* ASN of PEM public key */ - BYTE binaryKey[1024]; - DWORD binaryKeyLen = sizeof(binaryKey); - - /* Windows X509 public key info from ASN */ - LocalPtr publicPBLOB; - DWORD iPBLOBSize; - - /* RSA BLOB info from X509 public key */ - LocalPtr rsaPublicBLOB; - DWORD rsaPublicBLOBSize; - - /* Handle to public key */ - CryptKey keyOut; - - /* Handle to hash context */ - CryptHash hash; - - /* Signature in little-endian format */ - vector reversedSig; - - if (!CryptStringToBinaryA((LPCSTR)obs_pub, obs_pub_len, - CRYPT_STRING_BASE64HEADER, binaryKey, - &binaryKeyLen, nullptr, nullptr)) - return false; - - if (!CryptDecodeObjectEx(X509_ASN_ENCODING, X509_PUBLIC_KEY_INFO, - binaryKey, binaryKeyLen, - CRYPT_DECODE_ALLOC_FLAG, nullptr, &publicPBLOB, - &iPBLOBSize)) - return false; - - if (!CryptDecodeObjectEx(X509_ASN_ENCODING, RSA_CSP_PUBLICKEYBLOB, - publicPBLOB->PublicKey.pbData, - publicPBLOB->PublicKey.cbData, - CRYPT_DECODE_ALLOC_FLAG, nullptr, - &rsaPublicBLOB, &rsaPublicBLOBSize)) - return false; - - if (!CryptImportKey(provider, (const BYTE *)rsaPublicBLOB.get(), - rsaPublicBLOBSize, 0, 0, &keyOut)) - return false; - - if (!CryptCreateHash(provider, CALG_SHA_512, 0, 0, &hash)) - return false; - - if (!CryptHashData(hash, buf, (DWORD)len, 0)) - return false; - - /* Windows requires signature in little-endian. Every other crypto - * provider is big-endian of course. */ - reversedSig.resize(sigLen); - for (size_t i = 0; i < sigLen; i++) - reversedSig[i] = sig[sigLen - i - 1]; - - if (!CryptVerifySignature(hash, reversedSig.data(), (DWORD)sigLen, - keyOut, nullptr, 0)) - return false; - - return true; -} - -static inline void HexToByteArray(const char *hexStr, size_t hexLen, - vector &out) -{ - char ptr[3]; - - ptr[2] = 0; - - for (size_t i = 0; i < hexLen; i += 2) { - ptr[0] = hexStr[i]; - ptr[1] = hexStr[i + 1]; - out.push_back((uint8_t)strtoul(ptr, nullptr, 16)); - } -} - -static bool CheckDataSignature(const string &data, const char *name, - const char *hexSig, size_t sigLen) -try { - if (sigLen == 0 || sigLen > 0xFFFF || (sigLen & 1) != 0) - throw strprintf("Missing or invalid signature for %s", name); - - /* Convert TCHAR signature to byte array */ - vector signature; - signature.reserve(sigLen); - HexToByteArray(hexSig, sigLen, signature); - - if (!VerifyDigitalSignature((uint8_t *)data.data(), data.size(), - signature.data(), signature.size())) - throw strprintf("Signature check failed for %s", name); - - return true; - -} catch (string &text) { - blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); - return false; -} - -/* ------------------------------------------------------------------------ */ - -static bool FetchUpdaterModule(const char *url) -try { - long responseCode; - uint8_t updateFileHash[BLAKE2_HASH_LENGTH]; - vector extraHeaders; - - BPtr updateFilePath = - GetConfigPathPtr("obs-studio\\updates\\updater.exe"); - - if (CalculateFileHash(updateFilePath, updateFileHash)) { - char hashString[BLAKE2_HASH_STR_LENGTH]; - HashToString(updateFileHash, hashString); - - string header = "If-None-Match: "; - header += hashString; - extraHeaders.push_back(move(header)); - } - - string signature; - string error; - string data; - - bool success = GetRemoteFile(url, data, error, &responseCode, nullptr, - "", nullptr, extraHeaders, &signature); - - if (!success || (responseCode != 200 && responseCode != 304)) { - if (responseCode == 404) - return false; - - throw strprintf("Could not fetch '%s': %s", url, error.c_str()); - } - - /* A new file must be digitally signed */ - if (responseCode == 200) { - bool valid = CheckDataSignature(data, url, signature.data(), - signature.size()); - if (!valid) - throw string("Invalid updater module signature"); - - if (!QuickWriteFile(updateFilePath, data.data(), data.size())) - return false; - } - - return true; - -} catch (string &text) { - blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); - return false; -} - -/* ------------------------------------------------------------------------ */ - -#if defined(OBS_RELEASE_CANDIDATE) && OBS_RELEASE_CANDIDATE > 0 -#define CUR_VER \ - ((uint64_t)OBS_RELEASE_CANDIDATE_VER << 16ULL | OBS_RELEASE_CANDIDATE \ - << 8ULL) -#define PRE_RELEASE true -#elif OBS_BETA > 0 -#define CUR_VER ((uint64_t)OBS_BETA_VER << 16ULL | OBS_BETA) -#define PRE_RELEASE true -#elif defined(OBS_COMMIT) -#define CUR_VER 1 << 16ULL -#define CUR_COMMIT OBS_COMMIT -#define PRE_RELEASE true -#else -#define CUR_VER ((uint64_t)LIBOBS_API_VER << 16ULL) -#define PRE_RELEASE false -#endif - -#ifndef CUR_COMMIT -#define CUR_COMMIT "00000000" -#endif - -static bool ParseUpdateManifest(const char *manifest, bool *updatesAvailable, - string ¬es_str, uint64_t &updateVer, - string &branch) -try { - - string error; - Json root = Json::parse(manifest, error); - if (!error.empty()) - throw strprintf("Failed reading json string: %s", - error.c_str()); - - if (!root.is_object()) - throw string("Root of manifest is not an object"); - - int major = root["version_major"].int_value(); - int minor = root["version_minor"].int_value(); - int patch = root["version_patch"].int_value(); - int rc = root["rc"].int_value(); - int beta = root["beta"].int_value(); - string commit_hash = root["commit"].string_value(); - - if (major == 0 && commit_hash.empty()) - throw strprintf("Invalid version number: %d.%d.%d", major, - minor, patch); - - const Json ¬es = root["notes"]; - if (!notes.is_string()) - throw string("'notes' value invalid"); - - notes_str = notes.string_value(); - - const Json &packages = root["packages"]; - if (!packages.is_array()) - throw string("'packages' value invalid"); - - uint64_t cur_ver; - uint64_t new_ver; - - if (commit_hash.empty()) { - cur_ver = CUR_VER; - new_ver = MAKE_SEMANTIC_VERSION( - (uint64_t)major, (uint64_t)minor, (uint64_t)patch); - new_ver <<= 16; - /* RC builds are shifted so that rc1 and beta1 versions do not result - * in the same new_ver. */ - if (rc > 0) - new_ver |= (uint64_t)rc << 8; - else if (beta > 0) - new_ver |= (uint64_t)beta; - } else { - /* Test or nightly builds may not have a (valid) version number, - * so compare commit hashes instead. */ - cur_ver = stoul(CUR_COMMIT, nullptr, 16); - new_ver = stoul(commit_hash.substr(0, 8), nullptr, 16); - } - - updateVer = new_ver; - - /* When using a pre-release build or non-default branch we only check if - * the manifest version is different, so that it can be rolled-back. */ - if (branch != WIN_DEFAULT_BRANCH || PRE_RELEASE) - *updatesAvailable = new_ver != cur_ver; - else - *updatesAvailable = new_ver > cur_ver; - - return true; - -} catch (string &text) { - blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); - return false; -} - -#undef CUR_COMMIT -#undef CUR_VER -#undef PRE_RELEASE - -/* ------------------------------------------------------------------------ */ - -void GenerateGUID(string &guid) -{ - BYTE junk[20]; - - if (!CryptGenRandom(provider, sizeof(junk), junk)) - return; - - guid.resize(41); - HashToString(junk, &guid[0]); -} - -string GetProgramGUID() -{ - static mutex m; - lock_guard lock(m); - - /* NOTE: this is an arbitrary random number that we use to count the - * number of unique OBS installations and is not associated with any - * kind of identifiable information */ - const char *pguid = - config_get_string(GetGlobalConfig(), "General", "InstallGUID"); - string guid; - if (pguid) - guid = pguid; - - if (guid.empty()) { - GenerateGUID(guid); - - if (!guid.empty()) - config_set_string(GetGlobalConfig(), "General", - "InstallGUID", guid.c_str()); - } - - return guid; -} - -/* ------------------------------------------------------------------------ */ - -bool GetBranchAndUrl(string &selectedBranch, string &manifestUrl) -{ - const char *config_branch = - config_get_string(GetGlobalConfig(), "General", "UpdateBranch"); - if (!config_branch) - return true; - - bool found = false; - for (const UpdateBranch &branch : App()->GetBranches()) { - if (branch.name != config_branch) - continue; - /* A branch that is found but disabled will just silently fall back to - * the default. But if the branch was removed entirely, the user should - * be warned, so leave this false *only* if the branch was removed. */ - found = true; - - if (branch.is_enabled) { - selectedBranch = branch.name.toStdString(); - if (branch.name != WIN_DEFAULT_BRANCH) { - manifestUrl = WIN_MANIFEST_BASE_URL; - manifestUrl += "manifest_" + - branch.name.toStdString() + - ".json"; - } - } - break; - } - - return found; -} - -/* ------------------------------------------------------------------------ */ - -static bool -FetchAndVerifyFile(const char *name, const char *file, const char *url, - string &text, - const vector &extraHeaders = vector()) -{ - long responseCode; - vector headers; - string error; - string signature; - BYTE fileHash[BLAKE2_HASH_LENGTH]; - bool success; - - BPtr filePath = GetConfigPathPtr(file); - - if (!extraHeaders.empty()) { - headers.insert(headers.end(), extraHeaders.begin(), - extraHeaders.end()); - } - - /* ----------------------------------- * - * avoid downloading json again */ - - if (CalculateFileHash(filePath, fileHash)) { - char hashString[BLAKE2_HASH_STR_LENGTH]; - HashToString(fileHash, hashString); - - string header = "If-None-Match: "; - header += hashString; - headers.push_back(move(header)); - } - - /* ----------------------------------- * - * get current install GUID */ - - string guid = GetProgramGUID(); - - if (!guid.empty()) { - string header = "X-OBS2-GUID: "; - header += guid; - headers.push_back(move(header)); - } - - /* ----------------------------------- * - * get json from server */ - - success = GetRemoteFile(url, text, error, &responseCode, nullptr, "", - nullptr, headers, &signature); - - if (!success || (responseCode != 200 && responseCode != 304)) { - if (responseCode == 404) - return false; - - throw strprintf("Failed to fetch %s file: %s", name, - error.c_str()); - } - - /* ----------------------------------- * - * verify file signature */ - - if (responseCode == 200) { - success = CheckDataSignature(text, name, signature.data(), - signature.size()); - if (!success) - throw strprintf("Invalid %s signature", name); - } - - /* ----------------------------------- * - * write or load json */ - - if (responseCode == 200) { - if (!QuickWriteFile(filePath, text.data(), text.size())) - throw strprintf("Could not write file '%s'", - filePath.Get()); - } else { - if (!QuickReadFile(filePath, text)) - throw strprintf("Could not read file '%s'", - filePath.Get()); - } - - /* ----------------------------------- * - * success */ - return true; -} - -/* ------------------------------------------------------------------------ */ - -void AutoUpdateThread::infoMsg(const QString &title, const QString &text) -{ - OBSMessageBox::information(App()->GetMainWindow(), title, text); -} - -void AutoUpdateThread::info(const QString &title, const QString &text) -{ - QMetaObject::invokeMethod(this, "infoMsg", Qt::BlockingQueuedConnection, - Q_ARG(QString, title), Q_ARG(QString, text)); -} - -int AutoUpdateThread::queryUpdateSlot(bool localManualUpdate, - const QString &text) -{ - OBSUpdate updateDlg(App()->GetMainWindow(), localManualUpdate, text); - return updateDlg.exec(); -} - -int AutoUpdateThread::queryUpdate(bool localManualUpdate, const char *text_utf8) -{ - int ret = OBSUpdate::No; - QString text = text_utf8; - QMetaObject::invokeMethod(this, "queryUpdateSlot", - Qt::BlockingQueuedConnection, - Q_RETURN_ARG(int, ret), - Q_ARG(bool, localManualUpdate), - Q_ARG(QString, text)); - return ret; -} - -bool AutoUpdateThread::queryRepairSlot() -{ - QMessageBox::StandardButton res = OBSMessageBox::question( - App()->GetMainWindow(), QTStr("Updater.RepairConfirm.Title"), - QTStr("Updater.RepairConfirm.Text"), - QMessageBox::Yes | QMessageBox::Cancel); - - return res == QMessageBox::Yes; -} - -bool AutoUpdateThread::queryRepair() -{ - bool ret = false; - QMetaObject::invokeMethod(this, "queryRepairSlot", - Qt::BlockingQueuedConnection, - Q_RETURN_ARG(bool, ret)); - return ret; -} - -void AutoUpdateThread::run() -try { - string text; - string branch = WIN_DEFAULT_BRANCH; - string manifestUrl = WIN_MANIFEST_URL; - vector extraHeaders; - bool updatesAvailable = false; - CryptProvider localProvider; - - struct FinishedTrigger { - inline ~FinishedTrigger() - { - QMetaObject::invokeMethod(App()->GetMainWindow(), - "updateCheckFinished"); - } - } finishedTrigger; - - /* ----------------------------------- * - * create signature provider */ - - if (!CryptAcquireContext(&localProvider, nullptr, MS_ENH_RSA_AES_PROV, - PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) - throw strprintf("CryptAcquireContext failed: %lu", - GetLastError()); - - provider = localProvider; - - /* ----------------------------------- * - * get branches from server */ - - if (FetchAndVerifyFile("branches", "obs-studio\\updates\\branches.json", - WIN_BRANCHES_URL, text)) - App()->SetBranchData(text); - - /* ----------------------------------- * - * get branches from server */ - - if (!GetBranchAndUrl(branch, manifestUrl)) { - config_set_string(GetGlobalConfig(), "General", "UpdateBranch", - WIN_DEFAULT_BRANCH); - info(QTStr("Updater.BranchNotFound.Title"), - QTStr("Updater.BranchNotFound.Text")); - } - - /* allow server to know if this was a manual update check in case - * we want to allow people to bypass a configured rollout rate */ - if (manualUpdate) - extraHeaders.emplace_back("X-OBS2-ManualUpdate: 1"); - - /* ----------------------------------- * - * get manifest from server */ - - text.clear(); - if (!FetchAndVerifyFile("manifest", - "obs-studio\\updates\\manifest.json", - manifestUrl.c_str(), text, extraHeaders)) - return; - - /* ----------------------------------- * - * check manifest for update */ - - string notes; - uint64_t updateVer = 0; - - if (!ParseUpdateManifest(text.c_str(), &updatesAvailable, notes, - updateVer, branch)) - throw string("Failed to parse manifest"); - - if (!updatesAvailable && !repairMode) { - if (manualUpdate) - info(QTStr("Updater.NoUpdatesAvailable.Title"), - QTStr("Updater.NoUpdatesAvailable.Text")); - return; - } else if (updatesAvailable && repairMode) { - info(QTStr("Updater.RepairButUpdatesAvailable.Title"), - QTStr("Updater.RepairButUpdatesAvailable.Text")); - return; - } - - /* ----------------------------------- * - * skip this version if set to skip */ - - uint64_t skipUpdateVer = config_get_uint(GetGlobalConfig(), "General", - "SkipUpdateVersion"); - if (!manualUpdate && updateVer == skipUpdateVer && !repairMode) - return; - - /* ----------------------------------- * - * fetch updater module */ - - if (!FetchUpdaterModule(WIN_UPDATER_URL)) - return; - - /* ----------------------------------- * - * query user for update */ - - if (repairMode) { - if (!queryRepair()) - return; - } else { - int queryResult = queryUpdate(manualUpdate, notes.c_str()); - - if (queryResult == OBSUpdate::No) { - if (!manualUpdate) { - long long t = (long long)time(nullptr); - config_set_int(GetGlobalConfig(), "General", - "LastUpdateCheck", t); - } - return; - - } else if (queryResult == OBSUpdate::Skip) { - config_set_uint(GetGlobalConfig(), "General", - "SkipUpdateVersion", updateVer); - return; - } - } - - /* ----------------------------------- * - * get working dir */ - - wchar_t cwd[MAX_PATH]; - GetModuleFileNameW(nullptr, cwd, _countof(cwd) - 1); - wchar_t *p = wcsrchr(cwd, '\\'); - if (p) - *p = 0; - - /* ----------------------------------- * - * execute updater */ - - BPtr updateFilePath = - GetConfigPathPtr("obs-studio\\updates\\updater.exe"); - BPtr wUpdateFilePath; - - size_t size = os_utf8_to_wcs_ptr(updateFilePath, 0, &wUpdateFilePath); - if (!size) - throw string("Could not convert updateFilePath to wide"); - - /* note, can't use CreateProcess to launch as admin. */ - SHELLEXECUTEINFO execInfo = {}; - - execInfo.cbSize = sizeof(execInfo); - execInfo.lpFile = wUpdateFilePath; - - string parameters = ""; - if (App()->IsPortableMode()) - parameters += "--portable"; - if (branch != WIN_DEFAULT_BRANCH) - parameters += "--branch=" + branch; - - BPtr lpParameters; - size = os_utf8_to_wcs_ptr(parameters.c_str(), 0, &lpParameters); - if (!size && !parameters.empty()) - throw string("Could not convert parameters to wide"); - - execInfo.lpParameters = lpParameters; - execInfo.lpDirectory = cwd; - execInfo.nShow = SW_SHOWNORMAL; - - if (!ShellExecuteEx(&execInfo)) { - QString msg = QTStr("Updater.FailedToLaunch"); - info(msg, msg); - throw strprintf("Can't launch updater '%s': %d", - updateFilePath.Get(), GetLastError()); - } - - /* force OBS to perform another update check immediately after updating - * in case of issues with the new version */ - config_set_int(GetGlobalConfig(), "General", "LastUpdateCheck", 0); - config_set_int(GetGlobalConfig(), "General", "SkipUpdateVersion", 0); - - QMetaObject::invokeMethod(App()->GetMainWindow(), "close"); - -} catch (string &text) { - blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); -} - -/* ------------------------------------------------------------------------ */ - -void WhatsNewInfoThread::run() -try { - string text; - CryptProvider localProvider; - /* ----------------------------------- * - * create signature provider */ - - if (!CryptAcquireContext(&localProvider, nullptr, MS_ENH_RSA_AES_PROV, - PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) - throw strprintf("CryptAcquireContext failed: %lu", - GetLastError()); - - provider = localProvider; - - if (FetchAndVerifyFile("whatsnew", "obs-studio\\updates\\whatsnew.json", - WIN_WHATSNEW_URL, text)) { - emit Result(QString::fromStdString(text)); - } - -} catch (string &text) { - blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); -} - -/* ------------------------------------------------------------------------ */ - -void WhatsNewBrowserInitThread::run() -{ -#ifdef BROWSER_AVAILABLE - cef->wait_for_browser_init(); -#endif - emit Result(url); -} diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 743dfcd56..bd1d30686 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -77,12 +77,13 @@ #include #ifdef _WIN32 -#include "win-update/win-update.hpp" +#include "update/win-update.hpp" +#include "update/shared-update.hpp" #include "windows.h" #endif #if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "nix-update/nix-update.hpp" +#include "update/shared-update.hpp" #endif #include "ui_OBSBasic.h"