mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-17 09:23:10 +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
|
||||
run: |
|
||||
# If version.json exists, try to preserve downloads section
|
||||
if [ -f version.json ] && jq -e '.downloads' version.json > /dev/null 2>&1; then
|
||||
# Preserve download URLs only for this release, never an older version.
|
||||
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)
|
||||
if [ "$EXISTING_DOWNLOADS" != "null" ] && [ "$EXISTING_DOWNLOADS" != "{}" ]; then
|
||||
DOWNLOADS_JSON="$EXISTING_DOWNLOADS"
|
||||
|
||||
@@ -3286,9 +3286,12 @@ void GuiApplication::SyncWindowsUpdate() {
|
||||
}
|
||||
const State state = windows_updater_.GetState();
|
||||
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 ||
|
||||
state == State::LaunchFailed);
|
||||
state == State::LaunchFailed ||
|
||||
state == State::SecurityBlocked);
|
||||
const int language = localization_language_index_;
|
||||
std::string status;
|
||||
if (downloading) {
|
||||
@@ -3307,6 +3310,11 @@ void GuiApplication::SyncWindowsUpdate() {
|
||||
status = localization::update_download_failed[language];
|
||||
} else if (state == State::LaunchFailed) {
|
||||
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) {
|
||||
status = localization::update_launching[language];
|
||||
} else if (state == State::Launched) {
|
||||
|
||||
@@ -186,6 +186,12 @@ struct TranslationRow {
|
||||
u8"Доступна новая версия") \
|
||||
X(update_downloading, u8"正在下载安装包…", "Downloading installer…", \
|
||||
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"正在打开安装包,请确认管理员权限请求…", \
|
||||
"Opening installer. Please confirm the administrator permission request…", \
|
||||
u8"Запуск установщика. Подтвердите запрос прав администратора…") \
|
||||
|
||||
@@ -212,6 +212,7 @@ export component MainWindow inherits Window {
|
||||
in property <bool> update-available: false;
|
||||
in property <bool> update-downloading: false;
|
||||
in property <bool> update-failed: false;
|
||||
in property <bool> update-security-blocked: false;
|
||||
in property <float> update-progress: 0;
|
||||
in property <string> update-status: "";
|
||||
in property <string> current-version: "";
|
||||
@@ -1696,7 +1697,7 @@ export component MainWindow inherits Window {
|
||||
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(); } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <httplib.h>
|
||||
#include <shellapi.h>
|
||||
#include <shlobj.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <random>
|
||||
@@ -9,6 +8,7 @@
|
||||
|
||||
#include "installer_download.h"
|
||||
#include "rd_log.h"
|
||||
#include "windows_installer.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -20,7 +20,7 @@ WindowsUpdater::~WindowsUpdater() {
|
||||
|
||||
void WindowsUpdater::RemoveDownload() {
|
||||
// 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;
|
||||
std::error_code error;
|
||||
std::filesystem::remove(installer_, error);
|
||||
@@ -29,7 +29,9 @@ void WindowsUpdater::RemoveDownload() {
|
||||
|
||||
void WindowsUpdater::Start(const nlohmann::json& metadata) {
|
||||
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 ||
|
||||
state == State::Launched) {
|
||||
cancelled_ = false;
|
||||
@@ -51,11 +53,18 @@ void WindowsUpdater::Start(const nlohmann::json& metadata) {
|
||||
}
|
||||
try {
|
||||
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) {
|
||||
const auto directory =
|
||||
temp / ("CrossDesk-update-" + std::to_string(random()) + "-" +
|
||||
std::to_string(random()));
|
||||
const auto directory = updates / ("download-" + std::to_string(random()) +
|
||||
"-" + std::to_string(random()));
|
||||
if (std::filesystem::create_directory(directory)) {
|
||||
installer_ = directory / download->filename;
|
||||
break;
|
||||
@@ -63,20 +72,23 @@ void WindowsUpdater::Start(const nlohmann::json& metadata) {
|
||||
}
|
||||
if (installer_.empty())
|
||||
throw std::runtime_error("Cannot create update directory");
|
||||
client_ = std::make_shared<httplib::Client>(download->origin);
|
||||
client_->set_connection_timeout(5);
|
||||
client_->set_read_timeout(5);
|
||||
client_->set_write_timeout(5);
|
||||
source_url_ = download->origin + download->path;
|
||||
state_ = State::Downloading;
|
||||
worker_ = std::thread([this, path = download->path] {
|
||||
const bool success =
|
||||
DownloadInstaller(*client_, path, installer_,
|
||||
[this](uint64_t received, uint64_t total) {
|
||||
downloaded_ = received;
|
||||
total_ = total;
|
||||
return !cancelled_.load();
|
||||
});
|
||||
state_ = success ? State::Ready : State::Failed;
|
||||
worker_ = std::thread([this, download = *download] {
|
||||
const bool success = DownloadWindowsInstaller(
|
||||
download, installer_, [this](uint64_t received, uint64_t total) {
|
||||
downloaded_ = received;
|
||||
total_ = total;
|
||||
return !cancelled_.load();
|
||||
});
|
||||
if (success && !cancelled_) {
|
||||
state_ = State::Checking;
|
||||
state_ = CheckWindowsInstaller(installer_, source_url_)
|
||||
? State::Ready
|
||||
: State::SecurityBlocked;
|
||||
} else {
|
||||
state_ = State::Failed;
|
||||
}
|
||||
if (cancelled_) state_ = State::Idle;
|
||||
});
|
||||
} catch (const std::exception& error) {
|
||||
@@ -87,7 +99,6 @@ void WindowsUpdater::Start(const nlohmann::json& metadata) {
|
||||
|
||||
void WindowsUpdater::Cancel() {
|
||||
cancelled_ = true;
|
||||
if (client_) client_->stop();
|
||||
if (GetState() == State::Ready) state_ = State::Idle;
|
||||
}
|
||||
|
||||
@@ -98,22 +109,11 @@ void WindowsUpdater::Launch() {
|
||||
state != State::Launched)
|
||||
return;
|
||||
if (worker_.joinable()) worker_.join();
|
||||
const std::wstring executable = installer_.wstring();
|
||||
const std::wstring directory = installer_.parent_path().wstring();
|
||||
state_ = State::Launching;
|
||||
SHELLEXECUTEINFOW info{};
|
||||
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());
|
||||
if (!LaunchWindowsInstaller(installer_, source_url_)) {
|
||||
state_ = State::LaunchFailed;
|
||||
return;
|
||||
}
|
||||
if (info.hProcess) CloseHandle(info.hProcess);
|
||||
installer_launched_ = true;
|
||||
state_ = State::Launched;
|
||||
// NSIS already prompts to close CrossDesk and stops the installed service.
|
||||
|
||||
@@ -9,14 +9,10 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace httplib {
|
||||
class Client;
|
||||
}
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class WindowsUpdater {
|
||||
@@ -24,9 +20,11 @@ class WindowsUpdater {
|
||||
enum class State {
|
||||
Idle,
|
||||
Downloading,
|
||||
Checking,
|
||||
Ready,
|
||||
Launching,
|
||||
Failed,
|
||||
SecurityBlocked,
|
||||
LaunchFailed,
|
||||
Launched
|
||||
};
|
||||
@@ -46,12 +44,12 @@ class WindowsUpdater {
|
||||
std::atomic<bool> cancelled_{false};
|
||||
std::atomic<uint64_t> downloaded_{0};
|
||||
std::atomic<uint64_t> total_{0};
|
||||
std::shared_ptr<httplib::Client> client_;
|
||||
std::thread worker_;
|
||||
std::filesystem::path installer_;
|
||||
std::string source_url_;
|
||||
bool installer_launched_ = false;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,42 +1,6 @@
|
||||
#include "installer_download.h"
|
||||
|
||||
#include <httplib.h>
|
||||
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
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(
|
||||
const nlohmann::json& metadata) {
|
||||
if (!metadata.is_object()) return std::nullopt;
|
||||
@@ -74,55 +38,4 @@ std::optional<InstallerDownload> GetWindowsInstallerDownload(
|
||||
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
|
||||
|
||||
@@ -7,17 +7,10 @@
|
||||
#ifndef _INSTALLER_DOWNLOAD_H_
|
||||
#define _INSTALLER_DOWNLOAD_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace httplib {
|
||||
class Client;
|
||||
}
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct InstallerDownload {
|
||||
@@ -30,12 +23,6 @@ struct InstallerDownload {
|
||||
std::optional<InstallerDownload> GetWindowsInstallerDownload(
|
||||
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
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -286,7 +286,9 @@ function setup_targets()
|
||||
add_deps("rd_log")
|
||||
add_files("apps/desktop/src/version_checker/*.cpp")
|
||||
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
|
||||
add_includedirs("apps/desktop/src/version_checker", {public = true})
|
||||
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