refactor: 大规模架构重构

This commit is contained in:
Chenx Dust
2026-07-28 13:13:28 +08:00
parent 8a128fc860
commit c2c2b7a99d
102 changed files with 3443 additions and 1609 deletions

View File

@@ -34,32 +34,44 @@ find_package(Qt6 COMPONENTS
set(SOURCE_FILES
main.cpp
mainwindow.cpp
zjuconnectmode.cpp
presentation/main/mainwindow.cpp
presentation/main/mainwindowcoordinator.cpp
presentation/coordinators/authdialogcoordinator.cpp
presentation/coordinators/connectionuicontroller.cpp
application/applicationlogger.cpp
application/commandlineoptions.cpp
application/coreprocess.h
application/connectionsession.cpp
application/profileservice.cpp
application/settingsmigrator.cpp
application/systemproxysession.cpp
core/corecommandbuilder.cpp
core/coreoutputbuffer.cpp
core/coreoutputparser.cpp
application/defaultsettings.cpp
infrastructure/coreprocess/corecommandbuilder.cpp
infrastructure/coreprocess/consoleoutputdecoder.cpp
infrastructure/coreprocess/coreexecutable.cpp
infrastructure/coreprocess/devicetrust.cpp
infrastructure/coreprocess/coreoutputbuffer.cpp
infrastructure/coreprocess/coreoutputparser.cpp
core/connectionsessionstate.cpp
infrastructure/corelogfile.cpp
infrastructure/settingsprofileloader.cpp
infrastructure/platformsystemproxybackend.cpp
zjuconnectcontroller/zjuconnectcontroller.cpp
extrasettingwindow/extrasettingwindow.cpp
settingwindow/settingwindow.cpp
loginwindow/loginwindow.cpp
sudowindow/sudowindow.cpp
ssologinwebview/ssologinwebview.cpp
authinfowindow/authinfowindow.cpp
graphcaptchawindow/graphcaptchawindow.cpp
graphcaptchawindow/captchacanvas.cpp
utils/utils.cpp
utils/profilemanager.cpp
utils/proxysettings.cpp
utils/coreversion.cpp
utils/trustdevice.cpp
infrastructure/logging/corelogfile.cpp
infrastructure/platform/autostart.cpp
infrastructure/platform/privileges.cpp
infrastructure/settings/settingsprofileloader.cpp
infrastructure/storage/applicationpaths.cpp
infrastructure/update/updatechecker.cpp
infrastructure/platform/platformsystemproxybackend.cpp
infrastructure/platform/systemproxyfunctions.cpp
infrastructure/coreprocess/zjuconnectprocess.cpp
presentation/presentationhelpers.cpp
presentation/dialogs/extrasettingwindow/extrasettingwindow.cpp
presentation/dialogs/settingwindow/settingwindow.cpp
presentation/dialogs/loginwindow/loginwindow.cpp
presentation/dialogs/sudowindow/sudowindow.cpp
presentation/dialogs/ssologinwebview/ssologinwebview.cpp
presentation/dialogs/authinfowindow/authinfowindow.cpp
presentation/dialogs/graphcaptchawindow/graphcaptchawindow.cpp
presentation/dialogs/graphcaptchawindow/captchacanvas.cpp
infrastructure/settings/profilemanager.cpp
resource.qrc
)
@@ -132,7 +144,7 @@ if(BUILD_TESTING)
add_executable(corelogfile_test
tests/corelogfile_test.cpp
infrastructure/corelogfile.cpp
infrastructure/logging/corelogfile.cpp
)
target_include_directories(corelogfile_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(corelogfile_test Qt::Core)
@@ -140,7 +152,7 @@ if(BUILD_TESTING)
add_executable(corecommandbuilder_test
tests/corecommandbuilder_test.cpp
core/corecommandbuilder.cpp
infrastructure/coreprocess/corecommandbuilder.cpp
)
target_include_directories(corecommandbuilder_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(corecommandbuilder_test Qt::Core)
@@ -148,7 +160,7 @@ if(BUILD_TESTING)
add_executable(coreoutputparser_test
tests/coreoutputparser_test.cpp
core/coreoutputparser.cpp
infrastructure/coreprocess/coreoutputparser.cpp
)
target_include_directories(coreoutputparser_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(coreoutputparser_test Qt::Core)
@@ -156,7 +168,7 @@ if(BUILD_TESTING)
add_executable(coreoutputbuffer_test
tests/coreoutputbuffer_test.cpp
core/coreoutputbuffer.cpp
infrastructure/coreprocess/coreoutputbuffer.cpp
)
target_include_directories(coreoutputbuffer_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(coreoutputbuffer_test Qt::Core)
@@ -164,7 +176,7 @@ if(BUILD_TESTING)
add_executable(settingsprofileloader_test
tests/settingsprofileloader_test.cpp
infrastructure/settingsprofileloader.cpp
infrastructure/settings/settingsprofileloader.cpp
)
target_include_directories(settingsprofileloader_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(settingsprofileloader_test Qt::Core)
@@ -178,13 +190,55 @@ if(BUILD_TESTING)
target_link_libraries(connectionsessionstate_test Qt::Core)
add_test(NAME connectionsessionstate_test COMMAND connectionsessionstate_test)
add_executable(connectionsession_test
tests/connectionsession_test.cpp
application/coreprocess.h
application/connectionsession.cpp
core/connectionsessionstate.cpp
)
target_include_directories(connectionsession_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(connectionsession_test Qt::Core)
add_test(NAME connectionsession_test COMMAND connectionsession_test)
add_executable(systemproxysession_test
tests/systemproxysession_test.cpp
application/systemproxysession.cpp
infrastructure/platformsystemproxybackend.cpp
utils/proxysettings.cpp
)
target_include_directories(systemproxysession_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(systemproxysession_test Qt::Core Qt::Concurrent Qt::Network Qt::Widgets ${PLATFORM_LIBS})
target_link_libraries(systemproxysession_test Qt::Core Qt::Concurrent)
add_test(NAME systemproxysession_test COMMAND systemproxysession_test)
add_executable(updatechecker_test
tests/updatechecker_test.cpp
infrastructure/update/updatechecker.cpp
infrastructure/coreprocess/coreexecutable.cpp
)
target_include_directories(updatechecker_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(updatechecker_test Qt::Core Qt::Gui Qt::Widgets Qt::Network Qt::Core5Compat)
add_test(NAME updatechecker_test COMMAND updatechecker_test)
add_executable(settingsmigrator_test
tests/settingsmigrator_test.cpp
application/settingsmigrator.cpp
application/defaultsettings.cpp
)
target_include_directories(settingsmigrator_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(settingsmigrator_test Qt::Core)
add_test(NAME settingsmigrator_test COMMAND settingsmigrator_test)
add_executable(profileservice_test
tests/profileservice_test.cpp
application/profileservice.cpp
)
target_include_directories(profileservice_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(profileservice_test Qt::Core)
add_test(NAME profileservice_test COMMAND profileservice_test)
add_executable(profilemanager_test
tests/profilemanager_test.cpp
infrastructure/settings/profilemanager.cpp
)
target_include_directories(profilemanager_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(profilemanager_test Qt::Core)
add_test(NAME profilemanager_test COMMAND profilemanager_test)
endif()

View File

@@ -60,6 +60,11 @@
- [X] 上传 AUR 包
- [ ] 使用密钥链存储密码等信息
## 开发
项目的分层、依赖方向和新增代码归属规则见
[架构说明](docs/ARCHITECTURE.md)。
## 相关项目
以下均为 EasyConnect 客户端的桌面友好替代品,可按需使用:

View File

@@ -0,0 +1,14 @@
#ifndef APPLICATIONCONSTANTS_H
#define APPLICATIONCONSTANTS_H
#include <QString>
namespace ApplicationConstants
{
const inline QString RepositoryName("chenx-dust/EZ4Connect");
const inline QString CoreRepositoryName("Mythologyli/zju-connect");
const inline QString ApplicationName("EZ4Connect");
constexpr inline int ConfigVersion = 8;
}
#endif // APPLICATIONCONSTANTS_H

View File

@@ -0,0 +1,19 @@
#include "commandlineoptions.h"
QString CommandLineOptions::value(const QStringList &arguments, const QString &key)
{
const QString prefix = key + "=";
for (int index = 0; index < arguments.size(); ++index)
{
const QString &argument = arguments.at(index);
if (argument == key)
{
return index + 1 < arguments.size() ? arguments.at(index + 1) : QString();
}
if (argument.startsWith(prefix))
{
return argument.mid(prefix.size());
}
}
return {};
}

View File

@@ -0,0 +1,12 @@
#ifndef COMMANDLINEOPTIONS_H
#define COMMANDLINEOPTIONS_H
#include <QString>
#include <QStringList>
namespace CommandLineOptions
{
QString value(const QStringList &arguments, const QString &key);
}
#endif // COMMANDLINEOPTIONS_H

View File

@@ -1,29 +1,30 @@
#include "connectionsession.h"
ConnectionSession::ConnectionSession(QObject *parent)
ConnectionSession::ConnectionSession(CoreProcess *coreProcess, QObject *parent)
: QObject(parent),
controller(new ZjuConnectController(this))
coreProcess(coreProcess)
{
coreProcess->setParent(this);
reconnectTimer.setSingleShot(true);
connect(controller, &ZjuConnectController::outputRead, this, &ConnectionSession::outputRead);
connect(controller, &ZjuConnectController::graphCaptcha, this, &ConnectionSession::graphCaptcha);
connect(controller, &ZjuConnectController::smsCode, this, &ConnectionSession::smsCode);
connect(controller, &ZjuConnectController::totpCode, this, &ConnectionSession::totpCode);
connect(controller, &ZjuConnectController::ssoAuth, this, &ConnectionSession::ssoAuth);
connect(controller, &ZjuConnectController::askSudoPass,
connect(coreProcess, &CoreProcess::outputRead, this, &ConnectionSession::outputRead);
connect(coreProcess, &CoreProcess::graphCaptcha, this, &ConnectionSession::graphCaptcha);
connect(coreProcess, &CoreProcess::smsCode, this, &ConnectionSession::smsCode);
connect(coreProcess, &CoreProcess::totpCode, this, &ConnectionSession::totpCode);
connect(coreProcess, &CoreProcess::ssoAuth, this, &ConnectionSession::ssoAuth);
connect(coreProcess, &CoreProcess::askSudoPass,
this, &ConnectionSession::handleSudoPasswordRequest);
connect(controller, &ZjuConnectController::error, this, [this](ZJU_ERROR error)
connect(coreProcess, &CoreProcess::error, this, [this](ZJU_ERROR error)
{
sessionState.recordError(error);
});
connect(controller, &ZjuConnectController::started, this, [this]()
connect(coreProcess, &CoreProcess::started, this, [this]()
{
sessionState.processStarted();
emit stateChanged(sessionState.state());
});
connect(controller, &ZjuConnectController::finished,
connect(coreProcess, &CoreProcess::finished,
this, &ConnectionSession::handleCoreFinished);
connect(&reconnectTimer, &QTimer::timeout, this, [this]()
{
@@ -65,13 +66,13 @@ void ConnectionSession::stop()
if (processNeedsStop)
{
emit stateChanged(sessionState.state());
controller->stop();
coreProcess->stop();
}
}
void ConnectionSession::submitInput(const QByteArray &data)
{
controller->writeInput(data);
coreProcess->writeInput(data);
}
void ConnectionSession::submitSudoPassword(const QString &password, bool remember)
@@ -103,7 +104,7 @@ bool ConnectionSession::isActive() const
void ConnectionSession::startCore()
{
sudoPasswordSubmitted = false;
controller->start(currentProfile);
coreProcess->start(currentProfile);
}
void ConnectionSession::handleCoreFinished()

View File

@@ -4,16 +4,16 @@
#include <QObject>
#include <QTimer>
#include "application/coreprocess.h"
#include "core/connectionprofile.h"
#include "core/connectionsessionstate.h"
#include "zjuconnectcontroller/zjuconnectcontroller.h"
class ConnectionSession : public QObject
{
Q_OBJECT
public:
explicit ConnectionSession(QObject *parent = nullptr);
explicit ConnectionSession(CoreProcess *coreProcess, QObject *parent = nullptr);
bool start(const ConnectionProfile &profile, const ReconnectPolicy &policy);
void stop();
@@ -40,7 +40,7 @@ private:
void handleCoreFinished();
void handleSudoPasswordRequest();
ZjuConnectController *controller;
CoreProcess *coreProcess;
QTimer reconnectTimer;
ConnectionSessionState sessionState;
ConnectionProfile currentProfile;

36
application/coreprocess.h Normal file
View File

@@ -0,0 +1,36 @@
#ifndef COREPROCESS_H
#define COREPROCESS_H
#include <QObject>
#include "core/connectionerror.h"
#include "core/connectionprofile.h"
class CoreProcess : public QObject
{
Q_OBJECT
public:
explicit CoreProcess(QObject *parent = nullptr)
: QObject(parent)
{
}
~CoreProcess() override = default;
virtual void start(const ConnectionProfile &profile) = 0;
virtual void stop() = 0;
virtual void writeInput(const QByteArray &data) = 0;
signals:
void error(ZJU_ERROR error);
void outputRead(const QString &output);
void graphCaptcha(const QString &graphFile);
void smsCode(bool showSkipSecondaryAuthOption);
void totpCode();
void ssoAuth();
void askSudoPass();
void started();
void finished();
};
#endif // COREPROCESS_H

View File

@@ -0,0 +1,65 @@
#include "application/defaultsettings.h"
#include <QSettings>
#include "application/applicationconstants.h"
void DefaultSettings::reset(QSettings &settings)
{
settings.setValue("Credential/Username", "");
settings.setValue("Credential/Password", "");
settings.setValue("Credential/TOTPSecret", "");
settings.setValue("Common/ConnectAfterStart", false);
settings.setValue("Common/CheckUpdateAfterStart", false);
settings.setValue("Common/AutoSetProxy", false);
settings.setValue("Common/ReconnectTime", 1);
settings.setValue("Common/AutoReconnect", false);
settings.setValue("Common/SystemProxyBypass", "");
settings.setValue("ZJUConnect/ServerAddress", "trust.hitsz.edu.cn");
settings.setValue("ZJUConnect/ServerPort", 443);
settings.setValue("ZJUConnect/DNS", "");
settings.setValue("ZJUConnect/DNSAuto", true);
settings.setValue("ZJUConnect/SecondaryDNS", "");
settings.setValue("ZJUConnect/DNSTTL", 3600);
settings.setValue("ZJUConnect/SOCKS5Port", 11080);
settings.setValue("ZJUConnect/HTTPPort", 11081);
settings.setValue("ZJUConnect/ShadowsocksURL", "");
settings.setValue("ZJUConnect/DialDirectProxy", "");
settings.setValue("ZJUConnect/UpdateBestNodesInterval", 300);
settings.setValue("ZJUConnect/Protocol", "atrust");
settings.setValue("ZJUConnect/LoginDomain", "hitcas");
settings.setValue("ZJUConnect/AuthType", "cas");
settings.setValue("ZJUConnect/LoginURL", "");
settings.setValue("ZJUConnect/PhoneCountryCode", "86");
settings.setValue("ZJUConnect/PhoneNumber", "");
settings.setValue("ZJUConnect/MultiLine", false);
settings.setValue("ZJUConnect/KeepAlive", false);
settings.setValue("ZJUConnect/KeepAliveURL", "");
settings.setValue("ZJUConnect/BindInterface", "");
settings.setValue("ZJUConnect/OutsideAccess", false);
settings.setValue("ZJUConnect/SkipDomainResource", false);
settings.setValue("ZJUConnect/DisableServerConfig", false);
settings.setValue("ZJUConnect/ProxyAll", false);
settings.setValue("ZJUConnect/DisableZJUDNS", false);
settings.setValue("ZJUConnect/ZJUDefault", false);
settings.setValue("ZJUConnect/Debug", false);
settings.setValue("ZJUConnect/TUNMode", false);
settings.setValue("ZJUConnect/AddRoute", false);
settings.setValue("ZJUConnect/DNSHijack", false);
settings.setValue("ZJUConnect/FakeIP", false);
settings.setValue("ZJUConnect/TCPTunnelMode", false);
settings.setValue("ZJUConnect/AutoDetectInterface", false);
settings.setValue("ZJUConnect/TCPPortForwarding", "");
settings.setValue("ZJUConnect/UDPPortForwarding", "");
settings.setValue("ZJUConnect/CustomDNS", "");
settings.setValue("ZJUConnect/CustomProxyDomain", "");
settings.setValue("ZJUConnect/ExtraArguments", "");
settings.setValue("Common/ConfigVersion", ApplicationConstants::ConfigVersion);
}

View File

@@ -0,0 +1,11 @@
#ifndef DEFAULTSETTINGS_H
#define DEFAULTSETTINGS_H
class QSettings;
namespace DefaultSettings
{
void reset(QSettings &settings);
}
#endif // DEFAULTSETTINGS_H

View File

@@ -0,0 +1,30 @@
#ifndef PROFILEBACKEND_H
#define PROFILEBACKEND_H
#include <QString>
#include <QStringList>
class ProfileBackend
{
public:
virtual ~ProfileBackend() = default;
virtual QStringList listProfiles() const = 0;
virtual QString activeProfile() const = 0;
virtual bool setActiveProfile(const QString &profileId) const = 0;
virtual QString profilePath(const QString &profileId) const = 0;
virtual QString createProfile(
const QString &requestedName,
const QString &sourcePath = {}
) const = 0;
virtual bool renameProfile(
const QString &oldId,
const QString &newId
) const = 0;
virtual bool removeProfile(const QString &profileId) const = 0;
virtual QString normalizeProfileId(const QString &name) const = 0;
virtual bool silentStartEnabled() const = 0;
virtual void setAutoStartEnabled(bool enabled) const = 0;
};
#endif // PROFILEBACKEND_H

View File

@@ -0,0 +1,181 @@
#include "profileservice.h"
#include <QFileInfo>
#include <QSettings>
#include <utility>
#include "application/profilebackend.h"
ProfileService::ProfileService(
std::unique_ptr<ProfileBackend> backend,
const QString &overrideConfigPath,
QObject *parent
)
: QObject(parent),
profileBackend(std::move(backend)),
currentSettings(nullptr),
overrideConfiguration(!overrideConfigPath.isEmpty())
{
if (!overrideConfiguration)
{
currentId = profileBackend->activeProfile();
currentSettings = new QSettings(
profileBackend->profilePath(currentId),
QSettings::IniFormat
);
}
else
{
currentId = "custom";
currentSettings = new QSettings(overrideConfigPath, QSettings::IniFormat);
}
}
ProfileService::~ProfileService()
{
currentSettings->sync();
delete currentSettings;
}
QSettings *ProfileService::settings() const
{
return currentSettings;
}
QString ProfileService::currentProfileId() const
{
return currentId;
}
QStringList ProfileService::profiles() const
{
return profileBackend->listProfiles();
}
bool ProfileService::usesOverrideConfiguration() const
{
return overrideConfiguration;
}
bool ProfileService::switchTo(const QString &profileId)
{
if (!overrideConfiguration && profileId == currentId)
{
return true;
}
if (!QFileInfo::exists(profileBackend->profilePath(profileId)))
{
return false;
}
currentSettings->sync();
if (!profileBackend->setActiveProfile(profileId))
{
return false;
}
delete currentSettings;
currentSettings = new QSettings(
profileBackend->profilePath(profileId),
QSettings::IniFormat
);
currentId = profileId;
overrideConfiguration = false;
return true;
}
QString ProfileService::createAndSwitch(const QString &requestedName)
{
currentSettings->sync();
const QString profileId = profileBackend->createProfile(
requestedName,
currentSettings->fileName()
);
if (profileId.isEmpty() || !switchTo(profileId))
{
if (!profileId.isEmpty())
{
profileBackend->removeProfile(profileId);
}
return {};
}
return profileId;
}
bool ProfileService::renameCurrent(const QString &newProfileId)
{
if (currentId.isEmpty() || overrideConfiguration)
{
return false;
}
currentSettings->sync();
if (!profileBackend->renameProfile(currentId, newProfileId))
{
return false;
}
const QString previousId = currentId;
if (!profileBackend->setActiveProfile(newProfileId))
{
profileBackend->renameProfile(newProfileId, previousId);
return false;
}
currentId = newProfileId;
delete currentSettings;
currentSettings = new QSettings(
profileBackend->profilePath(currentId),
QSettings::IniFormat
);
return true;
}
bool ProfileService::removeCurrentAndSwitchToDefault()
{
if (currentId.isEmpty() || overrideConfiguration)
{
return false;
}
const QString removedProfileId = currentId;
const QString defaultProfilePath = profileBackend->profilePath("");
if (!QFileInfo::exists(defaultProfilePath))
{
return false;
}
currentSettings->sync();
if (!profileBackend->setActiveProfile(""))
{
return false;
}
if (!profileBackend->removeProfile(removedProfileId))
{
profileBackend->setActiveProfile(removedProfileId);
return false;
}
delete currentSettings;
currentSettings = new QSettings(defaultProfilePath, QSettings::IniFormat);
currentId.clear();
return true;
}
QString ProfileService::normalizeProfileId(const QString &name) const
{
return profileBackend->normalizeProfileId(name);
}
bool ProfileService::silentStartEnabled() const
{
return profileBackend->silentStartEnabled();
}
void ProfileService::migrateAutoStartSetting(bool enabled)
{
profileBackend->setAutoStartEnabled(enabled);
}

View File

@@ -0,0 +1,45 @@
#ifndef PROFILESERVICE_H
#define PROFILESERVICE_H
#include <QObject>
#include <QStringList>
#include <memory>
class ProfileBackend;
class QSettings;
class ProfileService : public QObject
{
Q_OBJECT
public:
explicit ProfileService(
std::unique_ptr<ProfileBackend> backend,
const QString &overrideConfigPath = {},
QObject *parent = nullptr
);
~ProfileService() override;
QSettings *settings() const;
QString currentProfileId() const;
QStringList profiles() const;
bool usesOverrideConfiguration() const;
bool switchTo(const QString &profileId);
QString createAndSwitch(const QString &requestedName);
bool renameCurrent(const QString &newProfileId);
bool removeCurrentAndSwitchToDefault();
QString normalizeProfileId(const QString &name) const;
bool silentStartEnabled() const;
void migrateAutoStartSetting(bool enabled);
private:
std::unique_ptr<ProfileBackend> profileBackend;
QSettings *currentSettings;
QString currentId;
bool overrideConfiguration;
};
#endif // PROFILESERVICE_H

View File

@@ -0,0 +1,44 @@
#include "settingsmigrator.h"
#include <QSettings>
#include "application/applicationconstants.h"
#include "application/defaultsettings.h"
SettingsMigrationAction SettingsMigrator::prepare(QSettings &settings)
{
const int configVersion = settings.value("Common/ConfigVersion", -1).toInt();
if (configVersion == -1)
{
DefaultSettings::reset(settings);
return SettingsMigrationAction::None;
}
if (configVersion == 4)
{
settings.setValue("ZJUConnect/Protocol", "easyconnect");
return SettingsMigrationAction::None;
}
if (configVersion == 6)
{
return SettingsMigrationAction::MigrateAutoStart;
}
if (configVersion < ApplicationConstants::ConfigVersion)
{
return SettingsMigrationAction::RecommendReset;
}
return SettingsMigrationAction::None;
}
void SettingsMigrator::finish(QSettings &settings, bool resetToDefaults)
{
if (resetToDefaults)
{
settings.clear();
DefaultSettings::reset(settings);
}
settings.setValue(
"Common/ConfigVersion",
ApplicationConstants::ConfigVersion
);
settings.sync();
}

View File

@@ -0,0 +1,20 @@
#ifndef SETTINGSMIGRATOR_H
#define SETTINGSMIGRATOR_H
class QSettings;
enum class SettingsMigrationAction
{
None,
MigrateAutoStart,
RecommendReset
};
class SettingsMigrator
{
public:
static SettingsMigrationAction prepare(QSettings &settings);
static void finish(QSettings &settings, bool resetToDefaults);
};
#endif // SETTINGSMIGRATOR_H

View File

@@ -2,13 +2,6 @@
#include <QtConcurrent>
#include "infrastructure/platformsystemproxybackend.h"
SystemProxySession::SystemProxySession(QObject *parent)
: SystemProxySession(std::make_unique<PlatformSystemProxyBackend>(), parent)
{
}
SystemProxySession::SystemProxySession(
std::unique_ptr<SystemProxyBackend> backend,
QObject *parent

View File

@@ -13,7 +13,6 @@ class SystemProxySession : public QObject
Q_OBJECT
public:
explicit SystemProxySession(QObject *parent = nullptr);
explicit SystemProxySession(std::unique_ptr<SystemProxyBackend> backend, QObject *parent = nullptr);
~SystemProxySession() override;

43
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,43 @@
# 架构说明
EZ4Connect 使用轻量级分层架构。界面层通过 Coordinator 组合应用服务和平台实现,避免
`MainWindow` 直接承担连接进程、配置存储及认证窗口的生命周期。
## 目录职责
```text
core/ 连接领域模型与状态机,不依赖界面和平台实现
application/ 用例、会话及基础设施端口
infrastructure/ QProcess、QSettings、系统代理、文件和更新检查等实现
presentation/ Qt Widgets 界面、对话框和界面流程 Coordinator
tests/ 按端口或纯逻辑边界测试各层行为
```
依赖方向为:
```text
presentation -> application -> core
| ^
v |
infrastructure -------+
```
`application` 不引用 `presentation` 或具体的 `infrastructure` 类型。具体实现统一由
`MainWindowCoordinator` 创建并注入:
- `CoreProcess``ZjuConnectProcess`
- `SystemProxyBackend``PlatformSystemProxyBackend`
- `ProfileBackend``ProfileManager`
## 主要流程
- `MainWindowCoordinator` 是主界面的组合入口,并维护 VPN 与系统代理之间的生命周期联动。
- `ConnectionUiController` 处理连接、断开、系统代理按钮及错误展示。
- `AuthDialogCoordinator` 管理登录、sudo、验证码、TOTP 和 SSO 对话框。
- `ConnectionSession` 负责核心进程、重连和连接状态,不依赖具体 `QProcess` 实现。
- `ProfileService` 持有当前配置上下文,`SettingsMigrator` 负责配置版本迁移。
新增代码应按“变化原因”归位:界面行为放入 `presentation`,用例状态放入
`application`,平台或文件 I/O 放入 `infrastructure`,可独立验证的连接规则放入
`core`。不要重新引入通用 `utils` 目录;只有跨多个职责且没有明确归属的代码才需要新建
共享模块。

View File

@@ -0,0 +1,46 @@
#include "consoleoutputdecoder.h"
#include <QTextCodec>
QString ConsoleOutputDecoder::decode(const QByteArray &bytes)
{
static QString encoding;
if (encoding == "UTF-8")
{
return QTextCodec::codecForName("UTF-8")->toUnicode(bytes);
}
if (encoding == "GBK")
{
return QTextCodec::codecForName("GBK")->toUnicode(bytes);
}
if (encoding == "locale")
{
return QString::fromLocal8Bit(bytes);
}
QTextCodec *utf8Codec = QTextCodec::codecForName("UTF-8");
const QString utf8 = utf8Codec->toUnicode(bytes);
const QByteArray utf8RoundTrip = utf8Codec->fromUnicode(utf8);
QTextCodec *gbkCodec = QTextCodec::codecForName("GBK");
const QString gbk = gbkCodec->toUnicode(bytes);
const QByteArray gbkRoundTrip = gbkCodec->fromUnicode(gbk);
if (utf8RoundTrip == bytes && gbkRoundTrip != bytes)
{
encoding = "UTF-8";
return utf8;
}
if (gbkRoundTrip == bytes && utf8RoundTrip != bytes)
{
encoding = "GBK";
return gbk;
}
if (gbkRoundTrip == bytes && utf8RoundTrip == bytes)
{
return utf8;
}
encoding = "locale";
return QString::fromLocal8Bit(bytes);
}

View File

@@ -0,0 +1,12 @@
#ifndef CONSOLEOUTPUTDECODER_H
#define CONSOLEOUTPUTDECODER_H
#include <QByteArray>
#include <QString>
namespace ConsoleOutputDecoder
{
QString decode(const QByteArray &bytes);
}
#endif // CONSOLEOUTPUTDECODER_H

View File

@@ -3,7 +3,7 @@
#include <QStringList>
#include "connectionprofile.h"
#include "core/connectionprofile.h"
struct CoreRuntimePaths
{

View File

@@ -0,0 +1,44 @@
#include "coreexecutable.h"
#include <QCoreApplication>
#include <QFileInfo>
#include <QProcess>
#include <QSysInfo>
#include <stdexcept>
QString CoreExecutable::path()
{
const QString fileName =
QSysInfo::productType() == "windows" ? "zju-connect.exe" : "zju-connect";
const QString bundledPath = QCoreApplication::applicationDirPath() + "/" + fileName;
return QFileInfo::exists(bundledPath) ? bundledPath : fileName;
}
QString CoreExecutable::version(QObject *parent)
{
QProcess process(parent);
process.start(path(), {"-version"});
if (!process.waitForStarted())
{
throw std::runtime_error("核心无法启动");
}
if (!process.waitForFinished())
{
throw std::runtime_error("核心运行超时");
}
const QByteArray errorOutput = process.readAllStandardError();
if (!errorOutput.isEmpty())
{
throw std::runtime_error(errorOutput);
}
const QString output = process.readAllStandardOutput();
const QString prefix("ZJU Connect v");
if (!output.startsWith(prefix))
{
throw std::runtime_error("无法解析核心版本号");
}
return output.mid(prefix.size()).trimmed();
}

View File

@@ -0,0 +1,14 @@
#ifndef COREEXECUTABLE_H
#define COREEXECUTABLE_H
#include <QString>
class QObject;
namespace CoreExecutable
{
QString path();
QString version(QObject *parent);
}
#endif // COREEXECUTABLE_H

View File

@@ -0,0 +1,54 @@
#include "devicetrust.h"
#include <QProcess>
#include <stdexcept>
#include "infrastructure/coreprocess/coreexecutable.h"
#include "infrastructure/storage/applicationpaths.h"
void DeviceTrust::set(
QObject *parent,
const QString &protocol,
const QString &server,
int port,
const QString &profileId,
bool trusted
)
{
QStringList arguments;
if (!protocol.isEmpty())
{
arguments << "-protocol" << protocol;
}
if (!server.isEmpty())
{
arguments << "-server" << server;
}
if (port != 0)
{
arguments << "-port" << QString::number(port);
}
arguments << "-client-data-file" << ApplicationPaths::clientDataFile(profileId);
arguments << (trusted ? "-trust-device" : "-untrust-device");
QProcess process(parent);
process.start(CoreExecutable::path(), arguments);
if (!process.waitForStarted())
{
throw std::runtime_error("核心无法启动");
}
if (!process.waitForFinished())
{
throw std::runtime_error("核心运行超时");
}
const QByteArray errorOutput = process.readAllStandardError();
const QByteArray expected =
trusted ? QByteArray("Device trusted successfully")
: QByteArray("Device untrusted successfully");
if (!errorOutput.contains(expected))
{
throw std::runtime_error(errorOutput);
}
}

View File

@@ -0,0 +1,20 @@
#ifndef DEVICETRUST_H
#define DEVICETRUST_H
#include <QString>
class QObject;
namespace DeviceTrust
{
void set(
QObject *parent,
const QString &protocol,
const QString &server,
int port,
const QString &profileId,
bool trusted
);
}
#endif // DEVICETRUST_H

View File

@@ -1,14 +1,16 @@
#include "zjuconnectcontroller.h"
#include "core/corecommandbuilder.h"
#include "core/coreoutputparser.h"
#include "utils/utils.h"
#include "zjuconnectprocess.h"
#include "infrastructure/coreprocess/corecommandbuilder.h"
#include "infrastructure/coreprocess/consoleoutputdecoder.h"
#include "infrastructure/coreprocess/coreoutputparser.h"
#include "infrastructure/platform/privileges.h"
#include "infrastructure/storage/applicationpaths.h"
#include <qcontainerfwd.h>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
ZjuConnectController::ZjuConnectController(QObject *parent) : QObject(parent)
ZjuConnectProcess::ZjuConnectProcess(QObject *parent) : CoreProcess(parent)
{
zjuConnectProcess = new QProcess(this);
@@ -48,7 +50,7 @@ ZjuConnectController::ZjuConnectController(QObject *parent) : QObject(parent)
});
}
void ZjuConnectController::processOutput(CoreOutputBuffer &buffer, const QByteArray &data, bool flushPending)
void ZjuConnectProcess::processOutput(CoreOutputBuffer &buffer, const QByteArray &data, bool flushPending)
{
QList<QByteArray> lines = buffer.append(data);
@@ -63,7 +65,7 @@ void ZjuConnectController::processOutput(CoreOutputBuffer &buffer, const QByteAr
processOutputLines(lines);
}
void ZjuConnectController::processOutputLines(const QList<QByteArray> &lines)
void ZjuConnectProcess::processOutputLines(const QList<QByteArray> &lines)
{
if (lines.isEmpty())
{
@@ -74,7 +76,7 @@ void ZjuConnectController::processOutputLines(const QList<QByteArray> &lines)
outputLines.reserve(lines.size());
for (const QByteArray &line : lines)
{
outputLines.append(Utils::consoleOutputToQString(line));
outputLines.append(ConsoleOutputDecoder::decode(line));
}
const QString output = outputLines.join('\n');
@@ -141,7 +143,7 @@ void ZjuConnectController::processOutputLines(const QList<QByteArray> &lines)
}
}
QString ZjuConnectController::copyCoreForAppImage(const QString &programPath)
QString ZjuConnectProcess::copyCoreForAppImage(const QString &programPath)
{
#if defined(Q_OS_UNIX)
static QString cachedSourcePath;
@@ -185,7 +187,7 @@ QString ZjuConnectController::copyCoreForAppImage(const QString &programPath)
#endif
}
void ZjuConnectController::start(const ConnectionProfile &profile)
void ZjuConnectProcess::start(const ConnectionProfile &profile)
{
CoreRuntimePaths runtimePaths;
@@ -199,7 +201,7 @@ void ZjuConnectController::start(const ConnectionProfile &profile)
}
graphFile = tempDir->filePath("graph.jpg");
runtimePaths.graphCodeFile = graphFile;
runtimePaths.clientDataFile = Utils::getClientDataPath(profile.profileId);
runtimePaths.clientDataFile = ApplicationPaths::clientDataFile(profile.profileId);
}
const CoreCommand command = CoreCommandBuilder::build(profile, runtimePaths);
@@ -218,7 +220,7 @@ void ZjuConnectController::start(const ConnectionProfile &profile)
QStringList finalArgs = command.arguments;
#if defined(Q_OS_UNIX)
if (profile.tunnel.tunMode && !Utils::isRunningAsAdmin())
if (profile.tunnel.tunMode && !Privileges::isElevated())
{
programToStart = copyCoreForAppImage(programToStart);
@@ -243,7 +245,7 @@ void ZjuConnectController::start(const ConnectionProfile &profile)
}
}
void ZjuConnectController::stop()
void ZjuConnectProcess::stop()
{
if (zjuConnectProcess->state() == QProcess::NotRunning)
{
@@ -261,12 +263,12 @@ void ZjuConnectController::stop()
}
}
void ZjuConnectController::writeInput(const QByteArray &data)
void ZjuConnectProcess::writeInput(const QByteArray &data)
{
zjuConnectProcess->write(data);
}
ZjuConnectController::~ZjuConnectController()
ZjuConnectProcess::~ZjuConnectProcess()
{
stop();
}

View File

@@ -0,0 +1,43 @@
#ifndef ZJUCONNECTPROCESS_H
#define ZJUCONNECTPROCESS_H
#include <QtCore>
#include "application/coreprocess.h"
#include "infrastructure/coreprocess/coreoutputbuffer.h"
class ZjuConnectProcess : public CoreProcess
{
Q_OBJECT
public:
explicit ZjuConnectProcess(QObject *parent = nullptr);
~ZjuConnectProcess() override;
void start(const ConnectionProfile &profile) override;
void stop() override;
void writeInput(const QByteArray &data) override;
private:
QString copyCoreForAppImage(const QString &programPath);
void processOutput(CoreOutputBuffer &buffer, const QByteArray &data, bool flushPending = false);
void processOutputLines(const QList<QByteArray> &lines);
QProcess *zjuConnectProcess;
QTemporaryDir *tempDir = nullptr;
QString graphFile;
CoreOutputBuffer standardOutputBuffer;
CoreOutputBuffer standardErrorBuffer;
bool stopRequested = false;
};
#endif // ZJUCONNECTPROCESS_H

View File

@@ -0,0 +1,161 @@
#include "autostart.h"
#include <QApplication>
#include <QCoreApplication>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QMessageBox>
#include <QProcess>
#include <QSettings>
#include <QStandardPaths>
#include <QTextStream>
namespace
{
QString nativeApplicationPath()
{
return QDir::toNativeSeparators(QCoreApplication::applicationFilePath());
}
QString macApplicationBundlePath()
{
#ifdef Q_OS_MAC
QDir directory(QCoreApplication::applicationDirPath());
directory.cdUp();
directory.cdUp();
return directory.absolutePath();
#else
return {};
#endif
}
QString macApplicationBundleName()
{
#ifdef Q_OS_MAC
return QFileInfo(macApplicationBundlePath()).baseName();
#else
return {};
#endif
}
}
void AutoStart::setEnabled(bool enabled)
{
#if defined(Q_OS_WINDOWS)
QSettings settings(
R"(HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run)",
QSettings::NativeFormat
);
if (enabled)
{
settings.setValue(QApplication::applicationName(), nativeApplicationPath());
}
else
{
settings.remove(QApplication::applicationName());
}
settings.sync();
#elif defined(Q_OS_MACOS)
{
const QStringList arguments{
"-e",
"tell application \"System Events\" to delete login item \"" +
macApplicationBundleName() + "\""
};
QProcess process;
process.start("osascript", arguments);
process.waitForFinished();
const QString error = process.readAllStandardError();
if (!enabled && process.error() != QProcess::UnknownError)
{
QMessageBox::critical(
nullptr,
"取消开机自启动失败",
"无法删除登录项:" + process.errorString()
);
return;
}
if (!enabled && process.exitCode() != 0)
{
QMessageBox::critical(nullptr, "取消开机自启动失败", "无法删除登录项:" + error);
return;
}
}
if (enabled)
{
const QStringList arguments{
"-e",
"tell application \"System Events\" to make login item at end with properties "
"{path:\"" + macApplicationBundlePath() + "\", hidden:false}"
};
QProcess process;
process.start("osascript", arguments);
process.waitForFinished();
if (process.error() != QProcess::UnknownError)
{
QMessageBox::critical(
nullptr,
"设置开机自启动失败",
"无法创建登录项:" + process.errorString()
);
return;
}
if (process.exitCode() != 0)
{
QMessageBox::critical(
nullptr,
"设置开机自启动失败",
"无法创建登录项:" + process.readAllStandardError()
);
}
}
#elif defined(Q_OS_LINUX)
const QString directoryPath =
QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/autostart/";
QDir directory(directoryPath);
QFile desktopFile(directoryPath + QApplication::applicationName() + ".desktop");
if (directory.exists() && desktopFile.exists() && !desktopFile.remove())
{
QMessageBox::critical(
nullptr,
"取消开机自启动失败",
"无法删除 .desktop 文件:" + desktopFile.fileName()
);
return;
}
if (!enabled)
{
return;
}
if (!directory.exists() && !directory.mkpath("."))
{
QMessageBox::critical(
nullptr,
"设置开机自启动失败",
"无法创建 autostart 目录:" + directoryPath
);
return;
}
if (!desktopFile.open(QIODevice::WriteOnly | QIODevice::Text))
{
QMessageBox::critical(
nullptr,
"设置开机自启动失败",
"无法创建 .desktop 文件:" + desktopFile.fileName()
);
return;
}
QTextStream output(&desktopFile);
output << "[Desktop Entry]\n";
output << "Type=Application\n";
output << "Name=" << QApplication::applicationName() << "\n";
output << "Exec=" << nativeApplicationPath() << "\n";
output << "X-GNOME-Autostart-enabled=true\n";
#else
Q_UNUSED(enabled)
#endif
}

View File

@@ -0,0 +1,9 @@
#ifndef AUTOSTART_H
#define AUTOSTART_H
namespace AutoStart
{
void setEnabled(bool enabled);
}
#endif // AUTOSTART_H

View File

@@ -1,18 +1,18 @@
#include "platformsystemproxybackend.h"
#include "utils/utils.h"
#include "infrastructure/platform/systemproxyfunctions.h"
bool PlatformSystemProxyBackend::hasConflict(const SystemProxyConfig &config)
{
return Utils::isSystemProxySet(config.httpPort, config.socksPort);
return PlatformSystemProxy::isSet(config.httpPort, config.socksPort);
}
void PlatformSystemProxyBackend::apply(const SystemProxyConfig &config)
{
Utils::setSystemProxy(config.httpPort, config.socksPort, config.bypass);
PlatformSystemProxy::set(config.httpPort, config.socksPort, config.bypass);
}
void PlatformSystemProxyBackend::clear()
{
Utils::clearSystemProxy();
PlatformSystemProxy::clear();
}

View File

@@ -0,0 +1,74 @@
#include "privileges.h"
#include <QCoreApplication>
#include <QStringList>
#if defined(Q_OS_WIN)
#include <shellapi.h>
#include <windows.h>
#elif defined(Q_OS_UNIX)
#include <unistd.h>
#endif
bool Privileges::isElevated()
{
#if defined(Q_OS_WIN)
BOOL isAdmin = FALSE;
PSID adminGroup = nullptr;
SID_IDENTIFIER_AUTHORITY ntAuthority = SECURITY_NT_AUTHORITY;
if (AllocateAndInitializeSid(
&ntAuthority,
2,
SECURITY_BUILTIN_DOMAIN_RID,
DOMAIN_ALIAS_RID_ADMINS,
0, 0, 0, 0, 0, 0,
&adminGroup
))
{
CheckTokenMembership(nullptr, adminGroup, &isAdmin);
FreeSid(adminGroup);
}
return isAdmin;
#elif defined(Q_OS_UNIX)
return geteuid() == 0;
#else
return false;
#endif
}
bool Privileges::relaunchElevated()
{
const QString program = QCoreApplication::applicationFilePath();
QStringList arguments = QCoreApplication::arguments();
if (!arguments.isEmpty())
{
arguments.removeFirst();
}
if (!arguments.contains("--connect"))
{
arguments << "--connect";
}
#if defined(Q_OS_WIN)
QStringList quotedArguments;
for (const QString &argument : arguments)
{
quotedArguments << (argument.contains(' ') ? "\"" + argument + "\"" : argument);
}
const QString joinedArguments = quotedArguments.join(' ');
const HINSTANCE result = ShellExecuteW(
nullptr,
L"runas",
reinterpret_cast<LPCWSTR>(program.utf16()),
reinterpret_cast<LPCWSTR>(joinedArguments.utf16()),
nullptr,
SW_SHOWNORMAL
);
return reinterpret_cast<INT_PTR>(result) > 32;
#else
Q_UNUSED(program)
Q_UNUSED(arguments)
return false;
#endif
}

View File

@@ -0,0 +1,10 @@
#ifndef PRIVILEGES_H
#define PRIVILEGES_H
namespace Privileges
{
bool isElevated();
bool relaunchElevated();
}
#endif // PRIVILEGES_H

View File

@@ -5,7 +5,7 @@
#include <QSettings>
#include <QStandardPaths>
#include <QThread>
#include "utils.h"
#include "systemproxyfunctions.h"
#if defined(Q_OS_WINDOWS)
#include "windows.h"
@@ -558,7 +558,7 @@ bool linuxIsSystemProxySet(int http_port, int socks_port)
return true;
}
bool Utils::isSystemProxySet(int http_port, int socks_port)
bool PlatformSystemProxy::isSet(int http_port, int socks_port)
{
#if defined(Q_OS_WINDOWS)
Q_UNUSED(socks_port)
@@ -587,7 +587,7 @@ bool Utils::isSystemProxySet(int http_port, int socks_port)
#endif
}
void Utils::setSystemProxy(int http_port, int socks_port, const QString &bypass)
void PlatformSystemProxy::set(int http_port, int socks_port, const QString &bypass)
{
#if defined(Q_OS_WINDOWS)
windowsSetProxyForAllConnections(
@@ -608,7 +608,7 @@ void Utils::setSystemProxy(int http_port, int socks_port, const QString &bypass)
#endif
}
void Utils::clearSystemProxy()
void PlatformSystemProxy::clear()
{
#if defined(Q_OS_WINDOWS)
windowsClearProxyForAllConnections();

View File

@@ -0,0 +1,13 @@
#ifndef SYSTEMPROXYFUNCTIONS_H
#define SYSTEMPROXYFUNCTIONS_H
#include <QString>
namespace PlatformSystemProxy
{
bool isSet(int httpPort = -1, int socksPort = -1);
void set(int httpPort, int socksPort, const QString &bypass);
void clear();
}
#endif // SYSTEMPROXYFUNCTIONS_H

View File

@@ -7,9 +7,26 @@
#include <QSettings>
#include <QStandardPaths>
ProfileManager::ProfileManager()
namespace
{
configRootPath = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
bool createEmptyFile(const QString &path)
{
const QFileInfo fileInfo(path);
if (fileInfo.exists())
{
return fileInfo.isFile();
}
QFile file(path);
return file.open(QIODevice::WriteOnly | QIODevice::NewOnly);
}
}
ProfileManager::ProfileManager(const QString &storageRoot)
{
configRootPath = storageRoot.isEmpty()
? QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)
: storageRoot;
profilesPath = QDir(configRootPath).filePath("profiles");
statePath = QDir(configRootPath).filePath("state.ini");
defaultProfilePath = QDir(configRootPath).filePath("config.ini");
@@ -42,8 +59,7 @@ QString ProfileManager::activeProfile() const
profileId = "";
if (!QFileInfo::exists(profilePath(profileId)))
{
QSettings defaultSettings(profilePath(profileId), QSettings::IniFormat);
defaultSettings.sync();
createEmptyFile(profilePath(profileId));
}
state.setValue("Profile/Active", profileId);
state.sync();
@@ -53,18 +69,16 @@ QString ProfileManager::activeProfile() const
bool ProfileManager::setActiveProfile(const QString &profileId) const
{
QString normalizedId = normalizeProfileId(profileId);
bool result = true;
if (normalizedId.isEmpty() || !QFileInfo::exists(profilePath(normalizedId)))
const QString normalizedId = normalizeProfileId(profileId);
if (!QFileInfo::exists(profilePath(normalizedId)))
{
result = false;
normalizedId = "";
return false;
}
QSettings state(statePath, QSettings::IniFormat);
state.setValue("Profile/Active", normalizedId);
state.sync();
return result;
return state.status() == QSettings::NoError;
}
QString ProfileManager::profilePath(const QString &profileId) const
@@ -95,8 +109,10 @@ QString ProfileManager::createProfile(const QString &requestedName, const QStrin
}
else
{
QSettings newSettings(path, QSettings::IniFormat);
newSettings.sync();
if (!createEmptyFile(path))
{
return {};
}
}
return normalizedId;

View File

@@ -0,0 +1,52 @@
#ifndef PROFILEMANAGER_H
#define PROFILEMANAGER_H
#include <QString>
#include <QStringList>
#include "application/profilebackend.h"
class ProfileManager : public ProfileBackend
{
public:
explicit ProfileManager(const QString &storageRoot = {});
QStringList listProfiles() const override;
QString activeProfile() const override;
bool setActiveProfile(const QString &profileId) const override;
QString profilePath(const QString &profileId) const override;
QString createProfile(
const QString &requestedName,
const QString &sourcePath = QString()
) const override;
bool renameProfile(const QString &oldId, const QString &newId) const override;
bool removeProfile(const QString &profileId) const override;
QString normalizeProfileId(const QString &name) const override;
bool autoStartEnabled() const;
void setAutoStartEnabled(bool enabled) const override;
bool silentStartEnabled() const override;
void setSilentStartEnabled(bool enabled) const;
private:
QString configRootPath;
QString profilesPath;
QString statePath;
QString defaultProfilePath;
QString ensureUniqueProfileId(const QString &baseId) const;
void ensureStorage() const;
};
#endif //PROFILEMANAGER_H

View File

@@ -0,0 +1,47 @@
#include "applicationpaths.h"
#include <QDir>
#include <QFile>
#include <QStandardPaths>
QString ApplicationPaths::clientDataFile(const QString &profileId)
{
QDir dataDirectory(
QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)
);
if (!dataDirectory.exists())
{
dataDirectory.mkpath(".");
}
QDir profileDirectory(dataDirectory.filePath("profiles/" + profileId));
if (!profileDirectory.exists())
{
profileDirectory.mkpath(".");
}
QFile clientData(profileDirectory.filePath("client-data.json"));
if (!clientData.exists() && clientData.open(QIODevice::WriteOnly))
{
clientData.write("{}");
clientData.close();
}
return clientData.fileName();
}
void ApplicationPaths::clearClientData(const QString &profileId)
{
QFile::remove(clientDataFile(profileId));
}
QString ApplicationPaths::logFile()
{
QDir logDirectory(
QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)
);
if (!logDirectory.exists())
{
logDirectory.mkpath(".");
}
return logDirectory.filePath("zjuconnect.log");
}

View File

@@ -0,0 +1,13 @@
#ifndef APPLICATIONPATHS_H
#define APPLICATIONPATHS_H
#include <QString>
namespace ApplicationPaths
{
QString clientDataFile(const QString &profileId);
void clearClientData(const QString &profileId);
QString logFile();
}
#endif // APPLICATIONPATHS_H

View File

@@ -0,0 +1,315 @@
#include "infrastructure/update/updatechecker.h"
#include <QApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QStringView>
#include <QVersionNumber>
#include "application/applicationconstants.h"
#include "infrastructure/coreprocess/coreexecutable.h"
namespace
{
enum class VersionSuffixKind
{
Prerelease,
Stable,
Hotfix
};
QString normalizedSuffix(const QString &version, qsizetype suffixIndex)
{
QString suffix = version.mid(suffixIndex);
while (!suffix.isEmpty() && QStringLiteral("-+_.").contains(suffix.front()))
{
suffix.removeFirst();
}
return suffix;
}
VersionSuffixKind suffixKind(const QString &suffix)
{
if (suffix.isEmpty())
{
return VersionSuffixKind::Stable;
}
if (suffix.startsWith(QStringLiteral("hotfix"), Qt::CaseInsensitive))
{
return VersionSuffixKind::Hotfix;
}
return VersionSuffixKind::Prerelease;
}
int compareSuffixes(const QString &currentSuffix, const QString &latestSuffix)
{
qsizetype currentIndex = 0;
qsizetype latestIndex = 0;
while (currentIndex < currentSuffix.size() &&
latestIndex < latestSuffix.size())
{
const bool currentIsDigit = currentSuffix.at(currentIndex).isDigit();
const bool latestIsDigit = latestSuffix.at(latestIndex).isDigit();
if (currentIsDigit && latestIsDigit)
{
qsizetype currentEnd = currentIndex;
qsizetype latestEnd = latestIndex;
while (currentEnd < currentSuffix.size() &&
currentSuffix.at(currentEnd).isDigit())
{
++currentEnd;
}
while (latestEnd < latestSuffix.size() &&
latestSuffix.at(latestEnd).isDigit())
{
++latestEnd;
}
qsizetype currentNumberStart = currentIndex;
qsizetype latestNumberStart = latestIndex;
while (currentNumberStart < currentEnd - 1 &&
currentSuffix.at(currentNumberStart) == '0')
{
++currentNumberStart;
}
while (latestNumberStart < latestEnd - 1 &&
latestSuffix.at(latestNumberStart) == '0')
{
++latestNumberStart;
}
const qsizetype currentLength = currentEnd - currentNumberStart;
const qsizetype latestLength = latestEnd - latestNumberStart;
if (currentLength != latestLength)
{
return currentLength < latestLength ? -1 : 1;
}
const QStringView currentNumber(currentSuffix);
const QStringView latestNumber(latestSuffix);
const int numberComparison =
currentNumber.mid(currentNumberStart, currentLength).compare(
latestNumber.mid(latestNumberStart, latestLength)
);
if (numberComparison != 0)
{
return numberComparison;
}
currentIndex = currentEnd;
latestIndex = latestEnd;
continue;
}
const QChar currentCharacter = currentSuffix.at(currentIndex).toLower();
const QChar latestCharacter = latestSuffix.at(latestIndex).toLower();
if (currentCharacter != latestCharacter)
{
return currentCharacter < latestCharacter ? -1 : 1;
}
++currentIndex;
++latestIndex;
}
if (currentIndex == currentSuffix.size() &&
latestIndex == latestSuffix.size())
{
return 0;
}
return currentIndex == currentSuffix.size() ? -1 : 1;
}
}
UpdateChecker::UpdateChecker(QObject *parent)
: QObject(parent),
versions{
QApplication::applicationVersion(),
QStringLiteral("正在检查"),
QStringLiteral("未知"),
QStringLiteral("正在检查")
},
uiNetworkManager(new QNetworkAccessManager(this)),
coreNetworkManager(new QNetworkAccessManager(this))
{
connect(uiNetworkManager, &QNetworkAccessManager::finished,
this, &UpdateChecker::handleUiReply);
connect(coreNetworkManager, &QNetworkAccessManager::finished,
this, &UpdateChecker::handleCoreReply);
}
const VersionInfo &UpdateChecker::versionInfo() const
{
return versions;
}
void UpdateChecker::markDisabled()
{
versions.uiLatest = QStringLiteral("已禁用");
versions.coreLatest = QStringLiteral("已禁用");
emit versionInfoChanged(versions);
}
void UpdateChecker::check()
{
try
{
versions.coreVersion = CoreExecutable::version(this);
qInfo().noquote() << "检查核心版本成功:" + versions.coreVersion;
}
catch (const std::runtime_error &error)
{
qWarning().noquote() << "检查核心版本失败:" + QString(error.what());
versions.coreVersion = QStringLiteral("错误");
}
emit versionInfoChanged(versions);
uiNetworkManager->get(QNetworkRequest(
QUrl(
"https://api.github.com/repos/" +
ApplicationConstants::RepositoryName +
"/releases/latest"
)
));
coreNetworkManager->get(QNetworkRequest(
QUrl(
"https://api.github.com/repos/" +
ApplicationConstants::CoreRepositoryName +
"/releases/latest"
)
));
}
QString UpdateChecker::normalizeVersionTag(const QString &tag)
{
return tag.startsWith('v') ? tag.mid(1) : tag;
}
std::optional<QString> UpdateChecker::parseReleaseVersion(
const QByteArray &payload
)
{
QJsonParseError parseError;
const QJsonDocument document =
QJsonDocument::fromJson(payload, &parseError);
if (parseError.error != QJsonParseError::NoError || !document.isObject())
{
return std::nullopt;
}
const QJsonValue tagValue = document.object().value("tag_name");
if (!tagValue.isString())
{
return std::nullopt;
}
const QString version =
normalizeVersionTag(tagValue.toString().trimmed());
return version.isEmpty()
? std::nullopt
: std::optional<QString>(version);
}
bool UpdateChecker::isNewerVersion(const QString &currentVersion, const QString &latestVersion)
{
qsizetype currentSuffix = 0;
qsizetype latestSuffix = 0;
const QVersionNumber current = QVersionNumber::fromString(currentVersion, &currentSuffix);
const QVersionNumber latest = QVersionNumber::fromString(latestVersion, &latestSuffix);
if (latest != current)
{
return latest > current;
}
const QString currentSuffixText =
normalizedSuffix(currentVersion, currentSuffix);
const QString latestSuffixText =
normalizedSuffix(latestVersion, latestSuffix);
const VersionSuffixKind currentKind = suffixKind(currentSuffixText);
const VersionSuffixKind latestKind = suffixKind(latestSuffixText);
if (latestKind != currentKind)
{
return latestKind > currentKind;
}
return compareSuffixes(currentSuffixText, latestSuffixText) < 0;
}
void UpdateChecker::handleUiReply(QNetworkReply *reply)
{
if (reply->error() != QNetworkReply::NoError)
{
const QString reason = reply->errorString();
qWarning().noquote() << "检查 UI 更新失败。原因是:" + reason;
versions.uiLatest = QStringLiteral("检查失败");
reply->deleteLater();
emit checkFailed(UpdateComponent::Ui, reason);
emit versionInfoChanged(versions);
return;
}
const std::optional<QString> latestVersion =
parseReleaseVersion(reply->readAll());
reply->deleteLater();
if (!latestVersion.has_value())
{
const QString reason =
QStringLiteral("响应中缺少有效的版本标签");
qWarning().noquote() << "检查 UI 更新失败。原因是:" + reason;
versions.uiLatest = QStringLiteral("检查失败");
emit checkFailed(UpdateComponent::Ui, reason);
emit versionInfoChanged(versions);
return;
}
qInfo().noquote()
<< "检查 UI 更新成功。最新版本:" + *latestVersion;
versions.uiLatest = *latestVersion;
emit versionInfoChanged(versions);
if (isNewerVersion(versions.uiVersion, *latestVersion))
{
emit uiUpdateAvailable(*latestVersion);
}
}
void UpdateChecker::handleCoreReply(QNetworkReply *reply)
{
if (reply->error() != QNetworkReply::NoError)
{
const QString reason = reply->errorString();
qWarning().noquote() << "检查核心更新失败。原因是:" + reason;
versions.coreLatest = QStringLiteral("检查失败");
reply->deleteLater();
emit checkFailed(UpdateComponent::Core, reason);
emit versionInfoChanged(versions);
return;
}
const std::optional<QString> latestVersion =
parseReleaseVersion(reply->readAll());
reply->deleteLater();
if (!latestVersion.has_value())
{
const QString reason =
QStringLiteral("响应中缺少有效的版本标签");
qWarning().noquote() << "检查核心更新失败。原因是:" + reason;
versions.coreLatest = QStringLiteral("检查失败");
emit checkFailed(UpdateComponent::Core, reason);
emit versionInfoChanged(versions);
return;
}
qInfo().noquote()
<< "检查核心更新成功。最新版本:" + *latestVersion;
versions.coreLatest = *latestVersion;
emit versionInfoChanged(versions);
if (isNewerVersion(versions.coreVersion, *latestVersion))
{
qInfo().noquote() << "核心版本存在更新,可手动更新或通知开发者更新。";
}
}

View File

@@ -0,0 +1,56 @@
#ifndef UPDATECHECKER_H
#define UPDATECHECKER_H
#include <QByteArray>
#include <QObject>
#include <QString>
#include <optional>
class QNetworkAccessManager;
class QNetworkReply;
struct VersionInfo
{
QString uiVersion;
QString uiLatest;
QString coreVersion;
QString coreLatest;
};
enum class UpdateComponent
{
Ui,
Core
};
class UpdateChecker : public QObject
{
Q_OBJECT
public:
explicit UpdateChecker(QObject *parent = nullptr);
const VersionInfo &versionInfo() const;
void markDisabled();
void check();
static QString normalizeVersionTag(const QString &tag);
static std::optional<QString> parseReleaseVersion(const QByteArray &payload);
static bool isNewerVersion(const QString &currentVersion, const QString &latestVersion);
signals:
void versionInfoChanged(const VersionInfo &versionInfo);
void checkFailed(UpdateComponent component, const QString &reason);
void uiUpdateAvailable(const QString &version);
private:
void handleUiReply(QNetworkReply *reply);
void handleCoreReply(QNetworkReply *reply);
VersionInfo versions;
QNetworkAccessManager *uiNetworkManager;
QNetworkAccessManager *coreNetworkManager;
};
#endif // UPDATECHECKER_H

View File

@@ -1,10 +1,14 @@
#include <QApplication>
#include <QDebug>
#include <QLibraryInfo>
#include <QLocale>
#include <QTranslator>
#include "SingleApplication"
#include "application/applicationlogger.h"
#include "mainwindow.h"
#include "utils/utils.h"
#include "application/applicationconstants.h"
#include "presentation/main/mainwindow.h"
#ifndef PROJ_VER
#define PROJ_VER "unknown"
@@ -13,8 +17,8 @@
int main(int argc, char *argv[])
{
SingleApplication app(argc, argv, false, SingleApplication::Mode::System);
QApplication::setApplicationName(Utils::APP_NAME);
QApplication::setApplicationDisplayName(Utils::APP_NAME);
QApplication::setApplicationName(ApplicationConstants::ApplicationName);
QApplication::setApplicationDisplayName(ApplicationConstants::ApplicationName);
QApplication::setApplicationVersion(PROJ_VER);
QLocale::setDefault(QLocale(QLocale::Chinese, QLocale::SimplifiedChineseScript, QLocale::China));

View File

@@ -0,0 +1,209 @@
#include "authdialogcoordinator.h"
#include <QCheckBox>
#include <QDebug>
#include <QDialog>
#include <QDialogButtonBox>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QSettings>
#include <QUrlQuery>
#include <QVBoxLayout>
#include <QWidget>
#include "presentation/dialogs/graphcaptchawindow/graphcaptchawindow.h"
#include "presentation/dialogs/loginwindow/loginwindow.h"
#include "presentation/dialogs/ssologinwebview/ssologinwebview.h"
#include "presentation/dialogs/sudowindow/sudowindow.h"
AuthDialogCoordinator::AuthDialogCoordinator(
QWidget *parentWidget,
QSettings *settings,
QObject *parent
)
: QObject(parent),
parentWidget(parentWidget),
settings(settings)
{
}
void AuthDialogCoordinator::setSettings(QSettings *newSettings)
{
settings = newSettings;
}
void AuthDialogCoordinator::requestLogin(
const QString &username,
const QString &password
)
{
if (loginWindow != nullptr)
{
loginWindow->raise();
loginWindow->activateWindow();
return;
}
loginWindow = new LoginWindow(parentWidget);
loginWindow->setAttribute(Qt::WA_DeleteOnClose);
loginWindow->setDetail(username, password);
connect(loginWindow, &LoginWindow::login, this,
&AuthDialogCoordinator::loginSubmitted);
loginWindow->show();
}
void AuthDialogCoordinator::requestSudoPassword()
{
if (sudoWindow != nullptr)
{
sudoWindow->raise();
sudoWindow->activateWindow();
return;
}
sudoWindow = new SudoWindow(parentWidget);
sudoWindow->setAttribute(Qt::WA_DeleteOnClose);
connect(sudoWindow, &SudoWindow::sudo, this,
&AuthDialogCoordinator::sudoPasswordSubmitted);
connect(sudoWindow, &QDialog::rejected, this,
[this]()
{
emit sudoPasswordSubmitted({}, false);
});
sudoWindow->show();
}
void AuthDialogCoordinator::requestGraphCaptcha(const QString &graphFile)
{
qInfo().noquote() << "需要图形验证码";
if (graphCaptchaWindow != nullptr)
{
graphCaptchaWindow->setGraph(graphFile);
graphCaptchaWindow->raise();
graphCaptchaWindow->activateWindow();
return;
}
graphCaptchaWindow = new GraphCaptchaWindow(parentWidget);
graphCaptchaWindow->setAttribute(Qt::WA_DeleteOnClose);
graphCaptchaWindow->setGraph(graphFile);
connect(graphCaptchaWindow, &GraphCaptchaWindow::finishCaptcha, this,
[this](const QByteArray &captcha)
{
qInfo().noquote() << "图形验证码已提交";
emit interactiveInputSubmitted(captcha + "\n");
});
graphCaptchaWindow->show();
}
void AuthDialogCoordinator::requestSmsCode(bool showSkipSecondaryAuthOption)
{
qInfo().noquote() << "需要短信验证码";
QDialog dialog(parentWidget);
dialog.setWindowTitle("短信验证码");
auto *layout = new QVBoxLayout(&dialog);
layout->addWidget(new QLabel("请输入短信验证码:", &dialog));
auto *codeEdit = new QLineEdit(&dialog);
layout->addWidget(codeEdit);
auto *skipCheckBox = new QCheckBox("跳过以后的短信验证", &dialog);
skipCheckBox->setVisible(showSkipSecondaryAuthOption);
layout->addWidget(skipCheckBox);
auto *buttonBox = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel,
&dialog
);
connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
layout->addWidget(buttonBox);
const bool accepted = dialog.exec() == QDialog::Accepted;
QByteArray input;
if (accepted)
{
input = codeEdit->text().toLocal8Bit();
if (showSkipSecondaryAuthOption && skipCheckBox->isChecked())
{
input.prepend('$');
}
}
qInfo().noquote() << (accepted ? "短信验证码已提交" : "短信验证码输入已取消");
emit interactiveInputSubmitted(input + "\n");
}
void AuthDialogCoordinator::requestTotpCode()
{
qInfo().noquote() << "需要 TOTP 验证码";
bool accepted = false;
const QString totp = QInputDialog::getText(
parentWidget,
"TOTP 验证码",
"请输入 TOTP 验证码:",
QLineEdit::Normal,
"",
&accepted
);
qInfo().noquote() << (accepted ? "TOTP 验证码已提交" : "TOTP 验证码输入已取消");
emit interactiveInputSubmitted(totp.toLocal8Bit() + "\n");
}
void AuthDialogCoordinator::requestSsoLogin()
{
if (settings == nullptr)
{
return;
}
if (ssoLoginWebView != nullptr)
{
ssoLoginWebView->raise();
ssoLoginWebView->activateWindow();
return;
}
const QString serverHost =
settings->value("ZJUConnect/ServerAddress", "trust.hitsz.edu.cn").toString();
const int serverPort = settings->value("ZJUConnect/ServerPort", 443).toInt();
QUrl serverUrl;
serverUrl.setScheme("https");
serverUrl.setHost(serverHost);
if (serverPort != 443)
{
serverUrl.setPort(serverPort);
}
QString ssoUrl = settings->value("ZJUConnect/LoginURL").toString();
if (ssoUrl.isEmpty())
{
QUrl defaultSsoUrl = serverUrl;
defaultSsoUrl.setPath("/passport/v1/public/casLogin");
QUrlQuery query;
query.addQueryItem(
"sfDomain",
settings->value("ZJUConnect/LoginDomain").toString()
);
defaultSsoUrl.setQuery(query);
ssoUrl = defaultSsoUrl.toString();
}
if (ssoUrl.startsWith('/'))
{
ssoUrl = serverUrl.resolved(QUrl(ssoUrl)).toString();
}
qInfo().noquote() << QStringLiteral("单点登录:") + ssoUrl;
ssoLoginWebView = new SsoLoginWebView(parentWidget);
ssoLoginWebView->setAttribute(Qt::WA_DeleteOnClose);
ssoLoginWebView->setCallbackServerUrl(serverUrl);
ssoLoginWebView->setInitialUrl(QUrl::fromUserInput(ssoUrl));
connect(ssoLoginWebView, &SsoLoginWebView::loginCompleted, this,
[this](const QString &url)
{
emit interactiveInputSubmitted(url.toLocal8Bit() + "\n");
});
ssoLoginWebView->show();
}

View File

@@ -0,0 +1,47 @@
#ifndef AUTHDIALOGCOORDINATOR_H
#define AUTHDIALOGCOORDINATOR_H
#include <QObject>
#include <QPointer>
class GraphCaptchaWindow;
class LoginWindow;
class QSettings;
class SsoLoginWebView;
class SudoWindow;
class QWidget;
class AuthDialogCoordinator : public QObject
{
Q_OBJECT
public:
explicit AuthDialogCoordinator(
QWidget *parentWidget,
QSettings *settings,
QObject *parent = nullptr
);
void setSettings(QSettings *settings);
void requestLogin(const QString &username, const QString &password);
void requestSudoPassword();
void requestGraphCaptcha(const QString &graphFile);
void requestSmsCode(bool showSkipSecondaryAuthOption);
void requestTotpCode();
void requestSsoLogin();
signals:
void loginSubmitted(const QString &username, const QString &password, bool saveDetails);
void sudoPasswordSubmitted(const QString &password, bool remember);
void interactiveInputSubmitted(const QByteArray &input);
private:
QWidget *parentWidget;
QSettings *settings;
QPointer<LoginWindow> loginWindow;
QPointer<SudoWindow> sudoWindow;
QPointer<GraphCaptchaWindow> graphCaptchaWindow;
QPointer<SsoLoginWebView> ssoLoginWebView;
};
#endif // AUTHDIALOGCOORDINATOR_H

View File

@@ -0,0 +1,345 @@
#include "connectionuicontroller.h"
#include <QAction>
#include <QApplication>
#include <QCheckBox>
#include <QDebug>
#include <QMessageBox>
#include <QPushButton>
#include <QSettings>
#include <QWidget>
#include <utility>
#include "application/applicationlogger.h"
#include "application/connectionsession.h"
#include "application/systemproxysession.h"
#include "infrastructure/coreprocess/coreexecutable.h"
#include "infrastructure/logging/corelogfile.h"
#include "infrastructure/platform/privileges.h"
#include "infrastructure/settings/settingsprofileloader.h"
#include "presentation/coordinators/authdialogcoordinator.h"
ConnectionUiController::ConnectionUiController(
QWidget *parentWidget,
QPushButton *connectButton,
QPushButton *proxyButton,
QAction *trayConnectAction,
ConnectionSession *connectionSession,
SystemProxySession *systemProxySession,
AuthDialogCoordinator *authenticationDialogs,
ApplicationLogger *applicationLogger,
CoreLogFile *coreLogFile,
SettingsProvider settingsProvider,
ProfileIdProvider profileIdProvider,
NotificationHandler notificationHandler,
QObject *parent
)
: QObject(parent),
parentWidget(parentWidget),
connectButton(connectButton),
proxyButton(proxyButton),
trayConnectAction(trayConnectAction),
connectionSession(connectionSession),
systemProxySession(systemProxySession),
authenticationDialogs(authenticationDialogs),
settingsProvider(std::move(settingsProvider)),
profileIdProvider(std::move(profileIdProvider)),
notificationHandler(std::move(notificationHandler))
{
connectButton->setText("连接服务器");
trayConnectAction->setText("连接服务器");
proxyButton->setText("设置系统代理");
proxyButton->hide();
connect(
connectionSession,
&ConnectionSession::outputRead,
applicationLogger,
&ApplicationLogger::appendCoreOutput
);
connect(
connectionSession,
&ConnectionSession::outputRead,
coreLogFile,
&CoreLogFile::appendOutput
);
connect(
connectionSession,
&ConnectionSession::savedSudoPasswordRejected,
this,
[]() { qWarning().noquote() << "sudo 密码可能有误,不使用记住的密码"; }
);
connect(
authenticationDialogs,
&AuthDialogCoordinator::loginSubmitted,
this,
[this](const QString &username, const QString &password, bool saveDetails)
{
if (saveDetails)
{
settings()->setValue("Credential/Username", username);
settings()->setValue(
"Credential/Password",
QString(password.toUtf8().toBase64())
);
settings()->sync();
}
startConnection(username, password);
}
);
connect(
connectionSession,
&ConnectionSession::reconnectScheduled,
this,
[](int) { qInfo().noquote() << "正在尝试重新连接..."; }
);
connect(
connectionSession,
&ConnectionSession::finished,
this,
[this](ZJU_ERROR error)
{
qInfo().noquote() << "VPN 断开!";
if (error != ZJU_ERROR::NONE)
{
this->notificationHandler(
"VPN",
"VPN 意外断开!",
QSystemTrayIcon::MessageIcon::Warning
);
}
this->connectButton->setText("连接服务器");
this->trayConnectAction->setText("连接服务器");
this->proxyButton->hide();
showConnectionError(error);
}
);
connect(
connectButton,
&QPushButton::clicked,
this,
&ConnectionUiController::handleConnectClicked
);
connect(
proxyButton,
&QPushButton::clicked,
this,
&ConnectionUiController::handleProxyClicked
);
}
QSettings *ConnectionUiController::settings() const
{
return settingsProvider();
}
void ConnectionUiController::handleConnectClicked()
{
if (connectionSession->isActive())
{
connectionSession->stop();
return;
}
if (settings()->contains("ZJUConnect/ServerAddress") &&
settings()->value("ZJUConnect/ServerAddress").toString().isEmpty())
{
QMessageBox::critical(parentWidget, "错误", "服务器地址不能为空");
return;
}
const QString username = settings()->value("Credential/Username", "").toString();
const QString password = QByteArray::fromBase64(
settings()->value("Credential/Password", "").toString().toUtf8()
);
const QString protocol =
settings()->value("ZJUConnect/Protocol", "easyconnect").toString();
const QString authType =
settings()->value("ZJUConnect/AuthType", "psw").toString();
#if defined(Q_OS_WIN)
if (settings()->value("ZJUConnect/TUNMode").toBool() &&
!Privileges::isElevated())
{
if (Privileges::relaunchElevated())
{
QApplication::quit();
}
else
{
QMessageBox::warning(
parentWidget,
"提升失败",
"无法以管理员权限重新启动,请手动以管理员方式运行。"
);
}
return;
}
#endif
const bool passwordLogin =
(protocol == "atrust" && authType == "psw") ||
(protocol == "easyconnect" &&
settings()->value("Credential/CertFile", "").toString().isEmpty());
if (passwordLogin && (username.isEmpty() || password.isEmpty()))
{
authenticationDialogs->requestLogin(username, password);
return;
}
startConnection(username, password);
}
void ConnectionUiController::handleProxyClicked()
{
if (systemProxySession->isBusy())
{
return;
}
if (systemProxySession->isEnabled())
{
systemProxySession->disable();
return;
}
const int httpPort = settings()->value("ZJUConnect/HTTPPort").toInt();
const int socksPort = settings()->value("ZJUConnect/SOCKS5Port").toInt();
const SystemProxyConfig proxyConfig{
httpPort,
socksPort,
settings()->value("Common/SystemProxyBypass").toString()
};
connect(
systemProxySession,
&SystemProxySession::conflictCheckFinished,
this,
[this, proxyConfig, httpPort, socksPort](bool conflict)
{
if (!connectionSession->isActive())
{
return;
}
if (conflict &&
!settings()->value(
"Common/SuppressProxyOverrideWarning",
false
).toBool())
{
QMessageBox messageBox(
QMessageBox::Warning,
"警告",
"当前已存在系统代理配置(可能是 Clash 或其它代理软件)\n"
"是否覆盖当前系统代理配置?",
QMessageBox::Yes | QMessageBox::No,
parentWidget
);
auto *dontShowCheckBox = new QCheckBox("不再提示");
messageBox.setCheckBox(dontShowCheckBox);
if (messageBox.exec() == QMessageBox::No)
{
return;
}
if (dontShowCheckBox->isChecked())
{
settings()->setValue(
"Common/SuppressProxyOverrideWarning",
true
);
settings()->sync();
}
}
else if (conflict)
{
qInfo().noquote() << "跳过系统代理覆盖警告,因为已设置了不再提示";
}
qInfo().noquote()
<< "设置系统代理HTTP端口 " + QString::number(httpPort)
+ "SOCKS5 端口 " + QString::number(socksPort);
systemProxySession->enable(proxyConfig);
},
Qt::SingleShotConnection
);
systemProxySession->checkConflict(proxyConfig);
}
void ConnectionUiController::startConnection(
const QString &username,
const QString &password
)
{
ConnectionProfile profile = SettingsProfileLoader::load(
*settings(),
profileIdProvider(),
username,
password
);
profile.program = CoreExecutable::path();
const ReconnectPolicy reconnectPolicy{
settings()->value("Common/AutoReconnect", false).toBool(),
settings()->value("Common/ReconnectTime", 1).toInt() * 1000
};
if (!connectionSession->start(profile, reconnectPolicy)
|| !connectionSession->isActive())
{
return;
}
connectButton->setText("断开服务器");
trayConnectAction->setText("断开服务器");
proxyButton->show();
if (settings()->value("Common/AutoSetProxy", false).toBool())
{
proxyButton->click();
}
}
void ConnectionUiController::showConnectionError(ZJU_ERROR error)
{
QString message;
switch (error)
{
case ZJU_ERROR::INVALID_DETAIL:
message = "登录失败!\n请检查设置中的网络账号和密码是否设置正确。";
break;
case ZJU_ERROR::BRUTE_FORCE:
message = "登录失败!\n登录尝试过于频繁IP 被风控,请稍后重试或换用 EasyConnect。";
break;
case ZJU_ERROR::OTHER_LOGIN_FAILED:
message = "登录失败!\n未知原因,可将日志反馈给开发者以便调查。";
break;
case ZJU_ERROR::ACCESS_DENIED:
message = "权限不足!\n请关闭程序,点击右键以管理员身份运行。";
break;
case ZJU_ERROR::LISTEN_FAILED:
message = "监听失败!\n请关闭占用端口的程序(如残留的 zju-connect.exe或者监听其它端口。";
break;
case ZJU_ERROR::CLIENT_FAILED:
message = "连接失败!\n可能是响应超时,请检查本地网络配置是否正常,服务器设置是否正确。";
break;
case ZJU_ERROR::CAPTCHA_FAILED:
message = "登录失败!\n验证码问题,可能是已验证码过期或者有误。";
break;
case ZJU_ERROR::PROGRAM_NOT_FOUND:
message = "程序未找到!\n请检查核心是否在正确路径下,检查是否解压在当前目录下。";
break;
case ZJU_ERROR::INTERACTIVE_ERROR:
message = "登录失败!\n请检查您的输入是否正确,检查是否完成 SSO 登录。";
break;
case ZJU_ERROR::AUTH_NOT_AVAILABLE:
message = "认证方式/登录域不可用!\n请通过“获取认证方式”按钮配置认证方式。";
break;
case ZJU_ERROR::AUTH_EXPIRED:
message = "认证已过期!\n请重新登录。";
break;
case ZJU_ERROR::OTHER:
message = "其它错误!\n未知原因,可将日志反馈给开发者以便调查。";
break;
case ZJU_ERROR::NONE:
return;
}
QMessageBox::critical(parentWidget, "错误", message);
}

View File

@@ -0,0 +1,69 @@
#ifndef CONNECTIONUICONTROLLER_H
#define CONNECTIONUICONTROLLER_H
#include <QObject>
#include <QSystemTrayIcon>
#include <functional>
#include "core/connectionerror.h"
class QAction;
class ApplicationLogger;
class AuthDialogCoordinator;
class ConnectionSession;
class CoreLogFile;
class QPushButton;
class QSettings;
class SystemProxySession;
class QWidget;
class ConnectionUiController : public QObject
{
Q_OBJECT
public:
using SettingsProvider = std::function<QSettings *()>;
using ProfileIdProvider = std::function<QString()>;
using NotificationHandler = std::function<void(
const QString &title,
const QString &content,
QSystemTrayIcon::MessageIcon icon
)>;
ConnectionUiController(
QWidget *parentWidget,
QPushButton *connectButton,
QPushButton *proxyButton,
QAction *trayConnectAction,
ConnectionSession *connectionSession,
SystemProxySession *systemProxySession,
AuthDialogCoordinator *authenticationDialogs,
ApplicationLogger *applicationLogger,
CoreLogFile *coreLogFile,
SettingsProvider settingsProvider,
ProfileIdProvider profileIdProvider,
NotificationHandler notificationHandler,
QObject *parent = nullptr
);
private:
void handleConnectClicked();
void handleProxyClicked();
void startConnection(const QString &username, const QString &password);
void showConnectionError(ZJU_ERROR error);
QSettings *settings() const;
QWidget *parentWidget;
QPushButton *connectButton;
QPushButton *proxyButton;
QAction *trayConnectAction;
ConnectionSession *connectionSession;
SystemProxySession *systemProxySession;
AuthDialogCoordinator *authenticationDialogs;
SettingsProvider settingsProvider;
ProfileIdProvider profileIdProvider;
NotificationHandler notificationHandler;
};
#endif // CONNECTIONUICONTROLLER_H

View File

@@ -1,6 +1,9 @@
#include "authinfowindow.h"
#include "utils/utils.h"
#include "infrastructure/coreprocess/consoleoutputdecoder.h"
#include "infrastructure/coreprocess/coreexecutable.h"
#include <QMetaEnum>
#include <QDebug>
#include <QJsonArray>
@@ -36,7 +39,7 @@ AuthInfoWindow::AuthInfoWindow(QWidget *parent)
[this]() { stdoutBuf_ += proc_->readAllStandardError(); });
connect(proc_, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this,
[this](int exitCode, QProcess::ExitStatus exitStatus) {
QString output = Utils::consoleOutputToQString(stdoutBuf_);
QString output = ConsoleOutputDecoder::decode(stdoutBuf_);
qInfo().noquote() << "可用认证方式:\n" + output;
QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8(), &jsonError);
@@ -75,7 +78,7 @@ void AuthInfoWindow::fetchAuthInfo(const QString& serverAddress, int port)
{
stdoutBuf_.clear();
stderrBuf_.clear();
proc_->start(Utils::getCorePath(),
proc_->start(CoreExecutable::path(),
{"-protocol", "atrust", "-server", serverAddress, "-port", QString::number(port), "-auth-info"});
qInfo().noquote() << "正在获取可用认证的方式...";
}

View File

@@ -33,7 +33,7 @@
<customwidget>
<class>CaptchaCanvas</class>
<extends>QWidget</extends>
<header>graphcaptchawindow/captchacanvas.h</header>
<header>presentation/dialogs/graphcaptchawindow/captchacanvas.h</header>
</customwidget>
</customwidgets>
<resources/>

View File

@@ -1,6 +1,6 @@
#include "loginwindow.h"
#include "utils/utils.h"
#include "presentation/presentationhelpers.h"
#include <QMessageBox>
#include <QPushButton>
@@ -18,7 +18,10 @@ LoginWindow::LoginWindow(QWidget *parent)
connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked,
[&]()
{
if (!Utils::credentialCheck(ui->usernameLineEdit->text(), ui->passwordLineEdit->text()))
if (!PresentationHelpers::confirmCredentials(
ui->usernameLineEdit->text(),
ui->passwordLineEdit->text()
))
return;
emit login(ui->usernameLineEdit->text(), ui->passwordLineEdit->text(), ui->saveLoginDetailCheckBox->isChecked());
emit accept();
@@ -53,4 +56,3 @@ void LoginWindow::keyPressEvent(QKeyEvent *event)
else
QDialog::keyPressEvent(event);
}

View File

@@ -7,8 +7,12 @@
#include "settingwindow.h"
#include "ui_settingwindow.h"
#include "utils/utils.h"
#include "utils/profilemanager.h"
#include "application/applicationconstants.h"
#include "application/defaultsettings.h"
#include "infrastructure/platform/autostart.h"
#include "infrastructure/storage/applicationpaths.h"
#include "presentation/presentationhelpers.h"
#include "infrastructure/settings/profilemanager.h"
SettingWindow::SettingWindow(QWidget *parent, QSettings *inputSettings, const QString &profileId) :
QDialog(parent),
@@ -44,19 +48,25 @@ SettingWindow::SettingWindow(QWidget *parent, QSettings *inputSettings, const QS
});
connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, [&]() {
if (shouldCheckCredential() && !Utils::credentialCheck(ui->usernameLineEdit->text(), ui->passwordLineEdit->text()))
if (shouldCheckCredential() && !PresentationHelpers::confirmCredentials(
ui->usernameLineEdit->text(),
ui->passwordLineEdit->text()
))
return;
if (isAuthSettingChanged())
Utils::clearClientData(this->profileId);
ApplicationPaths::clearClientData(this->profileId);
applySettings();
accept();
});
connect(ui->buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, [&]() {
if (shouldCheckCredential() && !Utils::credentialCheck(ui->usernameLineEdit->text(), ui->passwordLineEdit->text()))
if (shouldCheckCredential() && !PresentationHelpers::confirmCredentials(
ui->usernameLineEdit->text(),
ui->passwordLineEdit->text()
))
return;
if (isAuthSettingChanged())
Utils::clearClientData(this->profileId);
ApplicationPaths::clearClientData(this->profileId);
applySettings();
loadSettings();
});
@@ -68,7 +78,7 @@ SettingWindow::SettingWindow(QWidget *parent, QSettings *inputSettings, const QS
if (status == QMessageBox::Ok)
{
settings->clear();
Utils::resetDefaultSettings(*settings);
DefaultSettings::reset(*settings);
settings->sync();
loadSettings();
}
@@ -185,7 +195,8 @@ void SettingWindow::loadSettings()
{
ui->configVersionLabel->setText(
"当前配置文件版本:" + QString::number(settings->value("Common/ConfigVersion").toInt()) +
"\n程序配置文件版本:" + QString::number(Utils::CONFIG_VERSION)
"\n程序配置文件版本:" +
QString::number(ApplicationConstants::ConfigVersion)
);
ui->usernameLineEdit->setText(settings->value("Credential/Username").toString());
ui->passwordLineEdit->setText(
@@ -280,7 +291,7 @@ void SettingWindow::applySettings()
bool oldAutoStart = profileManager.autoStartEnabled();
bool newAutoStart = ui->autoStartCheckBox->isChecked();
if (oldAutoStart != newAutoStart)
Utils::setAutoStart(ui->autoStartCheckBox->isChecked());
AutoStart::setEnabled(ui->autoStartCheckBox->isChecked());
profileManager.setAutoStartEnabled(newAutoStart);
profileManager.setSilentStartEnabled(ui->silentStartCheckBox->isChecked());
@@ -355,7 +366,10 @@ void SettingWindow::applySettings()
settings->setValue("ZJUConnect/CustomProxyDomain", customProxyDomain);
settings->setValue("ZJUConnect/ExtraArguments", extraArguments);
settings->setValue("Common/ConfigVersion", Utils::CONFIG_VERSION);
settings->setValue(
"Common/ConfigVersion",
ApplicationConstants::ConfigVersion
);
settings->sync();
}

View File

@@ -4,8 +4,8 @@
#include <QDialog>
#include <QSettings>
#include "ui_settingwindow.h"
#include "extrasettingwindow/extrasettingwindow.h"
#include "authinfowindow/authinfowindow.h"
#include "presentation/dialogs/extrasettingwindow/extrasettingwindow.h"
#include "presentation/dialogs/authinfowindow/authinfowindow.h"
namespace Ui
{

View File

@@ -55,7 +55,7 @@ void SsoLoginWebView::setupConnections()
connect(ui->webEngineView->page(), &QWebEnginePage::navigationRequested, this,
[&](QWebEngineNavigationRequest &request) {
if (request.navigationType() == QWebEngineNavigationRequest::NavigationType::RedirectNavigation &&
request.url().host() == callbackServerHost)
isCallbackUrl(request.url()))
{
if (!loginCompletedEmitted)
{
@@ -79,9 +79,28 @@ void SsoLoginWebView::setInitialUrl(const QUrl &url)
}
}
void SsoLoginWebView::setCallbackServerHost(const QString &host)
void SsoLoginWebView::setCallbackServerUrl(const QUrl &url)
{
callbackServerHost = host;
callbackServerUrl = url;
}
bool SsoLoginWebView::isCallbackUrl(const QUrl &url) const
{
const auto effectivePort = [](const QUrl &value)
{
const int defaultPort =
value.scheme().compare("https", Qt::CaseInsensitive) == 0 ? 443 : 80;
return value.port(defaultPort);
};
return url.scheme().compare(
callbackServerUrl.scheme(),
Qt::CaseInsensitive
) == 0
&& url.host().compare(
callbackServerUrl.host(),
Qt::CaseInsensitive
) == 0
&& effectivePort(url) == effectivePort(callbackServerUrl);
}
QUrl SsoLoginWebView::currentUrl() const

View File

@@ -20,7 +20,7 @@ public:
~SsoLoginWebView() override;
void setInitialUrl(const QUrl &url);
void setCallbackServerHost(const QString &host);
void setCallbackServerUrl(const QUrl &url);
QUrl currentUrl() const;
signals:
@@ -31,9 +31,10 @@ protected:
private:
void setupConnections();
bool isCallbackUrl(const QUrl &url) const;
Ui::SsoLoginWebView *ui;
QString callbackServerHost;
QUrl callbackServerUrl;
bool loginCompletedEmitted = false;
};

View File

@@ -1,6 +1,4 @@
#include <QMessageBox>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSysInfo>
#include <QNetworkInterface>
#include <QClipboard>
@@ -14,42 +12,76 @@
#include "mainwindow.h"
#include "application/applicationlogger.h"
#include "infrastructure/corelogfile.h"
#include "application/applicationconstants.h"
#include "application/commandlineoptions.h"
#include "application/settingsmigrator.h"
#include "infrastructure/coreprocess/devicetrust.h"
#include "infrastructure/logging/corelogfile.h"
#include "infrastructure/storage/applicationpaths.h"
#include "infrastructure/update/updatechecker.h"
#include "presentation/coordinators/connectionuicontroller.h"
#include "presentation/presentationhelpers.h"
#include "ui_mainwindow.h"
#include "utils/utils.h"
#include "zjuconnectcontroller/zjuconnectcontroller.h"
MainWindow::MainWindow(ApplicationLogger *logger, QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
applicationLogger(logger)
{
profileManager = new ProfileManager();
const QString overrideConfigPath = Utils::getArgValue(QCoreApplication::arguments(), "--config-path");
if (overrideConfigPath.isEmpty())
{
profileManager = new ProfileManager();
currentProfileId = profileManager->activeProfile();
settings = new QSettings(profileManager->profilePath(currentProfileId), QSettings::IniFormat);
}
else
{
currentProfileId = "custom";
settings = new QSettings(overrideConfigPath, QSettings::IniFormat);
}
const QString overrideConfigPath =
CommandLineOptions::value(QCoreApplication::arguments(), "--config-path");
coordinator = new MainWindowCoordinator(this, overrideConfigPath, this);
profileService = coordinator->profiles();
currentProfileId = profileService->currentProfileId();
settings = profileService->settings();
upgradeSettings();
isFirstTimeSetMode = true;
ui->setupUi(this);
coreLogFile = new CoreLogFile(Utils::getLogFilePath(), this);
authenticationDialogs = coordinator->authenticationDialogs();
connectionSession = coordinator->connection();
systemProxySession = coordinator->systemProxy();
coreLogFile = new CoreLogFile(ApplicationPaths::logFile(), this);
updateChecker = coordinator->updates();
connect(updateChecker, &UpdateChecker::versionInfoChanged, this,
[this](const VersionInfo &) { updateVersionInfo(); });
connect(updateChecker, &UpdateChecker::checkFailed, this,
[this](UpdateComponent component, const QString &)
{
const QString componentName =
component == UpdateComponent::Ui ? QStringLiteral("UI") : QStringLiteral("核心");
ui->versionLabel->setText(
"当前版本:" + QApplication::applicationVersion() +
"\n检查" + componentName + "更新失败\n"
);
});
connect(updateChecker, &UpdateChecker::uiUpdateAvailable, this,
[this](const QString &latestVersion)
{
QMessageBox msgBox(this);
msgBox.setText("UI 版本更新");
msgBox.setInformativeText(
"存在 UI 版本更新:" + latestVersion + "\n是否前往 Github 发布页面查看?"
);
msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
msgBox.setDefaultButton(QMessageBox::Ok);
if (msgBox.exec() == QMessageBox::Ok)
{
QDesktopServices::openUrl(
QUrl(
"https://github.com/" +
ApplicationConstants::RepositoryName +
"/releases/latest"
)
);
}
});
connect(applicationLogger, &ApplicationLogger::entryAdded, this,
[this](const QString &entry)
{
ui->logPlainTextEdit->appendPlainText(entry);
});
systemProxySession = new SystemProxySession(this);
connect(systemProxySession, &SystemProxySession::busyChanged, this,
[this](bool busy)
{
@@ -65,14 +97,6 @@ MainWindow::MainWindow(ApplicationLogger *logger, QWidget *parent) :
ui->pushButton2->hide();
}
});
connect(systemProxySession, &SystemProxySession::operationFinished, this,
[this](bool enabled)
{
if (enabled && connectionSession != nullptr && !connectionSession->isActive())
{
systemProxySession->disable();
}
});
setupTrayIcon();
setupProfileMenu();
@@ -82,10 +106,6 @@ MainWindow::MainWindow(ApplicationLogger *logger, QWidget *parent) :
ui->applicationNameLabel->setText(QApplication::applicationDisplayName());
versionInfo.ui_version = QApplication::applicationVersion();
versionInfo.ui_latest = "正在检查";
versionInfo.core_version = "未知";
versionInfo.core_latest = "正在检查";
updateVersionInfo();
@@ -163,7 +183,7 @@ MainWindow::MainWindow(ApplicationLogger *logger, QWidget *parent) :
return;
}
Utils::clearClientData(currentProfileId);
ApplicationPaths::clearClientData(currentProfileId);
qInfo().noquote() << "已清理登录缓存";
});
@@ -173,7 +193,7 @@ MainWindow::MainWindow(ApplicationLogger *logger, QWidget *parent) :
{
try
{
Utils::setDeviceTrust(this,
DeviceTrust::set(this,
settings->value("ZJUConnect/Protocol", "easyconnect").toString(),
settings->value("ZJUConnect/ServerAddress").toString(),
settings->value("ZJUConnect/ServerPort").toInt(),
@@ -194,7 +214,7 @@ MainWindow::MainWindow(ApplicationLogger *logger, QWidget *parent) :
{
try
{
Utils::setDeviceTrust(this,
DeviceTrust::set(this,
settings->value("ZJUConnect/Protocol", "easyconnect").toString(),
settings->value("ZJUConnect/ServerAddress").toString(),
settings->value("ZJUConnect/ServerPort").toInt(),
@@ -210,24 +230,23 @@ MainWindow::MainWindow(ApplicationLogger *logger, QWidget *parent) :
});
// 帮助-检查更新
connect(ui->checkUpdateAction, &QAction::triggered, this,
[&]()
{
checkUpdate();
});
connect(ui->checkUpdateAction, &QAction::triggered,
updateChecker, &UpdateChecker::check);
// 帮助-项目主页
connect(ui->projectAction, &QAction::triggered,
[&]()
{
QDesktopServices::openUrl(QUrl("https://github.com/" + Utils::REPO_NAME));
QDesktopServices::openUrl(QUrl(
"https://github.com/" + ApplicationConstants::RepositoryName
));
});
// 帮助-关于本软件
connect(ui->aboutAction, &QAction::triggered,
[&]()
{
Utils::showAboutMessageBox(this);
PresentationHelpers::showAboutDialog(this);
});
// 复制日志
@@ -247,136 +266,51 @@ MainWindow::MainWindow(ApplicationLogger *logger, QWidget *parent) :
}
);
connect(this, &MainWindow::SetModeFinished, this,
[&]()
{
if (isFirstTimeSetMode)
{
isFirstTimeSetMode = false;
bool shouldConnect = settings->value("Common/ConnectAfterStart", false).toBool();
for (const QString &arg : qApp->arguments())
{
if (arg == "--connect")
{
shouldConnect = true;
break;
}
}
if (shouldConnect)
{
ui->pushButton1->click();
}
}
}
clearLog();
connectionUiController = new ConnectionUiController(
this,
ui->pushButton1,
ui->pushButton2,
trayConnectAction,
connectionSession,
systemProxySession,
authenticationDialogs,
applicationLogger,
coreLogFile,
[this]() { return settings; },
[this]() { return currentProfileId; },
[this](
const QString &title,
const QString &content,
QSystemTrayIcon::MessageIcon icon
)
{
showNotification(title, content, icon);
},
this
);
// 自动检查更新
checkUpdateNAM = new QNetworkAccessManager(this);
connect(checkUpdateNAM, &QNetworkAccessManager::finished,
this, [&](QNetworkReply* reply) {
if (reply->error() != QNetworkReply::NoError)
{
qWarning().noquote() << "检查 UI 更新失败。原因是:" + reply->errorString();
ui->versionLabel->setText(
"当前版本:" + QApplication::applicationVersion() + "\n检查 UI 更新失败\n"
);
reply->deleteLater();
return;
}
QJsonObject json = QJsonDocument::fromJson(reply->readAll()).object();
reply->deleteLater();
QString nowVersion = QApplication::applicationVersion();
QString latestVersion = json["tag_name"].toString();
// 移除开头的 'v'
if (latestVersion.startsWith('v'))
{
latestVersion = latestVersion.mid(1);
}
qInfo().noquote() << "检查 UI 更新成功。最新版本:" + latestVersion;
versionInfo.ui_latest = latestVersion;
updateVersionInfo();
qsizetype nowVersionSuffix, latestVersionSuffix;
auto nowVersionQ = QVersionNumber::fromString(nowVersion, &nowVersionSuffix);
auto latestVersionQ = QVersionNumber::fromString(latestVersion, &latestVersionSuffix);
if (latestVersionQ > nowVersionQ ||
(latestVersionQ == nowVersionQ && latestVersion.right(latestVersionSuffix) != nowVersion.right(nowVersionSuffix)))
{
QMessageBox msgBox;
msgBox.setText("UI 版本更新");
msgBox.setInformativeText("存在 UI 版本更新:" + latestVersion + "\n"
"是否前往 Github 发布页面查看?");
msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
msgBox.setDefaultButton(QMessageBox::Ok);
int ret = msgBox.exec();
if (ret == QMessageBox::Ok)
{
QDesktopServices::openUrl(QUrl("https://github.com/" + Utils::REPO_NAME + "/releases/latest"));
}
}
});
// 检查核心更新
checkCoreUpdateNAM = new QNetworkAccessManager(this);
connect(checkCoreUpdateNAM, &QNetworkAccessManager::finished,
this, [&](QNetworkReply* reply) {
if (reply->error() != QNetworkReply::NoError)
{
qWarning().noquote() << "检查核心更新失败。原因是:" + reply->errorString();
ui->versionLabel->setText(
"当前版本:" + QApplication::applicationVersion() + "\n检查核心更新失败\n"
);
reply->deleteLater();
return;
}
QJsonObject json = QJsonDocument::fromJson(reply->readAll()).object();
reply->deleteLater();
QString nowVersion = versionInfo.core_version;
QString latestVersion = json["tag_name"].toString();
// 移除开头的 'v'
if (latestVersion.startsWith('v'))
{
latestVersion = latestVersion.mid(1);
}
qInfo().noquote() << "检查核心更新成功。最新版本:" + latestVersion;
versionInfo.core_latest = latestVersion;
updateVersionInfo();
qsizetype nowVersionSuffix, latestVersionSuffix;
auto nowVersionQ = QVersionNumber::fromString(nowVersion, &nowVersionSuffix);
auto latestVersionQ = QVersionNumber::fromString(latestVersion, &latestVersionSuffix);
if (latestVersionQ > nowVersionQ ||
(latestVersionQ == nowVersionQ && latestVersion.right(latestVersionSuffix) != nowVersion.right(nowVersionSuffix)))
{
qInfo().noquote() << "核心版本存在更新,可手动更新或通知开发者更新。";
}
});
initZjuConnect();
bool shouldConnect =
settings->value("Common/ConnectAfterStart", false).toBool();
if (qApp->arguments().contains("--connect"))
{
shouldConnect = true;
}
if (shouldConnect)
{
ui->pushButton1->click();
}
if (settings->value("Common/CheckUpdateAfterStart", true).toBool())
{
checkUpdate();
updateChecker->check();
}
else
{
versionInfo.ui_latest = "已禁用";
versionInfo.core_latest = "已禁用";
updateVersionInfo();
updateChecker->markDisabled();
}
if (!profileManager->silentStartEnabled())
if (!profileService->silentStartEnabled())
{
show();
}
@@ -482,12 +416,6 @@ void MainWindow::setupTrayIcon()
void MainWindow::setupProfileMenu()
{
// 务必在 setupTrayIcon 之后调用,以确保 trayProfileMenu 已正确初始化
if (profileManager == nullptr)
{
ui->profileMenu->setEnabled(false);
return;
}
newProfileAction = ui->profileMenu->addAction("新建配置");
renameProfileAction = ui->profileMenu->addAction("重命名当前配置");
deleteProfileAction = ui->profileMenu->addAction("删除当前配置");
@@ -502,11 +430,6 @@ void MainWindow::setupProfileMenu()
void MainWindow::refreshProfileMenu()
{
if (profileManager == nullptr)
{
return;
}
const QList<QAction *> actions = ui->profileMenu->actions();
bool remove = false;
for (QAction *action : actions)
@@ -558,11 +481,14 @@ void MainWindow::refreshProfileMenu()
});
}
for (const QString &profileId : profileManager->listProfiles())
for (const QString &profileId : profileService->profiles())
{
QAction *action = ui->profileMenu->addAction(profileId);
action->setCheckable(true);
action->setChecked(profileId == currentProfileId);
action->setChecked(
!profileService->usesOverrideConfiguration()
&& profileId == currentProfileId
);
switchGroup->addAction(action);
connect(action, &QAction::triggered, this, [this, profileId]()
{
@@ -573,7 +499,10 @@ void MainWindow::refreshProfileMenu()
{
QAction *trayAction = trayProfileMenu->addAction(profileId);
trayAction->setCheckable(true);
trayAction->setChecked(profileId == currentProfileId);
trayAction->setChecked(
!profileService->usesOverrideConfiguration()
&& profileId == currentProfileId
);
traySwitchGroup->addAction(trayAction);
connect(trayAction, &QAction::triggered, this, [this, profileId]()
{
@@ -582,24 +511,17 @@ void MainWindow::refreshProfileMenu()
}
}
renameProfileAction->setEnabled(!currentProfileId.isEmpty());
deleteProfileAction->setEnabled(!currentProfileId.isEmpty());
const bool currentProfileIsManaged =
!currentProfileId.isEmpty()
&& !profileService->usesOverrideConfiguration();
renameProfileAction->setEnabled(currentProfileIsManaged);
deleteProfileAction->setEnabled(currentProfileIsManaged);
}
bool MainWindow::switchProfile(const QString &profileId)
{
if (profileManager == nullptr)
{
return false;
}
if (!QFileInfo::exists(profileManager->profilePath(profileId)))
{
refreshProfileMenu();
return false;
}
if (profileId == currentProfileId)
if (!profileService->usesOverrideConfiguration()
&& profileId == currentProfileId)
{
return true;
}
@@ -616,11 +538,14 @@ bool MainWindow::switchProfile(const QString &profileId)
settingWindow->close();
}
settings->sync();
delete settings;
settings = new QSettings(profileManager->profilePath(profileId), QSettings::IniFormat);
currentProfileId = profileId;
profileManager->setActiveProfile(currentProfileId);
if (!profileService->switchTo(profileId))
{
refreshProfileMenu();
return false;
}
settings = profileService->settings();
authenticationDialogs->setSettings(settings);
currentProfileId = profileService->currentProfileId();
upgradeSettings();
updateVersionInfo();
@@ -634,8 +559,9 @@ bool MainWindow::switchProfile(const QString &profileId)
void MainWindow::createProfile()
{
if (profileManager == nullptr)
if (connectionSession != nullptr && connectionSession->isActive())
{
QMessageBox::warning(this, "新建失败", "请先断开 VPN 连接,再新建配置。");
return;
}
@@ -646,23 +572,31 @@ void MainWindow::createProfile()
return;
}
const QString newProfileId = profileManager->createProfile(name, settings->fileName());
if (connectionSession != nullptr && connectionSession->isActive())
{
QMessageBox::warning(this, "新建失败", "请先断开 VPN 连接,再新建配置。");
return;
}
const QString newProfileId = profileService->createAndSwitch(name);
if (newProfileId.isEmpty())
{
QMessageBox::critical(this, "创建失败", "无法创建新配置。");
return;
}
switchProfile(newProfileId);
settings = profileService->settings();
currentProfileId = profileService->currentProfileId();
authenticationDialogs->setSettings(settings);
upgradeSettings();
updateVersionInfo();
resetZjuConnectUi();
clearLog();
refreshProfileMenu();
}
void MainWindow::renameCurrentProfile()
{
if (profileManager == nullptr)
{
return;
}
bool ok = false;
QString name = QInputDialog::getText(this, "重命名配置", "请输入新配置名称:\n(仅支持字母、数字、下划线)", QLineEdit::Normal, currentProfileId, &ok);
if (!ok)
@@ -670,7 +604,7 @@ void MainWindow::renameCurrentProfile()
return;
}
const QString normalizedName = profileManager->normalizeProfileId(name);
const QString normalizedName = profileService->normalizeProfileId(name);
if (normalizedName.isEmpty())
{
QMessageBox::warning(this, "重命名失败", "配置名称不能为空。");
@@ -686,21 +620,19 @@ void MainWindow::renameCurrentProfile()
return;
}
settings->sync();
if (settingWindow != nullptr)
{
settingWindow->close();
}
if (!profileManager->renameProfile(currentProfileId, normalizedName))
if (!profileService->renameCurrent(normalizedName))
{
QMessageBox::warning(this, "重命名失败", "目标配置已存在,或当前配置不可重命名。");
return;
}
currentProfileId = normalizedName;
profileManager->setActiveProfile(currentProfileId);
delete settings;
settings = new QSettings(profileManager->profilePath(currentProfileId), QSettings::IniFormat);
currentProfileId = profileService->currentProfileId();
settings = profileService->settings();
authenticationDialogs->setSettings(settings);
updateVersionInfo();
refreshProfileMenu();
clearLog();
@@ -709,11 +641,6 @@ void MainWindow::renameCurrentProfile()
void MainWindow::deleteCurrentProfile()
{
if (profileManager == nullptr)
{
return;
}
if (currentProfileId.isEmpty())
{
QMessageBox::warning(this, "删除失败", "默认配置不可删除。");
@@ -732,56 +659,42 @@ void MainWindow::deleteCurrentProfile()
}
const QString removedProfileId = currentProfileId;
if (!switchProfile(""))
if (connectionSession != nullptr && connectionSession->isActive())
{
QMessageBox::warning(this, "删除失败", "请先断开 VPN 连接,再删除配置。");
return;
}
if (!profileManager->removeProfile(removedProfileId))
if (settingWindow != nullptr)
{
settingWindow->close();
}
if (!profileService->removeCurrentAndSwitchToDefault())
{
QMessageBox::warning(this, "删除失败", "无法删除该配置文件。");
return;
}
currentProfileId = profileService->currentProfileId();
settings = profileService->settings();
authenticationDialogs->setSettings(settings);
upgradeSettings();
updateVersionInfo();
resetZjuConnectUi();
clearLog();
refreshProfileMenu();
qInfo().noquote() << "已删除配置:" + removedProfileId;
}
void MainWindow::checkUpdate()
{
try
{
versionInfo.core_version = Utils::checkCoreVersion(this);
qInfo().noquote() << "检查核心版本成功:" + versionInfo.core_version;
}
catch (const std::runtime_error& e)
{
qWarning().noquote() << "检查核心版本失败:" + QString(e.what());
versionInfo.core_version = "错误";
}
QNetworkRequest request(QUrl("https://api.github.com/repos/" + Utils::REPO_NAME + "/releases/latest"));
checkUpdateNAM->get(request);
QNetworkRequest request_c(QUrl("https://api.github.com/repos/" + Utils::CORE_REPO_NAME + "/releases/latest"));
checkCoreUpdateNAM->get(request_c);
}
void MainWindow::upgradeSettings()
{
int configVersion = settings->value("Common/ConfigVersion", -1).toInt();
if (configVersion == -1)
const SettingsMigrationAction action = SettingsMigrator::prepare(*settings);
if (action == SettingsMigrationAction::MigrateAutoStart)
{
Utils::resetDefaultSettings(*settings);
profileService->migrateAutoStartSetting(
settings->value("Common/AutoStart", false).toBool()
);
}
else if (configVersion == 4)
{
settings->setValue("ZJUConnect/Protocol", "easyconnect");
}
else if (configVersion == 6)
{
profileManager->setAutoStartEnabled(settings->value("Common/AutoStart", false).toBool());
}
else if (configVersion < Utils::CONFIG_VERSION)
else if (action == SettingsMigrationAction::RecommendReset)
{
QMessageBox msgBox;
msgBox.setText("存在配置更新");
@@ -789,23 +702,23 @@ void MainWindow::upgradeSettings()
msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
msgBox.setDefaultButton(QMessageBox::Cancel);
if (msgBox.exec() == QMessageBox::Ok)
const bool reset = msgBox.exec() == QMessageBox::Ok;
SettingsMigrator::finish(*settings, reset);
if (reset)
{
settings->clear();
Utils::resetDefaultSettings(*settings);
QMessageBox::information(this, "完成", "已恢复默认设置。");
}
return;
}
settings->setValue("Common/ConfigVersion", Utils::CONFIG_VERSION);
settings->sync();
SettingsMigrator::finish(*settings, false);
}
void MainWindow::updateVersionInfo()
{
const VersionInfo &versionInfo = updateChecker->versionInfo();
ui->versionLabel->setText(
"UI 版本:" + versionInfo.ui_version + " 最新:" + versionInfo.ui_latest + "\n"
"核心版本:" + versionInfo.core_version + " 最新:" + versionInfo.core_latest + "\n"
"UI 版本:" + versionInfo.uiVersion + " 最新:" + versionInfo.uiLatest + "\n"
"核心版本:" + versionInfo.coreVersion + " 最新:" + versionInfo.coreLatest + "\n"
"当前配置:" + (currentProfileId.isEmpty() ? "默认" : currentProfileId)
);
}
@@ -832,17 +745,18 @@ void MainWindow::showNotification(const QString &title, const QString &content,
void MainWindow::cleanUpWhenQuit()
{
// 保存配置
if (settings->value("Common/ConfigVersion", 0).toInt() <= Utils::CONFIG_VERSION)
if (settings->value("Common/ConfigVersion", 0).toInt() <=
ApplicationConstants::ConfigVersion)
{
settings->setValue("Common/ConfigVersion", Utils::CONFIG_VERSION);
settings->setValue(
"Common/ConfigVersion",
ApplicationConstants::ConfigVersion
);
}
settings->sync();
// 清除系统代理
if (systemProxySession != nullptr)
{
systemProxySession->clearBeforeShutdown();
}
coordinator->prepareForShutdown();
}
void MainWindow::gracefullyQuit()
@@ -861,23 +775,5 @@ void MainWindow::gracefullyQuit()
MainWindow::~MainWindow()
{
if (settings != nullptr)
{
settings->sync();
delete settings;
}
if (profileManager != nullptr)
{
delete profileManager;
}
if (connectionSession != nullptr)
{
disconnect(connectionSession, nullptr, this, nullptr);
delete connectionSession;
connectionSession = nullptr;
}
delete ui;
}

View File

@@ -4,20 +4,15 @@
#include <QMainWindow>
#include <QSystemTrayIcon>
#include <QCloseEvent>
#include <QProcess>
#include <QNetworkReply>
#include <QSettings>
#include <QPointer>
#include "application/connectionsession.h"
#include "application/profileservice.h"
#include "application/systemproxysession.h"
#include "loginwindow/loginwindow.h"
#include "sudowindow/sudowindow.h"
#include "ssologinwebview/ssologinwebview.h"
#include "zjuconnectcontroller/zjuconnectcontroller.h"
#include "settingwindow/settingwindow.h"
#include "graphcaptchawindow/graphcaptchawindow.h"
#include "utils/profilemanager.h"
#include "presentation/dialogs/settingwindow/settingwindow.h"
#include "presentation/coordinators/authdialogcoordinator.h"
#include "presentation/main/mainwindowcoordinator.h"
namespace Ui
{
@@ -25,7 +20,9 @@ namespace Ui
}
class ApplicationLogger;
class ConnectionUiController;
class CoreLogFile;
class UpdateChecker;
class MainWindow : public QMainWindow
{
@@ -40,18 +37,12 @@ public slots:
void cleanUpWhenQuit();
signals:
void SetModeFinished();
protected:
void closeEvent(QCloseEvent *e) override;
void changeEvent(QEvent *event) override;
private:
void checkUpdate();
void upgradeSettings();
void clearLog();
@@ -64,8 +55,6 @@ private:
QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon::Information
);
void initZjuConnect();
void updateVersionInfo();
void setupTrayIcon();
@@ -84,11 +73,6 @@ private:
void gracefullyQuit();
struct {
QString ui_version, ui_latest;
QString core_version, core_latest;
} versionInfo;
Ui::MainWindow *ui;
QSystemTrayIcon *trayIcon;
QMenu *trayMenu;
@@ -101,23 +85,17 @@ private:
QAction *deleteProfileAction;
ConnectionSession *connectionSession = nullptr;
SystemProxySession *systemProxySession = nullptr;
AuthDialogCoordinator *authenticationDialogs = nullptr;
ConnectionUiController *connectionUiController;
MainWindowCoordinator *coordinator;
ApplicationLogger *applicationLogger;
CoreLogFile *coreLogFile = nullptr;
QNetworkAccessManager *checkUpdateNAM;
QNetworkAccessManager *checkCoreUpdateNAM;
UpdateChecker *updateChecker;
QSettings *settings;
ProfileManager *profileManager;
ProfileService *profileService;
QString currentProfileId;
QObject *diagnosisContext;
QPointer<SettingWindow> settingWindow;
QPointer<LoginWindow> loginWindow;
QPointer<SudoWindow> sudoWindow;
QPointer<SsoLoginWebView> ssoLoginWebView;
QPointer<GraphCaptchaWindow> graphCaptchaWindow;
bool isFirstTimeSetMode;
};

View File

@@ -0,0 +1,135 @@
#include "mainwindowcoordinator.h"
#include <memory>
#include "application/connectionsession.h"
#include "application/profileservice.h"
#include "application/systemproxysession.h"
#include "infrastructure/coreprocess/zjuconnectprocess.h"
#include "infrastructure/platform/platformsystemproxybackend.h"
#include "infrastructure/settings/profilemanager.h"
#include "infrastructure/update/updatechecker.h"
#include "presentation/coordinators/authdialogcoordinator.h"
MainWindowCoordinator::MainWindowCoordinator(
QWidget *parentWidget,
const QString &overrideConfigPath,
QObject *parent
)
: QObject(parent),
profileService(new ProfileService(
std::make_unique<ProfileManager>(),
overrideConfigPath,
this
)),
connectionSession(new ConnectionSession(new ZjuConnectProcess(), this)),
systemProxySession(new SystemProxySession(
std::make_unique<PlatformSystemProxyBackend>(),
this
)),
updateChecker(new UpdateChecker(this)),
authenticationCoordinator(new AuthDialogCoordinator(
parentWidget,
profileService->settings(),
this
))
{
connect(
connectionSession,
&ConnectionSession::askSudoPass,
authenticationCoordinator,
&AuthDialogCoordinator::requestSudoPassword
);
connect(
connectionSession,
&ConnectionSession::graphCaptcha,
authenticationCoordinator,
&AuthDialogCoordinator::requestGraphCaptcha
);
connect(
connectionSession,
&ConnectionSession::smsCode,
authenticationCoordinator,
&AuthDialogCoordinator::requestSmsCode
);
connect(
connectionSession,
&ConnectionSession::totpCode,
authenticationCoordinator,
&AuthDialogCoordinator::requestTotpCode
);
connect(
connectionSession,
&ConnectionSession::ssoAuth,
authenticationCoordinator,
&AuthDialogCoordinator::requestSsoLogin
);
connect(
authenticationCoordinator,
&AuthDialogCoordinator::sudoPasswordSubmitted,
connectionSession,
&ConnectionSession::submitSudoPassword
);
connect(
authenticationCoordinator,
&AuthDialogCoordinator::interactiveInputSubmitted,
connectionSession,
&ConnectionSession::submitInput
);
connect(
systemProxySession,
&SystemProxySession::operationFinished,
this,
[this](bool enabled)
{
if (enabled && !connectionSession->isActive())
{
systemProxySession->disable();
}
}
);
connect(
connectionSession,
&ConnectionSession::finished,
this,
[this](ZJU_ERROR)
{
if (systemProxySession->isEnabled())
{
systemProxySession->disable();
}
}
);
}
ProfileService *MainWindowCoordinator::profiles() const
{
return profileService;
}
ConnectionSession *MainWindowCoordinator::connection() const
{
return connectionSession;
}
SystemProxySession *MainWindowCoordinator::systemProxy() const
{
return systemProxySession;
}
UpdateChecker *MainWindowCoordinator::updates() const
{
return updateChecker;
}
AuthDialogCoordinator *
MainWindowCoordinator::authenticationDialogs() const
{
return authenticationCoordinator;
}
void MainWindowCoordinator::prepareForShutdown()
{
systemProxySession->clearBeforeShutdown();
}

View File

@@ -0,0 +1,40 @@
#ifndef MAINWINDOWCOORDINATOR_H
#define MAINWINDOWCOORDINATOR_H
#include <QObject>
class AuthDialogCoordinator;
class ConnectionSession;
class ProfileService;
class SystemProxySession;
class UpdateChecker;
class QWidget;
class MainWindowCoordinator : public QObject
{
Q_OBJECT
public:
MainWindowCoordinator(
QWidget *parentWidget,
const QString &overrideConfigPath,
QObject *parent = nullptr
);
ProfileService *profiles() const;
ConnectionSession *connection() const;
SystemProxySession *systemProxy() const;
UpdateChecker *updates() const;
AuthDialogCoordinator *authenticationDialogs() const;
void prepareForShutdown();
private:
ProfileService *profileService;
ConnectionSession *connectionSession;
SystemProxySession *systemProxySession;
UpdateChecker *updateChecker;
AuthDialogCoordinator *authenticationCoordinator;
};
#endif // MAINWINDOWCOORDINATOR_H

View File

@@ -0,0 +1,94 @@
#include "presentationhelpers.h"
#include <QApplication>
#include <QMessageBox>
#include <QPixmap>
#include <QSizePolicy>
#include <QWidget>
#include "application/applicationconstants.h"
void PresentationHelpers::retainSizeWhenHidden(QWidget *widget)
{
QSizePolicy policy = widget->sizePolicy();
policy.setRetainSizeWhenHidden(true);
widget->setSizePolicy(policy);
}
void PresentationHelpers::showAboutDialog(QWidget *parent)
{
QMessageBox messageBox(parent);
messageBox.setWindowTitle("关于");
messageBox.setTextFormat(Qt::RichText);
const QString repository = ApplicationConstants::RepositoryName;
messageBox.setText(
QApplication::applicationDisplayName() + " " + QApplication::applicationVersion() +
"<br>改进的 ZJU-Connect 图形界面" +
"<br>作者:<a href='https://github.com/chenx-dust'>Chenx Dust</a>" +
"<br>项目主页:<a href='https://github.com/" + repository +
"'>https://github.com/" + repository + "</a>" +
"<br><br><b>致谢:</b>" +
"<br><br>ZJU-Connect-for-Windows" +
"<br>基于 Qt 编写的 ZJU 网络客户端" +
"<br>作者:<a href='https://myth.cx'>Myth</a>" +
"<br>项目主页:<a href='https://github.com/Mythologyli/ZJU-Connect-for-Windows'>"
"https://github.com/Mythologyli/ZJU-Connect-for-Windows</a>" +
"<br><br>zju-connect" +
"<br>ZJU RVPN 客户端的 Go 语言实现" +
"<br>作者:<a href='https://myth.cx'>Myth</a>" +
"<br>项目主页:<a href='https://github.com/Mythologyli/zju-connect'>"
"https://github.com/Mythologyli/zju-connect</a>" +
"<br><br>EasierConnect" +
"<br>EasyConnect 客户端的开源实现" +
"<br>作者:<a href='https://github.com/lyc8503'>lyc8503</a>" +
"<br>项目主页:<a href='https://github.com/lyc8503/EasierConnect'>"
"https://github.com/lyc8503/EasierConnect</a>"
);
messageBox.setIconPixmap(QPixmap(":/resource/icon.png").scaled(
100,
100,
Qt::KeepAspectRatio,
Qt::SmoothTransformation
));
messageBox.exec();
}
bool PresentationHelpers::confirmCredentials(
const QString &username,
const QString &password
)
{
if (username.isEmpty() || password.isEmpty())
{
return QMessageBox::warning(
nullptr,
"警告",
"账号或密码为空!\n\n是否继续?",
QMessageBox::Ok,
QMessageBox::Cancel
) == QMessageBox::Ok;
}
const auto containsNonAscii = [](const QString &value)
{
for (const QChar character : value)
{
if (character.unicode() > 127)
{
return true;
}
}
return false;
};
if (containsNonAscii(username) || containsNonAscii(password))
{
return QMessageBox::warning(
nullptr,
"警告",
"账号或密码存在非 ASCII 字符!\n建议检查输入法设置。\n\n是否继续?",
QMessageBox::Ok,
QMessageBox::Cancel
) == QMessageBox::Ok;
}
return true;
}

View File

@@ -0,0 +1,15 @@
#ifndef PRESENTATIONHELPERS_H
#define PRESENTATIONHELPERS_H
#include <QString>
class QWidget;
namespace PresentationHelpers
{
void retainSizeWhenHidden(QWidget *widget);
void showAboutDialog(QWidget *parent = nullptr);
bool confirmCredentials(const QString &username, const QString &password);
}
#endif // PRESENTATIONHELPERS_H

View File

@@ -0,0 +1,114 @@
#include <QCoreApplication>
#include <QDebug>
#include "application/connectionsession.h"
namespace
{
class FakeCoreProcess : public CoreProcess
{
public:
int startCalls = 0;
int stopCalls = 0;
QByteArray lastInput;
void start(const ConnectionProfile &) override
{
++startCalls;
emit started();
}
void stop() override
{
++stopCalls;
}
void writeInput(const QByteArray &data) override
{
lastInput = data;
}
void complete()
{
emit finished();
}
void requestSudoPassword()
{
emit askSudoPass();
}
};
bool delegatesProcessLifecycleThroughPort()
{
auto *coreProcess = new FakeCoreProcess();
ConnectionSession session(coreProcess);
ConnectionProfile profile;
int finishedCalls = 0;
QObject::connect(
&session,
&ConnectionSession::finished,
[&](ZJU_ERROR) { ++finishedCalls; }
);
if (!session.start(profile, {})
|| coreProcess->startCalls != 1
|| session.state() != ConnectionState::Running
|| !session.isActive()
|| session.start(profile, {}))
{
qCritical() << "start delegation failed";
return false;
}
session.submitInput("input");
session.stop();
if (coreProcess->lastInput != "input"
|| coreProcess->stopCalls != 1
|| session.state() != ConnectionState::Stopping)
{
qCritical() << "input or stop delegation failed";
return false;
}
coreProcess->complete();
if (finishedCalls != 1
|| session.state() != ConnectionState::Disconnected
|| session.isActive())
{
qCritical() << "completion delegation failed";
return false;
}
return true;
}
bool emptySudoPasswordStopsTheSession()
{
auto *coreProcess = new FakeCoreProcess();
ConnectionSession session(coreProcess);
ConnectionProfile profile;
if (!session.start(profile, {}))
{
qCritical() << "failed to start session for sudo cancellation";
return false;
}
coreProcess->requestSudoPassword();
session.submitSudoPassword({}, false);
if (coreProcess->stopCalls != 1
|| session.state() != ConnectionState::Stopping)
{
qCritical() << "empty sudo password did not stop the session";
return false;
}
return true;
}
}
int main(int argc, char *argv[])
{
QCoreApplication application(argc, argv);
return delegatesProcessLifecycleThroughPort()
&& emptySudoPasswordStopsTheSession() ? 0 : 1;
}

View File

@@ -1,7 +1,7 @@
#include <QCoreApplication>
#include <QDebug>
#include "core/corecommandbuilder.h"
#include "infrastructure/coreprocess/corecommandbuilder.h"
namespace
{

View File

@@ -3,7 +3,7 @@
#include <QFile>
#include <QTemporaryDir>
#include "infrastructure/corelogfile.h"
#include "infrastructure/logging/corelogfile.h"
namespace
{

View File

@@ -1,7 +1,7 @@
#include <QCoreApplication>
#include <QDebug>
#include "core/coreoutputbuffer.h"
#include "infrastructure/coreprocess/coreoutputbuffer.h"
namespace
{

View File

@@ -2,7 +2,7 @@
#include <QDebug>
#include <QList>
#include "core/coreoutputparser.h"
#include "infrastructure/coreprocess/coreoutputparser.h"
namespace
{

View File

@@ -0,0 +1,40 @@
#include <QCoreApplication>
#include <QDebug>
#include <QFileInfo>
#include <QTemporaryDir>
#include "infrastructure/settings/profilemanager.h"
namespace
{
bool createsPhysicalFilesForEmptyProfiles()
{
QTemporaryDir directory;
ProfileManager manager(directory.path());
if (!manager.activeProfile().isEmpty()
|| !QFileInfo::exists(manager.profilePath(""))
|| !manager.setActiveProfile(""))
{
qCritical() << "default profile file was not created";
return false;
}
const QString profileId = manager.createProfile("empty");
if (profileId != "empty"
|| !QFileInfo::exists(manager.profilePath(profileId))
|| !manager.listProfiles().contains(profileId)
|| !manager.setActiveProfile(profileId))
{
qCritical() << "empty named profile file was not created";
return false;
}
return true;
}
}
int main(int argc, char *argv[])
{
QCoreApplication application(argc, argv);
return createsPhysicalFilesForEmptyProfiles() ? 0 : 1;
}

View File

@@ -0,0 +1,270 @@
#include <QCoreApplication>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QSettings>
#include <QTemporaryDir>
#include <memory>
#include "application/profilebackend.h"
#include "application/profileservice.h"
namespace
{
class FakeProfileBackend : public ProfileBackend
{
public:
explicit FakeProfileBackend(const QString &rootPath)
: rootPath(rootPath)
{
QSettings defaultSettings(profilePath(""), QSettings::IniFormat);
defaultSettings.sync();
}
QStringList listProfiles() const override
{
QStringList result;
for (const QFileInfo &file : QDir(rootPath).entryInfoList(
{"*.ini"},
QDir::Files,
QDir::Name
))
{
if (file.baseName() != "default")
{
result << file.baseName();
}
}
return result;
}
QString activeProfile() const override
{
return activeId;
}
bool setActiveProfile(const QString &profileId) const override
{
if (!allowActiveProfileChange)
{
return false;
}
activeId = profileId;
return true;
}
QString profilePath(const QString &profileId) const override
{
return QDir(rootPath).filePath(
(profileId.isEmpty() ? QStringLiteral("default") : profileId) + ".ini"
);
}
QString createProfile(
const QString &requestedName,
const QString &sourcePath
) const override
{
const QString id = normalizeProfileId(requestedName);
return !id.isEmpty() && QFile::copy(sourcePath, profilePath(id)) ? id : QString();
}
bool renameProfile(
const QString &oldId,
const QString &newId
) const override
{
return QFile::rename(profilePath(oldId), profilePath(newId));
}
bool removeProfile(const QString &profileId) const override
{
return allowProfileRemoval && QFile::remove(profilePath(profileId));
}
QString normalizeProfileId(const QString &name) const override
{
return name.trimmed();
}
bool silentStartEnabled() const override
{
return false;
}
void setAutoStartEnabled(bool enabled) const override
{
migratedAutoStart = enabled;
}
mutable QString activeId;
mutable bool migratedAutoStart = false;
bool allowActiveProfileChange = true;
bool allowProfileRemoval = true;
private:
QString rootPath;
};
bool managesCurrentProfileAsOneUnit()
{
QTemporaryDir directory;
auto backend = std::make_unique<FakeProfileBackend>(directory.path());
FakeProfileBackend *fake = backend.get();
ProfileService service(std::move(backend));
service.settings()->setValue("Credential/Username", "alice");
service.settings()->sync();
fake->allowActiveProfileChange = false;
if (!service.createAndSwitch("blocked").isEmpty()
|| service.currentProfileId() != ""
|| QFileInfo::exists(fake->profilePath("blocked")))
{
qCritical() << "failed activation left a created profile behind";
return false;
}
fake->allowActiveProfileChange = true;
if (!service.switchTo("missing"))
{
// Expected: missing profiles are rejected.
}
else
{
qCritical() << "missing profile was accepted";
return false;
}
if (service.createAndSwitch("work") != "work"
|| service.currentProfileId() != "work"
|| service.settings()->value("Credential/Username").toString() != "alice")
{
qCritical() << "profile creation failed";
return false;
}
fake->allowActiveProfileChange = false;
QSettings *workSettings = service.settings();
if (service.renameCurrent("blocked")
|| service.currentProfileId() != "work"
|| service.settings() != workSettings
|| !QFileInfo::exists(fake->profilePath("work"))
|| QFileInfo::exists(fake->profilePath("blocked")))
{
qCritical() << "failed activation did not roll back profile rename";
return false;
}
fake->allowActiveProfileChange = true;
if (!service.renameCurrent("renamed")
|| service.currentProfileId() != "renamed")
{
qCritical() << "profile rename failed";
return false;
}
QSettings *renamedSettings = service.settings();
fake->allowActiveProfileChange = false;
if (service.removeCurrentAndSwitchToDefault()
|| service.currentProfileId() != "renamed"
|| service.settings() != renamedSettings
|| !QFileInfo::exists(fake->profilePath("renamed"))
|| fake->activeId != "renamed")
{
qCritical() << "failed default activation changed the current profile";
return false;
}
fake->allowActiveProfileChange = true;
fake->allowProfileRemoval = false;
if (service.removeCurrentAndSwitchToDefault()
|| service.currentProfileId() != "renamed"
|| service.settings() != renamedSettings
|| !QFileInfo::exists(fake->profilePath("renamed"))
|| fake->activeId != "renamed")
{
qCritical() << "failed removal changed the current profile";
return false;
}
fake->allowProfileRemoval = true;
service.migrateAutoStartSetting(true);
if (!fake->migratedAutoStart
|| !service.removeCurrentAndSwitchToDefault()
|| !service.currentProfileId().isEmpty())
{
qCritical() << "profile removal or global setting migration failed";
return false;
}
return true;
}
bool acceptsCustomAsARegularProfileId()
{
QTemporaryDir directory;
auto backend = std::make_unique<FakeProfileBackend>(directory.path());
FakeProfileBackend *fake = backend.get();
ProfileService service(std::move(backend));
service.settings()->setValue("Credential/Username", "custom-user");
service.settings()->sync();
if (service.createAndSwitch("custom") != "custom"
|| service.usesOverrideConfiguration()
|| service.currentProfileId() != "custom")
{
qCritical() << "custom profile creation failed";
return false;
}
if (!service.renameCurrent("renamed")
|| service.currentProfileId() != "renamed")
{
qCritical() << "custom profile rename failed";
return false;
}
if (!service.removeCurrentAndSwitchToDefault())
{
qCritical() << "renamed custom profile removal failed";
return false;
}
return fake->activeId.isEmpty();
}
bool switchesAwayFromAnOverrideNamedCustom()
{
QTemporaryDir directory;
const QString overridePath = directory.filePath("override.ini");
const QString profilesPath = directory.filePath("profiles");
QDir().mkpath(profilesPath);
{
QSettings overrideSettings(overridePath, QSettings::IniFormat);
overrideSettings.setValue("Credential/Username", "override-user");
overrideSettings.sync();
}
auto backend = std::make_unique<FakeProfileBackend>(profilesPath);
FakeProfileBackend *fake = backend.get();
ProfileService service(std::move(backend), overridePath);
if (!service.usesOverrideConfiguration()
|| service.createAndSwitch("custom") != "custom"
|| service.usesOverrideConfiguration()
|| service.currentProfileId() != "custom"
|| service.settings()->fileName() != fake->profilePath("custom")
|| fake->activeId != "custom")
{
qCritical() << "override configuration was confused with profile custom";
return false;
}
return true;
}
}
int main(int argc, char *argv[])
{
QCoreApplication application(argc, argv);
return managesCurrentProfileAsOneUnit()
&& acceptsCustomAsARegularProfileId()
&& switchesAwayFromAnOverrideNamedCustom() ? 0 : 1;
}

View File

@@ -0,0 +1,47 @@
#include <QCoreApplication>
#include <QDebug>
#include <QSettings>
#include <QTemporaryDir>
#include "application/settingsmigrator.h"
namespace
{
bool migratesKnownVersions()
{
QTemporaryDir directory;
QSettings settings(directory.filePath("profile.ini"), QSettings::IniFormat);
settings.setValue("Common/ConfigVersion", 4);
const auto action = SettingsMigrator::prepare(settings);
const bool passed =
action == SettingsMigrationAction::None
&& settings.value("ZJUConnect/Protocol").toString() == "easyconnect";
if (!passed)
{
qCritical() << "migratesKnownVersions failed";
}
return passed;
}
bool recommendsResetForUnsupportedVersions()
{
QTemporaryDir directory;
QSettings settings(directory.filePath("profile.ini"), QSettings::IniFormat);
settings.setValue("Common/ConfigVersion", 5);
const bool passed =
SettingsMigrator::prepare(settings) ==
SettingsMigrationAction::RecommendReset;
if (!passed)
{
qCritical() << "recommendsResetForUnsupportedVersions failed";
}
return passed;
}
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
return migratesKnownVersions() && recommendsResetForUnsupportedVersions() ? 0 : 1;
}

View File

@@ -3,7 +3,7 @@
#include <QSettings>
#include <QTemporaryDir>
#include "infrastructure/settingsprofileloader.h"
#include "infrastructure/settings/settingsprofileloader.h"
namespace
{

View File

@@ -0,0 +1,88 @@
#include <QCoreApplication>
#include <QDebug>
#include "infrastructure/update/updatechecker.h"
namespace
{
bool normalizesTags()
{
const bool passed =
UpdateChecker::normalizeVersionTag("v1.2.3") == "1.2.3"
&& UpdateChecker::normalizeVersionTag("1.2.3") == "1.2.3";
if (!passed)
{
qCritical() << "normalizesTags failed";
}
return passed;
}
bool validatesReleaseResponses()
{
const std::optional<QString> version =
UpdateChecker::parseReleaseVersion(R"({"tag_name":"v1.2.3"})");
const bool passed =
version == std::optional<QString>("1.2.3")
&& !UpdateChecker::parseReleaseVersion("not json").has_value()
&& !UpdateChecker::parseReleaseVersion("{}").has_value()
&& !UpdateChecker::parseReleaseVersion(
R"({"tag_name":""})"
).has_value()
&& !UpdateChecker::parseReleaseVersion(
R"({"tag_name":42})"
).has_value();
if (!passed)
{
qCritical() << "validatesReleaseResponses failed";
}
return passed;
}
bool comparesVersions()
{
struct VersionComparison
{
QString current;
QString latest;
bool expected;
};
const QList<VersionComparison> comparisons{
{"1.2.3", "1.2.4", true},
{"1.2.3-beta", "1.2.3", true},
{"1.2.3-pre.2", "1.2.3-pre.10", true},
{"1.2.3", "1.2.3-hotfix", true},
{"1.2.3-hotfix.2", "1.2.3-hotfix.10", true},
{"1.2.3", "1.2.3", false},
{"1.2.3", "1.2.3-beta", false},
{"1.2.3-hotfix", "1.2.3", false},
{"1.3.0", "1.2.9", false}
};
for (const VersionComparison &comparison : comparisons)
{
const bool actual = UpdateChecker::isNewerVersion(
comparison.current,
comparison.latest
);
if (actual != comparison.expected)
{
qCritical()
<< "comparesVersions failed:"
<< comparison.current
<< comparison.latest
<< "expected" << comparison.expected
<< "actual" << actual;
return false;
}
}
return true;
}
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
return normalizesTags()
&& validatesReleaseResponses()
&& comparesVersions() ? 0 : 1;
}

View File

@@ -1,57 +0,0 @@
#include <QProcess>
#include <QCoreApplication>
#include <QFileInfo>
#include "utils.h"
namespace Utils {
QString getCorePath()
{
QString program_filename;
if (QSysInfo::productType() == "windows")
{
program_filename = "zju-connect.exe";
}
else
{
program_filename = "zju-connect";
}
QString path = QCoreApplication::applicationDirPath() + "/" + program_filename;
if (!QFileInfo::exists(path)) {
return program_filename; // 可能在 PATH 中
} else {
return path;
}
}
QString checkCoreVersion(QObject* parent)
{
QProcess process(parent);
process.start(Utils::getCorePath(), { "-version" });
if (!process.waitForStarted()) {
throw std::runtime_error("核心无法启动");
}
if (!process.waitForFinished()) {
throw std::runtime_error("核心运行超时");
}
// 获取错误输出
QByteArray errorOutput = process.readAllStandardError();
if (!errorOutput.isEmpty()) {
throw std::runtime_error(errorOutput);
}
// 获取标准输出
QString output = process.readAllStandardOutput();
const QString prefix("ZJU Connect v");
if (!output.startsWith(prefix)) {
throw std::runtime_error("无法解析核心版本号");
}
return output.mid(prefix.size()).trimmed();
}
}

View File

@@ -1,47 +0,0 @@
#ifndef PROFILEMANAGER_H
#define PROFILEMANAGER_H
#include <QString>
#include <QStringList>
class ProfileManager
{
public:
ProfileManager();
QStringList listProfiles() const;
QString activeProfile() const;
bool setActiveProfile(const QString &profileId) const;
QString profilePath(const QString &profileId) const;
QString createProfile(const QString &requestedName, const QString &sourcePath = QString()) const;
bool renameProfile(const QString &oldId, const QString &newId) const;
bool removeProfile(const QString &profileId) const;
QString normalizeProfileId(const QString &name) const;
bool autoStartEnabled() const;
void setAutoStartEnabled(bool enabled) const;
bool silentStartEnabled() const;
void setSilentStartEnabled(bool enabled) const;
private:
QString configRootPath;
QString profilesPath;
QString statePath;
QString defaultProfilePath;
QString ensureUniqueProfileId(const QString &baseId) const;
void ensureStorage() const;
};
#endif //PROFILEMANAGER_H

View File

@@ -1,52 +0,0 @@
#include <QProcess>
#include "utils.h"
namespace Utils {
void setDeviceTrust(QObject *parent, const QString &protocol, const QString &server, int port, const QString &profileId, bool trust)
{
QStringList args;
if (!protocol.isEmpty())
{
args.append("-protocol");
args.append(protocol);
}
if (!server.isEmpty())
{
args.append("-server");
args.append(server);
}
if (port != 0)
{
args.append("-port");
args.append(QString::number(port));
}
args.append("-client-data-file");
args.append(Utils::getClientDataPath(profileId));
args.append(trust ? "-trust-device" : "-untrust-device");
QProcess process(parent);
process.start(Utils::getCorePath(), args);
if (!process.waitForStarted())
{
throw std::runtime_error("核心无法启动");
}
if (!process.waitForFinished())
{
throw std::runtime_error("核心运行超时");
}
QByteArray errorOutput = process.readAllStandardError();
if ((trust && !errorOutput.contains("Device trusted successfully")) || (!trust && !errorOutput.contains("Device untrusted successfully")))
{
throw std::runtime_error(errorOutput);
}
}
}

View File

@@ -1,520 +0,0 @@
#include <QMessageBox>
#include <QInputDialog>
#include <QLineEdit>
#include <QApplication>
#include <QNetworkInterface>
#include <QTextCodec>
#include <QSettings>
#include <QProcess>
#include <QtSystemDetection>
#include <QDir>
#include <QFileInfo>
#include <QCoreApplication>
#include <QStandardPaths>
#if defined(Q_OS_WIN)
#include <windows.h>
#include <shellapi.h>
#elif defined(Q_OS_UNIX)
#include <unistd.h>
#endif
#include "utils.h"
#include "profilemanager.h"
namespace
{
QString profileIdFromConfigPath(const QString &configPath)
{
QFileInfo info(configPath);
if (!info.exists() || info.suffix() != "ini")
{
return "custom";
}
if (info.fileName() == "state.ini")
{
return "custom";
}
QDir parentDir = info.dir();
if (parentDir.dirName() != "profiles")
{
return "custom";
}
return info.baseName();
}
}
QString Utils::consoleOutputToQString(const QByteArray &byteArray)
{
static QString codeName = "";
if (codeName == "UTF-8")
{
return QTextCodec::codecForName("UTF-8")->toUnicode(byteArray);;
}
else if (codeName == "GBK")
{
return QTextCodec::codecForName("GBK")->toUnicode(byteArray);;
}
else if (codeName == "locale")
{
return QString::fromLocal8Bit(byteArray);
}
else
{
QTextCodec *utf8Codec = QTextCodec::codecForName("UTF-8");
QString utf8Str = utf8Codec->toUnicode(byteArray);
QByteArray utf8ByteArrayBack = utf8Codec->fromUnicode(utf8Str);
QTextCodec *gbkCodec = QTextCodec::codecForName("GBK");
QString gbkStr = gbkCodec->toUnicode(byteArray);
QByteArray gbkByteArrayBack = gbkCodec->fromUnicode(gbkStr);
if (utf8ByteArrayBack == byteArray && gbkByteArrayBack != byteArray)
{
codeName = "UTF-8";
return utf8Str;
}
else if (gbkByteArrayBack == byteArray && utf8ByteArrayBack != byteArray)
{
codeName = "GBK";
return gbkStr;
}
else if (gbkByteArrayBack == byteArray && utf8ByteArrayBack == byteArray)
{
return utf8Str;
}
else
{
codeName = "locale";
return QString::fromLocal8Bit(byteArray);
}
}
}
QString Utils::getArgValue(const QStringList &args, const QString &key)
{
const QString prefix = key + "=";
for (int i = 0; i < args.size(); ++i)
{
const QString &arg = args.at(i);
if (arg == key)
{
if (i + 1 < args.size())
{
return args.at(i + 1);
}
return "";
}
if (arg.startsWith(prefix))
{
return arg.mid(prefix.size());
}
}
return "";
}
void Utils::setWidgetFixedWhenHidden(QWidget *widget)
{
QSizePolicy originPolicy = widget->sizePolicy();
originPolicy.setRetainSizeWhenHidden(true);
widget->setSizePolicy(originPolicy);
}
void Utils::showAboutMessageBox(QWidget *parent)
{
QMessageBox messageBox(parent);
messageBox.setWindowTitle("关于");
messageBox.setTextFormat(Qt::RichText);
QString aboutText = QApplication::applicationDisplayName() + " " + QApplication::applicationVersion() +
"<br>改进的 ZJU-Connect 图形界面" +
"<br>作者:<a href='https://github.com/chenx-dust'>Chenx Dust</a>" +
"<br>项目主页:<a href='https://github.com/" + REPO_NAME + "'>https://github.com/" + REPO_NAME + "</a>" +
"<br><br><b>致谢:</b>" +
"<br><br>ZJU-Connect-for-Windows" +
"<br>基于 Qt 编写的 ZJU 网络客户端" +
"<br>作者:<a href='https://myth.cx'>Myth</a>" +
"<br>项目主页:<a href='https://github.com/Mythologyli/ZJU-Connect-for-Windows'>https://github.com/Mythologyli/ZJU-Connect-for-Windows</a>" +
"<br><br>zju-connect" +
"<br>ZJU RVPN 客户端的 Go 语言实现" +
"<br>作者:<a href='https://myth.cx'>Myth</a>" +
"<br>项目主页:<a href='https://github.com/Mythologyli/zju-connect'>https://github.com/Mythologyli/zju-connect</a>" +
"<br><br>EasierConnect" +
"<br>EasyConnect 客户端的开源实现" +
"<br>作者:<a href='https://github.com/lyc8503'>lyc8503</a>" +
"<br>项目主页:<a href='https://github.com/lyc8503/EasierConnect'>https://github.com/lyc8503/EasierConnect</a>";
messageBox.setText(aboutText);
messageBox.setIconPixmap(QPixmap(":/resource/icon.png").scaled(
100, 100, Qt::KeepAspectRatio, Qt::SmoothTransformation
));
messageBox.exec();
}
QString Utils::getIpv4Address(const QString &interfaceName)
{
auto interfaces = QNetworkInterface::allInterfaces();
for (auto &singleInterface: interfaces)
{
if (singleInterface.humanReadableName() == interfaceName)
{
auto addresses =
singleInterface.addressEntries();
for (auto &address: addresses)
{
if (address.ip().protocol() == QAbstractSocket::IPv4Protocol)
{
return address.ip().toString();
}
}
}
}
return "";
}
QString macOSAppBundlePath()
{
#ifdef Q_OS_MAC
QDir dir = QDir(QCoreApplication::applicationDirPath());
dir.cdUp();
dir.cdUp();
QString absolutePath = dir.absolutePath();
// absolutePath will contain a "/" at the end,
// but we want the clean path to the .app bundle
if (absolutePath.length() > 0 && absolutePath.right(1) == "/")
{
absolutePath.chop(1);
}
return absolutePath;
#else
return "";
#endif
}
QString macOSAppBundleName()
{
#ifdef Q_OS_MAC
QString bundlePath = macOSAppBundlePath();
QFileInfo fileInfo(bundlePath);
return fileInfo.baseName();
#else
return "";
#endif
}
QString nativeAppPath()
{
return QDir::toNativeSeparators(QCoreApplication::applicationFilePath());
}
bool Utils::isRunningAsAdmin()
{
#if defined(Q_OS_WIN)
BOOL isAdmin = FALSE;
PSID adminGroup = nullptr;
SID_IDENTIFIER_AUTHORITY ntAuthority = SECURITY_NT_AUTHORITY;
if (AllocateAndInitializeSid(&ntAuthority, 2, SECURITY_BUILTIN_DOMAIN_RID,
DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &adminGroup))
{
CheckTokenMembership(nullptr, adminGroup, &isAdmin);
FreeSid(adminGroup);
}
return isAdmin;
#elif defined(Q_OS_UNIX)
return geteuid() == 0;
#else
return false;
#endif
}
bool Utils::relaunchAsAdmin()
{
QString program = QCoreApplication::applicationFilePath();
QStringList args = QCoreApplication::arguments();
if (!args.isEmpty())
{
args.removeFirst();
}
if (!args.contains("--connect"))
{
args << "--connect";
}
#if defined(Q_OS_WIN)
QStringList quotedArgs;
for (const QString &arg : args)
{
if (arg.contains(' '))
{
quotedArgs << "\"" + arg + "\"";
}
else
{
quotedArgs << arg;
}
}
QString arguments = quotedArgs.join(" ");
HINSTANCE res = ShellExecuteW(
nullptr,
L"runas",
reinterpret_cast<LPCWSTR>(program.utf16()),
reinterpret_cast<LPCWSTR>(arguments.utf16()),
nullptr,
SW_SHOWNORMAL
);
return reinterpret_cast<INT_PTR>(res) > 32;
#else
Q_UNUSED(program)
Q_UNUSED(args)
return false;
#endif
}
void Utils::setAutoStart(bool enable)
{
#if defined(Q_OS_WINDOWS)
QSettings autoStartSettings(R"(HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run)",
QSettings::NativeFormat);
if (enable)
{
autoStartSettings.setValue(
QApplication::applicationName(),
nativeAppPath()
);
}
else
{
autoStartSettings.remove(QApplication::applicationName());
}
autoStartSettings.sync();
#elif defined(Q_OS_MACOS)
{
QStringList args;
args << "-e" << "tell application \"System Events\" to delete login item \"" + macOSAppBundleName() + "\"";
qDebug() << args;
QProcess process;
process.start("osascript", args);
process.waitForFinished();
QString error = process.readAllStandardError();
qDebug() << process.readAllStandardOutput();
qDebug() << error;
if (!enable && process.error() != QProcess::UnknownError)
{
QMessageBox::critical(nullptr, "取消开机自启动失败", "无法删除登录项:" + process.errorString());
return;
}
if (!enable && process.exitCode() != 0)
{
QMessageBox::critical(nullptr, "取消开机自启动失败", "无法删除登录项:" + error);
return;
}
}
if (enable)
{
QStringList args;
args << "-e" << "tell application \"System Events\" to make login item at end with properties {path:\"" +
macOSAppBundlePath() + "\", hidden:false}";
qDebug() << args;
QProcess process;
process.start("osascript", args);
process.waitForFinished();
qDebug() << process.readAllStandardOutput();
qDebug() << process.readAllStandardError();
if (process.error() != QProcess::UnknownError)
{
QMessageBox::critical(nullptr, "设置开机自启动失败", "无法创建登录项:" + process.errorString());
return;
}
if (process.exitCode() != 0)
{
QMessageBox::critical(nullptr, "设置开机自启动失败", "无法创建登录项:" + process.readAllStandardError());
return;
}
}
#elif defined(Q_OS_LINUX)
QString autostartPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/autostart/";
QDir dir(autostartPath);
QString desktopFilePath = autostartPath + QApplication::applicationName() + ".desktop";
QFile desktopFile(desktopFilePath);
if (dir.exists() && desktopFile.exists())
{
if (!desktopFile.remove())
{
QMessageBox::critical(nullptr, "取消开机自启动失败", "无法删除 .desktop 文件:" + desktopFilePath);
return;
}
}
if (enable)
{
if (!dir.exists())
{
if (!dir.mkpath("."))
{
QMessageBox::critical(nullptr, "设置开机自启动失败", "无法创建 autostart 目录:" + autostartPath);
return;
}
}
if (!desktopFile.open(QIODevice::WriteOnly | QIODevice::Text))
{
QMessageBox::critical(nullptr, "设置开机自启动失败", "无法创建 .desktop 文件:" + desktopFilePath);
return;
}
QTextStream out(&desktopFile);
out << "[Desktop Entry]\n";
out << "Type=Application\n";
out << "Name=" << QApplication::applicationName() << "\n";
out << "Exec=" << nativeAppPath() << "\n";
out << "X-GNOME-Autostart-enabled=true\n";
desktopFile.close();
}
#endif
}
bool Utils::credentialCheck(const QString &username, const QString &password)
{
if (username.isEmpty() || password.isEmpty())
{
int status = QMessageBox::warning(nullptr, "警告", "账号或密码为空!\n\n是否继续?", QMessageBox::Ok, QMessageBox::Cancel);
return status == QMessageBox::Ok;
}
bool asciiOnly = true;
for (QChar c: username)
{
if (c.unicode() > 127)
{
asciiOnly = false;
break;
}
}
for (QChar c: password)
{
if (c.unicode() > 127)
{
asciiOnly = false;
break;
}
}
if (!asciiOnly)
{
int status = QMessageBox::warning(nullptr, "警告", "账号或密码存在非 ASCII 字符!\n建议检查输入法设置。\n\n是否继续?", QMessageBox::Ok, QMessageBox::Cancel);
return status == QMessageBox::Ok;
}
return true;
}
void Utils::clearClientData(const QString &profileId)
{
QFile::remove(getClientDataPath(profileId));
}
QString Utils::getClientDataPath(const QString &profileId)
{
QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
QDir dir(defaultPath);
if (!dir.exists())
{
dir.mkpath(".");
}
QDir profileDir(dir.filePath("profiles/" + profileId));
if (!profileDir.exists())
{
profileDir.mkpath(".");
}
QFile clientDataFile(profileDir.filePath("client-data.json"));
if (!clientDataFile.exists() && clientDataFile.open(QIODevice::WriteOnly))
{
clientDataFile.write("{}");
clientDataFile.close();
}
return profileDir.filePath("client-data.json");
}
QString Utils::getLogFilePath()
{
QString logPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
QDir logDir(logPath);
if (!logDir.exists())
{
logDir.mkpath(".");
}
return logDir.filePath("zjuconnect.log");
}
void Utils::resetDefaultSettings(QSettings& settings)
{
settings.setValue("Credential/Username", "");
settings.setValue("Credential/Password", "");
settings.setValue("Credential/TOTPSecret", "");
settings.setValue("Common/ConnectAfterStart", false);
settings.setValue("Common/CheckUpdateAfterStart", false);
settings.setValue("Common/AutoSetProxy", false);
settings.setValue("Common/ReconnectTime", 1);
settings.setValue("Common/AutoReconnect", false);
settings.setValue("Common/SystemProxyBypass", "");
settings.setValue("ZJUConnect/ServerAddress", "trust.hitsz.edu.cn");
settings.setValue("ZJUConnect/ServerPort", 443);
settings.setValue("ZJUConnect/DNS", "");
settings.setValue("ZJUConnect/DNSAuto", true);
settings.setValue("ZJUConnect/SecondaryDNS", "");
settings.setValue("ZJUConnect/DNSTTL", 3600);
settings.setValue("ZJUConnect/SOCKS5Port", 11080);
settings.setValue("ZJUConnect/HTTPPort", 11081);
settings.setValue("ZJUConnect/ShadowsocksURL", "");
settings.setValue("ZJUConnect/DialDirectProxy", "");
settings.setValue("ZJUConnect/UpdateBestNodesInterval", 300);
settings.setValue("ZJUConnect/Protocol", "atrust");
settings.setValue("ZJUConnect/LoginDomain", "hitcas");
settings.setValue("ZJUConnect/AuthType", "cas");
settings.setValue("ZJUConnect/LoginURL", "");
settings.setValue("ZJUConnect/PhoneCountryCode", "86");
settings.setValue("ZJUConnect/PhoneNumber", "");
settings.setValue("ZJUConnect/MultiLine", false);
settings.setValue("ZJUConnect/KeepAlive", false);
settings.setValue("ZJUConnect/KeepAliveURL", "");
settings.setValue("ZJUConnect/BindInterface", "");
settings.setValue("ZJUConnect/OutsideAccess", false);
settings.setValue("ZJUConnect/SkipDomainResource", false);
settings.setValue("ZJUConnect/DisableServerConfig", false);
settings.setValue("ZJUConnect/ProxyAll", false);
settings.setValue("ZJUConnect/DisableZJUDNS", false);
settings.setValue("ZJUConnect/ZJUDefault", false);
settings.setValue("ZJUConnect/Debug", false);
settings.setValue("ZJUConnect/TUNMode", false);
settings.setValue("ZJUConnect/AddRoute", false);
settings.setValue("ZJUConnect/DNSHijack", false);
settings.setValue("ZJUConnect/FakeIP", false);
settings.setValue("ZJUConnect/TCPTunnelMode", false);
settings.setValue("ZJUConnect/AutoDetectInterface", false);
settings.setValue("ZJUConnect/TCPPortForwarding", "");
settings.setValue("ZJUConnect/UDPPortForwarding", "");
settings.setValue("ZJUConnect/CustomDNS", "");
settings.setValue("ZJUConnect/CustomProxyDomain", "");
settings.setValue("ZJUConnect/ExtraArguments", "");
settings.setValue("Common/ConfigVersion", Utils::CONFIG_VERSION);
}

View File

@@ -1,58 +0,0 @@
#ifndef UTILS_H
#define UTILS_H
#include <QString>
#include <QByteArray>
#include <QNetworkReply>
#include <QWidget>
#include <QSettings>
namespace Utils
{
const inline QString REPO_NAME("chenx-dust/EZ4Connect");
const inline QString CORE_REPO_NAME("Mythologyli/zju-connect");
const inline QString APP_NAME("EZ4Connect");
constexpr inline int CONFIG_VERSION = 8;
QString consoleOutputToQString(const QByteArray &byteArray);
void setWidgetFixedWhenHidden(QWidget *widget);
void showAboutMessageBox(QWidget *parent = nullptr);
bool isSystemProxySet(int http_port = -1, int socks_port = -1);
void setSystemProxy(int http_port, int socks_port, const QString &bypass);
void clearSystemProxy();
QString getIpv4Address(const QString &interfaceName);
void setAutoStart(bool enable);
bool credentialCheck(const QString &username, const QString &password);
void resetDefaultSettings(QSettings &settings);
QString getCorePath();
QString checkCoreVersion(QObject *parent);
bool isRunningAsAdmin();
bool relaunchAsAdmin();
void clearClientData(const QString &profileId);
QString getClientDataPath(const QString &profileId);
QString getLogFilePath();
QString getArgValue(const QStringList &args, const QString &key);
void setDeviceTrust(QObject *parent, const QString &protocol, const QString &server, int port, const QString &profileId, bool trust);
}
#endif //UTILS_H

Some files were not shown because too many files have changed in this diff Show More