refactor: 提取核心命令构建器

This commit is contained in:
Chenx Dust
2026-07-27 16:17:47 +08:00
parent c875f5f21a
commit 0fdf728c46
6 changed files with 401 additions and 234 deletions

View File

@@ -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()

83
core/connectionprofile.h Normal file
View File

@@ -0,0 +1,83 @@
#ifndef CONNECTIONPROFILE_H
#define CONNECTIONPROFILE_H
#include <QString>
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

138
core/corecommandbuilder.cpp Normal file
View File

@@ -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;
}

26
core/corecommandbuilder.h Normal file
View File

@@ -0,0 +1,26 @@
#ifndef CORECOMMANDBUILDER_H
#define CORECOMMANDBUILDER_H
#include <QStringList>
#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

View File

@@ -0,0 +1,121 @@
#include <QCoreApplication>
#include <QDebug>
#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;
}

View File

@@ -1,5 +1,6 @@
#include "zjuconnectcontroller.h"
#include "mainwindow.h"
#include "core/corecommandbuilder.h"
#include "utils/utils.h"
#include <qcontainerfwd.h>
#include <QCoreApplication>
@@ -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())