From 0fdf728c46565289f74cbf9ff9fcae27f3a9a2a1 Mon Sep 17 00:00:00 2001 From: Chenx Dust Date: Mon, 27 Jul 2026 16:17:47 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8F=90=E5=8F=96=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E5=91=BD=E4=BB=A4=E6=9E=84=E5=BB=BA=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 13 + core/connectionprofile.h | 83 ++++++ core/corecommandbuilder.cpp | 138 ++++++++++ core/corecommandbuilder.h | 26 ++ tests/corecommandbuilder_test.cpp | 121 +++++++++ zjuconnectcontroller/zjuconnectcontroller.cpp | 254 ++---------------- 6 files changed, 401 insertions(+), 234 deletions(-) create mode 100644 core/connectionprofile.h create mode 100644 core/corecommandbuilder.cpp create mode 100644 core/corecommandbuilder.h create mode 100644 tests/corecommandbuilder_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a614688..1cc938a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,7 @@ set(SOURCE_FILES main.cpp mainwindow.cpp zjuconnectmode.cpp + core/corecommandbuilder.cpp zjuconnectcontroller/zjuconnectcontroller.cpp extrasettingwindow/extrasettingwindow.cpp settingwindow/settingwindow.cpp @@ -106,3 +107,15 @@ target_link_libraries(${PROJECT_NAME} SingleApplication::SingleApplication ${PLATFORM_LIBS} ) + +include(CTest) + +if(BUILD_TESTING) + add_executable(corecommandbuilder_test + tests/corecommandbuilder_test.cpp + core/corecommandbuilder.cpp + ) + target_include_directories(corecommandbuilder_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(corecommandbuilder_test Qt::Core) + add_test(NAME corecommandbuilder_test COMMAND corecommandbuilder_test) +endif() diff --git a/core/connectionprofile.h b/core/connectionprofile.h new file mode 100644 index 0000000..e30c686 --- /dev/null +++ b/core/connectionprofile.h @@ -0,0 +1,83 @@ +#ifndef CONNECTIONPROFILE_H +#define CONNECTIONPROFILE_H + +#include + +struct ConnectionCredentials +{ + QString username; + QString password; + QString totpSecret; + QString certFile; + QString certPassword; +}; + +struct ConnectionEndpoint +{ + QString protocol; + QString authType; + QString loginDomain; + QString phone; + QString server; + int port = 0; +}; + +struct DnsOptions +{ + QString primary; + bool automatic = false; + QString secondary; + int ttl = 3600; + bool disableZjuDns = false; + QString custom; +}; + +struct ProxyOptions +{ + QString socksBind; + QString httpBind; + QString shadowsocksUrl; + QString dialDirectProxy; + bool proxyAll = false; + QString customDomains; +}; + +struct TunnelOptions +{ + bool tunMode = false; + bool addRoute = false; + bool dnsHijack = false; + bool fakeIp = false; + bool tcpTunnelMode = false; + QString tcpPortForwarding; + QString udpPortForwarding; +}; + +struct ConnectionBehavior +{ + int updateBestNodesInterval = 300; + bool disableMultiLine = false; + bool disableKeepAlive = false; + QString keepAliveUrl; + QString bindInterface; + bool autoDetectInterface = false; + bool skipDomainResource = false; + bool disableServerConfig = false; + bool disableZjuConfig = false; + bool debugDump = false; +}; + +struct ConnectionProfile +{ + QString program; + QString profileId; + ConnectionCredentials credentials; + ConnectionEndpoint endpoint; + DnsOptions dns; + ProxyOptions proxy; + TunnelOptions tunnel; + ConnectionBehavior behavior; + QString extraArguments; +}; + +#endif // CONNECTIONPROFILE_H diff --git a/core/corecommandbuilder.cpp b/core/corecommandbuilder.cpp new file mode 100644 index 0000000..0618587 --- /dev/null +++ b/core/corecommandbuilder.cpp @@ -0,0 +1,138 @@ +#include "corecommandbuilder.h" + +namespace +{ +void appendOption(QStringList &arguments, const QString &name, const QString &value) +{ + if (!value.isEmpty()) + { + arguments << name << value; + } +} +} + +CoreCommand CoreCommandBuilder::build(const ConnectionProfile &profile, const CoreRuntimePaths &runtimePaths) +{ + QStringList arguments; + + appendOption(arguments, "-protocol", profile.endpoint.protocol); + if (!profile.endpoint.authType.isEmpty()) + { + arguments << "-auth-type" << "auth/" + profile.endpoint.authType; + } + + if (profile.endpoint.protocol == "atrust") + { + appendOption(arguments, "-graph-code-file", runtimePaths.graphCodeFile); + appendOption(arguments, "-client-data-file", runtimePaths.clientDataFile); + } + + appendOption(arguments, "-phone", profile.endpoint.phone); + appendOption(arguments, "-login-domain", profile.endpoint.loginDomain); + appendOption(arguments, "-server", profile.endpoint.server); + if (profile.endpoint.port != 0) + { + arguments << "-port" << QString::number(profile.endpoint.port); + } + + if (!profile.dns.primary.isEmpty() || profile.dns.automatic) + { + arguments << "-zju-dns-server" << (profile.dns.automatic ? "auto" : profile.dns.primary); + } + if (profile.dns.ttl != 3600) + { + arguments << "-dns-ttl" << QString::number(profile.dns.ttl); + } + appendOption(arguments, "-secondary-dns-server", profile.dns.secondary); + + if (profile.behavior.disableMultiLine) + { + arguments << "-disable-multi-line"; + } + if (profile.behavior.disableKeepAlive) + { + arguments << "-disable-keep-alive"; + } + appendOption(arguments, "-keep-alive-url", profile.behavior.keepAliveUrl); + appendOption(arguments, "-bind-interface", profile.behavior.bindInterface); + if (profile.behavior.autoDetectInterface) + { + arguments << "-auto-detect-interface"; + } + if (profile.behavior.disableZjuConfig) + { + arguments << "-disable-zju-config"; + } + if (profile.dns.disableZjuDns) + { + arguments << "-disable-zju-dns"; + } + if (profile.behavior.disableServerConfig) + { + arguments << "-disable-server-config"; + } + if (profile.proxy.proxyAll) + { + arguments << "-proxy-all"; + } + if (profile.behavior.skipDomainResource) + { + arguments << "-skip-domain-resource"; + } + + if (profile.tunnel.tunMode) + { + arguments << "-tun-mode"; + if (profile.tunnel.dnsHijack) + { + arguments << "-dns-hijack"; + if (profile.tunnel.fakeIp) + { + arguments << "-fake-ip"; + } + } + if (profile.tunnel.addRoute) + { + arguments << "-add-route"; + } + } + if (profile.tunnel.tcpTunnelMode) + { + arguments << "-tcp-tunnel-mode"; + } + if (profile.behavior.debugDump) + { + arguments << "-debug-dump"; + } + + appendOption(arguments, "-socks-bind", profile.proxy.socksBind); + appendOption(arguments, "-http-bind", profile.proxy.httpBind); + appendOption(arguments, "-shadowsocks-url", profile.proxy.shadowsocksUrl); + appendOption(arguments, "-dial-direct-proxy", profile.proxy.dialDirectProxy); + if (profile.behavior.updateBestNodesInterval != 300) + { + arguments << "-update-best-nodes-interval" + << QString::number(profile.behavior.updateBestNodesInterval); + } + appendOption(arguments, "-tcp-port-forwarding", profile.tunnel.tcpPortForwarding); + appendOption(arguments, "-udp-port-forwarding", profile.tunnel.udpPortForwarding); + appendOption(arguments, "-custom-dns", profile.dns.custom); + appendOption(arguments, "-custom-proxy-domain", profile.proxy.customDomains); + if (!profile.extraArguments.isEmpty()) + { + arguments.append(profile.extraArguments.split(" ")); + } + + CoreCommand command; + command.loggableArguments = arguments; + + QStringList credentials; + appendOption(credentials, "-username", profile.credentials.username); + appendOption(credentials, "-password", profile.credentials.password); + appendOption(credentials, "-totp-secret", profile.credentials.totpSecret); + + appendOption(arguments, "-cert-file", profile.credentials.certFile); + appendOption(arguments, "-cert-password", profile.credentials.certPassword); + command.arguments = credentials + arguments; + return command; +} diff --git a/core/corecommandbuilder.h b/core/corecommandbuilder.h new file mode 100644 index 0000000..5ed3d4c --- /dev/null +++ b/core/corecommandbuilder.h @@ -0,0 +1,26 @@ +#ifndef CORECOMMANDBUILDER_H +#define CORECOMMANDBUILDER_H + +#include + +#include "connectionprofile.h" + +struct CoreRuntimePaths +{ + QString graphCodeFile; + QString clientDataFile; +}; + +struct CoreCommand +{ + QStringList arguments; + QStringList loggableArguments; +}; + +class CoreCommandBuilder +{ +public: + static CoreCommand build(const ConnectionProfile &profile, const CoreRuntimePaths &runtimePaths = {}); +}; + +#endif // CORECOMMANDBUILDER_H diff --git a/tests/corecommandbuilder_test.cpp b/tests/corecommandbuilder_test.cpp new file mode 100644 index 0000000..7f90ff8 --- /dev/null +++ b/tests/corecommandbuilder_test.cpp @@ -0,0 +1,121 @@ +#include +#include + +#include "core/corecommandbuilder.h" + +namespace +{ +bool expectEqual(const QStringList &actual, const QStringList &expected, const char *testName) +{ + if (actual == expected) + { + return true; + } + + qCritical().noquote() << testName << "failed" + << "\nexpected:" << expected.join('|') + << "\nactual: " << actual.join('|'); + return false; +} + +bool buildsMinimalCommand() +{ + ConnectionProfile profile; + profile.endpoint.protocol = "easyconnect"; + + const CoreCommand command = CoreCommandBuilder::build(profile); + return expectEqual(command.arguments, {"-protocol", "easyconnect"}, "buildsMinimalCommand") + && expectEqual(command.loggableArguments, command.arguments, "minimalCommandIsLoggable"); +} + +bool buildsCompleteCommandInCompatibleOrder() +{ + ConnectionProfile profile; + profile.endpoint = {"atrust", "cas", "domain", "86-123", "vpn.example.edu", 8443}; + profile.credentials = {"alice", "secret", "TOTP", "/tmp/client.p12", "cert-secret"}; + profile.dns = {"10.0.0.1", false, "10.0.0.2", 60, true, "example.org=1.1.1.1"}; + profile.proxy = {"127.0.0.1:1080", "127.0.0.1:1081", "ss://url", "http://direct", + true, "example.org"}; + profile.tunnel = {true, true, true, true, true, "127.0.0.1:80/10.0.0.1:80", + "127.0.0.1:53/10.0.0.1:53"}; + profile.behavior = {30, true, true, "https://keepalive", "en0", true, true, true, + true, true}; + profile.extraArguments = "-foo bar"; + + const CoreRuntimePaths runtimePaths{"/tmp/graph.jpg", "/tmp/client-data.json"}; + const CoreCommand command = CoreCommandBuilder::build(profile, runtimePaths); + const QStringList expected{ + "-username", "alice", + "-password", "secret", + "-totp-secret", "TOTP", + "-protocol", "atrust", + "-auth-type", "auth/cas", + "-graph-code-file", "/tmp/graph.jpg", + "-client-data-file", "/tmp/client-data.json", + "-phone", "86-123", + "-login-domain", "domain", + "-server", "vpn.example.edu", + "-port", "8443", + "-zju-dns-server", "10.0.0.1", + "-dns-ttl", "60", + "-secondary-dns-server", "10.0.0.2", + "-disable-multi-line", + "-disable-keep-alive", + "-keep-alive-url", "https://keepalive", + "-bind-interface", "en0", + "-auto-detect-interface", + "-disable-zju-config", + "-disable-zju-dns", + "-disable-server-config", + "-proxy-all", + "-skip-domain-resource", + "-tun-mode", + "-dns-hijack", + "-fake-ip", + "-add-route", + "-tcp-tunnel-mode", + "-debug-dump", + "-socks-bind", "127.0.0.1:1080", + "-http-bind", "127.0.0.1:1081", + "-shadowsocks-url", "ss://url", + "-dial-direct-proxy", "http://direct", + "-update-best-nodes-interval", "30", + "-tcp-port-forwarding", "127.0.0.1:80/10.0.0.1:80", + "-udp-port-forwarding", "127.0.0.1:53/10.0.0.1:53", + "-custom-dns", "example.org=1.1.1.1", + "-custom-proxy-domain", "example.org", + "-foo", "bar", + "-cert-file", "/tmp/client.p12", + "-cert-password", "cert-secret" + }; + return expectEqual(command.arguments, expected, "buildsCompleteCommandInCompatibleOrder"); +} + +bool excludesCredentialsFromLoggableArguments() +{ + ConnectionProfile profile; + profile.endpoint.protocol = "easyconnect"; + profile.credentials = {"alice", "secret", "TOTP", "/tmp/client.p12", "cert-secret"}; + + const CoreCommand command = CoreCommandBuilder::build(profile); + const QString logLine = command.loggableArguments.join(' '); + const bool safe = !logLine.contains("alice") + && !logLine.contains("secret") + && !logLine.contains("TOTP") + && !logLine.contains("/tmp/client.p12"); + if (!safe) + { + qCritical() << "excludesCredentialsFromLoggableArguments failed:" << logLine; + } + return safe; +} +} + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + const bool passed = buildsMinimalCommand() + && buildsCompleteCommandInCompatibleOrder() + && excludesCredentialsFromLoggableArguments(); + return passed ? 0 : 1; +} diff --git a/zjuconnectcontroller/zjuconnectcontroller.cpp b/zjuconnectcontroller/zjuconnectcontroller.cpp index 18490cf..92c8227 100644 --- a/zjuconnectcontroller/zjuconnectcontroller.cpp +++ b/zjuconnectcontroller/zjuconnectcontroller.cpp @@ -1,5 +1,6 @@ #include "zjuconnectcontroller.h" #include "mainwindow.h" +#include "core/corecommandbuilder.h" #include "utils/utils.h" #include #include @@ -244,19 +245,21 @@ void ZjuConnectController::start( const QString& profileId ) { - QStringList args; + ConnectionProfile profile; + profile.program = program; + profile.profileId = profileId; + profile.credentials = {username, password, totpSecret, certFile, certPassword}; + profile.endpoint = {protocol, authType, loginDomain, phone, server, port}; + profile.dns = {dns, dnsAuto, secondaryDns, dnsTtl, disableZjuDns, customDNS}; + profile.proxy = {socksBind, httpBind, shadowsocksUrl, dialDirectProxy, proxyAll, customProxyDomain}; + profile.tunnel = {tunMode, addRoute, dnsHijack, fakeIp, tcpTunnelMode, + tcpPortForwarding, udpPortForwarding}; + profile.behavior = {updateBestNodesInterval, disableMultiLine, disableKeepAlive, + keepAliveUrl, bindInterface, autoDetectInterface, skipDomainResource, + disableServerConfig, disableZjuConfig, debugDump}; + profile.extraArguments = extraArguments; - if (!protocol.isEmpty()) - { - args.append("-protocol"); - args.append(protocol); - } - - if (!authType.isEmpty()) - { - args.append("-auth-type"); - args.append("auth/" + authType); - } + CoreRuntimePaths runtimePaths; if (protocol == "atrust") { @@ -267,242 +270,25 @@ void ZjuConnectController::start( tempDir->setAutoRemove(true); } graphFile = tempDir->filePath("graph.jpg"); - args.append("-graph-code-file"); - args.append(graphFile); - - // 存放 Client Data - args.append("-client-data-file"); - args.append(Utils::getClientDataPath(profileId)); - } - - if (!phone.isEmpty()) - { - args.append("-phone"); - args.append(phone); - } - - if (!loginDomain.isEmpty()) - { - args.append("-login-domain"); - args.append(loginDomain); - } - - if (!server.isEmpty()) - { - args.append("-server"); - args.append(server); - } - - if (port != 0) - { - args.append("-port"); - args.append(QString::number(port)); - } - - if (!dns.isEmpty() || dnsAuto) - { - args.append("-zju-dns-server"); - if (dnsAuto) - { - args.append("auto"); - } - else - { - args.append(dns); - } - } - - if (dnsTtl != 3600) - { - args.append("-dns-ttl"); - args.append(QString::number(dnsTtl)); - } - - if (!secondaryDns.isEmpty()) - { - args.append("-secondary-dns-server"); - args.append(secondaryDns); - } - - if (disableMultiLine) - { - args.append("-disable-multi-line"); - } - - if (disableKeepAlive) - { - args.append("-disable-keep-alive"); - } - - if (!keepAliveUrl.isEmpty()) - { - args.append("-keep-alive-url"); - args.append(keepAliveUrl); - } - - if (!bindInterface.isEmpty()) - { - args.append("-bind-interface"); - args.append(bindInterface); - } - - if (autoDetectInterface) - { - args.append("-auto-detect-interface"); - } - - if (disableZjuConfig) - { - args.append("-disable-zju-config"); - } - - if (disableZjuDns) - { - args.append("-disable-zju-dns"); - } - - if (disableServerConfig) - { - args.append("-disable-server-config"); - } - - if (proxyAll) - { - args.append("-proxy-all"); - } - - if (skipDomainResource) - { - args.append("-skip-domain-resource"); - } - - if (tunMode) - { - args.append("-tun-mode"); - - if (dnsHijack) - { - args.append("-dns-hijack"); - if (fakeIp) - { - args.append("-fake-ip"); - } - } - - if (addRoute) - { - args.append("-add-route"); - } - } - - if (tcpTunnelMode) - { - args.append("-tcp-tunnel-mode"); - } - - if (debugDump) - { - args.append("-debug-dump"); - } - - if (!socksBind.isEmpty()) - { - args.append("-socks-bind"); - args.append(socksBind); - } - - if (!httpBind.isEmpty()) - { - args.append("-http-bind"); - args.append(httpBind); - } - - if (!shadowsocksUrl.isEmpty()) - { - args.append("-shadowsocks-url"); - args.append(shadowsocksUrl); - } - - if (!dialDirectProxy.isEmpty()) - { - args.append("-dial-direct-proxy"); - args.append(dialDirectProxy); - } - - if (updateBestNodesInterval != 300) - { - args.append("-update-best-nodes-interval"); - args.append(QString::number(updateBestNodesInterval)); - } - - if (!tcpPortForwarding.isEmpty()) - { - args.append("-tcp-port-forwarding"); - args.append(tcpPortForwarding); - } - - if (!udpPortForwarding.isEmpty()) - { - args.append("-udp-port-forwarding"); - args.append(udpPortForwarding); - } - - if (!customDNS.isEmpty()) - { - args.append("-custom-dns"); - args.append(customDNS); - } - - if (!customProxyDomain.isEmpty()) - { - args.append("-custom-proxy-domain"); - args.append(customProxyDomain); - } - - if (!extraArguments.isEmpty()) - { - args.append(extraArguments.split(" ")); + runtimePaths.graphCodeFile = graphFile; + runtimePaths.clientDataFile = Utils::getClientDataPath(profileId); } + const CoreCommand command = CoreCommandBuilder::build(profile, runtimePaths); QString timeString = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); - emit outputRead(timeString + " VPN 启动!参数:" + args.join(' ')); - - QStringList credentialList; - - if (!username.isEmpty()) - { - credentialList.append("-username"); - credentialList.append(username); - } - - if (!password.isEmpty()) - { - credentialList.append("-password"); - credentialList.append(password); - } + emit outputRead(timeString + " VPN 启动!参数:" + command.loggableArguments.join(' ')); if (!totpSecret.isEmpty()) { emit outputRead(timeString + " 使用了 TOTP"); - credentialList.append("-totp-secret"); - credentialList.append(totpSecret); } - if (!certFile.isEmpty()) { emit outputRead(timeString + " 使用了证书文件"); - args.append("-cert-file"); - args.append(certFile); - } - - if (!certPassword.isEmpty()) - { - args.append("-cert-password"); - args.append(certPassword); } QString programToStart = program; - QStringList finalArgs = credentialList + args; + QStringList finalArgs = command.arguments; #if defined(Q_OS_UNIX) if (tunMode && !Utils::isRunningAsAdmin())