mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-31 02:49:35 +08:00
[fix] use WinHTTP and Windows security checks for installer updates
This commit is contained in:
@@ -92,8 +92,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate or update version.json
|
- name: Generate or update version.json
|
||||||
run: |
|
run: |
|
||||||
# If version.json exists, try to preserve downloads section
|
# Preserve download URLs only for this release, never an older version.
|
||||||
if [ -f version.json ] && jq -e '.downloads' version.json > /dev/null 2>&1; then
|
if [ -f version.json ] && jq -e --arg tag "${{ steps.version.outputs.TAG_NAME }}" \
|
||||||
|
'.tagName == $tag and (.downloads | type == "object")' version.json > /dev/null 2>&1; then
|
||||||
EXISTING_DOWNLOADS=$(jq -c '.downloads' version.json)
|
EXISTING_DOWNLOADS=$(jq -c '.downloads' version.json)
|
||||||
if [ "$EXISTING_DOWNLOADS" != "null" ] && [ "$EXISTING_DOWNLOADS" != "{}" ]; then
|
if [ "$EXISTING_DOWNLOADS" != "null" ] && [ "$EXISTING_DOWNLOADS" != "{}" ]; then
|
||||||
DOWNLOADS_JSON="$EXISTING_DOWNLOADS"
|
DOWNLOADS_JSON="$EXISTING_DOWNLOADS"
|
||||||
|
|||||||
@@ -3286,9 +3286,12 @@ void GuiApplication::SyncWindowsUpdate() {
|
|||||||
}
|
}
|
||||||
const State state = windows_updater_.GetState();
|
const State state = windows_updater_.GetState();
|
||||||
const bool downloading = state == State::Downloading;
|
const bool downloading = state == State::Downloading;
|
||||||
ui_->main->set_update_downloading(downloading || state == State::Launching);
|
ui_->main->set_update_downloading(downloading || state == State::Checking ||
|
||||||
|
state == State::Launching);
|
||||||
|
ui_->main->set_update_security_blocked(state == State::SecurityBlocked);
|
||||||
ui_->main->set_update_failed(state == State::Failed ||
|
ui_->main->set_update_failed(state == State::Failed ||
|
||||||
state == State::LaunchFailed);
|
state == State::LaunchFailed ||
|
||||||
|
state == State::SecurityBlocked);
|
||||||
const int language = localization_language_index_;
|
const int language = localization_language_index_;
|
||||||
std::string status;
|
std::string status;
|
||||||
if (downloading) {
|
if (downloading) {
|
||||||
@@ -3307,6 +3310,11 @@ void GuiApplication::SyncWindowsUpdate() {
|
|||||||
status = localization::update_download_failed[language];
|
status = localization::update_download_failed[language];
|
||||||
} else if (state == State::LaunchFailed) {
|
} else if (state == State::LaunchFailed) {
|
||||||
status = localization::update_launch_failed[language];
|
status = localization::update_launch_failed[language];
|
||||||
|
} else if (state == State::Checking) {
|
||||||
|
status = localization::update_checking[language];
|
||||||
|
ui_->main->set_update_progress(1.0f);
|
||||||
|
} else if (state == State::SecurityBlocked) {
|
||||||
|
status = localization::update_security_blocked[language];
|
||||||
} else if (state == State::Launching) {
|
} else if (state == State::Launching) {
|
||||||
status = localization::update_launching[language];
|
status = localization::update_launching[language];
|
||||||
} else if (state == State::Launched) {
|
} else if (state == State::Launched) {
|
||||||
|
|||||||
@@ -186,6 +186,12 @@ struct TranslationRow {
|
|||||||
u8"Доступна новая версия") \
|
u8"Доступна новая версия") \
|
||||||
X(update_downloading, u8"正在下载安装包…", "Downloading installer…", \
|
X(update_downloading, u8"正在下载安装包…", "Downloading installer…", \
|
||||||
u8"Загрузка установщика…") \
|
u8"Загрузка установщика…") \
|
||||||
|
X(update_checking, u8"正在进行 Windows 安全检查…", \
|
||||||
|
"Running Windows security checks…", u8"Проверка безопасности Windows…") \
|
||||||
|
X(update_security_blocked, \
|
||||||
|
u8"Windows 安全检查未通过,请查看安全软件日志并核实发布者。", \
|
||||||
|
"Windows security checks failed. Review security logs and verify the publisher.", \
|
||||||
|
u8"Проверка Windows не пройдена. Проверьте журнал защиты и издателя.") \
|
||||||
X(update_launching, u8"正在打开安装包,请确认管理员权限请求…", \
|
X(update_launching, u8"正在打开安装包,请确认管理员权限请求…", \
|
||||||
"Opening installer. Please confirm the administrator permission request…", \
|
"Opening installer. Please confirm the administrator permission request…", \
|
||||||
u8"Запуск установщика. Подтвердите запрос прав администратора…") \
|
u8"Запуск установщика. Подтвердите запрос прав администратора…") \
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ export component MainWindow inherits Window {
|
|||||||
in property <bool> update-available: false;
|
in property <bool> update-available: false;
|
||||||
in property <bool> update-downloading: false;
|
in property <bool> update-downloading: false;
|
||||||
in property <bool> update-failed: false;
|
in property <bool> update-failed: false;
|
||||||
|
in property <bool> update-security-blocked: false;
|
||||||
in property <float> update-progress: 0;
|
in property <float> update-progress: 0;
|
||||||
in property <string> update-status: "";
|
in property <string> update-status: "";
|
||||||
in property <string> current-version: "";
|
in property <string> current-version: "";
|
||||||
@@ -1696,7 +1697,7 @@ export component MainWindow inherits Window {
|
|||||||
background: #334fd1;
|
background: #334fd1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CompactButton { x: root.compact-language ? 163px : 130px; y: 309px; width: root.compact-language ? 34px : 70px; primary: true; text: UiStrings.download; enabled: !root.update-downloading; clicked => { root.start-update(); } }
|
CompactButton { x: root.compact-language ? 163px : 130px; y: 309px; width: root.compact-language ? 34px : 70px; primary: true; text: UiStrings.download; enabled: !root.update-downloading && !root.update-security-blocked; clicked => { root.start-update(); } }
|
||||||
CompactButton { x: root.compact-language ? 204px : 207px; y: 309px; width: root.compact-language ? 34px : 56px; text: UiStrings.later; clicked => { root.cancel-update(); } }
|
CompactButton { x: root.compact-language ? 204px : 207px; y: 309px; width: root.compact-language ? 34px : 56px; text: UiStrings.later; clicked => { root.cancel-update(); } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
#include "windows_installer.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <shobjidl.h>
|
||||||
|
#include <winhttp.h>
|
||||||
|
#include <wrl/client.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <fstream>
|
||||||
|
#include <memory>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
|
#include "rd_log.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr uint64_t kMaxInstallerSize = 1024ull * 1024 * 1024;
|
||||||
|
// Stable identity used only for Windows Attachment Services policy/UI.
|
||||||
|
constexpr GUID kAttachmentClient = {
|
||||||
|
0x72d17a6b,
|
||||||
|
0xeac1,
|
||||||
|
0x4816,
|
||||||
|
{0x91, 0x40, 0x33, 0x1b, 0xa7, 0x76, 0x22, 0xcd}};
|
||||||
|
|
||||||
|
void CheckWin32(BOOL success, const char* operation) {
|
||||||
|
if (!success) {
|
||||||
|
throw std::system_error(static_cast<int>(GetLastError()),
|
||||||
|
std::system_category(), operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InternetCloser {
|
||||||
|
void operator()(HINTERNET handle) const { WinHttpCloseHandle(handle); }
|
||||||
|
};
|
||||||
|
using InternetHandle = std::unique_ptr<void, InternetCloser>;
|
||||||
|
|
||||||
|
struct PartialFile {
|
||||||
|
std::filesystem::path path;
|
||||||
|
~PartialFile() {
|
||||||
|
std::error_code error;
|
||||||
|
std::filesystem::remove(path, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
bool IsWindowsExecutable(const std::filesystem::path& path) {
|
||||||
|
std::ifstream input(path, std::ios::binary);
|
||||||
|
std::array<unsigned char, 64> header{};
|
||||||
|
input.read(reinterpret_cast<char*>(header.data()), header.size());
|
||||||
|
if (!input || header[0] != 'M' || header[1] != 'Z') return false;
|
||||||
|
const uint32_t pe_offset =
|
||||||
|
uint32_t(header[60]) | (uint32_t(header[61]) << 8) |
|
||||||
|
(uint32_t(header[62]) << 16) | (uint32_t(header[63]) << 24);
|
||||||
|
if (pe_offset < header.size()) return false;
|
||||||
|
input.seekg(pe_offset);
|
||||||
|
std::array<char, 4> signature{};
|
||||||
|
input.read(signature.data(), signature.size());
|
||||||
|
return input && signature == std::array<char, 4>{'P', 'E', 0, 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t ContentLength(HINTERNET request) {
|
||||||
|
std::array<wchar_t, 32> value{};
|
||||||
|
DWORD bytes = static_cast<DWORD>(sizeof(value));
|
||||||
|
if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_CONTENT_LENGTH,
|
||||||
|
WINHTTP_HEADER_NAME_BY_INDEX, value.data(), &bytes,
|
||||||
|
WINHTTP_NO_HEADER_INDEX)) {
|
||||||
|
if (GetLastError() == ERROR_WINHTTP_HEADER_NOT_FOUND) return 0;
|
||||||
|
CheckWin32(FALSE, "WinHttpQueryHeaders(Content-Length)");
|
||||||
|
}
|
||||||
|
const std::wstring length(value.data());
|
||||||
|
if (length.empty() ||
|
||||||
|
length.find_first_not_of(L"0123456789") != std::wstring::npos) {
|
||||||
|
throw std::runtime_error("Invalid installer content length");
|
||||||
|
}
|
||||||
|
return std::stoull(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComApartment {
|
||||||
|
public:
|
||||||
|
ComApartment() : status_(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED)) {}
|
||||||
|
~ComApartment() {
|
||||||
|
if (SUCCEEDED(status_)) CoUninitialize();
|
||||||
|
}
|
||||||
|
bool Ready() const {
|
||||||
|
return SUCCEEDED(status_) || status_ == RPC_E_CHANGED_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
HRESULT status_;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool HandleAttachment(const std::filesystem::path& installer,
|
||||||
|
const std::string& source, bool execute) {
|
||||||
|
ComApartment apartment;
|
||||||
|
if (!apartment.Ready()) {
|
||||||
|
LOG_WARN("Cannot initialize Windows Attachment Services");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Microsoft::WRL::ComPtr<IAttachmentExecute> attachment;
|
||||||
|
HRESULT result =
|
||||||
|
CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_INPROC_SERVER,
|
||||||
|
IID_PPV_ARGS(&attachment));
|
||||||
|
const std::wstring source_url(source.begin(), source.end());
|
||||||
|
if (SUCCEEDED(result)) result = attachment->SetClientGuid(kAttachmentClient);
|
||||||
|
if (SUCCEEDED(result))
|
||||||
|
result = attachment->SetClientTitle(L"CrossDesk Update");
|
||||||
|
if (SUCCEEDED(result)) result = attachment->SetLocalPath(installer.c_str());
|
||||||
|
if (SUCCEEDED(result)) result = attachment->SetSource(source_url.c_str());
|
||||||
|
if (SUCCEEDED(result)) {
|
||||||
|
if (execute) {
|
||||||
|
HANDLE process = nullptr;
|
||||||
|
// Execute applies attachment policy, scanning, and any required prompts.
|
||||||
|
// The installer's manifest, rather than this process, requests elevation.
|
||||||
|
result = attachment->Execute(GetActiveWindow(), L"open", &process);
|
||||||
|
if (process) CloseHandle(process);
|
||||||
|
} else {
|
||||||
|
// Save records the source zone and invokes installed trust/AV services.
|
||||||
|
result = attachment->Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (FAILED(result)) {
|
||||||
|
LOG_WARN("Windows Attachment Services {} failed/blocked, HRESULT=0x{:08X}",
|
||||||
|
execute ? "execute" : "save", static_cast<uint32_t>(result));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool DownloadWindowsInstaller(
|
||||||
|
const InstallerDownload& download, const std::filesystem::path& destination,
|
||||||
|
const std::function<bool(uint64_t, uint64_t)>& progress) {
|
||||||
|
auto partial_path = destination;
|
||||||
|
partial_path += ".part";
|
||||||
|
PartialFile partial{partial_path};
|
||||||
|
try {
|
||||||
|
if (!progress(0, 0)) return false;
|
||||||
|
const std::string agent = "CrossDesk/" CROSSDESK_VERSION " WindowsUpdater";
|
||||||
|
const std::wstring user_agent(agent.begin(), agent.end());
|
||||||
|
InternetHandle session(
|
||||||
|
WinHttpOpen(user_agent.c_str(), WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
|
||||||
|
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0));
|
||||||
|
CheckWin32(session != nullptr, "WinHttpOpen");
|
||||||
|
CheckWin32(WinHttpSetTimeouts(session.get(), 5000, 5000, 5000, 5000),
|
||||||
|
"WinHttpSetTimeouts");
|
||||||
|
InternetHandle connection(WinHttpConnect(session.get(),
|
||||||
|
L"downloads.crossdesk.cn",
|
||||||
|
INTERNET_DEFAULT_HTTPS_PORT, 0));
|
||||||
|
CheckWin32(connection != nullptr, "WinHttpConnect");
|
||||||
|
const std::wstring path(download.path.begin(), download.path.end());
|
||||||
|
InternetHandle request(WinHttpOpenRequest(
|
||||||
|
connection.get(), L"GET", path.c_str(), nullptr, WINHTTP_NO_REFERER,
|
||||||
|
WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE));
|
||||||
|
CheckWin32(request != nullptr, "WinHttpOpenRequest");
|
||||||
|
DWORD disabled = WINHTTP_DISABLE_REDIRECTS;
|
||||||
|
CheckWin32(WinHttpSetOption(request.get(), WINHTTP_OPTION_DISABLE_FEATURE,
|
||||||
|
&disabled, sizeof(disabled)),
|
||||||
|
"Disable redirects");
|
||||||
|
// Use Windows certificate validation and configured proxies as-is. Do not
|
||||||
|
// ignore TLS errors, spoof a browser, or retry through alternative hosts.
|
||||||
|
CheckWin32(WinHttpSendRequest(request.get(), WINHTTP_NO_ADDITIONAL_HEADERS,
|
||||||
|
0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0),
|
||||||
|
"WinHttpSendRequest");
|
||||||
|
CheckWin32(WinHttpReceiveResponse(request.get(), nullptr),
|
||||||
|
"WinHttpReceiveResponse");
|
||||||
|
DWORD status = 0;
|
||||||
|
DWORD status_size = sizeof(status);
|
||||||
|
CheckWin32(WinHttpQueryHeaders(
|
||||||
|
request.get(),
|
||||||
|
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||||
|
WINHTTP_HEADER_NAME_BY_INDEX, &status, &status_size,
|
||||||
|
WINHTTP_NO_HEADER_INDEX),
|
||||||
|
"WinHttpQueryHeaders(status)");
|
||||||
|
if (status != 200) {
|
||||||
|
LOG_WARN("Windows installer download returned HTTP {}", status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const uint64_t total = ContentLength(request.get());
|
||||||
|
if (total > kMaxInstallerSize || !progress(0, total)) return false;
|
||||||
|
std::ofstream output(partial.path, std::ios::binary | std::ios::trunc);
|
||||||
|
if (!output)
|
||||||
|
throw std::runtime_error("Cannot create installer download file");
|
||||||
|
uint64_t received = 0;
|
||||||
|
std::array<unsigned char, 64 * 1024> buffer{};
|
||||||
|
while (progress(received, total)) {
|
||||||
|
DWORD count = 0;
|
||||||
|
CheckWin32(WinHttpReadData(request.get(), buffer.data(),
|
||||||
|
static_cast<DWORD>(buffer.size()), &count),
|
||||||
|
"WinHttpReadData");
|
||||||
|
if (count == 0) break;
|
||||||
|
received += count;
|
||||||
|
if (received > kMaxInstallerSize || (total > 0 && received > total))
|
||||||
|
return false;
|
||||||
|
output.write(reinterpret_cast<const char*>(buffer.data()), count);
|
||||||
|
if (!output)
|
||||||
|
throw std::runtime_error("Cannot write installer download file");
|
||||||
|
}
|
||||||
|
output.close();
|
||||||
|
if (output.fail() || received == 0 || (total > 0 && received != total) ||
|
||||||
|
!progress(received, total) || !IsWindowsExecutable(partial.path))
|
||||||
|
return false;
|
||||||
|
LOG_INFO("Windows installer downloaded, bytes={}", received);
|
||||||
|
std::filesystem::rename(partial.path, destination);
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
LOG_WARN("Windows installer download failed: {}", error.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CheckWindowsInstaller(const std::filesystem::path& installer,
|
||||||
|
const std::string& source) {
|
||||||
|
return HandleAttachment(installer, source, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LaunchWindowsInstaller(const std::filesystem::path& installer,
|
||||||
|
const std::string& source) {
|
||||||
|
return HandleAttachment(installer, source, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* @Author: DI JUNKUN
|
||||||
|
* @Date: 2026-09-09
|
||||||
|
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _WINDOWS_INSTALLER_H_
|
||||||
|
#define _WINDOWS_INSTALLER_H_
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "installer_download.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
bool DownloadWindowsInstaller(
|
||||||
|
const InstallerDownload& download, const std::filesystem::path& destination,
|
||||||
|
const std::function<bool(uint64_t, uint64_t)>& progress);
|
||||||
|
|
||||||
|
// Attachment Services records the Internet source and applies Windows policy
|
||||||
|
// and installed antivirus checks. Failure must never fall back to direct
|
||||||
|
// launch.
|
||||||
|
bool CheckWindowsInstaller(const std::filesystem::path& installer,
|
||||||
|
const std::string& source);
|
||||||
|
bool LaunchWindowsInstaller(const std::filesystem::path& installer,
|
||||||
|
const std::string& source);
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
#include "windows_updater.h"
|
#include "windows_updater.h"
|
||||||
|
|
||||||
#include <httplib.h>
|
#include <shlobj.h>
|
||||||
#include <shellapi.h>
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
#include <random>
|
#include <random>
|
||||||
@@ -9,6 +8,7 @@
|
|||||||
|
|
||||||
#include "installer_download.h"
|
#include "installer_download.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
|
#include "windows_installer.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ WindowsUpdater::~WindowsUpdater() {
|
|||||||
|
|
||||||
void WindowsUpdater::RemoveDownload() {
|
void WindowsUpdater::RemoveDownload() {
|
||||||
// A running NSIS installer still needs its source file. Leave that one in
|
// A running NSIS installer still needs its source file. Leave that one in
|
||||||
// the user's temporary directory; clean up cancelled/failed attempts.
|
// the user's update cache; clean up cancelled/failed attempts.
|
||||||
if (installer_.empty() || installer_launched_) return;
|
if (installer_.empty() || installer_launched_) return;
|
||||||
std::error_code error;
|
std::error_code error;
|
||||||
std::filesystem::remove(installer_, error);
|
std::filesystem::remove(installer_, error);
|
||||||
@@ -29,7 +29,9 @@ void WindowsUpdater::RemoveDownload() {
|
|||||||
|
|
||||||
void WindowsUpdater::Start(const nlohmann::json& metadata) {
|
void WindowsUpdater::Start(const nlohmann::json& metadata) {
|
||||||
const State state = GetState();
|
const State state = GetState();
|
||||||
if (state == State::Downloading || state == State::Launching) return;
|
if (state == State::Downloading || state == State::Checking ||
|
||||||
|
state == State::Launching || state == State::SecurityBlocked)
|
||||||
|
return;
|
||||||
if (state == State::Ready || state == State::LaunchFailed ||
|
if (state == State::Ready || state == State::LaunchFailed ||
|
||||||
state == State::Launched) {
|
state == State::Launched) {
|
||||||
cancelled_ = false;
|
cancelled_ = false;
|
||||||
@@ -51,11 +53,18 @@ void WindowsUpdater::Start(const nlohmann::json& metadata) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
std::random_device random;
|
std::random_device random;
|
||||||
const auto temp = std::filesystem::temp_directory_path();
|
PWSTR local_app_data = nullptr;
|
||||||
|
const HRESULT path_result = SHGetKnownFolderPath(
|
||||||
|
FOLDERID_LocalAppData, KF_FLAG_CREATE, nullptr, &local_app_data);
|
||||||
|
if (FAILED(path_result))
|
||||||
|
throw std::runtime_error("Cannot locate LocalAppData");
|
||||||
|
const std::filesystem::path data_path(local_app_data);
|
||||||
|
CoTaskMemFree(local_app_data);
|
||||||
|
const auto updates = data_path / L"CrossDesk" / L"Updates";
|
||||||
|
std::filesystem::create_directories(updates);
|
||||||
for (int attempt = 0; attempt < 10; ++attempt) {
|
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||||
const auto directory =
|
const auto directory = updates / ("download-" + std::to_string(random()) +
|
||||||
temp / ("CrossDesk-update-" + std::to_string(random()) + "-" +
|
"-" + std::to_string(random()));
|
||||||
std::to_string(random()));
|
|
||||||
if (std::filesystem::create_directory(directory)) {
|
if (std::filesystem::create_directory(directory)) {
|
||||||
installer_ = directory / download->filename;
|
installer_ = directory / download->filename;
|
||||||
break;
|
break;
|
||||||
@@ -63,20 +72,23 @@ void WindowsUpdater::Start(const nlohmann::json& metadata) {
|
|||||||
}
|
}
|
||||||
if (installer_.empty())
|
if (installer_.empty())
|
||||||
throw std::runtime_error("Cannot create update directory");
|
throw std::runtime_error("Cannot create update directory");
|
||||||
client_ = std::make_shared<httplib::Client>(download->origin);
|
source_url_ = download->origin + download->path;
|
||||||
client_->set_connection_timeout(5);
|
|
||||||
client_->set_read_timeout(5);
|
|
||||||
client_->set_write_timeout(5);
|
|
||||||
state_ = State::Downloading;
|
state_ = State::Downloading;
|
||||||
worker_ = std::thread([this, path = download->path] {
|
worker_ = std::thread([this, download = *download] {
|
||||||
const bool success =
|
const bool success = DownloadWindowsInstaller(
|
||||||
DownloadInstaller(*client_, path, installer_,
|
download, installer_, [this](uint64_t received, uint64_t total) {
|
||||||
[this](uint64_t received, uint64_t total) {
|
downloaded_ = received;
|
||||||
downloaded_ = received;
|
total_ = total;
|
||||||
total_ = total;
|
return !cancelled_.load();
|
||||||
return !cancelled_.load();
|
});
|
||||||
});
|
if (success && !cancelled_) {
|
||||||
state_ = success ? State::Ready : State::Failed;
|
state_ = State::Checking;
|
||||||
|
state_ = CheckWindowsInstaller(installer_, source_url_)
|
||||||
|
? State::Ready
|
||||||
|
: State::SecurityBlocked;
|
||||||
|
} else {
|
||||||
|
state_ = State::Failed;
|
||||||
|
}
|
||||||
if (cancelled_) state_ = State::Idle;
|
if (cancelled_) state_ = State::Idle;
|
||||||
});
|
});
|
||||||
} catch (const std::exception& error) {
|
} catch (const std::exception& error) {
|
||||||
@@ -87,7 +99,6 @@ void WindowsUpdater::Start(const nlohmann::json& metadata) {
|
|||||||
|
|
||||||
void WindowsUpdater::Cancel() {
|
void WindowsUpdater::Cancel() {
|
||||||
cancelled_ = true;
|
cancelled_ = true;
|
||||||
if (client_) client_->stop();
|
|
||||||
if (GetState() == State::Ready) state_ = State::Idle;
|
if (GetState() == State::Ready) state_ = State::Idle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,22 +109,11 @@ void WindowsUpdater::Launch() {
|
|||||||
state != State::Launched)
|
state != State::Launched)
|
||||||
return;
|
return;
|
||||||
if (worker_.joinable()) worker_.join();
|
if (worker_.joinable()) worker_.join();
|
||||||
const std::wstring executable = installer_.wstring();
|
|
||||||
const std::wstring directory = installer_.parent_path().wstring();
|
|
||||||
state_ = State::Launching;
|
state_ = State::Launching;
|
||||||
SHELLEXECUTEINFOW info{};
|
if (!LaunchWindowsInstaller(installer_, source_url_)) {
|
||||||
info.cbSize = sizeof(info);
|
|
||||||
info.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC | SEE_MASK_FLAG_NO_UI;
|
|
||||||
info.lpVerb = L"runas";
|
|
||||||
info.lpFile = executable.c_str();
|
|
||||||
info.lpDirectory = directory.c_str();
|
|
||||||
info.nShow = SW_SHOWNORMAL;
|
|
||||||
if (!ShellExecuteExW(&info)) {
|
|
||||||
LOG_WARN("Cannot launch Windows installer, error={}", GetLastError());
|
|
||||||
state_ = State::LaunchFailed;
|
state_ = State::LaunchFailed;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (info.hProcess) CloseHandle(info.hProcess);
|
|
||||||
installer_launched_ = true;
|
installer_launched_ = true;
|
||||||
state_ = State::Launched;
|
state_ = State::Launched;
|
||||||
// NSIS already prompts to close CrossDesk and stops the installed service.
|
// NSIS already prompts to close CrossDesk and stops the installed service.
|
||||||
|
|||||||
@@ -9,14 +9,10 @@
|
|||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <memory>
|
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
namespace httplib {
|
|
||||||
class Client;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
class WindowsUpdater {
|
class WindowsUpdater {
|
||||||
@@ -24,9 +20,11 @@ class WindowsUpdater {
|
|||||||
enum class State {
|
enum class State {
|
||||||
Idle,
|
Idle,
|
||||||
Downloading,
|
Downloading,
|
||||||
|
Checking,
|
||||||
Ready,
|
Ready,
|
||||||
Launching,
|
Launching,
|
||||||
Failed,
|
Failed,
|
||||||
|
SecurityBlocked,
|
||||||
LaunchFailed,
|
LaunchFailed,
|
||||||
Launched
|
Launched
|
||||||
};
|
};
|
||||||
@@ -46,12 +44,12 @@ class WindowsUpdater {
|
|||||||
std::atomic<bool> cancelled_{false};
|
std::atomic<bool> cancelled_{false};
|
||||||
std::atomic<uint64_t> downloaded_{0};
|
std::atomic<uint64_t> downloaded_{0};
|
||||||
std::atomic<uint64_t> total_{0};
|
std::atomic<uint64_t> total_{0};
|
||||||
std::shared_ptr<httplib::Client> client_;
|
|
||||||
std::thread worker_;
|
std::thread worker_;
|
||||||
std::filesystem::path installer_;
|
std::filesystem::path installer_;
|
||||||
|
std::string source_url_;
|
||||||
bool installer_launched_ = false;
|
bool installer_launched_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,42 +1,6 @@
|
|||||||
#include "installer_download.h"
|
#include "installer_download.h"
|
||||||
|
|
||||||
#include <httplib.h>
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <fstream>
|
|
||||||
|
|
||||||
#include "rd_log.h"
|
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
namespace {
|
|
||||||
|
|
||||||
// Reject error pages and files without Windows executable headers before
|
|
||||||
// invoking the loader. The download uses the verified official HTTPS URL.
|
|
||||||
bool IsWindowsExecutable(const std::filesystem::path& path) {
|
|
||||||
std::ifstream input(path, std::ios::binary);
|
|
||||||
std::array<unsigned char, 64> header{};
|
|
||||||
input.read(reinterpret_cast<char*>(header.data()), header.size());
|
|
||||||
if (!input || header[0] != 'M' || header[1] != 'Z') return false;
|
|
||||||
const uint32_t pe_offset =
|
|
||||||
uint32_t(header[60]) | (uint32_t(header[61]) << 8) |
|
|
||||||
(uint32_t(header[62]) << 16) | (uint32_t(header[63]) << 24);
|
|
||||||
if (pe_offset < header.size()) return false;
|
|
||||||
input.seekg(pe_offset);
|
|
||||||
std::array<char, 4> signature{};
|
|
||||||
input.read(signature.data(), signature.size());
|
|
||||||
return input && signature == std::array<char, 4>{'P', 'E', 0, 0};
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PartialFile {
|
|
||||||
std::filesystem::path path;
|
|
||||||
~PartialFile() {
|
|
||||||
std::error_code error;
|
|
||||||
std::filesystem::remove(path, error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
std::optional<InstallerDownload> GetWindowsInstallerDownload(
|
std::optional<InstallerDownload> GetWindowsInstallerDownload(
|
||||||
const nlohmann::json& metadata) {
|
const nlohmann::json& metadata) {
|
||||||
if (!metadata.is_object()) return std::nullopt;
|
if (!metadata.is_object()) return std::nullopt;
|
||||||
@@ -74,55 +38,4 @@ std::optional<InstallerDownload> GetWindowsInstallerDownload(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DownloadInstaller(
|
|
||||||
httplib::Client& client, const std::string& path,
|
|
||||||
const std::filesystem::path& destination,
|
|
||||||
const std::function<bool(uint64_t, uint64_t)>& progress) {
|
|
||||||
auto partial_path = destination;
|
|
||||||
partial_path += ".part";
|
|
||||||
PartialFile partial{partial_path};
|
|
||||||
try {
|
|
||||||
std::ofstream output(partial.path, std::ios::binary | std::ios::trunc);
|
|
||||||
if (!output) return false;
|
|
||||||
uint64_t received = 0;
|
|
||||||
uint64_t total = 0;
|
|
||||||
bool accepted = false;
|
|
||||||
// Do not follow a redirect to an unverified origin or downgrade to HTTP.
|
|
||||||
client.set_follow_location(false);
|
|
||||||
client.set_decompress(false);
|
|
||||||
const auto response = client.Get(
|
|
||||||
path,
|
|
||||||
[&](const httplib::Response& headers) {
|
|
||||||
if (headers.status != 200) return false;
|
|
||||||
if (headers.has_header("Content-Length")) {
|
|
||||||
total = headers.get_header_value_u64("Content-Length");
|
|
||||||
}
|
|
||||||
accepted = progress(0, total);
|
|
||||||
return accepted;
|
|
||||||
},
|
|
||||||
[&](const char* data, size_t size) {
|
|
||||||
if (!accepted || !progress(received, total)) return false;
|
|
||||||
output.write(data, static_cast<std::streamsize>(size));
|
|
||||||
received += size;
|
|
||||||
return output.good() && progress(received, total);
|
|
||||||
});
|
|
||||||
output.close();
|
|
||||||
if (!response || response->status != 200 || !accepted || output.fail() ||
|
|
||||||
received == 0 || (total != 0 && received != total) ||
|
|
||||||
!progress(received, total) || !IsWindowsExecutable(partial.path)) {
|
|
||||||
LOG_WARN(
|
|
||||||
"Installer download failed or cancelled: HTTP={}, error={}, "
|
|
||||||
"received={}, expected={}",
|
|
||||||
response ? response->status : 0, httplib::to_string(response.error()),
|
|
||||||
received, total);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
std::filesystem::rename(partial.path, destination);
|
|
||||||
return true;
|
|
||||||
} catch (const std::exception& error) {
|
|
||||||
LOG_WARN("Installer download failed: {}", error.what());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -7,17 +7,10 @@
|
|||||||
#ifndef _INSTALLER_DOWNLOAD_H_
|
#ifndef _INSTALLER_DOWNLOAD_H_
|
||||||
#define _INSTALLER_DOWNLOAD_H_
|
#define _INSTALLER_DOWNLOAD_H_
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <functional>
|
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace httplib {
|
|
||||||
class Client;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
struct InstallerDownload {
|
struct InstallerDownload {
|
||||||
@@ -30,12 +23,6 @@ struct InstallerDownload {
|
|||||||
std::optional<InstallerDownload> GetWindowsInstallerDownload(
|
std::optional<InstallerDownload> GetWindowsInstallerDownload(
|
||||||
const nlohmann::json& metadata);
|
const nlohmann::json& metadata);
|
||||||
|
|
||||||
// Streams to a .part file and publishes the installer only after a complete,
|
|
||||||
// successful response. Returning false from progress cancels the transfer.
|
|
||||||
bool DownloadInstaller(httplib::Client& client, const std::string& path,
|
|
||||||
const std::filesystem::path& destination,
|
|
||||||
const std::function<bool(uint64_t, uint64_t)>& progress);
|
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -286,7 +286,9 @@ function setup_targets()
|
|||||||
add_deps("rd_log")
|
add_deps("rd_log")
|
||||||
add_files("apps/desktop/src/version_checker/*.cpp")
|
add_files("apps/desktop/src/version_checker/*.cpp")
|
||||||
if is_os("windows") then
|
if is_os("windows") then
|
||||||
add_files("apps/desktop/src/platform/windows/windows_updater.cpp")
|
add_files("apps/desktop/src/platform/windows/windows_updater.cpp",
|
||||||
|
"apps/desktop/src/platform/windows/windows_installer.cpp")
|
||||||
|
add_links("winhttp", "ole32", "uuid")
|
||||||
end
|
end
|
||||||
add_includedirs("apps/desktop/src/version_checker", {public = true})
|
add_includedirs("apps/desktop/src/version_checker", {public = true})
|
||||||
if is_os("macosx") then
|
if is_os("macosx") then
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Windows 更新与安全软件兼容性
|
||||||
|
|
||||||
|
Windows 的更新按钮在后台下载发布信息中指定版本的安装包,完成校验及
|
||||||
|
Windows 附件安全检查后打开安装向导。客户端使用 WinHTTP,保留系统的
|
||||||
|
HTTPS 证书检查和代理配置,并用 `CrossDesk/<version> WindowsUpdater`
|
||||||
|
标识请求。
|
||||||
|
|
||||||
|
安装包保存在 `%LOCALAPPDATA%\CrossDesk\Updates` 下的独立目录中。
|
||||||
|
下载过程写入 `.part` 文件;HTTP 响应、文件长度、Windows 可执行文件头
|
||||||
|
均通过检查后才生成最终安装包。
|
||||||
|
|
||||||
|
客户端通过 Windows Attachment Services 的 `Save` 记录下载来源并请求
|
||||||
|
系统安全检查,再通过 `Execute` 打开安装包。这些 API 会按 Windows 策略
|
||||||
|
调用扫描、信任检查和必要的提示。安装程序由自身的管理员权限清单请求
|
||||||
|
UAC 授权。安全检查失败时停止流程,不回退到直接启动,也不删除来源标记。
|
||||||
|
|
||||||
|
## 发行版签名
|
||||||
|
|
||||||
|
应使用受信任的正式代码签名证书签署发布的主程序、服务、辅助程序、DLL
|
||||||
|
和最终安装包。只签安装包不能建立已安装主程序的发布者身份。
|
||||||
|
SignPath 的 `test-signing` 用于测试,不能代替公开发行所需的受信任签名。
|
||||||
|
当前流水线中的测试签名配置仍保持为可选项;正式策略需在取得证书及
|
||||||
|
SignPath 授权后配置。
|
||||||
|
|
||||||
|
检查实际分发文件的签名:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-AuthenticodeSignature .\crossdesk.exe
|
||||||
|
Get-AuthenticodeSignature .\crossdesk-win-x64-<version>.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## 网络防护告警的处理
|
||||||
|
|
||||||
|
2026-09-09 的天擎日志记录了在域名解析阶段对
|
||||||
|
`downloads.crossdesk.cn` 和 `101.132.143.203` 的阻断。
|
||||||
|
该记录不能单独证明 HTTP 库、提权方式或文件内容是命中原因。
|
||||||
|
切换到标准 Windows API 也不能保证域名/IP 信誉告警消失。
|
||||||
|
|
||||||
|
出现同类告警时,保留告警时间、策略名称、域名/IP、程序版本及签名状态,
|
||||||
|
由安全管理员向厂商核查命中原因,同时检查服务器是否存在异常活动。
|
||||||
|
确认误报后按厂商流程纠正信誉或企业策略。应在策略允许的测试环境中
|
||||||
|
验证下载、取消、不完整文件、安全检查失败及安装向导启动流程。
|
||||||
|
|
||||||
|
相关 API:
|
||||||
|
|
||||||
|
- [WinHttpOpen](https://learn.microsoft.com/windows/win32/api/winhttp/nf-winhttp-winhttpopen)
|
||||||
|
- [IAttachmentExecute::Save](https://learn.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-iattachmentexecute-save)
|
||||||
|
- [IAttachmentExecute::Execute](https://learn.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-iattachmentexecute-execute)
|
||||||
Reference in New Issue
Block a user