mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-19 17:36:19 +03:00
Compare commits
5 Commits
refactorin
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb5666057b | ||
|
|
a49892c7e7 | ||
|
|
277b295fd8 | ||
|
|
8c33779fc3 | ||
|
|
f0299ca9fe |
@@ -35,6 +35,8 @@ set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/installers/torInstaller.h
|
||||
${CLIENT_ROOT_DIR}/core/installers/sftpInstaller.h
|
||||
${CLIENT_ROOT_DIR}/core/installers/socks5Installer.h
|
||||
${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.h
|
||||
${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.h
|
||||
@@ -110,6 +112,8 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/installers/torInstaller.cpp
|
||||
${CLIENT_ROOT_DIR}/core/installers/sftpInstaller.cpp
|
||||
${CLIENT_ROOT_DIR}/core/installers/socks5Installer.cpp
|
||||
${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.cpp
|
||||
${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.cpp
|
||||
@@ -201,12 +205,14 @@ file(GLOB UI_MODELS_H CONFIGURE_DEPENDS
|
||||
${CLIENT_ROOT_DIR}/ui/models/*.h
|
||||
${CLIENT_ROOT_DIR}/ui/models/protocols/*.h
|
||||
${CLIENT_ROOT_DIR}/ui/models/services/*.h
|
||||
${CLIENT_ROOT_DIR}/ui/models/utils/*.h
|
||||
${CLIENT_ROOT_DIR}/ui/models/api/*.h
|
||||
)
|
||||
file(GLOB UI_MODELS_CPP CONFIGURE_DEPENDS
|
||||
${CLIENT_ROOT_DIR}/ui/models/*.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/models/protocols/*.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/models/services/*.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/models/utils/*.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/models/api/*.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -20,14 +20,123 @@
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
|
||||
namespace {
|
||||
Logger logger("XrayConfigurator");
|
||||
}
|
||||
Logger logger("XrayConfigurator");
|
||||
|
||||
QString normalizeXhttpMode(const QString &m) {
|
||||
const QString t = m.trimmed();
|
||||
if (t.isEmpty() || t.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("auto");
|
||||
}
|
||||
if (t.compare(QLatin1String("Packet-up"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("packet-up");
|
||||
if (t.compare(QLatin1String("Stream-up"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("stream-up");
|
||||
if (t.compare(QLatin1String("Stream-one"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("stream-one");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
// Xray-core: empty → path; "None" in UI → omit (core default path)
|
||||
QString normalizeSessionSeqPlacement(const QString &p)
|
||||
{
|
||||
if (p.isEmpty() || p.compare(QLatin1String("None"), Qt::CaseInsensitive) == 0)
|
||||
return {};
|
||||
return p.toLower();
|
||||
}
|
||||
|
||||
QString normalizeUplinkDataPlacement(const QString &p)
|
||||
{
|
||||
if (p.isEmpty() || p.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("body");
|
||||
if (p.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("auto");
|
||||
if (p.compare(QLatin1String("Query"), Qt::CaseInsensitive) == 0)
|
||||
// "Query" is not valid for uplink payload in splithttp; closest documented mode
|
||||
return QStringLiteral("header");
|
||||
return p.toLower();
|
||||
}
|
||||
|
||||
// splithttp: cookie | header | query | queryInHeader (not "body")
|
||||
QString normalizeXPaddingPlacement(const QString &p)
|
||||
{
|
||||
QString t = p.trimmed();
|
||||
if (t.isEmpty())
|
||||
return QString::fromLatin1(amnezia::protocols::xray::defaultXPaddingPlacement).toLower();
|
||||
if (t.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("queryInHeader");
|
||||
if (t.contains(QLatin1String("queryInHeader"), Qt::CaseInsensitive)
|
||||
|| t.compare(QLatin1String("Query in header"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("queryInHeader");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
// splithttp: repeat-x | tokenish
|
||||
QString normalizeXPaddingMethod(const QString &m)
|
||||
{
|
||||
QString t = m.trimmed();
|
||||
if (t.isEmpty() || t.compare(QLatin1String("Repeat-x"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("repeat-x");
|
||||
if (t.compare(QLatin1String("Tokenish"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("tokenish");
|
||||
if (t.compare(QLatin1String("Random"), Qt::CaseInsensitive) == 0
|
||||
|| t.compare(QLatin1String("Zero"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("repeat-x");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
void putIntRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin,
|
||||
const char *fallbackMax)
|
||||
{
|
||||
if (minV.isEmpty() && maxV.isEmpty())
|
||||
return;
|
||||
if (minV.isEmpty())
|
||||
minV = QString::fromLatin1(fallbackMin);
|
||||
if (maxV.isEmpty())
|
||||
maxV = QString::fromLatin1(fallbackMax);
|
||||
QJsonObject r;
|
||||
r[QStringLiteral("from")] = minV.toInt();
|
||||
r[QStringLiteral("to")] = maxV.toInt();
|
||||
obj[QString::fromUtf8(key)] = r;
|
||||
}
|
||||
|
||||
// Desktop applies this in XrayProtocol::start(); iOS/Android pass JSON straight to libxray — same fixes here.
|
||||
void sanitizeXrayNativeConfig(amnezia::ProtocolConfig &pc)
|
||||
{
|
||||
QString c = pc.nativeConfig();
|
||||
if (c.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
bool changed = false;
|
||||
if (c.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
c.replace(QLatin1String("Mozilla/5.0"), QString::fromLatin1(amnezia::protocols::xray::defaultFingerprint),
|
||||
Qt::CaseInsensitive);
|
||||
changed = true;
|
||||
}
|
||||
const QString legacyListen = QString::fromLatin1(amnezia::protocols::xray::defaultLocalAddr);
|
||||
const QString listenOk = QString::fromLatin1(amnezia::protocols::xray::defaultLocalListenAddr);
|
||||
if (c.contains(legacyListen)) {
|
||||
c.replace(legacyListen, listenOk);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
pc.setNativeConfig(c);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
XrayConfigurator::XrayConfigurator(SshSession* sshSession, QObject *parent)
|
||||
: ConfiguratorBase(sshSession, parent)
|
||||
{
|
||||
}
|
||||
|
||||
amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
|
||||
amnezia::ProtocolConfig protocolConfig)
|
||||
{
|
||||
applyDnsToNativeConfig(settings.dns, protocolConfig);
|
||||
sanitizeXrayNativeConfig(protocolConfig);
|
||||
return protocolConfig;
|
||||
}
|
||||
|
||||
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
@@ -35,11 +144,19 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
{
|
||||
// Generate new UUID for client
|
||||
QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
|
||||
|
||||
// Get flow value from settings (default xtls-rprx-vision)
|
||||
QString flowValue = "xtls-rprx-vision";
|
||||
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
if (!xrayCfg->serverConfig.flow.isEmpty()) {
|
||||
flowValue = xrayCfg->serverConfig.flow;
|
||||
}
|
||||
}
|
||||
|
||||
// Get current server config
|
||||
QString currentConfig = m_sshSession->getTextFileFromContainer(
|
||||
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
|
||||
|
||||
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to get server config file";
|
||||
return "";
|
||||
@@ -54,7 +171,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
}
|
||||
|
||||
QJsonObject serverConfig = doc.object();
|
||||
|
||||
|
||||
// Validate server config structure
|
||||
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
|
||||
logger.error() << "Server config missing 'inbounds' field";
|
||||
@@ -68,7 +185,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
QJsonObject inbound = inbounds[0].toObject();
|
||||
if (!inbound.contains(amnezia::protocols::xray::settings)) {
|
||||
logger.error() << "Inbound missing 'settings' field";
|
||||
@@ -84,26 +201,29 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
}
|
||||
|
||||
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
|
||||
|
||||
|
||||
// Create configuration for new client
|
||||
QJsonObject clientConfig {
|
||||
{amnezia::protocols::xray::id, clientId},
|
||||
{amnezia::protocols::xray::flow, "xtls-rprx-vision"}
|
||||
};
|
||||
|
||||
clientConfig[amnezia::protocols::xray::id] = clientId;
|
||||
if (!flowValue.isEmpty()) {
|
||||
clientConfig[amnezia::protocols::xray::flow] = flowValue;
|
||||
}
|
||||
|
||||
clients.append(clientConfig);
|
||||
|
||||
|
||||
// Update config
|
||||
settings[amnezia::protocols::xray::clients] = clients;
|
||||
inbound[amnezia::protocols::xray::settings] = settings;
|
||||
inbounds[0] = inbound;
|
||||
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
|
||||
|
||||
|
||||
// Save updated config to server
|
||||
QString updatedConfig = QJsonDocument(serverConfig).toJson();
|
||||
errorCode = m_sshSession->uploadTextFileToContainer(
|
||||
container,
|
||||
credentials,
|
||||
container,
|
||||
credentials,
|
||||
updatedConfig,
|
||||
amnezia::protocols::xray::serverConfigPath,
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting
|
||||
@@ -116,7 +236,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
// Restart container
|
||||
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
|
||||
errorCode = m_sshSession->runScript(
|
||||
credentials,
|
||||
credentials,
|
||||
m_sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns))
|
||||
);
|
||||
|
||||
@@ -128,75 +248,286 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
return clientId;
|
||||
}
|
||||
|
||||
ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
ErrorCode &errorCode)
|
||||
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
|
||||
{
|
||||
const XrayServerConfig* serverConfig = nullptr;
|
||||
if (auto* xrayConfig = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
serverConfig = &xrayConfig->serverConfig;
|
||||
QJsonObject streamSettings;
|
||||
const auto &xhttp = srv.xhttp;
|
||||
const auto &mkcp = srv.mkcp;
|
||||
namespace px = amnezia::protocols::xray;
|
||||
|
||||
QString networkValue = QStringLiteral("tcp");
|
||||
if (srv.transport == QLatin1String("xhttp"))
|
||||
networkValue = QStringLiteral("xhttp");
|
||||
else if (srv.transport == QLatin1String("mkcp"))
|
||||
networkValue = QStringLiteral("kcp");
|
||||
streamSettings[px::network] = networkValue;
|
||||
|
||||
streamSettings[px::security] = srv.security;
|
||||
|
||||
if (srv.security == QLatin1String("tls")) {
|
||||
QJsonObject tlsSettings;
|
||||
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
|
||||
tlsSettings[px::serverName] = sniEff;
|
||||
const QString alpnEff = srv.alpn.isEmpty() ? QString::fromLatin1(px::defaultAlpn) : srv.alpn;
|
||||
QJsonArray alpnArray;
|
||||
for (const QString &a : alpnEff.split(QLatin1Char(','))) {
|
||||
const QString t = a.trimmed();
|
||||
if (!t.isEmpty())
|
||||
alpnArray.append(t);
|
||||
}
|
||||
if (!alpnArray.isEmpty())
|
||||
tlsSettings[QStringLiteral("alpn")] = alpnArray;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
tlsSettings[px::fingerprint] = fpEff;
|
||||
streamSettings[QStringLiteral("tlsSettings")] = tlsSettings;
|
||||
}
|
||||
|
||||
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
QJsonObject realSettings;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
realSettings[px::fingerprint] = fpEff;
|
||||
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
|
||||
realSettings[px::serverName] = sniEff;
|
||||
streamSettings[px::realitySettings] = realSettings;
|
||||
}
|
||||
|
||||
// XHTTP — JSON must match Xray-core SplitHTTPConfig (flat xPadding fields, see transport_internet.go)
|
||||
if (srv.transport == QLatin1String("xhttp")) {
|
||||
QJsonObject xo;
|
||||
const QString hostEff = xhttp.host.isEmpty() ? QString::fromLatin1(px::defaultXhttpHost) : xhttp.host;
|
||||
xo[QStringLiteral("host")] = hostEff;
|
||||
if (!xhttp.path.isEmpty())
|
||||
xo[QStringLiteral("path")] = xhttp.path;
|
||||
xo[QStringLiteral("mode")] = normalizeXhttpMode(xhttp.mode);
|
||||
|
||||
if (xhttp.headersTemplate.compare(QLatin1String("HTTP"), Qt::CaseInsensitive) == 0) {
|
||||
QJsonObject headers;
|
||||
headers[QStringLiteral("Host")] = hostEff;
|
||||
xo[QStringLiteral("headers")] = headers;
|
||||
}
|
||||
|
||||
const QString methodEff =
|
||||
xhttp.uplinkMethod.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkMethod) : xhttp.uplinkMethod;
|
||||
xo[QStringLiteral("uplinkHTTPMethod")] = methodEff.toUpper();
|
||||
|
||||
xo[QStringLiteral("noGRPCHeader")] = xhttp.disableGrpc;
|
||||
xo[QStringLiteral("noSSEHeader")] = xhttp.disableSse;
|
||||
|
||||
const QString sessPl = normalizeSessionSeqPlacement(xhttp.sessionPlacement);
|
||||
if (!sessPl.isEmpty())
|
||||
xo[QStringLiteral("sessionPlacement")] = sessPl;
|
||||
const QString seqPl = normalizeSessionSeqPlacement(xhttp.seqPlacement);
|
||||
if (!seqPl.isEmpty())
|
||||
xo[QStringLiteral("seqPlacement")] = seqPl;
|
||||
if (!xhttp.sessionKey.isEmpty())
|
||||
xo[QStringLiteral("sessionKey")] = xhttp.sessionKey;
|
||||
if (!xhttp.seqKey.isEmpty())
|
||||
xo[QStringLiteral("seqKey")] = xhttp.seqKey;
|
||||
|
||||
xo[QStringLiteral("uplinkDataPlacement")] = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement);
|
||||
if (!xhttp.uplinkDataKey.isEmpty())
|
||||
xo[QStringLiteral("uplinkDataKey")] = xhttp.uplinkDataKey;
|
||||
|
||||
const QString ucs = xhttp.uplinkChunkSize.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkChunkSize)
|
||||
: xhttp.uplinkChunkSize;
|
||||
if (!ucs.isEmpty() && ucs != QLatin1String("0")) {
|
||||
const int v = ucs.toInt();
|
||||
QJsonObject chunkR;
|
||||
chunkR[QStringLiteral("from")] = v;
|
||||
chunkR[QStringLiteral("to")] = v;
|
||||
xo[QStringLiteral("uplinkChunkSize")] = chunkR;
|
||||
}
|
||||
|
||||
if (!xhttp.scMaxBufferedPosts.isEmpty())
|
||||
xo[QStringLiteral("scMaxBufferedPosts")] = xhttp.scMaxBufferedPosts.toLongLong();
|
||||
|
||||
putIntRangeIfAny(xo, "scMaxEachPostBytes", xhttp.scMaxEachPostBytesMin, xhttp.scMaxEachPostBytesMax,
|
||||
px::defaultXhttpScMaxEachPostBytesMin, px::defaultXhttpScMaxEachPostBytesMax);
|
||||
putIntRangeIfAny(xo, "scMinPostsIntervalMs", xhttp.scMinPostsIntervalMsMin, xhttp.scMinPostsIntervalMsMax,
|
||||
px::defaultXhttpScMinPostsIntervalMsMin, px::defaultXhttpScMinPostsIntervalMsMax);
|
||||
putIntRangeIfAny(xo, "scStreamUpServerSecs", xhttp.scStreamUpServerSecsMin, xhttp.scStreamUpServerSecsMax,
|
||||
px::defaultXhttpScStreamUpServerSecsMin, px::defaultXhttpScStreamUpServerSecsMax);
|
||||
|
||||
const auto &pad = xhttp.xPadding;
|
||||
xo[QStringLiteral("xPaddingObfsMode")] = pad.obfsMode;
|
||||
if (pad.obfsMode) {
|
||||
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
|
||||
QJsonObject br;
|
||||
br[QStringLiteral("from")] = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt();
|
||||
br[QStringLiteral("to")] = pad.bytesMax.isEmpty() ? (pad.bytesMin.isEmpty() ? 256 : pad.bytesMin.toInt())
|
||||
: pad.bytesMax.toInt();
|
||||
xo[QStringLiteral("xPaddingBytes")] = br;
|
||||
}
|
||||
xo[QStringLiteral("xPaddingKey")] = pad.key.isEmpty() ? QStringLiteral("x_padding") : pad.key;
|
||||
xo[QStringLiteral("xPaddingHeader")] = pad.header.isEmpty() ? QStringLiteral("X-Padding") : pad.header;
|
||||
xo[QStringLiteral("xPaddingPlacement")] = normalizeXPaddingPlacement(
|
||||
pad.placement.isEmpty() ? QString::fromLatin1(px::defaultXPaddingPlacement) : pad.placement);
|
||||
xo[QStringLiteral("xPaddingMethod")] = normalizeXPaddingMethod(
|
||||
pad.method.isEmpty() ? QString::fromLatin1(px::defaultXPaddingMethod) : pad.method);
|
||||
}
|
||||
|
||||
// xmux: Xray has no "enabled" flag; omit object when UI disables multiplex tuning.
|
||||
if (xhttp.xmux.enabled) {
|
||||
QJsonObject mux;
|
||||
auto addMuxRange = [&](const char *key, const QString &a, const QString &b) {
|
||||
if (a.isEmpty() && b.isEmpty())
|
||||
return;
|
||||
QJsonObject r;
|
||||
r[QStringLiteral("from")] = a.isEmpty() ? 0 : a.toInt();
|
||||
r[QStringLiteral("to")] = b.isEmpty() ? 0 : b.toInt();
|
||||
mux[QString::fromUtf8(key)] = r;
|
||||
};
|
||||
addMuxRange("maxConcurrency", xhttp.xmux.maxConcurrencyMin, xhttp.xmux.maxConcurrencyMax);
|
||||
addMuxRange("maxConnections", xhttp.xmux.maxConnectionsMin, xhttp.xmux.maxConnectionsMax);
|
||||
addMuxRange("cMaxReuseTimes", xhttp.xmux.cMaxReuseTimesMin, xhttp.xmux.cMaxReuseTimesMax);
|
||||
addMuxRange("hMaxRequestTimes", xhttp.xmux.hMaxRequestTimesMin, xhttp.xmux.hMaxRequestTimesMax);
|
||||
addMuxRange("hMaxReusableSecs", xhttp.xmux.hMaxReusableSecsMin, xhttp.xmux.hMaxReusableSecsMax);
|
||||
if (!xhttp.xmux.hKeepAlivePeriod.isEmpty())
|
||||
mux[QStringLiteral("hKeepAlivePeriod")] = xhttp.xmux.hKeepAlivePeriod.toLongLong();
|
||||
if (!mux.isEmpty())
|
||||
xo[QStringLiteral("xmux")] = mux;
|
||||
}
|
||||
|
||||
streamSettings[QStringLiteral("xhttpSettings")] = xo;
|
||||
}
|
||||
|
||||
if (srv.transport == QLatin1String("mkcp")) {
|
||||
QJsonObject kcpObj;
|
||||
const QString ttiEff = mkcp.tti.isEmpty() ? QString::fromLatin1(px::defaultMkcpTti) : mkcp.tti;
|
||||
const QString upEff = mkcp.uplinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpUplinkCapacity)
|
||||
: mkcp.uplinkCapacity;
|
||||
const QString downEff = mkcp.downlinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpDownlinkCapacity)
|
||||
: mkcp.downlinkCapacity;
|
||||
const QString rbufEff = mkcp.readBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpReadBufferSize)
|
||||
: mkcp.readBufferSize;
|
||||
const QString wbufEff = mkcp.writeBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpWriteBufferSize)
|
||||
: mkcp.writeBufferSize;
|
||||
kcpObj[QStringLiteral("tti")] = ttiEff.toInt();
|
||||
kcpObj[QStringLiteral("uplinkCapacity")] = upEff.toInt();
|
||||
kcpObj[QStringLiteral("downlinkCapacity")] = downEff.toInt();
|
||||
kcpObj[QStringLiteral("readBufferSize")] = rbufEff.toInt();
|
||||
kcpObj[QStringLiteral("writeBufferSize")] = wbufEff.toInt();
|
||||
kcpObj[QStringLiteral("congestion")] = mkcp.congestion;
|
||||
streamSettings[QStringLiteral("kcpSettings")] = kcpObj;
|
||||
}
|
||||
|
||||
return streamSettings;
|
||||
}
|
||||
|
||||
ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
ErrorCode &errorCode)
|
||||
{
|
||||
const XrayServerConfig *serverConfig = nullptr;
|
||||
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
serverConfig = &xrayCfg->serverConfig;
|
||||
}
|
||||
|
||||
if (!serverConfig) {
|
||||
logger.error() << "No XrayProtocolConfig found";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
|
||||
const XrayServerConfig &srv = *serverConfig;
|
||||
|
||||
QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, dnsSettings, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) {
|
||||
logger.error() << "Failed to prepare server config";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = ErrorCode::InternalError;
|
||||
}
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
|
||||
amnezia::ScriptVars vars = amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns);
|
||||
vars.append(amnezia::genProtocolVarsForContainer(container, containerConfig));
|
||||
QString config = m_sshSession->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), vars);
|
||||
|
||||
if (config.isEmpty()) {
|
||||
logger.error() << "Failed to get config template";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
// Fetch server keys (Reality only)
|
||||
QString xrayPublicKey;
|
||||
QString xrayShortId;
|
||||
|
||||
if (srv.security == "reality") {
|
||||
xrayPublicKey = m_sshSession->getTextFileFromContainer(container, credentials,
|
||||
amnezia::protocols::xray::PublicKeyPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
|
||||
logger.error() << "Failed to get public key";
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = ErrorCode::InternalError;
|
||||
}
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
xrayPublicKey.replace("\n", "");
|
||||
|
||||
xrayShortId = m_sshSession->getTextFileFromContainer(container, credentials,
|
||||
amnezia::protocols::xray::shortidPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
|
||||
logger.error() << "Failed to get short ID";
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = ErrorCode::InternalError;
|
||||
}
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
xrayShortId.replace("\n", "");
|
||||
}
|
||||
|
||||
QString xrayPublicKey =
|
||||
m_sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
|
||||
logger.error() << "Failed to get public key";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
xrayPublicKey.replace("\n", "");
|
||||
|
||||
QString xrayShortId =
|
||||
m_sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
|
||||
logger.error() << "Failed to get short ID";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
xrayShortId.replace("\n", "");
|
||||
|
||||
if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) {
|
||||
logger.error() << "Config template missing required variables:"
|
||||
<< "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID")
|
||||
<< "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY")
|
||||
<< "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID");
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
// Build outbound
|
||||
QJsonObject userObj;
|
||||
userObj[amnezia::protocols::xray::id] = xrayClientId;
|
||||
userObj[amnezia::protocols::xray::encryption] = "none";
|
||||
if (!srv.flow.isEmpty()) {
|
||||
userObj[amnezia::protocols::xray::flow] = srv.flow;
|
||||
}
|
||||
|
||||
config.replace("$XRAY_CLIENT_ID", xrayClientId);
|
||||
config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey);
|
||||
config.replace("$XRAY_SHORT_ID", xrayShortId);
|
||||
QJsonObject vnextEntry;
|
||||
vnextEntry[amnezia::protocols::xray::address] = credentials.hostName;
|
||||
vnextEntry[amnezia::protocols::xray::port] = srv.port.toInt();
|
||||
vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj };
|
||||
|
||||
QJsonObject outboundSettings;
|
||||
outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry };
|
||||
|
||||
QJsonObject outbound;
|
||||
outbound["protocol"] = "vless";
|
||||
outbound[amnezia::protocols::xray::settings] = outboundSettings;
|
||||
|
||||
// Build streamSettings
|
||||
QJsonObject streamObj = buildStreamSettings(srv, xrayClientId);
|
||||
|
||||
// Inject Reality keys
|
||||
if (srv.security == "reality") {
|
||||
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
|
||||
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
|
||||
rs[amnezia::protocols::xray::shortId] = xrayShortId;
|
||||
rs[amnezia::protocols::xray::spiderX] = "";
|
||||
streamObj[amnezia::protocols::xray::realitySettings] = rs;
|
||||
}
|
||||
|
||||
outbound[amnezia::protocols::xray::streamSettings] = streamObj;
|
||||
|
||||
// Build full client config
|
||||
QJsonObject inboundObj;
|
||||
inboundObj["listen"] = amnezia::protocols::xray::defaultLocalListenAddr;
|
||||
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort;
|
||||
inboundObj["protocol"] = "socks";
|
||||
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { "udp", true } };
|
||||
|
||||
QJsonObject clientJson;
|
||||
clientJson["log"] = QJsonObject { { "loglevel", "error" } };
|
||||
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj };
|
||||
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound };
|
||||
|
||||
QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
|
||||
|
||||
// Return
|
||||
XrayProtocolConfig protocolConfig;
|
||||
if (serverConfig) {
|
||||
protocolConfig.serverConfig = *serverConfig;
|
||||
}
|
||||
|
||||
protocolConfig.serverConfig = srv;
|
||||
|
||||
XrayClientConfig clientConfig;
|
||||
clientConfig.nativeConfig = config;
|
||||
clientConfig.localPort = "";
|
||||
qDebug() << "config:" << config;
|
||||
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
clientConfig.id = xrayClientId;
|
||||
|
||||
|
||||
protocolConfig.setClientConfig(clientConfig);
|
||||
|
||||
|
||||
return protocolConfig;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@
|
||||
#define XRAY_CONFIGURATOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "configuratorBase.h"
|
||||
#include "core/utils/errorCodes.h"
|
||||
#include "core/utils/routeModes.h"
|
||||
#include "core/utils/commonStructs.h"
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
|
||||
class XrayConfigurator : public ConfiguratorBase
|
||||
{
|
||||
@@ -18,10 +20,17 @@ public:
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
amnezia::ErrorCode &errorCode) override;
|
||||
|
||||
amnezia::ProtocolConfig processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
|
||||
amnezia::ProtocolConfig protocolConfig) override;
|
||||
|
||||
private:
|
||||
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
amnezia::ErrorCode &errorCode);
|
||||
|
||||
// Builds the native xray "streamSettings" JSON object from XrayServerConfig
|
||||
QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
|
||||
const QString &clientId) const;
|
||||
};
|
||||
|
||||
#endif // XRAY_CONFIGURATOR_H
|
||||
|
||||
@@ -86,6 +86,9 @@ void CoreController::initModels()
|
||||
m_xrayConfigModel = new XrayConfigModel(this);
|
||||
setQmlContextProperty("XrayConfigModel", m_xrayConfigModel);
|
||||
|
||||
m_xrayConfigSnapshotsModel = new XrayConfigSnapshotsModel(m_appSettingsRepository, m_xrayConfigModel, this);
|
||||
setQmlContextProperty("XrayConfigSnapshotsModel", m_xrayConfigSnapshotsModel);
|
||||
|
||||
m_torConfigModel = new TorConfigModel(this);
|
||||
setQmlContextProperty("TorConfigModel", m_torConfigModel);
|
||||
|
||||
@@ -100,6 +103,12 @@ void CoreController::initModels()
|
||||
m_socks5ConfigModel = new Socks5ProxyConfigModel(this);
|
||||
setQmlContextProperty("Socks5ProxyConfigModel", m_socks5ConfigModel);
|
||||
|
||||
m_mtProxyConfigModel = new MtProxyConfigModel(this);
|
||||
setQmlContextProperty("MtProxyConfigModel", m_mtProxyConfigModel);
|
||||
|
||||
m_telemtConfigModel = new TelemtConfigModel(this);
|
||||
setQmlContextProperty("TelemtConfigModel", m_telemtConfigModel);
|
||||
|
||||
m_clientManagementModel = new ClientManagementModel(this);
|
||||
setQmlContextProperty("ClientManagementModel", m_clientManagementModel);
|
||||
|
||||
@@ -169,7 +178,7 @@ void CoreController::initControllers()
|
||||
#ifdef Q_OS_WINDOWS
|
||||
m_ikev2ConfigModel,
|
||||
#endif
|
||||
m_sftpConfigModel, m_socks5ConfigModel, this);
|
||||
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel, this);
|
||||
setQmlContextProperty("InstallController", m_installUiController);
|
||||
|
||||
m_importController = new ImportUiController(m_importCoreController, this);
|
||||
@@ -202,6 +211,10 @@ void CoreController::initControllers()
|
||||
m_systemController = new SystemController(this);
|
||||
setQmlContextProperty("SystemController", m_systemController);
|
||||
|
||||
m_networkReachabilityController = new NetworkReachabilityController(this);
|
||||
m_engine->rootContext()->setContextProperty("NetworkReachabilityController", m_networkReachabilityController);
|
||||
m_engine->rootContext()->setContextProperty("NetworkReachability", m_networkReachabilityController);
|
||||
|
||||
m_servicesCatalogUiController = new ServicesCatalogUiController(m_servicesCatalogController, m_apiServicesModel, this);
|
||||
setQmlContextProperty("ServicesCatalogUiController", m_servicesCatalogUiController);
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "ui/controllers/languageUiController.h"
|
||||
#include "ui/controllers/updateUiController.h"
|
||||
#include "ui/controllers/api/servicesCatalogUiController.h"
|
||||
#include "ui/controllers/networkReachabilityController.h"
|
||||
|
||||
#include "core/controllers/serversController.h"
|
||||
#include "core/controllers/selfhosted/usersController.h"
|
||||
@@ -64,11 +65,15 @@
|
||||
#include "ui/models/protocols/openvpnConfigModel.h"
|
||||
#include "ui/models/protocols/wireguardConfigModel.h"
|
||||
#include "ui/models/protocols/xrayConfigModel.h"
|
||||
#include "ui/models/protocols/xrayConfigSnapshotsModel.h"
|
||||
#include "ui/models/protocolsModel.h"
|
||||
#include "ui/models/services/torConfigModel.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "ui/models/services/sftpConfigModel.h"
|
||||
#include "ui/models/services/socks5ProxyConfigModel.h"
|
||||
#include "ui/models/services/mtProxyConfigModel.h"
|
||||
#include "ui/models/services/telemtConfigModel.h"
|
||||
|
||||
#include "ui/models/ipSplitTunnelingModel.h"
|
||||
#include "ui/models/newsModel.h"
|
||||
|
||||
@@ -156,6 +161,7 @@ private:
|
||||
ServersUiController* m_serversUiController;
|
||||
IpSplitTunnelingUiController* m_ipSplitTunnelingUiController;
|
||||
SystemController* m_systemController;
|
||||
NetworkReachabilityController* m_networkReachabilityController;
|
||||
AppSplitTunnelingUiController* m_appSplitTunnelingUiController;
|
||||
AllowedDnsUiController* m_allowedDnsUiController;
|
||||
LanguageUiController* m_languageUiController;
|
||||
@@ -200,6 +206,7 @@ private:
|
||||
|
||||
OpenVpnConfigModel* m_openVpnConfigModel;
|
||||
XrayConfigModel* m_xrayConfigModel;
|
||||
XrayConfigSnapshotsModel* m_xrayConfigSnapshotsModel;
|
||||
TorConfigModel* m_torConfigModel;
|
||||
WireGuardConfigModel* m_wireGuardConfigModel;
|
||||
AwgConfigModel* m_awgConfigModel;
|
||||
@@ -208,6 +215,8 @@ private:
|
||||
#endif
|
||||
SftpConfigModel* m_sftpConfigModel;
|
||||
Socks5ProxyConfigModel* m_socks5ConfigModel;
|
||||
MtProxyConfigModel* m_mtProxyConfigModel;
|
||||
TelemtConfigModel* m_telemtConfigModel;
|
||||
|
||||
CoreSignalHandlers* m_signalHandlers;
|
||||
};
|
||||
|
||||
@@ -323,6 +323,18 @@ ExportController::ExportResult ExportController::generateXrayConfig(const QStrin
|
||||
vlessServer.shortId = realitySettings.value(amnezia::protocols::xray::shortId).toString();
|
||||
vlessServer.fingerprint = realitySettings.value(amnezia::protocols::xray::fingerprint).toString("chrome");
|
||||
vlessServer.spiderX = realitySettings.value(amnezia::protocols::xray::spiderX).toString("");
|
||||
} else if (vlessServer.security == "tls") {
|
||||
QJsonObject tlsSettings = streamSettings.value("tlsSettings").toObject();
|
||||
vlessServer.serverName = tlsSettings.value(amnezia::protocols::xray::serverName).toString();
|
||||
vlessServer.fingerprint = tlsSettings.value(amnezia::protocols::xray::fingerprint).toString();
|
||||
// alpn: serialize array back to comma-separated for VLESS URI
|
||||
QJsonArray alpnArr = tlsSettings.value("alpn").toArray();
|
||||
QStringList alpnList;
|
||||
for (const QJsonValue &v : alpnArr) {
|
||||
alpnList << v.toString();
|
||||
}
|
||||
// alpn goes into vless URI query param — handled by Serialize via serverName/alpn fields
|
||||
// VlessServerObject doesn't have alpn field, so we embed in serverName if needed
|
||||
}
|
||||
|
||||
result.nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, "AmneziaVPN");
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
#include "core/installers/openvpnInstaller.h"
|
||||
#include "core/installers/sftpInstaller.h"
|
||||
#include "core/installers/socks5Installer.h"
|
||||
#include "core/installers/mtProxyInstaller.h"
|
||||
#include "core/installers/telemtInstaller.h"
|
||||
#include "core/installers/torInstaller.h"
|
||||
#include "core/installers/wireguardInstaller.h"
|
||||
#include "core/installers/xrayInstaller.h"
|
||||
@@ -34,6 +36,7 @@
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/awgProtocolConfig.h"
|
||||
#include "ui/models/protocols/wireguardConfigModel.h"
|
||||
#include "core/utils/utilities.h"
|
||||
@@ -53,6 +56,21 @@ using namespace ProtocolUtils;
|
||||
namespace
|
||||
{
|
||||
Logger logger("InstallController");
|
||||
|
||||
bool dockerDaemonContainerMissing(const QString &out, const QString &containerDockerName)
|
||||
{
|
||||
if (!out.contains(QLatin1String("Error response from daemon"), Qt::CaseInsensitive)) {
|
||||
return false;
|
||||
}
|
||||
if (out.contains(QLatin1String("No such container"), Qt::CaseInsensitive)
|
||||
&& out.contains(containerDockerName, Qt::CaseInsensitive)) {
|
||||
return true;
|
||||
}
|
||||
if (out.size() < 700 && out.contains(QLatin1String("is not running"), Qt::CaseInsensitive)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
InstallController::InstallController(SecureServersRepository *serversRepository,
|
||||
@@ -136,6 +154,15 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession(this);
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession(this);
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
}
|
||||
adminConfig->updateContainerConfig(container, newConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
return ErrorCode::NoError;
|
||||
@@ -165,6 +192,11 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
|
||||
}
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
}
|
||||
clearCachedProfile(serverId, container);
|
||||
adminConfig->updateContainerConfig(container, newConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
@@ -408,9 +440,24 @@ ErrorCode InstallController::configureContainerWorker(const ServerCredentials &c
|
||||
sshSession.replaceVars(amnezia::scriptData(ProtocolScriptType::configure_container, container), baseVars),
|
||||
cbReadStdOut, cbReadStdErr);
|
||||
|
||||
if (e != ErrorCode::NoError) {
|
||||
return e;
|
||||
}
|
||||
|
||||
if (dockerDaemonContainerMissing(stdOut, ContainerUtils::containerToString(container))) {
|
||||
qDebug() << "configureContainerWorker: Docker daemon reports container missing/stopped, output:" << stdOut;
|
||||
return ErrorCode::ServerContainerMissingError;
|
||||
}
|
||||
|
||||
updateContainerConfigAfterInstallation(container, config, stdOut);
|
||||
|
||||
return e;
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config);
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::startupContainerWorker(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession)
|
||||
@@ -563,6 +610,79 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
const auto *oldMt = oldConfig.getMtProxyProtocolConfig();
|
||||
const auto *newMt = newConfig.getMtProxyProtocolConfig();
|
||||
if (oldMt && newMt) {
|
||||
const QString oldPort =
|
||||
oldMt->port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : oldMt->port;
|
||||
const QString newPort =
|
||||
newMt->port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : newMt->port;
|
||||
if (oldPort != newPort) {
|
||||
return true;
|
||||
}
|
||||
const QString oldTransport = oldMt->transportMode.isEmpty() ? QString(
|
||||
protocols::mtProxy::transportModeStandard)
|
||||
: oldMt->transportMode;
|
||||
const QString newTransport = newMt->transportMode.isEmpty() ? QString(
|
||||
protocols::mtProxy::transportModeStandard)
|
||||
: newMt->transportMode;
|
||||
if (oldTransport != newTransport) {
|
||||
return true;
|
||||
}
|
||||
if (oldMt->tlsDomain != newMt->tlsDomain) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Telemt) {
|
||||
const auto *oldT = oldConfig.getTelemtProtocolConfig();
|
||||
const auto *newT = newConfig.getTelemtProtocolConfig();
|
||||
if (oldT && newT) {
|
||||
const QString oldPort =
|
||||
oldT->port.isEmpty() ? QString(protocols::telemt::defaultPort) : oldT->port;
|
||||
const QString newPort =
|
||||
newT->port.isEmpty() ? QString(protocols::telemt::defaultPort) : newT->port;
|
||||
if (oldPort != newPort) {
|
||||
return true;
|
||||
}
|
||||
const QString oldTransport = oldT->transportMode.isEmpty()
|
||||
? QString(protocols::telemt::transportModeStandard)
|
||||
: oldT->transportMode;
|
||||
const QString newTransport = newT->transportMode.isEmpty()
|
||||
? QString(protocols::telemt::transportModeStandard)
|
||||
: newT->transportMode;
|
||||
if (oldTransport != newTransport) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->tlsDomain != newT->tlsDomain) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->maskEnabled != newT->maskEnabled) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->tlsEmulation != newT->tlsEmulation) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->useMiddleProxy != newT->useMiddleProxy) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->tag != newT->tag) {
|
||||
return true;
|
||||
}
|
||||
const QString oldUser = oldT->userName.isEmpty()
|
||||
? QString::fromUtf8(protocols::telemt::defaultUserName)
|
||||
: oldT->userName;
|
||||
const QString newUser = newT->userName.isEmpty()
|
||||
? QString::fromUtf8(protocols::telemt::defaultUserName)
|
||||
: newT->userName;
|
||||
if (oldUser != newUser) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Socks5Proxy) {
|
||||
return true;
|
||||
}
|
||||
@@ -654,7 +774,7 @@ ErrorCode InstallController::isUserInSudo(const ServerCredentials &credentials,
|
||||
return ErrorCode::ServerUserDirectoryNotAccessible;
|
||||
if (stdOut.contains("sudoers") || stdOut.contains("is not allowed to run sudo on"))
|
||||
return ErrorCode::ServerUserNotAllowedInSudoers;
|
||||
if (stdOut.contains("password is required"))
|
||||
if (stdOut.contains("password is required") || stdOut.contains("authentication is required"))
|
||||
return ErrorCode::ServerUserPasswordRequired;
|
||||
|
||||
return error;
|
||||
@@ -823,6 +943,8 @@ QScopedPointer<InstallerBase> InstallController::createInstaller(DockerContainer
|
||||
case DockerContainer::TorWebSite: return QScopedPointer<InstallerBase>(new TorInstaller(this));
|
||||
case DockerContainer::Sftp: return QScopedPointer<InstallerBase>(new SftpInstaller(this));
|
||||
case DockerContainer::Socks5Proxy: return QScopedPointer<InstallerBase>(new Socks5Installer(this));
|
||||
case DockerContainer::MtProxy: return QScopedPointer<InstallerBase>(new MtProxyInstaller(this));
|
||||
case DockerContainer::Telemt: return QScopedPointer<InstallerBase>(new TelemtInstaller(this));
|
||||
default: return QScopedPointer<InstallerBase>(new InstallerBase(this));
|
||||
}
|
||||
}
|
||||
@@ -861,6 +983,20 @@ bool InstallController::isUpdateDockerContainerRequired(DockerContainer containe
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (container == DockerContainer::MtProxy) {
|
||||
const auto *oldMt = oldConfig.getMtProxyProtocolConfig();
|
||||
const auto *newMt = newConfig.getMtProxyProtocolConfig();
|
||||
if (!oldMt || !newMt) {
|
||||
return true;
|
||||
}
|
||||
return !oldMt->equalsDockerDeploymentSettings(*newMt);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
const auto *oldT = oldConfig.getTelemtProtocolConfig();
|
||||
const auto *newT = newConfig.getTelemtProtocolConfig();
|
||||
if (!oldT || !newT) {
|
||||
return true;
|
||||
}
|
||||
return !oldT->equalsDockerDeploymentSettings(*newT);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -1164,6 +1300,56 @@ void InstallController::updateContainerConfigAfterInstallation(DockerContainer c
|
||||
onion.replace("\n", "");
|
||||
torProtocolConfig->serverConfig.site = onion;
|
||||
}
|
||||
} else if (container == DockerContainer::MtProxy) {
|
||||
if (auto* mtProxyConfig = containerConfig.getMtProxyProtocolConfig()) {
|
||||
qDebug() << "amnezia mtproxy" << stdOut;
|
||||
|
||||
static const QRegularExpression reSecret(
|
||||
QStringLiteral(R"(\[\*\]\s+Secret:\s+([0-9a-fA-F]{32}))"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
static const QRegularExpression reTgLink(QStringLiteral(R"(\[\*\]\s+tg://\s+link:\s+(tg://proxy\?[^\s]+))"));
|
||||
static const QRegularExpression reTmeLink(
|
||||
QStringLiteral(R"(\[\*\]\s+t\.me\s+link:\s+(https://t\.me/proxy\?[^\s]+))"));
|
||||
|
||||
const QRegularExpressionMatch mSecret = reSecret.match(stdOut);
|
||||
const QRegularExpressionMatch mTgLink = reTgLink.match(stdOut);
|
||||
const QRegularExpressionMatch mTmeLink = reTmeLink.match(stdOut);
|
||||
|
||||
if (mSecret.hasMatch()) {
|
||||
mtProxyConfig->secret = mSecret.captured(1);
|
||||
}
|
||||
if (mTgLink.hasMatch()) {
|
||||
mtProxyConfig->tgLink = mTgLink.captured(1);
|
||||
}
|
||||
if (mTmeLink.hasMatch()) {
|
||||
mtProxyConfig->tmeLink = mTmeLink.captured(1);
|
||||
}
|
||||
}
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
if (auto *telemtConfig = containerConfig.getTelemtProtocolConfig()) {
|
||||
qDebug() << "amnezia-telemt configure stdout" << stdOut;
|
||||
|
||||
static const QRegularExpression reSecret(
|
||||
QStringLiteral(R"(\[\*\]\s+Secret:\s+([0-9a-fA-F]{32}))"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
static const QRegularExpression reTgLink(QStringLiteral(R"(\[\*\]\s+tg://\s+link:\s+(tg://proxy\?[^\s]+))"));
|
||||
static const QRegularExpression reTmeLink(
|
||||
QStringLiteral(R"(\[\*\]\s+t\.me\s+link:\s+(https://t\.me/proxy\?[^\s]+))"));
|
||||
|
||||
const QRegularExpressionMatch mSecret = reSecret.match(stdOut);
|
||||
const QRegularExpressionMatch mTgLink = reTgLink.match(stdOut);
|
||||
const QRegularExpressionMatch mTmeLink = reTmeLink.match(stdOut);
|
||||
|
||||
if (mSecret.hasMatch()) {
|
||||
telemtConfig->secret = mSecret.captured(1);
|
||||
}
|
||||
if (mTgLink.hasMatch()) {
|
||||
telemtConfig->tgLink = mTgLink.captured(1);
|
||||
}
|
||||
if (mTmeLink.hasMatch()) {
|
||||
telemtConfig->tmeLink = mTmeLink.captured(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1248,3 +1434,126 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::setDockerContainerEnabledState(const QString &serverId, DockerContainer container, bool enabled)
|
||||
{
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
SshSession sshSession(this);
|
||||
const QString script = enabled ? QStringLiteral("sudo docker start %1").arg(containerName)
|
||||
: QStringLiteral("sudo docker stop %1").arg(containerName);
|
||||
const ErrorCode runError = sshSession.runScript(credentials, script);
|
||||
if (runError != ErrorCode::NoError) {
|
||||
return runError;
|
||||
}
|
||||
ContainerConfig currentConfig = adminConfig->containerConfig(container);
|
||||
bool persist = false;
|
||||
if (auto *mtConfig = currentConfig.getMtProxyProtocolConfig()) {
|
||||
mtConfig->isEnabled = enabled;
|
||||
persist = true;
|
||||
} else if (auto *telemtConfig = currentConfig.getTelemtProtocolConfig()) {
|
||||
telemtConfig->isEnabled = enabled;
|
||||
persist = true;
|
||||
}
|
||||
if (persist) {
|
||||
adminConfig->updateContainerConfig(container, currentConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::queryDockerContainerStatus(const QString &serverId, DockerContainer container, int &statusOut)
|
||||
{
|
||||
statusOut = 3;
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
SshSession sshSession(this);
|
||||
const QString script = QStringLiteral(
|
||||
"sudo docker inspect --format '{{.State.Status}}' %1 2>/dev/null || echo 'not_found'")
|
||||
.arg(containerName);
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
const QString status = stdOut.trimmed();
|
||||
if (status == QLatin1String("running")) {
|
||||
statusOut = 1;
|
||||
} else if (status == QLatin1String("not_found") || status.isEmpty()) {
|
||||
statusOut = 0;
|
||||
} else if (status == QLatin1String("exited") || status == QLatin1String("created")
|
||||
|| status == QLatin1String("paused")) {
|
||||
statusOut = 2;
|
||||
} else {
|
||||
statusOut = 3;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::queryMtProxyDiagnostics(const QString &serverId, DockerContainer container, int listenPort,
|
||||
MtProxyContainerDiagnostics &out)
|
||||
{
|
||||
out = {};
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession(this);
|
||||
return MtProxyInstaller::queryDiagnostics(sshSession, credentials, container, listenPort, out);
|
||||
}
|
||||
|
||||
QString InstallController::fetchDockerContainerSecret(const QString &serverId, DockerContainer container)
|
||||
{
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return {};
|
||||
}
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return {};
|
||||
}
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
if (!credentials.isValid()) {
|
||||
return {};
|
||||
}
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
SshSession sshSession(this);
|
||||
const QString path = QStringLiteral("/data/secret");
|
||||
const QString cmd = QStringLiteral("sudo docker exec %1 cat %2").arg(containerName, path);
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, cmd, cbReadStdOut);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return {};
|
||||
}
|
||||
const QString secret = stdOut.trimmed();
|
||||
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
|
||||
return hex32.match(secret).hasMatch() ? secret : QString();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/installers/mtProxyInstaller.h"
|
||||
|
||||
class SshSession;
|
||||
class InstallerBase;
|
||||
@@ -39,6 +40,16 @@ public:
|
||||
ErrorCode removeAllContainers(const QString &serverId);
|
||||
ErrorCode removeContainer(const QString &serverId, DockerContainer container);
|
||||
|
||||
ErrorCode setDockerContainerEnabledState(const QString &serverId, DockerContainer container, bool enabled);
|
||||
|
||||
/// statusOut: 0 = not deployed, 1 = running, 2 = stopped, 3 = error
|
||||
ErrorCode queryDockerContainerStatus(const QString &serverId, DockerContainer container, int &statusOut);
|
||||
|
||||
ErrorCode queryMtProxyDiagnostics(const QString &serverId, DockerContainer container, int listenPort,
|
||||
MtProxyContainerDiagnostics &out);
|
||||
|
||||
QString fetchDockerContainerSecret(const QString &serverId, DockerContainer container);
|
||||
|
||||
ContainerConfig generateConfig(DockerContainer container, int port, TransportProto transportProto);
|
||||
ErrorCode getAlreadyInstalledContainers(const ServerCredentials &credentials, QMap<DockerContainer, ContainerConfig> &installedContainers, SshSession &sshSession);
|
||||
|
||||
|
||||
16
client/core/diagnostics/containerDiagnostics.h
Normal file
16
client/core/diagnostics/containerDiagnostics.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#ifndef CONTAINERDIAGNOSTICS_H
|
||||
#define CONTAINERDIAGNOSTICS_H
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
struct ContainerDiagnostics
|
||||
{
|
||||
bool available = false;
|
||||
bool portReachable = false;
|
||||
|
||||
virtual ~ContainerDiagnostics() = default;
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // CONTAINERDIAGNOSTICS_H
|
||||
18
client/core/diagnostics/mtProxyDiagnostics.h
Normal file
18
client/core/diagnostics/mtProxyDiagnostics.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#ifndef MTPROXYDIAGNOSTICS_H
|
||||
#define MTPROXYDIAGNOSTICS_H
|
||||
|
||||
#include "containerDiagnostics.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace amnezia {
|
||||
struct MtProxyDiagnostics : ContainerDiagnostics {
|
||||
bool upstreamReachable = false;
|
||||
int clientsConnected = -1;
|
||||
QString lastConfigRefresh;
|
||||
QString statsEndpoint;
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // MTPROXYDIAGNOSTICS_H
|
||||
20
client/core/diagnostics/telemtDiagnostics.h
Normal file
20
client/core/diagnostics/telemtDiagnostics.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef TELEMTDIAGNOSTICS_H
|
||||
#define TELEMTDIAGNOSTICS_H
|
||||
|
||||
#include "containerDiagnostics.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
struct TelemtDiagnostics : ContainerDiagnostics
|
||||
{
|
||||
bool upstreamReachable = false;
|
||||
int clientsConnected = -1;
|
||||
QString lastConfigRefresh;
|
||||
QString statsEndpoint;
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // TELEMTDIAGNOSTICS_H
|
||||
@@ -14,6 +14,8 @@
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "core/models/protocols/sftpProtocolConfig.h"
|
||||
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
#include "core/models/protocols/ikev2ProtocolConfig.h"
|
||||
#include "core/models/protocols/torProtocolConfig.h"
|
||||
|
||||
@@ -91,6 +93,18 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
|
||||
config.protocolConfig = socks5Config;
|
||||
break;
|
||||
}
|
||||
case Proto::MtProxy: {
|
||||
MtProxyProtocolConfig mtConfig;
|
||||
mtConfig.port = portStr;
|
||||
config.protocolConfig = mtConfig;
|
||||
break;
|
||||
}
|
||||
case Proto::Telemt: {
|
||||
TelemtProtocolConfig telemtConfig;
|
||||
telemtConfig.port = portStr;
|
||||
config.protocolConfig = telemtConfig;
|
||||
break;
|
||||
}
|
||||
case Proto::Ikev2: {
|
||||
Ikev2ProtocolConfig ikev2Config;
|
||||
config.protocolConfig = ikev2Config;
|
||||
|
||||
130
client/core/installers/mtProxyInstaller.cpp
Normal file
130
client/core/installers/mtProxyInstaller.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include "mtProxyInstaller.h"
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace {
|
||||
constexpr QLatin1String kMtProxyClientJsonPath("/data/amnezia-mtproxy-client.json");
|
||||
constexpr QLatin1String kMtProxyClientJsonUploadPath("data/amnezia-mtproxy-client.json");
|
||||
constexpr QLatin1String kMtProxySecretPath("/data/secret");
|
||||
}
|
||||
|
||||
MtProxyInstaller::MtProxyInstaller(QObject *parent)
|
||||
: InstallerBase(parent) {
|
||||
}
|
||||
|
||||
ErrorCode MtProxyInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession *sshSession, ContainerConfig &config) {
|
||||
if (container != DockerContainer::MtProxy || !sshSession) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
MtProxyProtocolConfig *mt = config.getMtProxyProtocolConfig();
|
||||
if (!mt) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode jsonErr = ErrorCode::NoError;
|
||||
const QByteArray jsonRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyClientJsonPath), jsonErr);
|
||||
if (jsonErr == ErrorCode::NoError && !jsonRaw.trimmed().isEmpty()) {
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(jsonRaw.trimmed(), &parseError);
|
||||
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
|
||||
QJsonObject merged = mt->toJson();
|
||||
const QJsonObject snap = doc.object();
|
||||
for (auto it = snap.constBegin(); it != snap.constEnd(); ++it) {
|
||||
merged.insert(it.key(), it.value());
|
||||
}
|
||||
*mt = MtProxyProtocolConfig::fromJson(merged);
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCode secretErr = ErrorCode::NoError;
|
||||
const QByteArray secretRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxySecretPath), secretErr);
|
||||
const QString sec = QString::fromUtf8(secretRaw).trimmed();
|
||||
if (sec.length() == 32) {
|
||||
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
|
||||
if (hex32.match(sec).hasMatch()) {
|
||||
mt->secret = sec;
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode MtProxyInstaller::queryDiagnostics(SshSession &sshSession, const ServerCredentials &credentials,
|
||||
DockerContainer container, int listenPort,
|
||||
MtProxyContainerDiagnostics &out)
|
||||
{
|
||||
out = {};
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
const QString script =
|
||||
QStringLiteral(
|
||||
"PORT_OK=$(sudo docker exec %1 sh -c 'ss -tlnp 2>/dev/null | grep -q :%2 && echo yes || echo no' 2>/dev/null || echo no); "
|
||||
"TG_OK=$(curl -s --max-time 5 -o /dev/null -w '%%{http_code}' https://core.telegram.org/getProxySecret 2>/dev/null | grep -q '200' && echo yes || echo no); "
|
||||
"CLIENTS=$(sudo docker exec amnezia-mtproxy sh -c 'curl -s --max-time 3 http://localhost:2398/stats 2>/dev/null | grep -o \"total_special_connections:[0-9]*\" | cut -d: -f2' 2>/dev/null); "
|
||||
"CONF_TIME=$(sudo docker exec amnezia-mtproxy sh -c 'stat -c \"%%y\" /data/proxy-multi.conf 2>/dev/null | cut -d. -f1' 2>/dev/null || echo unknown); "
|
||||
"echo \"PORT_OK=${PORT_OK}\"; "
|
||||
"echo \"TG_OK=${TG_OK}\"; "
|
||||
"echo \"CLIENTS=${CLIENTS:-0}\"; "
|
||||
"echo \"CONF_TIME=${CONF_TIME}\"; "
|
||||
"echo \"STATS=http://localhost:2398/stats\";")
|
||||
.arg(containerName)
|
||||
.arg(listenPort);
|
||||
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
for (const QString &line : stdOut.split('\n', Qt::SkipEmptyParts)) {
|
||||
if (line.startsWith(QLatin1String("PORT_OK="))) {
|
||||
out.portReachable = line.mid(8).trimmed() == QLatin1String("yes");
|
||||
} else if (line.startsWith(QLatin1String("TG_OK="))) {
|
||||
out.upstreamReachable = line.mid(6).trimmed() == QLatin1String("yes");
|
||||
} else if (line.startsWith(QLatin1String("CLIENTS="))) {
|
||||
out.clientsConnected = line.mid(8).trimmed().toInt();
|
||||
} else if (line.startsWith(QLatin1String("CONF_TIME="))) {
|
||||
out.lastConfigRefresh = line.mid(10).trimmed();
|
||||
} else if (line.startsWith(QLatin1String("STATS="))) {
|
||||
out.statsEndpoint = line.mid(6).trimmed();
|
||||
}
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
void MtProxyInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,
|
||||
DockerContainer container, const ContainerConfig &config) {
|
||||
const MtProxyProtocolConfig *mt = config.getMtProxyProtocolConfig();
|
||||
if (!mt) {
|
||||
return;
|
||||
}
|
||||
const QByteArray payload = QJsonDocument(mt->toJson()).toJson(QJsonDocument::Compact);
|
||||
const ErrorCode err = sshSession.uploadTextFileToContainer(container, credentials, QString::fromUtf8(payload),
|
||||
QString(kMtProxyClientJsonUploadPath));
|
||||
if (err != ErrorCode::NoError) {
|
||||
qWarning() << "MtProxyInstaller::uploadClientSettingsSnapshot failed" << err;
|
||||
}
|
||||
}
|
||||
34
client/core/installers/mtProxyInstaller.h
Normal file
34
client/core/installers/mtProxyInstaller.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#ifndef MTPROXYINSTALLER_H
|
||||
#define MTPROXYINSTALLER_H
|
||||
|
||||
#include "installerBase.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
struct MtProxyContainerDiagnostics {
|
||||
bool portReachable = false;
|
||||
bool upstreamReachable = false;
|
||||
int clientsConnected = -1;
|
||||
QString lastConfigRefresh;
|
||||
QString statsEndpoint;
|
||||
};
|
||||
|
||||
class MtProxyInstaller : public InstallerBase {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MtProxyInstaller(QObject *parent = nullptr);
|
||||
|
||||
amnezia::ErrorCode
|
||||
extractConfigFromContainer(amnezia::DockerContainer container, const amnezia::ServerCredentials &credentials,
|
||||
SshSession *sshSession, amnezia::ContainerConfig &config) override;
|
||||
|
||||
static void uploadClientSettingsSnapshot(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
const amnezia::ContainerConfig &config);
|
||||
|
||||
static amnezia::ErrorCode queryDiagnostics(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container, int listenPort,
|
||||
MtProxyContainerDiagnostics &out);
|
||||
};
|
||||
|
||||
#endif // MTPROXYINSTALLER_H
|
||||
79
client/core/installers/telemtInstaller.cpp
Normal file
79
client/core/installers/telemtInstaller.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#include "telemtInstaller.h"
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace {
|
||||
constexpr QLatin1String kTelemtClientJsonPath("/data/amnezia-telemt-client.json");
|
||||
constexpr QLatin1String kTelemtClientJsonUploadPath("data/amnezia-telemt-client.json");
|
||||
constexpr QLatin1String kTelemtSecretPath("/data/secret");
|
||||
}
|
||||
|
||||
TelemtInstaller::TelemtInstaller(QObject *parent) : InstallerBase(parent) {}
|
||||
|
||||
ErrorCode TelemtInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession *sshSession, ContainerConfig &config) {
|
||||
if (container != DockerContainer::Telemt || !sshSession) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
TelemtProtocolConfig *tc = config.getTelemtProtocolConfig();
|
||||
if (!tc) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode jsonErr = ErrorCode::NoError;
|
||||
const QByteArray jsonRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtClientJsonPath), jsonErr);
|
||||
if (jsonErr == ErrorCode::NoError && !jsonRaw.trimmed().isEmpty()) {
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(jsonRaw.trimmed(), &parseError);
|
||||
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
|
||||
QJsonObject merged = tc->toJson();
|
||||
const QJsonObject snap = doc.object();
|
||||
for (auto it = snap.constBegin(); it != snap.constEnd(); ++it) {
|
||||
merged.insert(it.key(), it.value());
|
||||
}
|
||||
*tc = TelemtProtocolConfig::fromJson(merged);
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCode secretErr = ErrorCode::NoError;
|
||||
const QByteArray secretRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtSecretPath), secretErr);
|
||||
const QString sec = QString::fromUtf8(secretRaw).trimmed();
|
||||
if (sec.length() == 32) {
|
||||
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
|
||||
if (hex32.match(sec).hasMatch()) {
|
||||
tc->secret = sec;
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
void TelemtInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,
|
||||
DockerContainer container, const ContainerConfig &config) {
|
||||
const TelemtProtocolConfig *tc = config.getTelemtProtocolConfig();
|
||||
if (!tc) {
|
||||
return;
|
||||
}
|
||||
const QByteArray payload = QJsonDocument(tc->toJson()).toJson(QJsonDocument::Compact);
|
||||
const ErrorCode err = sshSession.uploadTextFileToContainer(container, credentials, QString::fromUtf8(payload),
|
||||
QString(kTelemtClientJsonUploadPath));
|
||||
if (err != ErrorCode::NoError) {
|
||||
qWarning() << "TelemtInstaller::uploadClientSettingsSnapshot failed" << err;
|
||||
}
|
||||
}
|
||||
20
client/core/installers/telemtInstaller.h
Normal file
20
client/core/installers/telemtInstaller.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef TELEMTINSTALLER_H
|
||||
#define TELEMTINSTALLER_H
|
||||
|
||||
#include "installerBase.h"
|
||||
|
||||
class TelemtInstaller : public InstallerBase {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TelemtInstaller(QObject *parent = nullptr);
|
||||
|
||||
amnezia::ErrorCode
|
||||
extractConfigFromContainer(amnezia::DockerContainer container, const amnezia::ServerCredentials &credentials,
|
||||
SshSession *sshSession, amnezia::ContainerConfig &config) override;
|
||||
|
||||
static void uploadClientSettingsSnapshot(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
const amnezia::ContainerConfig &config);
|
||||
};
|
||||
|
||||
#endif // TELEMTINSTALLER_H
|
||||
@@ -14,8 +14,18 @@
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "logger.h"
|
||||
|
||||
namespace {
|
||||
namespace
|
||||
{
|
||||
Logger logger("XrayInstaller");
|
||||
|
||||
// Xray expects uTLS preset names (chrome, firefox, …). Old Amnezia/server templates used "Mozilla/5.0".
|
||||
QString normalizeXrayFingerprint(const QString &fp)
|
||||
{
|
||||
if (fp.isEmpty() || fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
return QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
}
|
||||
return fp;
|
||||
}
|
||||
}
|
||||
|
||||
using namespace amnezia;
|
||||
@@ -63,18 +73,251 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
}
|
||||
|
||||
QJsonObject streamSettings = inbound[protocols::xray::streamSettings].toObject();
|
||||
QJsonObject realitySettings = streamSettings[protocols::xray::realitySettings].toObject();
|
||||
if (!realitySettings.contains(protocols::xray::serverNames)) {
|
||||
logger.error() << "Settings missing 'serverNames' field";
|
||||
auto *xrayConfig = config.getXrayProtocolConfig();
|
||||
if (!xrayConfig) {
|
||||
logger.error() << "No XrayProtocolConfig in ContainerConfig";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QString siteName = realitySettings[protocols::xray::serverNames][0].toString();
|
||||
XrayServerConfig &srv = xrayConfig->serverConfig;
|
||||
|
||||
if (auto* xrayConfig = config.getXrayProtocolConfig()) {
|
||||
xrayConfig->serverConfig.site = siteName;
|
||||
// ── Port ─────────────────────────────────────────────────────────
|
||||
if (inbound.contains(protocols::xray::port)) {
|
||||
srv.port = QString::number(inbound[protocols::xray::port].toInt());
|
||||
}
|
||||
|
||||
|
||||
// ── Network (transport) ───────────────────────────────────────────
|
||||
QString networkVal = streamSettings.value(protocols::xray::network).toString("tcp");
|
||||
if (networkVal == "xhttp") {
|
||||
srv.transport = "xhttp";
|
||||
} else if (networkVal == "kcp") {
|
||||
srv.transport = "mkcp";
|
||||
} else {
|
||||
srv.transport = "raw";
|
||||
}
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────────
|
||||
srv.security = streamSettings.value(protocols::xray::security).toString("reality");
|
||||
|
||||
// ── Reality settings ──────────────────────────────────────────────
|
||||
if (srv.security == "reality") {
|
||||
QJsonObject rs = streamSettings.value(protocols::xray::realitySettings).toObject();
|
||||
|
||||
// serverNames array → site + sni
|
||||
if (rs.contains(protocols::xray::serverNames)) {
|
||||
QString sniVal = rs[protocols::xray::serverNames].toArray().first().toString();
|
||||
srv.sni = sniVal;
|
||||
srv.site = sniVal;
|
||||
} else if (rs.contains(protocols::xray::serverName)) {
|
||||
srv.sni = rs[protocols::xray::serverName].toString();
|
||||
srv.site = srv.sni;
|
||||
}
|
||||
|
||||
srv.fingerprint = normalizeXrayFingerprint(rs.value(protocols::xray::fingerprint).toString());
|
||||
}
|
||||
|
||||
// ── TLS settings ──────────────────────────────────────────────────
|
||||
if (srv.security == "tls") {
|
||||
QJsonObject tls = streamSettings.value("tlsSettings").toObject();
|
||||
srv.sni = tls.value(protocols::xray::serverName).toString();
|
||||
srv.fingerprint = normalizeXrayFingerprint(tls.value(protocols::xray::fingerprint).toString());
|
||||
|
||||
QJsonArray alpnArr = tls.value("alpn").toArray();
|
||||
QStringList alpnList;
|
||||
for (const QJsonValue &v : alpnArr) {
|
||||
alpnList << v.toString();
|
||||
}
|
||||
srv.alpn = alpnList.join(",");
|
||||
}
|
||||
|
||||
// ── Flow (from users array) ───────────────────────────────────────
|
||||
if (inbound.contains(protocols::xray::settings)) {
|
||||
QJsonObject s = inbound[protocols::xray::settings].toObject();
|
||||
QJsonArray clientsArr = s.value(protocols::xray::clients).toArray();
|
||||
if (!clientsArr.isEmpty()) {
|
||||
srv.flow = clientsArr[0].toObject().value(protocols::xray::flow).toString();
|
||||
}
|
||||
}
|
||||
|
||||
// ── XHTTP settings (Xray-core SplitHTTPConfig + legacy Amnezia keys) ──
|
||||
if (srv.transport == "xhttp") {
|
||||
QJsonObject xhttpObj = streamSettings.value("xhttpSettings").toObject();
|
||||
{
|
||||
const QString m = xhttpObj.value("mode").toString();
|
||||
if (m.isEmpty() || m == QLatin1String("auto"))
|
||||
srv.xhttp.mode = QStringLiteral("Auto");
|
||||
else if (m == QLatin1String("packet-up"))
|
||||
srv.xhttp.mode = QStringLiteral("Packet-up");
|
||||
else if (m == QLatin1String("stream-up"))
|
||||
srv.xhttp.mode = QStringLiteral("Stream-up");
|
||||
else if (m == QLatin1String("stream-one"))
|
||||
srv.xhttp.mode = QStringLiteral("Stream-one");
|
||||
else
|
||||
srv.xhttp.mode = m;
|
||||
}
|
||||
|
||||
srv.xhttp.host = xhttpObj.value("host").toString();
|
||||
srv.xhttp.path = xhttpObj.value("path").toString();
|
||||
|
||||
{
|
||||
const QJsonObject hdrs = xhttpObj.value("headers").toObject();
|
||||
if (hdrs.contains(QLatin1String("Host")) || !hdrs.isEmpty())
|
||||
srv.xhttp.headersTemplate = QStringLiteral("HTTP");
|
||||
}
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("uplinkHTTPMethod")))
|
||||
srv.xhttp.uplinkMethod = xhttpObj.value("uplinkHTTPMethod").toString();
|
||||
else
|
||||
srv.xhttp.uplinkMethod = xhttpObj.value("method").toString();
|
||||
|
||||
srv.xhttp.disableGrpc = xhttpObj.value("noGRPCHeader").toBool(true);
|
||||
srv.xhttp.disableSse = xhttpObj.value("noSSEHeader").toBool(true);
|
||||
|
||||
auto sessionSeqUi = [](const QString &core) -> QString {
|
||||
if (core.isEmpty() || core == QLatin1String("path"))
|
||||
return QStringLiteral("Path");
|
||||
if (core == QLatin1String("cookie"))
|
||||
return QStringLiteral("Cookie");
|
||||
if (core == QLatin1String("header"))
|
||||
return QStringLiteral("Header");
|
||||
if (core == QLatin1String("query"))
|
||||
return QStringLiteral("Query");
|
||||
return core;
|
||||
};
|
||||
QString sess = xhttpObj.value("sessionPlacement").toString();
|
||||
if (sess.isEmpty())
|
||||
sess = xhttpObj.value("scSessionPlacement").toString();
|
||||
srv.xhttp.sessionPlacement = sessionSeqUi(sess);
|
||||
|
||||
QString seq = xhttpObj.value("seqPlacement").toString();
|
||||
if (seq.isEmpty())
|
||||
seq = xhttpObj.value("scSeqPlacement").toString();
|
||||
srv.xhttp.seqPlacement = sessionSeqUi(seq);
|
||||
|
||||
auto uplinkDataUi = [](const QString &core) -> QString {
|
||||
if (core.isEmpty() || core == QLatin1String("body"))
|
||||
return QStringLiteral("Body");
|
||||
if (core == QLatin1String("auto"))
|
||||
return QStringLiteral("Auto");
|
||||
if (core == QLatin1String("header"))
|
||||
return QStringLiteral("Header");
|
||||
if (core == QLatin1String("cookie"))
|
||||
return QStringLiteral("Cookie");
|
||||
return core;
|
||||
};
|
||||
QString udata = xhttpObj.value("uplinkDataPlacement").toString();
|
||||
if (udata.isEmpty())
|
||||
udata = xhttpObj.value("scUplinkDataPlacement").toString();
|
||||
srv.xhttp.uplinkDataPlacement = uplinkDataUi(udata);
|
||||
|
||||
srv.xhttp.sessionKey = xhttpObj.value("sessionKey").toString();
|
||||
srv.xhttp.seqKey = xhttpObj.value("seqKey").toString();
|
||||
srv.xhttp.uplinkDataKey = xhttpObj.value("uplinkDataKey").toString();
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("uplinkChunkSize"))) {
|
||||
QJsonObject uc = xhttpObj.value("uplinkChunkSize").toObject();
|
||||
if (!uc.isEmpty())
|
||||
srv.xhttp.uplinkChunkSize = QString::number(uc.value("from").toInt());
|
||||
} else if (xhttpObj.contains(QLatin1String("xhttpUplinkChunkSize"))) {
|
||||
srv.xhttp.uplinkChunkSize = QString::number(xhttpObj.value("xhttpUplinkChunkSize").toInt());
|
||||
}
|
||||
if (xhttpObj.contains(QLatin1String("scMaxBufferedPosts"))) {
|
||||
srv.xhttp.scMaxBufferedPosts = QString::number(xhttpObj.value("scMaxBufferedPosts").toVariant().toLongLong());
|
||||
}
|
||||
|
||||
auto readRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
QJsonObject r = xhttpObj.value(QLatin1String(key)).toObject();
|
||||
if (!r.isEmpty()) {
|
||||
minOut = QString::number(r.value("from").toInt());
|
||||
maxOut = QString::number(r.value("to").toInt());
|
||||
}
|
||||
};
|
||||
readRange("scMaxEachPostBytes", srv.xhttp.scMaxEachPostBytesMin, srv.xhttp.scMaxEachPostBytesMax);
|
||||
readRange("scMinPostsIntervalMs", srv.xhttp.scMinPostsIntervalMsMin, srv.xhttp.scMinPostsIntervalMsMax);
|
||||
readRange("scStreamUpServerSecs", srv.xhttp.scStreamUpServerSecsMin, srv.xhttp.scStreamUpServerSecsMax);
|
||||
|
||||
auto loadPaddingFromObject = [&](const QJsonObject &pad) {
|
||||
if (pad.contains(QLatin1String("xPaddingObfsMode")))
|
||||
srv.xhttp.xPadding.obfsMode = pad.value("xPaddingObfsMode").toBool(true);
|
||||
srv.xhttp.xPadding.key = pad.value("xPaddingKey").toString();
|
||||
srv.xhttp.xPadding.header = pad.value("xPaddingHeader").toString();
|
||||
srv.xhttp.xPadding.placement = pad.value("xPaddingPlacement").toString();
|
||||
srv.xhttp.xPadding.method = pad.value("xPaddingMethod").toString();
|
||||
QJsonObject bytesRange = pad.value("xPaddingBytes").toObject();
|
||||
if (!bytesRange.isEmpty()) {
|
||||
srv.xhttp.xPadding.bytesMin = QString::number(bytesRange.value("from").toInt());
|
||||
srv.xhttp.xPadding.bytesMax = QString::number(bytesRange.value("to").toInt());
|
||||
}
|
||||
QString pl = srv.xhttp.xPadding.placement.toLower();
|
||||
if (pl == QLatin1String("cookie"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Cookie");
|
||||
else if (pl == QLatin1String("header"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Header");
|
||||
else if (pl == QLatin1String("query"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Query");
|
||||
else if (pl == QLatin1String("queryinheader"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Query in header");
|
||||
QString met = srv.xhttp.xPadding.method.toLower();
|
||||
if (met == QLatin1String("repeat-x"))
|
||||
srv.xhttp.xPadding.method = QStringLiteral("Repeat-x");
|
||||
else if (met == QLatin1String("tokenish"))
|
||||
srv.xhttp.xPadding.method = QStringLiteral("Tokenish");
|
||||
};
|
||||
if (xhttpObj.contains(QLatin1String("xPaddingObfsMode")) || xhttpObj.contains(QLatin1String("xPaddingKey"))
|
||||
|| !xhttpObj.value("xPaddingBytes").toObject().isEmpty()) {
|
||||
loadPaddingFromObject(xhttpObj);
|
||||
} else if (xhttpObj.contains(QLatin1String("xPadding")) && xhttpObj.value("xPadding").isObject()) {
|
||||
const QJsonObject nested = xhttpObj.value("xPadding").toObject();
|
||||
if (!nested.isEmpty()) {
|
||||
loadPaddingFromObject(nested);
|
||||
if (!nested.contains(QLatin1String("xPaddingObfsMode")))
|
||||
srv.xhttp.xPadding.obfsMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("xmux"))) {
|
||||
QJsonObject mux = xhttpObj.value("xmux").toObject();
|
||||
srv.xhttp.xmux.enabled = true;
|
||||
|
||||
auto readMuxRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
QJsonObject r = mux.value(QLatin1String(key)).toObject();
|
||||
if (!r.isEmpty()) {
|
||||
minOut = QString::number(r.value("from").toInt());
|
||||
maxOut = QString::number(r.value("to").toInt());
|
||||
}
|
||||
};
|
||||
readMuxRange("maxConcurrency", srv.xhttp.xmux.maxConcurrencyMin, srv.xhttp.xmux.maxConcurrencyMax);
|
||||
readMuxRange("maxConnections", srv.xhttp.xmux.maxConnectionsMin, srv.xhttp.xmux.maxConnectionsMax);
|
||||
readMuxRange("cMaxReuseTimes", srv.xhttp.xmux.cMaxReuseTimesMin, srv.xhttp.xmux.cMaxReuseTimesMax);
|
||||
readMuxRange("hMaxRequestTimes", srv.xhttp.xmux.hMaxRequestTimesMin, srv.xhttp.xmux.hMaxRequestTimesMax);
|
||||
readMuxRange("hMaxReusableSecs", srv.xhttp.xmux.hMaxReusableSecsMin, srv.xhttp.xmux.hMaxReusableSecsMax);
|
||||
|
||||
if (mux.contains(QLatin1String("hKeepAlivePeriod")))
|
||||
srv.xhttp.xmux.hKeepAlivePeriod = QString::number(mux.value("hKeepAlivePeriod").toVariant().toLongLong());
|
||||
}
|
||||
}
|
||||
|
||||
// ── mKCP settings ─────────────────────────────────────────────────
|
||||
if (srv.transport == "mkcp") {
|
||||
QJsonObject kcp = streamSettings.value("kcpSettings").toObject();
|
||||
if (kcp.contains("tti")) {
|
||||
srv.mkcp.tti = QString::number(kcp["tti"].toInt());
|
||||
}
|
||||
if (kcp.contains("uplinkCapacity")) {
|
||||
srv.mkcp.uplinkCapacity = QString::number(kcp["uplinkCapacity"].toInt());
|
||||
}
|
||||
if (kcp.contains("downlinkCapacity")) {
|
||||
srv.mkcp.downlinkCapacity = QString::number(kcp["downlinkCapacity"].toInt());
|
||||
}
|
||||
if (kcp.contains("readBufferSize")) {
|
||||
srv.mkcp.readBufferSize = QString::number(kcp["readBufferSize"].toInt());
|
||||
}
|
||||
if (kcp.contains("writeBufferSize")) {
|
||||
srv.mkcp.writeBufferSize = QString::number(kcp["writeBufferSize"].toInt());
|
||||
}
|
||||
srv.mkcp.congestion = kcp.value("congestion").toBool(true);
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,26 @@ const Socks5ProxyProtocolConfig* ContainerConfig::getSocks5ProxyProtocolConfig()
|
||||
return protocolConfig.as<Socks5ProxyProtocolConfig>();
|
||||
}
|
||||
|
||||
MtProxyProtocolConfig* ContainerConfig::getMtProxyProtocolConfig()
|
||||
{
|
||||
return protocolConfig.as<MtProxyProtocolConfig>();
|
||||
}
|
||||
|
||||
const MtProxyProtocolConfig* ContainerConfig::getMtProxyProtocolConfig() const
|
||||
{
|
||||
return protocolConfig.as<MtProxyProtocolConfig>();
|
||||
}
|
||||
|
||||
TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig()
|
||||
{
|
||||
return protocolConfig.as<TelemtProtocolConfig>();
|
||||
}
|
||||
|
||||
const TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig() const
|
||||
{
|
||||
return protocolConfig.as<TelemtProtocolConfig>();
|
||||
}
|
||||
|
||||
Ikev2ProtocolConfig* ContainerConfig::getIkev2ProtocolConfig()
|
||||
{
|
||||
return protocolConfig.as<Ikev2ProtocolConfig>();
|
||||
|
||||
@@ -57,6 +57,12 @@ struct ContainerConfig {
|
||||
Socks5ProxyProtocolConfig* getSocks5ProxyProtocolConfig();
|
||||
const Socks5ProxyProtocolConfig* getSocks5ProxyProtocolConfig() const;
|
||||
|
||||
MtProxyProtocolConfig* getMtProxyProtocolConfig();
|
||||
const MtProxyProtocolConfig* getMtProxyProtocolConfig() const;
|
||||
|
||||
TelemtProtocolConfig* getTelemtProtocolConfig();
|
||||
const TelemtProtocolConfig* getTelemtProtocolConfig() const;
|
||||
|
||||
Ikev2ProtocolConfig* getIkev2ProtocolConfig();
|
||||
const Ikev2ProtocolConfig* getIkev2ProtocolConfig() const;
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/models/protocols/ikev2ProtocolConfig.h"
|
||||
#include "core/models/protocols/dnsProtocolConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -38,6 +40,10 @@ Proto ProtocolConfig::type() const
|
||||
return Proto::TorWebSite;
|
||||
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
|
||||
return Proto::Dns;
|
||||
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
|
||||
return Proto::MtProxy;
|
||||
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
|
||||
return Proto::Telemt;
|
||||
}
|
||||
return Proto::Unknown;
|
||||
}, data);
|
||||
@@ -65,6 +71,10 @@ QString ProtocolConfig::port() const
|
||||
return QString();
|
||||
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
|
||||
return QString();
|
||||
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
|
||||
return arg.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : arg.port;
|
||||
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
|
||||
return arg.port.isEmpty() ? QString(protocols::telemt::defaultPort) : arg.port;
|
||||
}
|
||||
return QString();
|
||||
}, data);
|
||||
@@ -88,6 +98,10 @@ QString ProtocolConfig::transportProto() const
|
||||
return QString();
|
||||
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
|
||||
return QString();
|
||||
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
|
||||
return QStringLiteral("tcp");
|
||||
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
|
||||
return QStringLiteral("tcp");
|
||||
}
|
||||
return QString();
|
||||
}, data);
|
||||
@@ -299,6 +313,10 @@ ProtocolConfig ProtocolConfig::fromJson(const QJsonObject& json, Proto type)
|
||||
return ProtocolConfig{TorProtocolConfig::fromJson(json)};
|
||||
case Proto::Dns:
|
||||
return ProtocolConfig{DnsProtocolConfig::fromJson(json)};
|
||||
case Proto::MtProxy:
|
||||
return ProtocolConfig{MtProxyProtocolConfig::fromJson(json)};
|
||||
case Proto::Telemt:
|
||||
return ProtocolConfig{TelemtProtocolConfig::fromJson(json)};
|
||||
default:
|
||||
return ProtocolConfig{AwgProtocolConfig{}};
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include "core/models/protocols/ikev2ProtocolConfig.h"
|
||||
#include "core/models/protocols/torProtocolConfig.h"
|
||||
#include "core/models/protocols/dnsProtocolConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -36,6 +38,8 @@ struct ProtocolConfig {
|
||||
XrayProtocolConfig,
|
||||
SftpProtocolConfig,
|
||||
Socks5ProxyProtocolConfig,
|
||||
MtProxyProtocolConfig,
|
||||
TelemtProtocolConfig,
|
||||
Ikev2ProtocolConfig,
|
||||
TorProtocolConfig,
|
||||
DnsProtocolConfig
|
||||
|
||||
147
client/core/models/protocols/mtProxyProtocolConfig.cpp
Normal file
147
client/core/models/protocols/mtProxyProtocolConfig.cpp
Normal file
@@ -0,0 +1,147 @@
|
||||
#include "mtProxyProtocolConfig.h"
|
||||
|
||||
#include "../../../core/utils/protocolEnum.h"
|
||||
#include "../../../core/protocols/protocolUtils.h"
|
||||
#include "../../../core/utils/constants/configKeys.h"
|
||||
#include "../../../core/utils/constants/protocolConstants.h"
|
||||
#include <QJsonArray>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace amnezia {
|
||||
|
||||
QJsonObject MtProxyProtocolConfig::toJson() const {
|
||||
QJsonObject obj;
|
||||
|
||||
if (!port.isEmpty()) {
|
||||
obj[configKey::port] = port;
|
||||
}
|
||||
if (!secret.isEmpty()) {
|
||||
obj[protocols::mtProxy::secretKey] = secret;
|
||||
}
|
||||
if (!tag.isEmpty()) {
|
||||
obj[protocols::mtProxy::tagKey] = tag;
|
||||
}
|
||||
if (!tgLink.isEmpty()) {
|
||||
obj[protocols::mtProxy::tgLinkKey] = tgLink;
|
||||
}
|
||||
if (!tmeLink.isEmpty()) {
|
||||
obj[protocols::mtProxy::tmeLinkKey] = tmeLink;
|
||||
}
|
||||
obj[protocols::mtProxy::isEnabledKey] = isEnabled;
|
||||
if (!publicHost.isEmpty()) {
|
||||
obj[protocols::mtProxy::publicHostKey] = publicHost;
|
||||
}
|
||||
if (!transportMode.isEmpty()) {
|
||||
obj[protocols::mtProxy::transportModeKey] = transportMode;
|
||||
}
|
||||
if (!tlsDomain.isEmpty()) {
|
||||
obj[protocols::mtProxy::tlsDomainKey] = tlsDomain;
|
||||
}
|
||||
if (!additionalSecrets.isEmpty()) {
|
||||
obj[protocols::mtProxy::additionalSecretsKey] = QJsonArray::fromStringList(additionalSecrets);
|
||||
}
|
||||
if (!workersMode.isEmpty()) {
|
||||
obj[protocols::mtProxy::workersModeKey] = workersMode;
|
||||
}
|
||||
if (!workers.isEmpty()) {
|
||||
obj[protocols::mtProxy::workersKey] = workers;
|
||||
}
|
||||
obj[protocols::mtProxy::natEnabledKey] = natEnabled;
|
||||
if (!natInternalIp.isEmpty()) {
|
||||
obj[protocols::mtProxy::natInternalIpKey] = natInternalIp;
|
||||
}
|
||||
if (!natExternalIp.isEmpty()) {
|
||||
obj[protocols::mtProxy::natExternalIpKey] = natExternalIp;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
MtProxyProtocolConfig MtProxyProtocolConfig::fromJson(const QJsonObject &json) {
|
||||
MtProxyProtocolConfig config;
|
||||
|
||||
config.port = json.value(configKey::port).toString();
|
||||
config.secret = json.value(protocols::mtProxy::secretKey).toString();
|
||||
config.tag = json.value(protocols::mtProxy::tagKey).toString();
|
||||
config.tgLink = json.value(protocols::mtProxy::tgLinkKey).toString();
|
||||
config.tmeLink = json.value(protocols::mtProxy::tmeLinkKey).toString();
|
||||
config.isEnabled = json.value(protocols::mtProxy::isEnabledKey).toBool(true);
|
||||
config.publicHost = json.value(protocols::mtProxy::publicHostKey).toString();
|
||||
config.transportMode = json.value(protocols::mtProxy::transportModeKey).toString();
|
||||
config.tlsDomain = json.value(protocols::mtProxy::tlsDomainKey).toString();
|
||||
for (const auto &v: json.value(protocols::mtProxy::additionalSecretsKey).toArray()) {
|
||||
const QString s = v.toString();
|
||||
if (!s.isEmpty()) {
|
||||
config.additionalSecrets.append(s);
|
||||
}
|
||||
}
|
||||
config.workersMode = json.value(protocols::mtProxy::workersModeKey).toString();
|
||||
config.workers = json.value(protocols::mtProxy::workersKey).toString();
|
||||
config.natEnabled = json.value(protocols::mtProxy::natEnabledKey).toBool(false);
|
||||
config.natInternalIp = json.value(protocols::mtProxy::natInternalIpKey).toString();
|
||||
config.natExternalIp = json.value(protocols::mtProxy::natExternalIpKey).toString();
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
bool MtProxyProtocolConfig::equalsDockerDeploymentSettings(const MtProxyProtocolConfig &other) const {
|
||||
const auto normPort = [](const QString &p) {
|
||||
return p.isEmpty() ? QString(protocols::mtProxy::defaultPort) : p;
|
||||
};
|
||||
const auto normTransport = [](const QString &t) {
|
||||
return t.isEmpty() ? QString(protocols::mtProxy::transportModeStandard) : t;
|
||||
};
|
||||
const auto normWorkersMode = [](const QString &m) {
|
||||
return m.isEmpty() ? QString(protocols::mtProxy::workersModeAuto) : m;
|
||||
};
|
||||
|
||||
if (normPort(port) != normPort(other.port)) {
|
||||
return false;
|
||||
}
|
||||
if (normTransport(transportMode) != normTransport(other.transportMode)) {
|
||||
return false;
|
||||
}
|
||||
if (tlsDomain != other.tlsDomain) {
|
||||
return false;
|
||||
}
|
||||
if (secret != other.secret) {
|
||||
return false;
|
||||
}
|
||||
if (tag != other.tag) {
|
||||
return false;
|
||||
}
|
||||
if (publicHost != other.publicHost) {
|
||||
return false;
|
||||
}
|
||||
if (normWorkersMode(workersMode) != normWorkersMode(other.workersMode)) {
|
||||
return false;
|
||||
}
|
||||
if (workers != other.workers) {
|
||||
return false;
|
||||
}
|
||||
if (natEnabled != other.natEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (natInternalIp != other.natInternalIp) {
|
||||
return false;
|
||||
}
|
||||
if (natExternalIp != other.natExternalIp) {
|
||||
return false;
|
||||
}
|
||||
if (isEnabled != other.isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QStringList aa = additionalSecrets;
|
||||
QStringList bb = other.additionalSecrets;
|
||||
aa.removeAll(QString());
|
||||
bb.removeAll(QString());
|
||||
std::sort(aa.begin(), aa.end());
|
||||
std::sort(bb.begin(), bb.end());
|
||||
return aa == bb;
|
||||
}
|
||||
|
||||
} // namespace amnezia
|
||||
38
client/core/models/protocols/mtProxyProtocolConfig.h
Normal file
38
client/core/models/protocols/mtProxyProtocolConfig.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef MTPROXYPROTOCOLCONFIG_H
|
||||
#define MTPROXYPROTOCOLCONFIG_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace amnezia {
|
||||
|
||||
struct MtProxyProtocolConfig {
|
||||
QString port;
|
||||
QString secret;
|
||||
QString tag;
|
||||
QString tgLink;
|
||||
QString tmeLink;
|
||||
bool isEnabled = true;
|
||||
QString publicHost;
|
||||
QString transportMode;
|
||||
QString tlsDomain;
|
||||
QStringList additionalSecrets;
|
||||
QString workersMode;
|
||||
QString workers;
|
||||
bool natEnabled = false;
|
||||
QString natInternalIp;
|
||||
QString natExternalIp;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
|
||||
static MtProxyProtocolConfig fromJson(const QJsonObject &json);
|
||||
|
||||
// Port, transport, TLS, secrets, NAT, workers, isEnabled, additionalSecrets (order-independent).
|
||||
// Ignores tgLink / tmeLink (derived / display).
|
||||
bool equalsDockerDeploymentSettings(const MtProxyProtocolConfig &other) const;
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // MTPROXYPROTOCOLCONFIG_H
|
||||
162
client/core/models/protocols/telemtProtocolConfig.cpp
Normal file
162
client/core/models/protocols/telemtProtocolConfig.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
#include "telemtProtocolConfig.h"
|
||||
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <algorithm>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
QJsonObject TelemtProtocolConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
if (!port.isEmpty()) {
|
||||
obj[QString(configKey::port)] = port;
|
||||
}
|
||||
if (!secret.isEmpty()) {
|
||||
obj[protocols::telemt::secretKey] = secret;
|
||||
}
|
||||
if (!tag.isEmpty()) {
|
||||
obj[protocols::telemt::tagKey] = tag;
|
||||
}
|
||||
if (!tgLink.isEmpty()) {
|
||||
obj[protocols::telemt::tgLinkKey] = tgLink;
|
||||
}
|
||||
if (!tmeLink.isEmpty()) {
|
||||
obj[protocols::telemt::tmeLinkKey] = tmeLink;
|
||||
}
|
||||
obj[protocols::telemt::isEnabledKey] = isEnabled;
|
||||
if (!publicHost.isEmpty()) {
|
||||
obj[protocols::telemt::publicHostKey] = publicHost;
|
||||
}
|
||||
if (!transportMode.isEmpty()) {
|
||||
obj[protocols::telemt::transportModeKey] = transportMode;
|
||||
}
|
||||
if (!tlsDomain.isEmpty()) {
|
||||
obj[protocols::telemt::tlsDomainKey] = tlsDomain;
|
||||
}
|
||||
obj[protocols::telemt::maskEnabledKey] = maskEnabled;
|
||||
obj[protocols::telemt::tlsEmulationKey] = tlsEmulation;
|
||||
obj[protocols::telemt::useMiddleProxyKey] = useMiddleProxy;
|
||||
if (!userName.isEmpty()) {
|
||||
obj[protocols::telemt::userNameKey] = userName;
|
||||
}
|
||||
if (!additionalSecrets.isEmpty()) {
|
||||
obj[protocols::telemt::additionalSecretsKey] = QJsonArray::fromStringList(additionalSecrets);
|
||||
}
|
||||
if (!workersMode.isEmpty()) {
|
||||
obj[protocols::telemt::workersModeKey] = workersMode;
|
||||
}
|
||||
if (!workers.isEmpty()) {
|
||||
obj[protocols::telemt::workersKey] = workers;
|
||||
}
|
||||
obj[protocols::telemt::natEnabledKey] = natEnabled;
|
||||
if (!natInternalIp.isEmpty()) {
|
||||
obj[protocols::telemt::natInternalIpKey] = natInternalIp;
|
||||
}
|
||||
if (!natExternalIp.isEmpty()) {
|
||||
obj[protocols::telemt::natExternalIpKey] = natExternalIp;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
TelemtProtocolConfig TelemtProtocolConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
TelemtProtocolConfig c;
|
||||
c.port = json.value(QString(configKey::port)).toString();
|
||||
c.secret = json.value(protocols::telemt::secretKey).toString();
|
||||
c.tag = json.value(protocols::telemt::tagKey).toString();
|
||||
c.tgLink = json.value(protocols::telemt::tgLinkKey).toString();
|
||||
c.tmeLink = json.value(protocols::telemt::tmeLinkKey).toString();
|
||||
c.isEnabled = json.value(protocols::telemt::isEnabledKey).toBool(true);
|
||||
c.publicHost = json.value(protocols::telemt::publicHostKey).toString();
|
||||
c.transportMode = json.value(protocols::telemt::transportModeKey).toString();
|
||||
c.tlsDomain = json.value(protocols::telemt::tlsDomainKey).toString();
|
||||
c.maskEnabled = json.value(protocols::telemt::maskEnabledKey).toBool(true);
|
||||
c.tlsEmulation = json.value(protocols::telemt::tlsEmulationKey).toBool(false);
|
||||
c.useMiddleProxy = json.value(protocols::telemt::useMiddleProxyKey).toBool(true);
|
||||
c.userName = json.value(protocols::telemt::userNameKey).toString();
|
||||
for (const auto &v : json.value(protocols::telemt::additionalSecretsKey).toArray()) {
|
||||
const QString s = v.toString();
|
||||
if (!s.isEmpty()) {
|
||||
c.additionalSecrets.append(s);
|
||||
}
|
||||
}
|
||||
c.workersMode = json.value(protocols::telemt::workersModeKey).toString();
|
||||
c.workers = json.value(protocols::telemt::workersKey).toString();
|
||||
c.natEnabled = json.value(protocols::telemt::natEnabledKey).toBool(false);
|
||||
c.natInternalIp = json.value(protocols::telemt::natInternalIpKey).toString();
|
||||
c.natExternalIp = json.value(protocols::telemt::natExternalIpKey).toString();
|
||||
return c;
|
||||
}
|
||||
|
||||
bool TelemtProtocolConfig::equalsDockerDeploymentSettings(const TelemtProtocolConfig &other) const
|
||||
{
|
||||
const auto normPort = [](const QString &p) {
|
||||
return p.isEmpty() ? QString(protocols::telemt::defaultPort) : p;
|
||||
};
|
||||
const auto normTransport = [](const QString &t) {
|
||||
return t.isEmpty() ? QString(protocols::telemt::transportModeStandard) : t;
|
||||
};
|
||||
const auto normWorkersMode = [](const QString &m) {
|
||||
return m.isEmpty() ? QString(protocols::telemt::workersModeAuto) : m;
|
||||
};
|
||||
|
||||
if (normPort(port) != normPort(other.port)) {
|
||||
return false;
|
||||
}
|
||||
if (normTransport(transportMode) != normTransport(other.transportMode)) {
|
||||
return false;
|
||||
}
|
||||
if (tlsDomain != other.tlsDomain) {
|
||||
return false;
|
||||
}
|
||||
if (secret != other.secret) {
|
||||
return false;
|
||||
}
|
||||
if (tag != other.tag) {
|
||||
return false;
|
||||
}
|
||||
if (publicHost != other.publicHost) {
|
||||
return false;
|
||||
}
|
||||
if (maskEnabled != other.maskEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (tlsEmulation != other.tlsEmulation) {
|
||||
return false;
|
||||
}
|
||||
if (useMiddleProxy != other.useMiddleProxy) {
|
||||
return false;
|
||||
}
|
||||
if (userName != other.userName) {
|
||||
return false;
|
||||
}
|
||||
if (normWorkersMode(workersMode) != normWorkersMode(other.workersMode)) {
|
||||
return false;
|
||||
}
|
||||
if (workers != other.workers) {
|
||||
return false;
|
||||
}
|
||||
if (natEnabled != other.natEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (natInternalIp != other.natInternalIp) {
|
||||
return false;
|
||||
}
|
||||
if (natExternalIp != other.natExternalIp) {
|
||||
return false;
|
||||
}
|
||||
if (isEnabled != other.isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QStringList aa = additionalSecrets;
|
||||
QStringList bb = other.additionalSecrets;
|
||||
aa.removeAll(QString());
|
||||
bb.removeAll(QString());
|
||||
std::sort(aa.begin(), aa.end());
|
||||
std::sort(bb.begin(), bb.end());
|
||||
return aa == bb;
|
||||
}
|
||||
38
client/core/models/protocols/telemtProtocolConfig.h
Normal file
38
client/core/models/protocols/telemtProtocolConfig.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef TELEMTPROTOCOLCONFIG_H
|
||||
#define TELEMTPROTOCOLCONFIG_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace amnezia {
|
||||
|
||||
struct TelemtProtocolConfig {
|
||||
QString port;
|
||||
QString secret;
|
||||
QString tag;
|
||||
QString tgLink;
|
||||
QString tmeLink;
|
||||
bool isEnabled = true;
|
||||
QString publicHost;
|
||||
QString transportMode;
|
||||
QString tlsDomain;
|
||||
bool maskEnabled = true;
|
||||
bool tlsEmulation = false;
|
||||
bool useMiddleProxy = true;
|
||||
QString userName;
|
||||
QStringList additionalSecrets;
|
||||
QString workersMode;
|
||||
QString workers;
|
||||
bool natEnabled = false;
|
||||
QString natInternalIp;
|
||||
QString natExternalIp;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static TelemtProtocolConfig fromJson(const QJsonObject &json);
|
||||
bool equalsDockerDeploymentSettings(const TelemtProtocolConfig &other) const;
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // TELEMTPROTOCOLCONFIG_H
|
||||
@@ -3,20 +3,173 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
|
||||
#include "../../../core/utils/protocolEnum.h"
|
||||
#include "../../../core/protocols/protocolUtils.h"
|
||||
#include "../../../core/utils/constants/configKeys.h"
|
||||
#include "../../../core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
|
||||
using namespace amnezia;
|
||||
using namespace ProtocolUtils;
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
QJsonObject XrayXPaddingConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
if (!bytesMin.isEmpty()) obj[configKey::xPaddingBytesMin] = bytesMin;
|
||||
if (!bytesMax.isEmpty()) obj[configKey::xPaddingBytesMax] = bytesMax;
|
||||
obj[configKey::xPaddingObfsMode] = obfsMode;
|
||||
if (!key.isEmpty()) obj[configKey::xPaddingKey] = key;
|
||||
if (!header.isEmpty()) obj[configKey::xPaddingHeader] = header;
|
||||
if (!placement.isEmpty()) obj[configKey::xPaddingPlacement] = placement;
|
||||
if (!method.isEmpty()) obj[configKey::xPaddingMethod] = method;
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayXPaddingConfig XrayXPaddingConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayXPaddingConfig c;
|
||||
c.bytesMin = json.value(configKey::xPaddingBytesMin).toString();
|
||||
c.bytesMax = json.value(configKey::xPaddingBytesMax).toString();
|
||||
c.obfsMode = json.value(configKey::xPaddingObfsMode).toBool(true);
|
||||
c.key = json.value(configKey::xPaddingKey).toString(protocols::xray::defaultSite);
|
||||
c.header = json.value(configKey::xPaddingHeader).toString();
|
||||
c.placement = json.value(configKey::xPaddingPlacement).toString(protocols::xray::defaultXPaddingPlacement);
|
||||
c.method = json.value(configKey::xPaddingMethod).toString(protocols::xray::defaultXPaddingMethod);
|
||||
return c;
|
||||
}
|
||||
|
||||
QJsonObject XrayXmuxConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj[configKey::xmuxEnabled] = enabled;
|
||||
if (!maxConcurrencyMin.isEmpty()) obj[configKey::xmuxMaxConcurrencyMin] = maxConcurrencyMin;
|
||||
if (!maxConcurrencyMax.isEmpty()) obj[configKey::xmuxMaxConcurrencyMax] = maxConcurrencyMax;
|
||||
if (!maxConnectionsMin.isEmpty()) obj[configKey::xmuxMaxConnectionsMin] = maxConnectionsMin;
|
||||
if (!maxConnectionsMax.isEmpty()) obj[configKey::xmuxMaxConnectionsMax] = maxConnectionsMax;
|
||||
if (!cMaxReuseTimesMin.isEmpty()) obj[configKey::xmuxCMaxReuseTimesMin] = cMaxReuseTimesMin;
|
||||
if (!cMaxReuseTimesMax.isEmpty()) obj[configKey::xmuxCMaxReuseTimesMax] = cMaxReuseTimesMax;
|
||||
if (!hMaxRequestTimesMin.isEmpty()) obj[configKey::xmuxHMaxRequestTimesMin] = hMaxRequestTimesMin;
|
||||
if (!hMaxRequestTimesMax.isEmpty()) obj[configKey::xmuxHMaxRequestTimesMax] = hMaxRequestTimesMax;
|
||||
if (!hMaxReusableSecsMin.isEmpty()) obj[configKey::xmuxHMaxReusableSecsMin] = hMaxReusableSecsMin;
|
||||
if (!hMaxReusableSecsMax.isEmpty()) obj[configKey::xmuxHMaxReusableSecsMax] = hMaxReusableSecsMax;
|
||||
if (!hKeepAlivePeriod.isEmpty()) obj[configKey::xmuxHKeepAlivePeriod] = hKeepAlivePeriod;
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayXmuxConfig XrayXmuxConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayXmuxConfig c;
|
||||
c.enabled = json.value(configKey::xmuxEnabled).toBool(true);
|
||||
c.maxConcurrencyMin = json.value(configKey::xmuxMaxConcurrencyMin).toString("0");
|
||||
c.maxConcurrencyMax = json.value(configKey::xmuxMaxConcurrencyMax).toString("0");
|
||||
c.maxConnectionsMin = json.value(configKey::xmuxMaxConnectionsMin).toString("0");
|
||||
c.maxConnectionsMax = json.value(configKey::xmuxMaxConnectionsMax).toString("0");
|
||||
c.cMaxReuseTimesMin = json.value(configKey::xmuxCMaxReuseTimesMin).toString("0");
|
||||
c.cMaxReuseTimesMax = json.value(configKey::xmuxCMaxReuseTimesMax).toString("0");
|
||||
c.hMaxRequestTimesMin = json.value(configKey::xmuxHMaxRequestTimesMin).toString("0");
|
||||
c.hMaxRequestTimesMax = json.value(configKey::xmuxHMaxRequestTimesMax).toString("0");
|
||||
c.hMaxReusableSecsMin = json.value(configKey::xmuxHMaxReusableSecsMin).toString("0");
|
||||
c.hMaxReusableSecsMax = json.value(configKey::xmuxHMaxReusableSecsMax).toString("0");
|
||||
c.hKeepAlivePeriod = json.value(configKey::xmuxHKeepAlivePeriod).toString();
|
||||
return c;
|
||||
}
|
||||
|
||||
QJsonObject XrayXhttpConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
if (!mode.isEmpty()) obj[configKey::xhttpMode] = mode;
|
||||
if (!host.isEmpty()) obj[configKey::xhttpHost] = host;
|
||||
if (!path.isEmpty()) obj[configKey::xhttpPath] = path;
|
||||
if (!headersTemplate.isEmpty()) obj[configKey::xhttpHeadersTemplate] = headersTemplate;
|
||||
if (!uplinkMethod.isEmpty()) obj[configKey::xhttpUplinkMethod] = uplinkMethod;
|
||||
obj[configKey::xhttpDisableGrpc] = disableGrpc;
|
||||
obj[configKey::xhttpDisableSse] = disableSse;
|
||||
|
||||
if (!sessionPlacement.isEmpty()) obj[configKey::xhttpSessionPlacement] = sessionPlacement;
|
||||
if (!sessionKey.isEmpty()) obj[configKey::xhttpSessionKey] = sessionKey;
|
||||
if (!seqPlacement.isEmpty()) obj[configKey::xhttpSeqPlacement] = seqPlacement;
|
||||
if (!seqKey.isEmpty()) obj[configKey::xhttpSeqKey] = seqKey;
|
||||
if (!uplinkDataPlacement.isEmpty()) obj[configKey::xhttpUplinkDataPlacement] = uplinkDataPlacement;
|
||||
if (!uplinkDataKey.isEmpty()) obj[configKey::xhttpUplinkDataKey] = uplinkDataKey;
|
||||
|
||||
if (!uplinkChunkSize.isEmpty()) obj[configKey::xhttpUplinkChunkSize] = uplinkChunkSize;
|
||||
if (!scMaxBufferedPosts.isEmpty()) obj[configKey::xhttpScMaxBufferedPosts] = scMaxBufferedPosts;
|
||||
if (!scMaxEachPostBytesMin.isEmpty()) obj[configKey::xhttpScMaxEachPostBytesMin] = scMaxEachPostBytesMin;
|
||||
if (!scMaxEachPostBytesMax.isEmpty()) obj[configKey::xhttpScMaxEachPostBytesMax] = scMaxEachPostBytesMax;
|
||||
if (!scMinPostsIntervalMsMin.isEmpty()) obj[configKey::xhttpScMinPostsIntervalMsMin] = scMinPostsIntervalMsMin;
|
||||
if (!scMinPostsIntervalMsMax.isEmpty()) obj[configKey::xhttpScMinPostsIntervalMsMax] = scMinPostsIntervalMsMax;
|
||||
if (!scStreamUpServerSecsMin.isEmpty()) obj[configKey::xhttpScStreamUpServerSecsMin] = scStreamUpServerSecsMin;
|
||||
if (!scStreamUpServerSecsMax.isEmpty()) obj[configKey::xhttpScStreamUpServerSecsMax] = scStreamUpServerSecsMax;
|
||||
|
||||
obj["xPadding"] = xPadding.toJson();
|
||||
obj["xmux"] = xmux.toJson();
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayXhttpConfig c;
|
||||
c.mode = json.value(configKey::xhttpMode).toString(protocols::xray::defaultXhttpMode);
|
||||
c.host = json.value(configKey::xhttpHost).toString(protocols::xray::defaultSite);
|
||||
c.path = json.value(configKey::xhttpPath).toString();
|
||||
c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString(protocols::xray::defaultXhttpHeadersTemplate);
|
||||
c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString(protocols::xray::defaultXhttpUplinkMethod);
|
||||
c.disableGrpc = json.value(configKey::xhttpDisableGrpc).toBool(true);
|
||||
c.disableSse = json.value(configKey::xhttpDisableSse).toBool(true);
|
||||
|
||||
c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString(protocols::xray::defaultXhttpSessionPlacement);
|
||||
c.sessionKey = json.value(configKey::xhttpSessionKey).toString();
|
||||
c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString(protocols::xray::defaultXhttpSessionPlacement);
|
||||
c.seqKey = json.value(configKey::xhttpSeqKey).toString();
|
||||
c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString(protocols::xray::defaultXhttpUplinkDataPlacement);
|
||||
c.uplinkDataKey = json.value(configKey::xhttpUplinkDataKey).toString();
|
||||
|
||||
c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString("0");
|
||||
c.scMaxBufferedPosts = json.value(configKey::xhttpScMaxBufferedPosts).toString();
|
||||
c.scMaxEachPostBytesMin = json.value(configKey::xhttpScMaxEachPostBytesMin).toString("1");
|
||||
c.scMaxEachPostBytesMax = json.value(configKey::xhttpScMaxEachPostBytesMax).toString("100");
|
||||
c.scMinPostsIntervalMsMin = json.value(configKey::xhttpScMinPostsIntervalMsMin).toString("100");
|
||||
c.scMinPostsIntervalMsMax = json.value(configKey::xhttpScMinPostsIntervalMsMax).toString("800");
|
||||
c.scStreamUpServerSecsMin = json.value(configKey::xhttpScStreamUpServerSecsMin).toString("1");
|
||||
c.scStreamUpServerSecsMax = json.value(configKey::xhttpScStreamUpServerSecsMax).toString("100");
|
||||
|
||||
c.xPadding = XrayXPaddingConfig::fromJson(json.value("xPadding").toObject());
|
||||
c.xmux = XrayXmuxConfig::fromJson(json.value("xmux").toObject());
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
QJsonObject XrayMkcpConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
if (!tti.isEmpty()) obj[configKey::mkcpTti] = tti;
|
||||
if (!uplinkCapacity.isEmpty()) obj[configKey::mkcpUplinkCapacity] = uplinkCapacity;
|
||||
if (!downlinkCapacity.isEmpty()) obj[configKey::mkcpDownlinkCapacity] = downlinkCapacity;
|
||||
if (!readBufferSize.isEmpty()) obj[configKey::mkcpReadBufferSize] = readBufferSize;
|
||||
if (!writeBufferSize.isEmpty()) obj[configKey::mkcpWriteBufferSize] = writeBufferSize;
|
||||
obj[configKey::mkcpCongestion] = congestion;
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayMkcpConfig XrayMkcpConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayMkcpConfig c;
|
||||
c.tti = json.value(configKey::mkcpTti).toString();
|
||||
c.uplinkCapacity = json.value(configKey::mkcpUplinkCapacity).toString();
|
||||
c.downlinkCapacity = json.value(configKey::mkcpDownlinkCapacity).toString();
|
||||
c.readBufferSize = json.value(configKey::mkcpReadBufferSize).toString();
|
||||
c.writeBufferSize = json.value(configKey::mkcpWriteBufferSize).toString();
|
||||
c.congestion = json.value(configKey::mkcpCongestion).toBool(true);
|
||||
return c;
|
||||
}
|
||||
|
||||
QJsonObject XrayServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
|
||||
|
||||
// Existing fields
|
||||
if (!port.isEmpty()) {
|
||||
obj[configKey::port] = port;
|
||||
}
|
||||
@@ -29,60 +182,96 @@ QJsonObject XrayServerConfig::toJson() const
|
||||
if (!site.isEmpty()) {
|
||||
obj[configKey::site] = site;
|
||||
}
|
||||
|
||||
|
||||
if (isThirdPartyConfig) {
|
||||
obj[configKey::isThirdPartyConfig] = isThirdPartyConfig;
|
||||
}
|
||||
|
||||
|
||||
// New: Security
|
||||
if (!security.isEmpty()) {
|
||||
obj[configKey::xraySecurity] = security;
|
||||
}
|
||||
if (!flow.isEmpty()) {
|
||||
obj[configKey::xrayFlow] = flow;
|
||||
}
|
||||
if (!fingerprint.isEmpty()) {
|
||||
obj[configKey::xrayFingerprint] = fingerprint;
|
||||
}
|
||||
if (!sni.isEmpty()) {
|
||||
obj[configKey::xraySni] = sni;
|
||||
}
|
||||
if (!alpn.isEmpty()) {
|
||||
obj[configKey::xrayAlpn] = alpn;
|
||||
}
|
||||
|
||||
// New: Transport
|
||||
if (!transport.isEmpty()) {
|
||||
obj[configKey::xrayTransport] = transport;
|
||||
}
|
||||
obj["xhttp"] = xhttp.toJson();
|
||||
obj["mkcp"] = mkcp.toJson();
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayServerConfig XrayServerConfig::fromJson(const QJsonObject& json)
|
||||
XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayServerConfig config;
|
||||
|
||||
config.port = json.value(configKey::port).toString();
|
||||
config.transportProto = json.value(configKey::transportProto).toString();
|
||||
config.subnetAddress = json.value(configKey::subnetAddress).toString();
|
||||
config.site = json.value(configKey::site).toString();
|
||||
|
||||
config.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
|
||||
|
||||
return config;
|
||||
XrayServerConfig c;
|
||||
|
||||
// Existing fields
|
||||
c.port = json.value(configKey::port).toString();
|
||||
c.transportProto = json.value(configKey::transportProto).toString();
|
||||
c.subnetAddress = json.value(configKey::subnetAddress).toString();
|
||||
c.site = json.value(configKey::site).toString();
|
||||
c.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
|
||||
|
||||
// New: Security
|
||||
c.security = json.value(configKey::xraySecurity).toString(protocols::xray::defaultSecurity);
|
||||
c.flow = json.value(configKey::xrayFlow).toString(protocols::xray::defaultFlow);
|
||||
c.fingerprint = json.value(configKey::xrayFingerprint).toString(protocols::xray::defaultFingerprint);
|
||||
if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
}
|
||||
c.sni = json.value(configKey::xraySni).toString(protocols::xray::defaultSni);
|
||||
c.alpn = json.value(configKey::xrayAlpn).toString(protocols::xray::defaultAlpn);
|
||||
|
||||
// New: Transport
|
||||
c.transport = json.value(configKey::xrayTransport).toString(protocols::xray::defaultTransport);
|
||||
c.xhttp = XrayXhttpConfig::fromJson(json.value("xhttp").toObject());
|
||||
c.mkcp = XrayMkcpConfig::fromJson(json.value("mkcp").toObject());
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig& other) const
|
||||
bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) const
|
||||
{
|
||||
return port == other.port && site == other.site;
|
||||
return port == other.port
|
||||
&& site == other.site
|
||||
&& security == other.security
|
||||
&& flow == other.flow
|
||||
&& transport == other.transport
|
||||
&& fingerprint == other.fingerprint
|
||||
&& sni == other.sni;
|
||||
}
|
||||
|
||||
QJsonObject XrayClientConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
|
||||
if (!nativeConfig.isEmpty()) {
|
||||
obj[configKey::config] = nativeConfig;
|
||||
}
|
||||
if (!localPort.isEmpty()) {
|
||||
obj[configKey::localPort] = localPort;
|
||||
}
|
||||
if (!id.isEmpty()) {
|
||||
obj[configKey::clientId] = id;
|
||||
}
|
||||
|
||||
if (!nativeConfig.isEmpty()) obj[configKey::config] = nativeConfig;
|
||||
if (!localPort.isEmpty()) obj[configKey::localPort] = localPort;
|
||||
if (!id.isEmpty()) obj[configKey::clientId] = id;
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json)
|
||||
XrayClientConfig XrayClientConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayClientConfig config;
|
||||
|
||||
config.nativeConfig = json.value(configKey::config).toString();
|
||||
config.localPort = json.value(configKey::localPort).toString();
|
||||
config.id = json.value(configKey::clientId).toString();
|
||||
|
||||
if (config.id.isEmpty() && !config.nativeConfig.isEmpty()) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(config.nativeConfig.toUtf8());
|
||||
XrayClientConfig c;
|
||||
c.nativeConfig = json.value(configKey::config).toString();
|
||||
c.localPort = json.value(configKey::localPort).toString();
|
||||
c.id = json.value(configKey::clientId).toString();
|
||||
|
||||
if (c.id.isEmpty() && !c.nativeConfig.isEmpty()) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(c.nativeConfig.toUtf8());
|
||||
if (!doc.isNull() && doc.isObject()) {
|
||||
QJsonObject configObj = doc.object();
|
||||
if (configObj.contains(protocols::xray::outbounds)) {
|
||||
@@ -100,7 +289,7 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json)
|
||||
if (!users.isEmpty()) {
|
||||
QJsonObject user = users[0].toObject();
|
||||
if (user.contains(protocols::xray::id)) {
|
||||
config.id = user[protocols::xray::id].toString();
|
||||
c.id = user[protocols::xray::id].toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,16 +300,15 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
QJsonObject XrayProtocolConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj = serverConfig.toJson();
|
||||
|
||||
|
||||
if (clientConfig.has_value()) {
|
||||
// Third-party import: nativeConfig is raw Xray JSON (inbounds/outbounds)
|
||||
QJsonDocument doc = QJsonDocument::fromJson(clientConfig->nativeConfig.toUtf8());
|
||||
if (!doc.isNull() && doc.isObject() && doc.object().contains(protocols::xray::outbounds)
|
||||
&& !doc.object().contains(configKey::config)) {
|
||||
@@ -130,22 +318,20 @@ QJsonObject XrayProtocolConfig::toJson() const
|
||||
obj[configKey::lastConfig] = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject& json)
|
||||
XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayProtocolConfig config;
|
||||
|
||||
config.serverConfig = XrayServerConfig::fromJson(json);
|
||||
|
||||
XrayProtocolConfig c;
|
||||
c.serverConfig = XrayServerConfig::fromJson(json);
|
||||
|
||||
QString lastConfigStr = json.value(configKey::lastConfig).toString();
|
||||
if (!lastConfigStr.isEmpty()) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(lastConfigStr.toUtf8());
|
||||
if (doc.isObject()) {
|
||||
QJsonObject parsed = doc.object();
|
||||
// Third-party import stores raw Xray config (inbounds/outbounds) directly
|
||||
if (parsed.contains(protocols::xray::outbounds) && !parsed.contains(configKey::config)) {
|
||||
XrayClientConfig clientCfg;
|
||||
clientCfg.nativeConfig = lastConfigStr;
|
||||
@@ -158,14 +344,14 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject& json)
|
||||
}
|
||||
}
|
||||
}
|
||||
config.clientConfig = clientCfg;
|
||||
c.clientConfig = clientCfg;
|
||||
} else {
|
||||
config.clientConfig = XrayClientConfig::fromJson(parsed);
|
||||
c.clientConfig = XrayClientConfig::fromJson(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
bool XrayProtocolConfig::hasClientConfig() const
|
||||
@@ -173,7 +359,7 @@ bool XrayProtocolConfig::hasClientConfig() const
|
||||
return clientConfig.has_value();
|
||||
}
|
||||
|
||||
void XrayProtocolConfig::setClientConfig(const XrayClientConfig& config)
|
||||
void XrayProtocolConfig::setClientConfig(const XrayClientConfig &config)
|
||||
{
|
||||
clientConfig = config;
|
||||
}
|
||||
@@ -184,4 +370,3 @@ void XrayProtocolConfig::clearClientConfig()
|
||||
}
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
|
||||
@@ -2,47 +2,145 @@
|
||||
#define XRAYPROTOCOLCONFIG_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include <QString>
|
||||
#include <optional>
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
|
||||
// ── xPadding ─────────────────────────────────────────────────────────────────
|
||||
struct XrayXPaddingConfig {
|
||||
QString bytesMin; // xPaddingBytes min
|
||||
QString bytesMax; // xPaddingBytes max
|
||||
bool obfsMode = true; // xPaddingObfsMode
|
||||
QString key; // xPaddingKey
|
||||
QString header; // xPaddingHeader
|
||||
QString placement = protocols::xray::defaultXPaddingPlacement; // xPaddingPlacement: Cookie|Header|Query|Body
|
||||
QString method = protocols::xray::defaultXPaddingMethod; // xPaddingMethod: Repeat-x|Random|Zero
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayXPaddingConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
// ── xmux ─────────────────────────────────────────────────────────────────────
|
||||
struct XrayXmuxConfig {
|
||||
bool enabled = true;
|
||||
|
||||
QString maxConcurrencyMin = "0";
|
||||
QString maxConcurrencyMax = "0";
|
||||
QString maxConnectionsMin = "0";
|
||||
QString maxConnectionsMax = "0";
|
||||
QString cMaxReuseTimesMin = "0";
|
||||
QString cMaxReuseTimesMax = "0";
|
||||
QString hMaxRequestTimesMin = "0";
|
||||
QString hMaxRequestTimesMax = "0";
|
||||
QString hMaxReusableSecsMin = "0";
|
||||
QString hMaxReusableSecsMax = "0";
|
||||
QString hKeepAlivePeriod;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayXmuxConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
// ── XHTTP transport ───────────────────────────────────────────────────────────
|
||||
struct XrayXhttpConfig {
|
||||
QString mode = protocols::xray::defaultXhttpMode; // Auto|Packet-up|Stream-up|Stream-one
|
||||
QString host = protocols::xray::defaultXhttpHost;
|
||||
QString path;
|
||||
QString headersTemplate = protocols::xray::defaultXhttpHeadersTemplate; // HTTP|None
|
||||
QString uplinkMethod = protocols::xray::defaultXhttpUplinkMethod; // POST|PUT|PATCH
|
||||
bool disableGrpc = true;
|
||||
bool disableSse = true;
|
||||
|
||||
// Session & Sequence
|
||||
QString sessionPlacement = protocols::xray::defaultXhttpSessionPlacement;
|
||||
QString sessionKey = protocols::xray::defaultXhttpSessionKey;
|
||||
QString seqPlacement = protocols::xray::defaultXhttpSeqPlacement;
|
||||
QString seqKey;
|
||||
QString uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement;
|
||||
QString uplinkDataKey;
|
||||
|
||||
// Traffic Shaping
|
||||
QString uplinkChunkSize = protocols::xray::defaultXhttpUplinkChunkSize;
|
||||
QString scMaxBufferedPosts;
|
||||
QString scMaxEachPostBytesMin = protocols::xray::defaultXhttpScMaxEachPostBytesMin;
|
||||
QString scMaxEachPostBytesMax = protocols::xray::defaultXhttpScMaxEachPostBytesMax;
|
||||
QString scMinPostsIntervalMsMin = protocols::xray::defaultXhttpScMinPostsIntervalMsMin;
|
||||
QString scMinPostsIntervalMsMax = protocols::xray::defaultXhttpScMinPostsIntervalMsMax;
|
||||
QString scStreamUpServerSecsMin = protocols::xray::defaultXhttpScStreamUpServerSecsMin;
|
||||
QString scStreamUpServerSecsMax = protocols::xray::defaultXhttpScStreamUpServerSecsMax;
|
||||
|
||||
XrayXPaddingConfig xPadding;
|
||||
XrayXmuxConfig xmux;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayXhttpConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
// ── mKCP transport ────────────────────────────────────────────────────────────
|
||||
struct XrayMkcpConfig {
|
||||
QString tti;
|
||||
QString uplinkCapacity;
|
||||
QString downlinkCapacity;
|
||||
QString readBufferSize;
|
||||
QString writeBufferSize;
|
||||
bool congestion = true;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayMkcpConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
// ── Server config (settings editable by user) ─────────────────────────────────
|
||||
struct XrayServerConfig {
|
||||
QString port;
|
||||
QString transportProto;
|
||||
QString subnetAddress;
|
||||
QString site;
|
||||
bool isThirdPartyConfig = false;
|
||||
|
||||
|
||||
// New: Security
|
||||
QString security = protocols::xray::defaultSecurity;
|
||||
QString flow = protocols::xray::defaultFlow;
|
||||
QString fingerprint = protocols::xray::defaultFingerprint;
|
||||
QString sni = protocols::xray::defaultSni;
|
||||
QString alpn = protocols::xray::defaultAlpn;
|
||||
|
||||
// New: Transport
|
||||
QString transport = protocols::xray::defaultTransport;
|
||||
XrayXhttpConfig xhttp;
|
||||
XrayMkcpConfig mkcp;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayServerConfig fromJson(const QJsonObject& json);
|
||||
|
||||
bool hasEqualServerSettings(const XrayServerConfig& other) const;
|
||||
|
||||
static XrayServerConfig fromJson(const QJsonObject &json);
|
||||
|
||||
bool hasEqualServerSettings(const XrayServerConfig &other) const;
|
||||
};
|
||||
|
||||
// ── Client config (generated, not edited by user) ─────────────────────────────
|
||||
struct XrayClientConfig {
|
||||
QString nativeConfig;
|
||||
QString localPort;
|
||||
QString id;
|
||||
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayClientConfig fromJson(const QJsonObject& json);
|
||||
static XrayClientConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
// ── Top-level protocol config ──────────────────────────────────────────────────
|
||||
struct XrayProtocolConfig {
|
||||
XrayServerConfig serverConfig;
|
||||
std::optional<XrayClientConfig> clientConfig;
|
||||
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayProtocolConfig fromJson(const QJsonObject& json);
|
||||
|
||||
static XrayProtocolConfig fromJson(const QJsonObject &json);
|
||||
|
||||
bool hasClientConfig() const;
|
||||
void setClientConfig(const XrayClientConfig& config);
|
||||
void setClientConfig(const XrayClientConfig &config);
|
||||
void clearClientConfig();
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // XRAYPROTOCOLCONFIG_H
|
||||
|
||||
|
||||
@@ -68,7 +68,10 @@ QMap<Proto, QString> ProtocolUtils::protocolHumanNames()
|
||||
{ Proto::TorWebSite, "Website in Tor network" },
|
||||
{ Proto::Dns, "DNS Service" },
|
||||
{ Proto::Sftp, QObject::tr("SFTP service") },
|
||||
{ Proto::Socks5Proxy, QObject::tr("SOCKS5 proxy server") } };
|
||||
{ Proto::Socks5Proxy, QObject::tr("SOCKS5 proxy server") },
|
||||
{ Proto::MtProxy, QObject::tr("MTProxy (Telegram)") },
|
||||
{ Proto::Telemt, QObject::tr("Telemt (Telegram)") },
|
||||
};
|
||||
}
|
||||
|
||||
QMap<Proto, QString> ProtocolUtils::protocolDescriptions()
|
||||
@@ -92,6 +95,8 @@ ServiceType ProtocolUtils::protocolService(Proto p)
|
||||
case Proto::Dns: return ServiceType::Other;
|
||||
case Proto::Sftp: return ServiceType::Other;
|
||||
case Proto::Socks5Proxy: return ServiceType::Other;
|
||||
case Proto::MtProxy: return ServiceType::Other;
|
||||
case Proto::Telemt: return ServiceType::Other;
|
||||
default: return ServiceType::Other;
|
||||
}
|
||||
}
|
||||
@@ -104,6 +109,8 @@ int ProtocolUtils::getPortForInstall(Proto p)
|
||||
case OpenVpn:
|
||||
case Socks5Proxy:
|
||||
return QRandomGenerator::global()->bounded(30000, 50000);
|
||||
case MtProxy:
|
||||
case Telemt:
|
||||
default:
|
||||
return defaultPort(p);
|
||||
}
|
||||
@@ -123,6 +130,8 @@ int ProtocolUtils::defaultPort(Proto p)
|
||||
case Proto::Dns: return 53;
|
||||
case Proto::Sftp: return 222;
|
||||
case Proto::Socks5Proxy: return 38080;
|
||||
case Proto::MtProxy: return QString(protocols::mtProxy::defaultPort).toInt();
|
||||
case Proto::Telemt: return QString(protocols::telemt::defaultPort).toInt();
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
@@ -141,6 +150,8 @@ bool ProtocolUtils::defaultPortChangeable(Proto p)
|
||||
case Proto::Dns: return false;
|
||||
case Proto::Sftp: return true;
|
||||
case Proto::Socks5Proxy: return true;
|
||||
case Proto::MtProxy: return true;
|
||||
case Proto::Telemt: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
@@ -161,6 +172,8 @@ TransportProto ProtocolUtils::defaultTransportProto(Proto p)
|
||||
case Proto::Dns: return TransportProto::Udp;
|
||||
case Proto::Sftp: return TransportProto::Tcp;
|
||||
case Proto::Socks5Proxy: return TransportProto::Tcp;
|
||||
case Proto::MtProxy: return TransportProto::Tcp;
|
||||
case Proto::Telemt: return TransportProto::Tcp;
|
||||
default: return TransportProto::Udp;
|
||||
}
|
||||
}
|
||||
@@ -180,9 +193,10 @@ bool ProtocolUtils::defaultTransportProtoChangeable(Proto p)
|
||||
case Proto::Dns: return false;
|
||||
case Proto::Sftp: return false;
|
||||
case Proto::Socks5Proxy: return false;
|
||||
case Proto::MtProxy: return false;
|
||||
case Proto::Telemt: return false;
|
||||
default: return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QString ProtocolUtils::key_proto_config_data(Proto p)
|
||||
@@ -208,4 +222,3 @@ QString ProtocolUtils::getProtocolVersionString(const QJsonObject &protocolConfi
|
||||
if (version == protocols::awg::awgV1_5) return QObject::tr(" (version 1.5)");
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
48
client/core/protocols/xrayProtocol.cpp
Executable file → Normal file
48
client/core/protocols/xrayProtocol.cpp
Executable file → Normal file
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/ipcClient.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/serialization/serialization.h"
|
||||
@@ -9,6 +10,7 @@
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QJsonDocument>
|
||||
#include <QTimer>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkInterface>
|
||||
#include <QtCore/qlogging.h>
|
||||
@@ -79,12 +81,29 @@ ErrorCode XrayProtocol::start()
|
||||
m_socksPassword = creds.password;
|
||||
m_socksPort = creds.port;
|
||||
|
||||
const QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact);
|
||||
QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact);
|
||||
if (xrayConfigStr.isEmpty()) {
|
||||
qCritical() << "Xray config is empty";
|
||||
return ErrorCode::XrayExecutableCrashed;
|
||||
}
|
||||
|
||||
// Fix fingerprint: old configs may contain "Mozilla/5.0" which xray-core rejects.
|
||||
// Replace with the correct default at runtime so stale stored configs still work.
|
||||
if (xrayConfigStr.contains("Mozilla/5.0", Qt::CaseInsensitive)) {
|
||||
xrayConfigStr.replace("Mozilla/5.0", amnezia::protocols::xray::defaultFingerprint,
|
||||
Qt::CaseInsensitive);
|
||||
qDebug() << "XrayProtocol: patched legacy fingerprint to"
|
||||
<< amnezia::protocols::xray::defaultFingerprint;
|
||||
}
|
||||
|
||||
// Fix inbound listen address: old configs may use "10.33.0.2" which doesn't exist
|
||||
// until TUN is created. xray must listen on 127.0.0.1 so tun2socks can connect.
|
||||
if (xrayConfigStr.contains(amnezia::protocols::xray::defaultLocalAddr)) {
|
||||
xrayConfigStr.replace(amnezia::protocols::xray::defaultLocalAddr,
|
||||
amnezia::protocols::xray::defaultLocalListenAddr);
|
||||
qDebug() << "XrayProtocol: patched legacy inbound listen address to 127.0.0.1";
|
||||
}
|
||||
|
||||
return IpcClient::withInterface(
|
||||
[&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStart = iface->xrayStart(xrayConfigStr);
|
||||
@@ -188,6 +207,33 @@ ErrorCode XrayProtocol::startTun2Socks()
|
||||
connect(
|
||||
m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this,
|
||||
[this](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||
// Check stdout for "resource busy" — the TUN device was not yet released
|
||||
// by the previous tun2socks instance. Retry after a short delay.
|
||||
bool resourceBusy = false;
|
||||
if (m_tun2socksProcess) {
|
||||
auto readOut = m_tun2socksProcess->readAllStandardOutput();
|
||||
if (readOut.waitForFinished()) {
|
||||
resourceBusy = readOut.returnValue().contains("resource busy");
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceBusy && m_tun2socksRetryCount < maxTun2SocksRetries) {
|
||||
m_tun2socksRetryCount++;
|
||||
qWarning() << QString("Tun2socks: TUN resource busy, retrying (%1/%2) in %3ms...")
|
||||
.arg(m_tun2socksRetryCount)
|
||||
.arg(maxTun2SocksRetries)
|
||||
.arg(tun2socksRetryDelayMs);
|
||||
QTimer::singleShot(tun2socksRetryDelayMs, this, [this]() {
|
||||
if (ErrorCode err = startTun2Socks(); err != ErrorCode::NoError) {
|
||||
stop();
|
||||
setLastError(err);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
m_tun2socksRetryCount = 0;
|
||||
|
||||
if (exitStatus == QProcess::ExitStatus::CrashExit) {
|
||||
qCritical() << "Tun2socks process crashed!";
|
||||
} else {
|
||||
|
||||
@@ -35,6 +35,9 @@ private:
|
||||
int m_socksPort = 10808;
|
||||
|
||||
QSharedPointer<IpcProcessInterfaceReplica> m_tun2socksProcess;
|
||||
int m_tun2socksRetryCount = 0;
|
||||
static constexpr int maxTun2SocksRetries = 5;
|
||||
static constexpr int tun2socksRetryDelayMs = 400;
|
||||
};
|
||||
|
||||
#endif // XRAYPROTOCOL_H
|
||||
|
||||
@@ -451,4 +451,12 @@ void SecureAppSettingsRepository::setInstallationUuid(const QString &uuid)
|
||||
m_settings->setValue("Conf/installationUuid", uuid);
|
||||
}
|
||||
|
||||
QByteArray SecureAppSettingsRepository::xraySavedConfigs() const
|
||||
{
|
||||
return value("Xray/savedConfigs").toByteArray();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setXraySavedConfigs(const QByteArray &data)
|
||||
{
|
||||
setValue("Xray/savedConfigs", data);
|
||||
}
|
||||
|
||||
@@ -92,6 +92,9 @@ public:
|
||||
|
||||
QString nextAvailableServerName() const;
|
||||
|
||||
QByteArray xraySavedConfigs() const;
|
||||
void setXraySavedConfigs(const QByteArray &data);
|
||||
|
||||
signals:
|
||||
void appLanguageChanged(QLocale locale);
|
||||
void allowedDnsServersChanged(const QStringList &servers);
|
||||
|
||||
@@ -93,6 +93,8 @@ namespace amnezia
|
||||
constexpr QLatin1String xray("xray");
|
||||
constexpr QLatin1String ssxray("ssxray");
|
||||
constexpr QLatin1String socks5proxy("socks5proxy");
|
||||
constexpr QLatin1String mtproxy("mtproxy");
|
||||
constexpr QLatin1String telemt("telemt");
|
||||
|
||||
constexpr QLatin1String splitTunnelSites("splitTunnelSites");
|
||||
constexpr QLatin1String splitTunnelType("splitTunnelType");
|
||||
@@ -124,6 +126,76 @@ namespace amnezia
|
||||
constexpr QLatin1String dataSent("dataSent");
|
||||
|
||||
constexpr QLatin1String storageServerId("storageServerId");
|
||||
|
||||
// ── Xray-specific keys ────────────────────────────────────────
|
||||
|
||||
// Security
|
||||
constexpr QLatin1String xraySecurity("xray_security"); // none | tls | reality
|
||||
constexpr QLatin1String xrayFlow("xray_flow"); // "" | xtls-rprx-vision | xtls-rprx-vision-udp443
|
||||
constexpr QLatin1String xrayFingerprint("xray_fingerprint"); // Mozilla/5.0 | chrome | firefox | ...
|
||||
constexpr QLatin1String xraySni("xray_sni"); // Server Name (SNI)
|
||||
constexpr QLatin1String xrayAlpn("xray_alpn"); // HTTP/2 | HTTP/1.1 | HTTP/2,HTTP/1.1
|
||||
|
||||
// Transport — common
|
||||
constexpr QLatin1String xrayTransport("xray_transport"); // raw | xhttp | mkcp
|
||||
|
||||
// Transport — XHTTP
|
||||
constexpr QLatin1String xhttpMode("xhttp_mode"); // Auto | Packet-up | Stream-up | Stream-one
|
||||
constexpr QLatin1String xhttpHost("xhttp_host");
|
||||
constexpr QLatin1String xhttpPath("xhttp_path");
|
||||
constexpr QLatin1String xhttpHeadersTemplate("xhttp_headers_template"); // HTTP | None
|
||||
constexpr QLatin1String xhttpUplinkMethod("xhttp_uplink_method"); // POST | PUT | PATCH
|
||||
constexpr QLatin1String xhttpDisableGrpc("xhttp_disable_grpc"); // bool
|
||||
constexpr QLatin1String xhttpDisableSse("xhttp_disable_sse"); // bool
|
||||
|
||||
// Transport — XHTTP Session & Sequence
|
||||
constexpr QLatin1String xhttpSessionPlacement("xhttp_session_placement"); // Path | Header | Cookie | None
|
||||
constexpr QLatin1String xhttpSessionKey("xhttp_session_key");
|
||||
constexpr QLatin1String xhttpSeqPlacement("xhttp_seq_placement");
|
||||
constexpr QLatin1String xhttpSeqKey("xhttp_seq_key");
|
||||
constexpr QLatin1String xhttpUplinkDataPlacement("xhttp_uplink_data_placement"); // Body | Query
|
||||
constexpr QLatin1String xhttpUplinkDataKey("xhttp_uplink_data_key");
|
||||
|
||||
// Transport — XHTTP Traffic Shaping
|
||||
constexpr QLatin1String xhttpUplinkChunkSize("xhttp_uplink_chunk_size");
|
||||
constexpr QLatin1String xhttpScMaxBufferedPosts("xhttp_sc_max_buffered_posts");
|
||||
constexpr QLatin1String xhttpScMaxEachPostBytesMin("xhttp_sc_max_each_post_bytes_min");
|
||||
constexpr QLatin1String xhttpScMaxEachPostBytesMax("xhttp_sc_max_each_post_bytes_max");
|
||||
constexpr QLatin1String xhttpScMinPostsIntervalMsMin("xhttp_sc_min_posts_interval_ms_min");
|
||||
constexpr QLatin1String xhttpScMinPostsIntervalMsMax("xhttp_sc_min_posts_interval_ms_max");
|
||||
constexpr QLatin1String xhttpScStreamUpServerSecsMin("xhttp_sc_stream_up_server_secs_min");
|
||||
constexpr QLatin1String xhttpScStreamUpServerSecsMax("xhttp_sc_stream_up_server_secs_max");
|
||||
|
||||
// Transport — mKCP
|
||||
constexpr QLatin1String mkcpTti("mkcp_tti");
|
||||
constexpr QLatin1String mkcpUplinkCapacity("mkcp_uplink_capacity");
|
||||
constexpr QLatin1String mkcpDownlinkCapacity("mkcp_downlink_capacity");
|
||||
constexpr QLatin1String mkcpReadBufferSize("mkcp_read_buffer_size");
|
||||
constexpr QLatin1String mkcpWriteBufferSize("mkcp_write_buffer_size");
|
||||
constexpr QLatin1String mkcpCongestion("mkcp_congestion"); // bool
|
||||
|
||||
// xPadding
|
||||
constexpr QLatin1String xPaddingBytesMin("xpadding_bytes_min");
|
||||
constexpr QLatin1String xPaddingBytesMax("xpadding_bytes_max");
|
||||
constexpr QLatin1String xPaddingObfsMode("xpadding_obfs_mode"); // bool
|
||||
constexpr QLatin1String xPaddingKey("xpadding_key");
|
||||
constexpr QLatin1String xPaddingHeader("xpadding_header");
|
||||
constexpr QLatin1String xPaddingPlacement("xpadding_placement"); // Cookie | Header | Query | Body
|
||||
constexpr QLatin1String xPaddingMethod("xpadding_method"); // Repeat-x | Random | Zero
|
||||
|
||||
// xmux
|
||||
constexpr QLatin1String xmuxEnabled("xmux_enabled"); // bool
|
||||
constexpr QLatin1String xmuxMaxConcurrencyMin("xmux_max_concurrency_min");
|
||||
constexpr QLatin1String xmuxMaxConcurrencyMax("xmux_max_concurrency_max");
|
||||
constexpr QLatin1String xmuxMaxConnectionsMin("xmux_max_connections_min");
|
||||
constexpr QLatin1String xmuxMaxConnectionsMax("xmux_max_connections_max");
|
||||
constexpr QLatin1String xmuxCMaxReuseTimesMin("xmux_c_max_reuse_times_min");
|
||||
constexpr QLatin1String xmuxCMaxReuseTimesMax("xmux_c_max_reuse_times_max");
|
||||
constexpr QLatin1String xmuxHMaxRequestTimesMin("xmux_h_max_request_times_min");
|
||||
constexpr QLatin1String xmuxHMaxRequestTimesMax("xmux_h_max_request_times_max");
|
||||
constexpr QLatin1String xmuxHMaxReusableSecsMin("xmux_h_max_reusable_secs_min");
|
||||
constexpr QLatin1String xmuxHMaxReusableSecsMax("xmux_h_max_reusable_secs_max");
|
||||
constexpr QLatin1String xmuxHKeepAlivePeriod("xmux_h_keep_alive_period");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
|
||||
namespace protocols
|
||||
{
|
||||
|
||||
@@ -57,6 +58,40 @@ namespace amnezia
|
||||
constexpr char defaultPort[] = "443";
|
||||
constexpr char defaultLocalProxyPort[] = "10808";
|
||||
constexpr char defaultLocalAddr[] = "10.33.0.2";
|
||||
constexpr char defaultLocalListenAddr[] = "127.0.0.1";
|
||||
|
||||
constexpr char defaultSecurity[] = "reality";
|
||||
constexpr char defaultFlow[] = "xtls-rprx-vision";
|
||||
constexpr char defaultTransport[] = "raw";
|
||||
constexpr char defaultFingerprint[] = "chrome";
|
||||
constexpr char defaultSni[] = "cdn.example.com";
|
||||
constexpr char defaultAlpn[] = "HTTP/2";
|
||||
|
||||
constexpr char defaultXhttpMode[] = "Auto";
|
||||
constexpr char defaultXhttpHeadersTemplate[] = "HTTP";
|
||||
constexpr char defaultXhttpUplinkMethod[] = "POST";
|
||||
constexpr char defaultXhttpSessionPlacement[] = "Path";
|
||||
constexpr char defaultXhttpSessionKey[] = "Path";
|
||||
constexpr char defaultXhttpSeqPlacement[] = "Path";
|
||||
constexpr char defaultXhttpUplinkDataPlacement[] = "Body";
|
||||
|
||||
constexpr char defaultXhttpHost[] = "www.googletagmanager.com";
|
||||
constexpr char defaultXhttpUplinkChunkSize[] = "0";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMin[] = "1";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMax[] = "100";
|
||||
constexpr char defaultXhttpScMinPostsIntervalMsMin[] = "100";
|
||||
constexpr char defaultXhttpScMinPostsIntervalMsMax[] = "800";
|
||||
constexpr char defaultXhttpScStreamUpServerSecsMin[] = "1";
|
||||
constexpr char defaultXhttpScStreamUpServerSecsMax[] = "100";
|
||||
|
||||
constexpr char defaultXPaddingPlacement[] = "Cookie";
|
||||
constexpr char defaultXPaddingMethod[] = "Repeat-x";
|
||||
|
||||
constexpr char defaultMkcpTti[] = "50";
|
||||
constexpr char defaultMkcpUplinkCapacity[] = "5";
|
||||
constexpr char defaultMkcpDownlinkCapacity[] = "20";
|
||||
constexpr char defaultMkcpReadBufferSize[] = "2";
|
||||
constexpr char defaultMkcpWriteBufferSize[] = "2";
|
||||
|
||||
constexpr char outbounds[] = "outbounds";
|
||||
constexpr char inbounds[] = "inbounds";
|
||||
@@ -174,9 +209,71 @@ namespace amnezia
|
||||
constexpr char proxyConfigPath[] = "/usr/local/3proxy/conf/3proxy.cfg";
|
||||
}
|
||||
|
||||
namespace mtProxy
|
||||
{
|
||||
constexpr char secretKey[] = "mtproxy_secret";
|
||||
constexpr char tagKey[] = "mtproxy_tag";
|
||||
constexpr char tgLinkKey[] = "mtproxy_tg_link";
|
||||
constexpr char tmeLinkKey[] = "mtproxy_tme_link";
|
||||
constexpr char isEnabledKey[] = "mtproxy_is_enabled";
|
||||
constexpr char publicHostKey[] = "mtproxy_public_host";
|
||||
constexpr char transportModeKey[] = "mtproxy_transport_mode";
|
||||
constexpr char tlsDomainKey[] = "mtproxy_tls_domain";
|
||||
constexpr char additionalSecretsKey[] = "mtproxy_additional_secrets";
|
||||
constexpr char workersKey[] = "mtproxy_workers";
|
||||
constexpr char workersModeKey[] = "mtproxy_workers_mode";
|
||||
constexpr char natEnabledKey[] = "mtproxy_nat_enabled";
|
||||
constexpr char natInternalIpKey[] = "mtproxy_nat_internal_ip";
|
||||
constexpr char natExternalIpKey[] = "mtproxy_nat_external_ip";
|
||||
|
||||
constexpr char transportModeStandard[] = "standard";
|
||||
constexpr char transportModeFakeTLS[] = "faketls";
|
||||
|
||||
constexpr char workersModeAuto[] = "auto";
|
||||
constexpr char workersModeManual[] = "manual";
|
||||
|
||||
constexpr char defaultPort[] = "443";
|
||||
constexpr char defaultWorkers[] = "2";
|
||||
constexpr int maxWorkers = 32;
|
||||
constexpr int botTagHexLength = 32;
|
||||
constexpr char defaultTlsDomain[] = "googletagmanager.com";
|
||||
}
|
||||
|
||||
namespace telemt
|
||||
{
|
||||
constexpr char secretKey[] = "telemt_secret";
|
||||
constexpr char tagKey[] = "telemt_tag";
|
||||
constexpr char tgLinkKey[] = "telemt_tg_link";
|
||||
constexpr char tmeLinkKey[] = "telemt_tme_link";
|
||||
constexpr char isEnabledKey[] = "telemt_is_enabled";
|
||||
constexpr char publicHostKey[] = "telemt_public_host";
|
||||
constexpr char transportModeKey[] = "telemt_transport_mode";
|
||||
constexpr char tlsDomainKey[] = "telemt_tls_domain";
|
||||
constexpr char maskEnabledKey[] = "telemt_mask_enabled";
|
||||
constexpr char tlsEmulationKey[] = "telemt_tls_emulation";
|
||||
constexpr char useMiddleProxyKey[] = "telemt_use_middle_proxy";
|
||||
constexpr char userNameKey[] = "telemt_user_name";
|
||||
// Stored for UI only (Telemt server ignores these; same controls as MTProxy page)
|
||||
constexpr char additionalSecretsKey[] = "telemt_additional_secrets";
|
||||
constexpr char workersKey[] = "telemt_workers";
|
||||
constexpr char workersModeKey[] = "telemt_workers_mode";
|
||||
constexpr char natEnabledKey[] = "telemt_nat_enabled";
|
||||
constexpr char natInternalIpKey[] = "telemt_nat_internal_ip";
|
||||
constexpr char natExternalIpKey[] = "telemt_nat_external_ip";
|
||||
|
||||
constexpr char transportModeStandard[] = "standard";
|
||||
constexpr char transportModeFakeTLS[] = "faketls";
|
||||
|
||||
constexpr char defaultPort[] = "443";
|
||||
constexpr char defaultTlsDomain[] = "googletagmanager.com";
|
||||
constexpr char defaultUserName[] = "amnezia";
|
||||
constexpr char defaultWorkers[] = "2";
|
||||
constexpr char workersModeAuto[] = "auto";
|
||||
constexpr char workersModeManual[] = "manual";
|
||||
constexpr int maxWorkers = 32;
|
||||
}
|
||||
|
||||
} // namespace protocols
|
||||
}
|
||||
|
||||
#endif // PROTOCOLCONSTANTS_H
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ namespace amnezia
|
||||
TorWebSite,
|
||||
Dns,
|
||||
Sftp,
|
||||
Socks5Proxy
|
||||
Socks5Proxy,
|
||||
MtProxy,
|
||||
Telemt,
|
||||
};
|
||||
Q_ENUM_NS(DockerContainer)
|
||||
} // namespace ContainerEnumNS
|
||||
|
||||
@@ -72,7 +72,10 @@ QMap<DockerContainer, QString> ContainerUtils::containerHumanNames()
|
||||
{ DockerContainer::TorWebSite, QObject::tr("Website in Tor network") },
|
||||
{ DockerContainer::Dns, QObject::tr("AmneziaDNS") },
|
||||
{ DockerContainer::Sftp, QObject::tr("SFTP file sharing service") },
|
||||
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") } };
|
||||
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") },
|
||||
{ DockerContainer::MtProxy, QObject::tr("MTProxy (Telegram)") },
|
||||
{ DockerContainer::Telemt, QObject::tr("Telemt (Telegram)") },
|
||||
};
|
||||
}
|
||||
|
||||
QMap<DockerContainer, QString> ContainerUtils::containerDescriptions()
|
||||
@@ -102,7 +105,12 @@ QMap<DockerContainer, QString> ContainerUtils::containerDescriptions()
|
||||
{ DockerContainer::Sftp,
|
||||
QObject::tr("Create a file vault on your server to securely store and transfer files.") },
|
||||
{ DockerContainer::Socks5Proxy,
|
||||
QObject::tr("") } };
|
||||
QObject::tr("") },
|
||||
{ DockerContainer::MtProxy,
|
||||
QObject::tr("Telegram MTProto proxy server") },
|
||||
{ DockerContainer::Telemt,
|
||||
QObject::tr("Telegram MTProto proxy (Telemt, Rust)") },
|
||||
};
|
||||
}
|
||||
|
||||
QMap<DockerContainer, QString> ContainerUtils::containerDetailedDescriptions()
|
||||
@@ -172,7 +180,15 @@ QMap<DockerContainer, QString> ContainerUtils::containerDetailedDescriptions()
|
||||
"You will be able to access it using\n FileZilla or other SFTP clients, "
|
||||
"as well as mount the disk on your device to access\n it directly from your device.\n\n"
|
||||
"For more detailed information, you can\n find it in the support section under \"Create SFTP file storage.\" ") },
|
||||
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") }
|
||||
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") },
|
||||
{ DockerContainer::MtProxy,
|
||||
QObject::tr("Telegram MTProto proxy server. "
|
||||
"Allows Telegram clients to connect through your server "
|
||||
"using the MTProto protocol. Supports FakeTLS mode for "
|
||||
"bypassing DPI-based blocking.") },
|
||||
{ DockerContainer::Telemt,
|
||||
QObject::tr("Telegram MTProto proxy powered by Telemt (Rust). "
|
||||
"Supports secure and TLS fronting modes with optional traffic masking.") },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -197,6 +213,8 @@ Proto ContainerUtils::defaultProtocol(DockerContainer c)
|
||||
case DockerContainer::Dns: return Proto::Dns;
|
||||
case DockerContainer::Sftp: return Proto::Sftp;
|
||||
case DockerContainer::Socks5Proxy: return Proto::Socks5Proxy;
|
||||
case DockerContainer::MtProxy: return Proto::MtProxy;
|
||||
case DockerContainer::Telemt: return Proto::Telemt;
|
||||
default: return Proto::Unknown;
|
||||
}
|
||||
}
|
||||
@@ -224,6 +242,8 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Xray: return true;
|
||||
case DockerContainer::SSXray: return true;
|
||||
case DockerContainer::MtProxy: return true;
|
||||
case DockerContainer::Telemt: return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -237,7 +257,8 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Xray: return true;
|
||||
case DockerContainer::SSXray: return true;
|
||||
return false;
|
||||
case DockerContainer::MtProxy: return true;
|
||||
case DockerContainer::Telemt: return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -256,6 +277,8 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Xray: return true;
|
||||
case DockerContainer::SSXray: return true;
|
||||
case DockerContainer::MtProxy: return true;
|
||||
case DockerContainer::Telemt: return true;
|
||||
default: return false;
|
||||
}
|
||||
|
||||
@@ -318,6 +341,8 @@ bool ContainerUtils::isShareable(DockerContainer container)
|
||||
case DockerContainer::Dns: return false;
|
||||
case DockerContainer::Sftp: return false;
|
||||
case DockerContainer::Socks5Proxy: return false;
|
||||
case DockerContainer::MtProxy: return false;
|
||||
case DockerContainer::Telemt: return false;
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
@@ -346,8 +371,10 @@ int ContainerUtils::installPageOrder(DockerContainer container)
|
||||
case DockerContainer::Xray: return 3;
|
||||
case DockerContainer::Ipsec: return 7;
|
||||
case DockerContainer::SSXray: return 8;
|
||||
case DockerContainer::MtProxy:
|
||||
case DockerContainer::Telemt:
|
||||
return 20;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,9 @@ namespace amnezia
|
||||
TorWebSite,
|
||||
Dns,
|
||||
Sftp,
|
||||
Socks5Proxy
|
||||
Socks5Proxy,
|
||||
MtProxy,
|
||||
Telemt,
|
||||
};
|
||||
Q_ENUM_NS(Proto)
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
@@ -20,6 +19,8 @@
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "core/models/protocols/sftpProtocolConfig.h"
|
||||
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
|
||||
using namespace amnezia;
|
||||
using namespace ProtocolUtils;
|
||||
@@ -38,6 +39,8 @@ QString amnezia::scriptFolder(amnezia::DockerContainer container)
|
||||
case DockerContainer::Dns: return QLatin1String("dns");
|
||||
case DockerContainer::Sftp: return QLatin1String("sftp");
|
||||
case DockerContainer::Socks5Proxy: return QLatin1String("socks5_proxy");
|
||||
case DockerContainer::MtProxy: return QLatin1String("mtproxy");
|
||||
case DockerContainer::Telemt: return QLatin1String("telemt");
|
||||
default: return QString();
|
||||
}
|
||||
}
|
||||
@@ -284,6 +287,86 @@ amnezia::ScriptVars amnezia::genSocks5ProxyVars(const ContainerConfig &container
|
||||
return vars;
|
||||
}
|
||||
|
||||
amnezia::ScriptVars amnezia::genMtProxyVars(const ContainerConfig &containerConfig) {
|
||||
ScriptVars vars;
|
||||
|
||||
if (auto *mtProxyProtocolConfig = containerConfig.getMtProxyProtocolConfig()) {
|
||||
const MtProxyProtocolConfig &c = *mtProxyProtocolConfig;
|
||||
|
||||
vars.append({{"$MTPROXY_PORT", c.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : c.port}});
|
||||
vars.append({{"$MTPROXY_SECRET", c.secret}});
|
||||
vars.append({{"$MTPROXY_TAG", c.tag}});
|
||||
vars.append({{"$MTPROXY_TRANSPORT_MODE",
|
||||
c.transportMode.isEmpty() ? QString(protocols::mtProxy::transportModeStandard)
|
||||
: c.transportMode}});
|
||||
|
||||
QString tlsDomain = c.tlsDomain;
|
||||
if (tlsDomain.isEmpty()) {
|
||||
tlsDomain = QString(protocols::mtProxy::defaultTlsDomain);
|
||||
}
|
||||
vars.append({{"$MTPROXY_TLS_DOMAIN", tlsDomain}});
|
||||
vars.append({{"$MTPROXY_PUBLIC_HOST", c.publicHost}});
|
||||
|
||||
QStringList additionalList;
|
||||
for (const QString &s: c.additionalSecrets) {
|
||||
if (!s.isEmpty()) {
|
||||
additionalList << s;
|
||||
}
|
||||
}
|
||||
vars.append({{"$MTPROXY_ADDITIONAL_SECRETS", additionalList.join(QLatin1Char(','))}});
|
||||
|
||||
const QString workersMode = c.workersMode.isEmpty() ? QString(protocols::mtProxy::workersModeAuto)
|
||||
: c.workersMode;
|
||||
QString workers;
|
||||
if (workersMode == QLatin1String(protocols::mtProxy::workersModeManual)) {
|
||||
workers = c.workers.isEmpty() ? QString(protocols::mtProxy::defaultWorkers) : c.workers;
|
||||
} else {
|
||||
const QString transportMode =
|
||||
c.transportMode.isEmpty() ? QString(protocols::mtProxy::transportModeStandard) : c.transportMode;
|
||||
workers = (transportMode == QLatin1String(protocols::mtProxy::transportModeFakeTLS)) ? QStringLiteral("0")
|
||||
: QStringLiteral("2");
|
||||
}
|
||||
vars.append({{"$MTPROXY_WORKERS", workers}});
|
||||
|
||||
vars.append({{"$MTPROXY_NAT_ENABLED", c.natEnabled ? QStringLiteral("1") : QStringLiteral("0")}});
|
||||
vars.append({{"$MTPROXY_NAT_INTERNAL_IP", c.natInternalIp}});
|
||||
vars.append({{"$MTPROXY_NAT_EXTERNAL_IP", c.natExternalIp}});
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
amnezia::ScriptVars amnezia::genTelemtVars(const ContainerConfig &containerConfig)
|
||||
{
|
||||
ScriptVars vars;
|
||||
|
||||
if (auto *telemtProtocolConfig = containerConfig.getTelemtProtocolConfig()) {
|
||||
const TelemtProtocolConfig &c = *telemtProtocolConfig;
|
||||
|
||||
const QString transport = c.transportMode.isEmpty() ? QString(protocols::telemt::transportModeStandard)
|
||||
: c.transportMode;
|
||||
const bool faketls = (transport == QLatin1String(protocols::telemt::transportModeFakeTLS));
|
||||
vars.append({ { "$TELEMT_TOML_SECURE", faketls ? QLatin1String("false") : QLatin1String("true") } });
|
||||
vars.append({ { "$TELEMT_TOML_TLS", faketls ? QLatin1String("true") : QLatin1String("false") } });
|
||||
vars.append({ { "$TELEMT_PORT", c.port.isEmpty() ? QString(protocols::telemt::defaultPort) : c.port } });
|
||||
vars.append({ { "$TELEMT_SECRET", c.secret } });
|
||||
vars.append({ { "$TELEMT_TAG", c.tag } });
|
||||
QString tlsDomain = c.tlsDomain;
|
||||
if (tlsDomain.isEmpty()) {
|
||||
tlsDomain = QString(protocols::telemt::defaultTlsDomain);
|
||||
}
|
||||
vars.append({ { "$TELEMT_TLS_DOMAIN", tlsDomain } });
|
||||
vars.append({ { "$TELEMT_PUBLIC_HOST", c.publicHost } });
|
||||
vars.append({ { "$TELEMT_USER_NAME",
|
||||
c.userName.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultUserName) : c.userName } });
|
||||
vars.append({ { "$TELEMT_USE_MIDDLE_PROXY", c.useMiddleProxy ? QLatin1String("true") : QLatin1String("false") } });
|
||||
vars.append({ { "$TELEMT_MASK", c.maskEnabled ? QLatin1String("true") : QLatin1String("false") } });
|
||||
vars.append({ { "$TELEMT_TLS_EMULATION", c.tlsEmulation ? QLatin1String("true") : QLatin1String("false") } });
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig)
|
||||
{
|
||||
ScriptVars vars;
|
||||
@@ -308,6 +391,12 @@ amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer contain
|
||||
case Proto::Socks5Proxy:
|
||||
vars.append(genSocks5ProxyVars(containerConfig));
|
||||
break;
|
||||
case Proto::MtProxy:
|
||||
vars.append(genMtProxyVars(containerConfig));
|
||||
break;
|
||||
case Proto::Telemt:
|
||||
vars.append(genTelemtVars(containerConfig));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ ScriptVars genWireGuardVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genAwgVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genSftpVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genSocks5ProxyVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genMtProxyVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genTelemtVars(const ContainerConfig &containerConfig);
|
||||
|
||||
ScriptVars genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace libssh {
|
||||
QEventLoop wait;
|
||||
connect(&watcher, &QFutureWatcher<ErrorCode>::finished, &wait, &QEventLoop::quit);
|
||||
watcher.setFuture(future);
|
||||
wait.exec();
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
int connectionResult = watcher.result();
|
||||
|
||||
@@ -189,7 +189,7 @@ namespace libssh {
|
||||
|
||||
QEventLoop wait;
|
||||
QObject::connect(this, &Client::writeToChannelFinished, &wait, &QEventLoop::quit);
|
||||
wait.exec();
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
return watcher.result();
|
||||
}
|
||||
@@ -284,7 +284,7 @@ namespace libssh {
|
||||
|
||||
QEventLoop wait;
|
||||
QObject::connect(this, &Client::scpFileCopyFinished, &wait, &QEventLoop::quit);
|
||||
wait.exec();
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
closeScpSession();
|
||||
return watcher.result();
|
||||
|
||||
@@ -103,8 +103,8 @@ ErrorCode SshSession::runContainerScript(const ServerCredentials &credentials, D
|
||||
if (e)
|
||||
return e;
|
||||
|
||||
QString runner =
|
||||
QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, (container == DockerContainer::Socks5Proxy ? "sh" : "bash"));
|
||||
const bool useSh = container == DockerContainer::Socks5Proxy || container == DockerContainer::MtProxy || container == DockerContainer::Telemt;
|
||||
QString runner = QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, useSh ? "sh" : "bash");
|
||||
e = runScript(credentials, replaceVars(runner, amnezia::genBaseVars(credentials, container, QString(), QString())), cbReadStdOut, cbReadStdErr);
|
||||
|
||||
QString remover = QString("sudo docker exec -i $CONTAINER_NAME rm %1 ").arg(fileName);
|
||||
|
||||
@@ -220,7 +220,7 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur
|
||||
m_rawConfig = configuration;
|
||||
m_serverAddress = configuration.value(configKey::hostName).toString().toNSString();
|
||||
|
||||
const QString serverDescription = configuration.value(config_key::description).toString().trimmed();
|
||||
const QString serverDescription = configuration.value(configKey::description).toString().trimmed();
|
||||
QString tunnelName;
|
||||
if (serverDescription.isEmpty()) {
|
||||
tunnelName = ProtocolUtils::protoToString(proto);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); silent_inst="-yq install"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\
|
||||
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); silent_inst="-yq install --install-recommends"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\
|
||||
elif which dnf > /dev/null 2>&1; then pm=$(which dnf); silent_inst="-yq install"; check_pkgs="-yq check-update"; docker_pkg="docker"; dist="fedora";\
|
||||
elif which yum > /dev/null 2>&1; then pm=$(which yum); silent_inst="-y -q install"; check_pkgs="-y -q check-update"; docker_pkg="docker"; dist="centos";\
|
||||
elif which zypper > /dev/null 2>&1; then pm=$(which zypper); silent_inst="-nq install"; check_pkgs="-nq refresh"; docker_pkg="docker"; dist="opensuse";\
|
||||
|
||||
9
client/server_scripts/mtproxy/Dockerfile
Normal file
9
client/server_scripts/mtproxy/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM amneziavpn/mtproxy:latest
|
||||
|
||||
RUN mkdir -p /opt/amnezia /data
|
||||
RUN printf '#!/bin/sh\ntail -f /dev/null\n' > /opt/amnezia/start.sh && \
|
||||
chmod a+x /opt/amnezia/start.sh
|
||||
|
||||
VOLUME /data
|
||||
ENTRYPOINT ["/bin/sh", "/opt/amnezia/start.sh"]
|
||||
CMD [""]
|
||||
60
client/server_scripts/mtproxy/configure_container.sh
Normal file
60
client/server_scripts/mtproxy/configure_container.sh
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Download Telegram config files
|
||||
curl -s https://core.telegram.org/getProxySecret -o /data/proxy-secret
|
||||
curl -s https://core.telegram.org/getProxyConfig -o /data/proxy-multi.conf
|
||||
|
||||
# Determine secret: env var -> saved file -> generate new
|
||||
if [ -n "$MTPROXY_SECRET" ]; then
|
||||
SECRET="$MTPROXY_SECRET"
|
||||
elif [ -f /data/secret ]; then
|
||||
SECRET=$(cat /data/secret)
|
||||
else
|
||||
SECRET=$(openssl rand -hex 16)
|
||||
fi
|
||||
|
||||
# Validate: must be exactly 32 hex chars
|
||||
echo "$SECRET" | grep -qE '^[0-9a-fA-F]{32}$' || SECRET=$(openssl rand -hex 16)
|
||||
|
||||
# Persist secret for start.sh restarts
|
||||
echo "$SECRET" > /data/secret
|
||||
|
||||
# Detect external IP
|
||||
IP=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null)
|
||||
[ -z "$IP" ] && IP=$(curl -s --max-time 5 https://ifconfig.me 2>/dev/null)
|
||||
[ -z "$IP" ] && IP=$(curl -s --max-time 5 https://icanhazip.com 2>/dev/null)
|
||||
|
||||
# Use custom public host/domain if provided, otherwise fall back to detected IP
|
||||
if [ -n "$MTPROXY_PUBLIC_HOST" ]; then
|
||||
LINK_HOST="$MTPROXY_PUBLIC_HOST"
|
||||
else
|
||||
LINK_HOST="$IP"
|
||||
fi
|
||||
|
||||
PORT=$MTPROXY_PORT
|
||||
|
||||
# Transport mode is substituted by replaceVars — plain variable, no curly braces
|
||||
TRANSPORT_MODE=$MTPROXY_TRANSPORT_MODE
|
||||
|
||||
PADDED_SECRET="dd${SECRET}"
|
||||
|
||||
if [ "$TRANSPORT_MODE" = "faketls" ] && [ -n "$MTPROXY_TLS_DOMAIN" ]; then
|
||||
DOMAIN_HEX=$(echo -n "$MTPROXY_TLS_DOMAIN" | od -A n -t x1 | tr -d ' \n')
|
||||
FAKETLS_SECRET="ee${SECRET}${DOMAIN_HEX}"
|
||||
else
|
||||
FAKETLS_SECRET=""
|
||||
fi
|
||||
|
||||
# Active link secret depends on transport mode
|
||||
if [ "$TRANSPORT_MODE" = "faketls" ] && [ -n "$FAKETLS_SECRET" ]; then
|
||||
LINK_SECRET="$FAKETLS_SECRET"
|
||||
else
|
||||
LINK_SECRET="$PADDED_SECRET"
|
||||
fi
|
||||
|
||||
# Output stable markers — parsed by updateContainerConfigAfterInstallation()
|
||||
echo "[*] MTProxy configuration"
|
||||
echo "[*] Secret: ${SECRET}"
|
||||
echo "[*] FakeTLS: ${FAKETLS_SECRET}"
|
||||
echo "[*] tg:// link: tg://proxy?server=${LINK_HOST}&port=${PORT}&secret=${LINK_SECRET}"
|
||||
echo "[*] t.me link: https://t.me/proxy?server=${LINK_HOST}&port=${PORT}&secret=${LINK_SECRET}"
|
||||
9
client/server_scripts/mtproxy/run_container.sh
Normal file
9
client/server_scripts/mtproxy/run_container.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
# Run container
|
||||
sudo docker run -d \
|
||||
--log-driver none \
|
||||
--restart always \
|
||||
-p $MTPROXY_PORT:$MTPROXY_PORT/tcp \
|
||||
-v amnezia-mtproxy-data:/data \
|
||||
--name $CONTAINER_NAME \
|
||||
$CONTAINER_NAME
|
||||
|
||||
71
client/server_scripts/mtproxy/start.sh
Normal file
71
client/server_scripts/mtproxy/start.sh
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Container startup"
|
||||
|
||||
# Read persisted secret
|
||||
SECRET=""
|
||||
if [ -f /data/secret ]; then
|
||||
SECRET=$(cat /data/secret)
|
||||
fi
|
||||
|
||||
if [ -z "$SECRET" ]; then
|
||||
echo "ERROR: /data/secret not found — run configure_container first"
|
||||
tail -f /dev/null
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build tag argument
|
||||
TAG_ARG=""
|
||||
if [ -n "$MTPROXY_TAG" ]; then
|
||||
TAG_ARG="-P $MTPROXY_TAG"
|
||||
fi
|
||||
|
||||
# Build domain argument for FakeTLS mode
|
||||
DOMAIN_ARG=""
|
||||
if [ "$MTPROXY_TRANSPORT_MODE" = "faketls" ] && [ -n "$MTPROXY_TLS_DOMAIN" ]; then
|
||||
DOMAIN_ARG="--domain $MTPROXY_TLS_DOMAIN"
|
||||
fi
|
||||
|
||||
WORKERS=$MTPROXY_WORKERS
|
||||
STATS_PORT=2398
|
||||
LISTEN_PORT=$MTPROXY_PORT
|
||||
|
||||
NAT_FLAG=""
|
||||
NAT_VALUE=""
|
||||
if [ "$MTPROXY_NAT_ENABLED" = "1" ] && [ -n "$MTPROXY_NAT_INTERNAL_IP" ] && [ -n "$MTPROXY_NAT_EXTERNAL_IP" ]; then
|
||||
NAT_FLAG="--nat-info"
|
||||
NAT_VALUE="$MTPROXY_NAT_INTERNAL_IP:$MTPROXY_NAT_EXTERNAL_IP"
|
||||
else
|
||||
INTERNAL_IP=$(hostname -i 2>/dev/null | awk '{print $1}')
|
||||
EXTERNAL_IP=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null)
|
||||
[ -z "$EXTERNAL_IP" ] && EXTERNAL_IP=$(curl -s --max-time 5 https://ifconfig.me 2>/dev/null)
|
||||
|
||||
if [ -n "$INTERNAL_IP" ] && [ -n "$EXTERNAL_IP" ] && [ "$INTERNAL_IP" != "$EXTERNAL_IP" ]; then
|
||||
NAT_FLAG="--nat-info"
|
||||
NAT_VALUE="${INTERNAL_IP}:${EXTERNAL_IP}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build additional secrets arguments
|
||||
ADDITIONAL_SECRETS_ARG=""
|
||||
if [ -n "$MTPROXY_ADDITIONAL_SECRETS" ]; then
|
||||
for S in $(echo "$MTPROXY_ADDITIONAL_SECRETS" | tr ',' ' '); do
|
||||
ADDITIONAL_SECRETS_ARG="$ADDITIONAL_SECRETS_ARG -S $S"
|
||||
done
|
||||
fi
|
||||
|
||||
# Start proxy (foreground)
|
||||
exec mtproto-proxy \
|
||||
-u root \
|
||||
-p ${STATS_PORT} \
|
||||
-H ${LISTEN_PORT} \
|
||||
-S ${SECRET} \
|
||||
${ADDITIONAL_SECRETS_ARG} \
|
||||
--aes-pwd /data/proxy-secret \
|
||||
-M ${WORKERS} \
|
||||
-C 60000 \
|
||||
--allow-skip-dh \
|
||||
${NAT_FLAG:+${NAT_FLAG} ${NAT_VALUE}} \
|
||||
${TAG_ARG} \
|
||||
${DOMAIN_ARG} \
|
||||
/data/proxy-multi.conf
|
||||
@@ -24,6 +24,14 @@
|
||||
<file>ipsec/run_container.sh</file>
|
||||
<file>ipsec/start.sh</file>
|
||||
<file>ipsec/strongswan.profile</file>
|
||||
<file>mtproxy/configure_container.sh</file>
|
||||
<file>mtproxy/Dockerfile</file>
|
||||
<file>mtproxy/run_container.sh</file>
|
||||
<file>mtproxy/start.sh</file>
|
||||
<file>telemt/configure_container.sh</file>
|
||||
<file>telemt/Dockerfile</file>
|
||||
<file>telemt/run_container.sh</file>
|
||||
<file>telemt/start.sh</file>
|
||||
<file>openvpn/configure_container.sh</file>
|
||||
<file>openvpn/Dockerfile</file>
|
||||
<file>openvpn/run_container.sh</file>
|
||||
@@ -55,4 +63,3 @@
|
||||
<file>xray/template.json</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
|
||||
42
client/server_scripts/telemt/Dockerfile
Normal file
42
client/server_scripts/telemt/Dockerfile
Normal file
@@ -0,0 +1,42 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Debian-based image with Telemt binary (shell + jq for Amnezia configure scripts).
|
||||
# Binary from https://github.com/telemt/telemt releases (same pattern as upstream Dockerfile minimal stage).
|
||||
|
||||
FROM debian:12-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
binutils \
|
||||
ca-certificates \
|
||||
curl \
|
||||
jq \
|
||||
openssl \
|
||||
tar \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Use machine arch (works with classic `docker build`; TARGETARCH is only set with BuildKit).
|
||||
RUN set -eux; \
|
||||
ARCH="$(uname -m)"; \
|
||||
case "$ARCH" in \
|
||||
x86_64) ASSET="telemt-x86_64-linux-musl.tar.gz" ;; \
|
||||
aarch64|arm64) ASSET="telemt-aarch64-linux-musl.tar.gz" ;; \
|
||||
*) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
curl -fL --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 120 \
|
||||
-o "/tmp/${ASSET}" "https://github.com/telemt/telemt/releases/latest/download/${ASSET}"; \
|
||||
curl -fL --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 120 \
|
||||
-o "/tmp/${ASSET}.sha256" "https://github.com/telemt/telemt/releases/latest/download/${ASSET}.sha256"; \
|
||||
cd /tmp && sha256sum -c "${ASSET}.sha256"; \
|
||||
tar -xzf "${ASSET}" -C /tmp; \
|
||||
test -f /tmp/telemt; \
|
||||
install -m 0755 /tmp/telemt /usr/local/bin/telemt; \
|
||||
strip --strip-unneeded /usr/local/bin/telemt || true; \
|
||||
rm -f "/tmp/${ASSET}" "/tmp/${ASSET}.sha256" /tmp/telemt
|
||||
|
||||
RUN mkdir -p /opt/amnezia /data
|
||||
RUN printf '#!/bin/sh\ntail -f /dev/null\n' > /opt/amnezia/start.sh && \
|
||||
chmod a+x /opt/amnezia/start.sh
|
||||
|
||||
VOLUME /data
|
||||
ENTRYPOINT ["/bin/sh", "/opt/amnezia/start.sh"]
|
||||
CMD [""]
|
||||
73
client/server_scripts/telemt/configure_container.sh
Normal file
73
client/server_scripts/telemt/configure_container.sh
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/bin/sh
|
||||
# Do not use set -e: Telemt / curl / kill edge cases should not abort the whole configure step.
|
||||
|
||||
echo "[*] Amnezia Telemt: configure script start"
|
||||
mkdir -p /data/tlsfront
|
||||
|
||||
# Secret: substituted $TELEMT_SECRET -> saved file -> openssl (same rules as MTProxy configure)
|
||||
if [ -n "$TELEMT_SECRET" ]; then
|
||||
SECRET="$TELEMT_SECRET"
|
||||
elif [ -f /data/secret ]; then
|
||||
SECRET=$(cat /data/secret)
|
||||
else
|
||||
SECRET=$(openssl rand -hex 16)
|
||||
fi
|
||||
# Must be exactly 32 hex chars
|
||||
echo "$SECRET" | grep -qE '^[0-9a-fA-F]{32}$' || SECRET=$(openssl rand -hex 16)
|
||||
|
||||
# Build config.toml (other variables substituted on the host by Amnezia before upload)
|
||||
rm -f /data/config.toml
|
||||
|
||||
{
|
||||
echo "### Amnezia Telemt — generated"
|
||||
echo "[general]"
|
||||
echo "use_middle_proxy = $TELEMT_USE_MIDDLE_PROXY"
|
||||
echo "log_level = \"normal\""
|
||||
if [ -n "$TELEMT_TAG" ]; then
|
||||
echo "ad_tag = \"$TELEMT_TAG\""
|
||||
fi
|
||||
echo ""
|
||||
echo "[general.modes]"
|
||||
echo "classic = false"
|
||||
echo "secure = $TELEMT_TOML_SECURE"
|
||||
echo "tls = $TELEMT_TOML_TLS"
|
||||
echo ""
|
||||
echo "[general.links]"
|
||||
echo "show = \"*\""
|
||||
if [ -n "$TELEMT_PUBLIC_HOST" ]; then
|
||||
echo "public_host = \"$TELEMT_PUBLIC_HOST\""
|
||||
fi
|
||||
echo "public_port = $TELEMT_PORT"
|
||||
echo ""
|
||||
echo "[server]"
|
||||
echo "port = $TELEMT_PORT"
|
||||
echo ""
|
||||
echo "[server.api]"
|
||||
echo "enabled = true"
|
||||
echo "listen = \"0.0.0.0:9091\""
|
||||
# Match upstream Telemt default: localhost API only (curl in this script uses 127.0.0.1).
|
||||
echo "whitelist = [\"127.0.0.0/8\"]"
|
||||
echo ""
|
||||
echo "[[server.listeners]]"
|
||||
echo "ip = \"0.0.0.0\""
|
||||
echo ""
|
||||
echo "[censorship]"
|
||||
echo "tls_domain = \"$TELEMT_TLS_DOMAIN\""
|
||||
echo "mask = $TELEMT_MASK"
|
||||
echo "tls_emulation = $TELEMT_TLS_EMULATION"
|
||||
echo "tls_front_dir = \"/data/tlsfront\""
|
||||
echo ""
|
||||
echo "[access.users]"
|
||||
echo "$TELEMT_USER_NAME = \"$SECRET\""
|
||||
} > /data/config.toml
|
||||
|
||||
echo "$SECRET" > /data/secret
|
||||
chmod 600 /data/secret 2>/dev/null || true
|
||||
|
||||
# Do not start telemt here: a long-lived process + curl loop inside `docker exec` can confuse SSH/Docker
|
||||
# timing and is unnecessary — start.sh runs telemt after configure. Links can be empty until the service
|
||||
# is up; the client still parses Secret below.
|
||||
echo "[*] Telemt configuration"
|
||||
echo "[*] Secret: $SECRET"
|
||||
echo "[*] tg:// link: "
|
||||
echo "[*] t.me link: "
|
||||
9
client/server_scripts/telemt/run_container.sh
Normal file
9
client/server_scripts/telemt/run_container.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
# Run container (ulimit per Telemt docs — avoids "Too many open files" under load)
|
||||
sudo docker run -d \
|
||||
--log-driver none \
|
||||
--restart always \
|
||||
--ulimit nofile=65536:65536 \
|
||||
-p $TELEMT_PORT:$TELEMT_PORT/tcp \
|
||||
-v amnezia-telemt-data:/data \
|
||||
--name $CONTAINER_NAME \
|
||||
$CONTAINER_NAME
|
||||
12
client/server_scripts/telemt/start.sh
Normal file
12
client/server_scripts/telemt/start.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Container startup (Telemt)"
|
||||
|
||||
if [ ! -f /data/config.toml ]; then
|
||||
echo "ERROR: /data/config.toml not found — run configure_container first"
|
||||
tail -f /dev/null
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p /data/tlsfront
|
||||
exec /usr/local/bin/telemt /data/config.toml
|
||||
@@ -1312,6 +1312,21 @@ Thank you for staying with us!</source>
|
||||
</context>
|
||||
<context>
|
||||
<name>PageProtocolXraySettings</name>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageProtocolXraySettings.qml" line="61"/>
|
||||
<source>XRay VLESS settings</source>
|
||||
<translation>Настройки XRay VLESS</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageProtocolXraySettings.qml" line="80"/>
|
||||
<source>More about settings</source>
|
||||
<translation>Подробнее о настройках</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageProtocolXraySettings.qml" line="188"/>
|
||||
<source>Reset settings</source>
|
||||
<translation>Сбросить настройки</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageProtocolXraySettings.qml" line="57"/>
|
||||
<source>XRay settings</source>
|
||||
|
||||
@@ -201,3 +201,12 @@ bool ImportUiController::decodeQrCode(const QString &code)
|
||||
return mInstance->parseQrCodeChunk(code);
|
||||
}
|
||||
#endif
|
||||
|
||||
QString ImportUiController::readTextFile(const QString &fileName)
|
||||
{
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return {};
|
||||
}
|
||||
return QString::fromUtf8(file.readAll());
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public slots:
|
||||
QString getMaliciousWarningText();
|
||||
bool isNativeWireGuardConfig();
|
||||
void processNativeWireGuardConfig();
|
||||
QString readTextFile(const QString &fileName);
|
||||
|
||||
#if defined Q_OS_ANDROID || defined Q_OS_IOS
|
||||
void startDecodingQr();
|
||||
|
||||
46
client/ui/controllers/networkReachabilityController.cpp
Normal file
46
client/ui/controllers/networkReachabilityController.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#include "networkReachabilityController.h"
|
||||
|
||||
#include <QNetworkInformation>
|
||||
|
||||
namespace {
|
||||
|
||||
bool reachabilityAllowsRemoteOperations(QNetworkInformation::Reachability r) {
|
||||
using R = QNetworkInformation::Reachability;
|
||||
// Unknown: no backend or not yet determined — do not block UI.
|
||||
return r == R::Online || r == R::Unknown;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NetworkReachabilityController::NetworkReachabilityController(QObject *parent) : QObject(parent) {
|
||||
attachToNetworkInformation();
|
||||
}
|
||||
|
||||
bool NetworkReachabilityController::hasInternetAccess() const {
|
||||
return m_hasInternetAccess;
|
||||
}
|
||||
|
||||
void NetworkReachabilityController::attachToNetworkInformation() {
|
||||
if (!QNetworkInformation::loadDefaultBackend()) {
|
||||
return;
|
||||
}
|
||||
QNetworkInformation *ni = QNetworkInformation::instance();
|
||||
if (!ni) {
|
||||
return;
|
||||
}
|
||||
const bool initial = reachabilityAllowsRemoteOperations(ni->reachability());
|
||||
const bool previous = m_hasInternetAccess;
|
||||
m_hasInternetAccess = initial;
|
||||
if (previous != m_hasInternetAccess) {
|
||||
emit hasInternetAccessChanged();
|
||||
}
|
||||
connect(ni, &QNetworkInformation::reachabilityChanged, this,
|
||||
[this](QNetworkInformation::Reachability r) {
|
||||
const bool ok = reachabilityAllowsRemoteOperations(r);
|
||||
if (ok == m_hasInternetAccess) {
|
||||
return;
|
||||
}
|
||||
m_hasInternetAccess = ok;
|
||||
emit hasInternetAccessChanged();
|
||||
});
|
||||
}
|
||||
30
client/ui/controllers/networkReachabilityController.h
Normal file
30
client/ui/controllers/networkReachabilityController.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef NETWORKREACHABILITYCONTROLLER_H
|
||||
#define NETWORKREACHABILITYCONTROLLER_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
// Exposes QNetworkInformation to QML for UI that must not run remote operations offline.
|
||||
// Note: mozilla/networkwatcher.h has NetworkWatcher::getReachability() using the same API,
|
||||
// but networkwatcher.cpp is not linked into the desktop client (only the service process).
|
||||
|
||||
class NetworkReachabilityController final : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(bool hasInternetAccess READ hasInternetAccess NOTIFY hasInternetAccessChanged)
|
||||
|
||||
public:
|
||||
explicit NetworkReachabilityController(QObject *parent = nullptr);
|
||||
|
||||
bool hasInternetAccess() const;
|
||||
|
||||
signals:
|
||||
|
||||
void hasInternetAccessChanged();
|
||||
|
||||
private:
|
||||
void attachToNetworkInformation();
|
||||
|
||||
bool m_hasInternetAccess = true;
|
||||
};
|
||||
|
||||
#endif // NETWORKREACHABILITYCONTROLLER_H
|
||||
@@ -50,6 +50,8 @@ namespace PageLoader
|
||||
PageServiceTorWebsiteSettings,
|
||||
PageServiceDnsSettings,
|
||||
PageServiceSocksProxySettings,
|
||||
PageServiceMtProxySettings,
|
||||
PageServiceTelemtSettings,
|
||||
|
||||
PageSetupWizardStart,
|
||||
PageSetupWizardCredentials,
|
||||
@@ -80,7 +82,15 @@ namespace PageLoader
|
||||
PageSetupWizardApiPremiumInfo,
|
||||
PageSetupWizardApiTrialEmail,
|
||||
|
||||
PageDevMenu
|
||||
PageDevMenu,
|
||||
|
||||
PageProtocolXraySnapshots,
|
||||
PageProtocolXrayTransportSettings,
|
||||
PageProtocolXrayXmuxSettings,
|
||||
PageProtocolXrayXPaddingSettings,
|
||||
PageProtocolXrayFlowSettings,
|
||||
PageProtocolXraySecuritySettings,
|
||||
PageProtocolXrayXPaddingBytesSettings,
|
||||
};
|
||||
Q_ENUM_NS(PageEnum)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <QDebug>
|
||||
|
||||
#include "../systemController.h"
|
||||
#include "core/utils/qrCodeUtils.h"
|
||||
|
||||
ExportUiController::ExportUiController(ExportController* exportController, QObject *parent)
|
||||
: QObject(parent),
|
||||
@@ -53,6 +54,14 @@ void ExportUiController::generateXrayConfig(const QString &serverId, const QStri
|
||||
applyExportResult(result);
|
||||
}
|
||||
|
||||
void ExportUiController::generateQrFromString(const QString &text)
|
||||
{
|
||||
clearPreviousConfig();
|
||||
m_config = text;
|
||||
m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(text.toUtf8());
|
||||
emit exportConfigChanged();
|
||||
}
|
||||
|
||||
QString ExportUiController::getConfig()
|
||||
{
|
||||
return m_config;
|
||||
@@ -118,3 +127,13 @@ void ExportUiController::applyExportResult(const ExportController::ExportResult
|
||||
|
||||
emit exportConfigChanged();
|
||||
}
|
||||
|
||||
void ExportUiController::setConfigFromString(const QString &config, const QString &fileName)
|
||||
{
|
||||
clearPreviousConfig();
|
||||
m_config = config;
|
||||
emit exportConfigChanged();
|
||||
if (!fileName.isEmpty()) {
|
||||
SystemController::saveFile(fileName, m_config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,14 @@ public slots:
|
||||
void generateWireGuardConfig(const QString &serverId, const QString &clientName);
|
||||
void generateAwgConfig(const QString &serverId, int containerIndex, const QString &clientName);
|
||||
void generateXrayConfig(const QString &serverId, const QString &clientName);
|
||||
void generateQrFromString(const QString &text);
|
||||
|
||||
QString getConfig();
|
||||
QString getNativeConfigString();
|
||||
QList<QString> getQrCodes();
|
||||
|
||||
void exportConfig(const QString &fileName);
|
||||
void setConfigFromString(const QString &config, const QString &fileName);
|
||||
|
||||
void updateClientManagementModel(const QString &serverId, int containerIndex);
|
||||
|
||||
|
||||
136
client/ui/controllers/selfhosted/installUiController.cpp
Executable file → Normal file
136
client/ui/controllers/selfhosted/installUiController.cpp
Executable file → Normal file
@@ -5,11 +5,13 @@
|
||||
#include <QEventLoop>
|
||||
#include <QJsonObject>
|
||||
#include <QRandomGenerator>
|
||||
#include <QRegularExpression>
|
||||
#include <QStandardPaths>
|
||||
#include <QFutureWatcher>
|
||||
#include <QtConcurrent>
|
||||
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/controllers/selfhosted/installController.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
@@ -47,6 +49,8 @@ InstallUiController::InstallUiController(InstallController *installController,
|
||||
#endif
|
||||
SftpConfigModel *sftpConfigModel,
|
||||
Socks5ProxyConfigModel *socks5ConfigModel,
|
||||
MtProxyConfigModel* mtConfigModel,
|
||||
TelemtConfigModel *telemtConfigModel,
|
||||
QObject *parent)
|
||||
: QObject(parent),
|
||||
m_installController(installController),
|
||||
@@ -63,7 +67,9 @@ InstallUiController::InstallUiController(InstallController *installController,
|
||||
m_ikev2ConfigModel(ikev2ConfigModel),
|
||||
#endif
|
||||
m_sftpConfigModel(sftpConfigModel),
|
||||
m_socks5ConfigModel(socks5ConfigModel)
|
||||
m_socks5ConfigModel(socks5ConfigModel),
|
||||
m_mtProxyConfigModel(mtConfigModel),
|
||||
m_telemtConfigModel(telemtConfigModel)
|
||||
{
|
||||
connect(m_installController, &InstallController::configValidated, this, &InstallUiController::configValidated);
|
||||
connect(m_installController, &InstallController::validationErrorOccurred, this, [this](ErrorCode errorCode) {
|
||||
@@ -199,7 +205,7 @@ void InstallUiController::scanServerForInstalledContainers(const QString &server
|
||||
emit installationErrorOccurred(errorCode);
|
||||
}
|
||||
|
||||
void InstallUiController::updateContainer(const QString &serverId, int containerIndex, int protocolIndex)
|
||||
void InstallUiController::updateContainer(const QString &serverId, int containerIndex, int protocolIndex, bool closePage)
|
||||
{
|
||||
DockerContainer container = static_cast<DockerContainer>(containerIndex);
|
||||
|
||||
@@ -238,6 +244,14 @@ void InstallUiController::updateContainer(const QString &serverId, int container
|
||||
containerConfig.protocolConfig = m_socks5ConfigModel->getProtocolConfig();
|
||||
break;
|
||||
}
|
||||
case Proto::MtProxy: {
|
||||
containerConfig.protocolConfig = m_mtProxyConfigModel->getProtocolConfig();
|
||||
break;
|
||||
}
|
||||
case Proto::Telemt: {
|
||||
containerConfig.protocolConfig = m_telemtConfigModel->getProtocolConfig();
|
||||
break;
|
||||
}
|
||||
#ifdef Q_OS_WINDOWS
|
||||
case Proto::Ikev2: {
|
||||
containerConfig.protocolConfig = m_ikev2ConfigModel->getProtocolConfig();
|
||||
@@ -249,19 +263,128 @@ void InstallUiController::updateContainer(const QString &serverId, int container
|
||||
}
|
||||
ContainerConfig oldContainerConfig = m_serversController->getContainerConfig(serverId, container);
|
||||
|
||||
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
|
||||
emit serverIsBusy(true);
|
||||
auto *watcher = new QFutureWatcher<ErrorCode>(this);
|
||||
QObject::connect(watcher, &QFutureWatcher<ErrorCode>::finished, this,
|
||||
[this, watcher, serverId, container, closePage]() {
|
||||
const ErrorCode errorCode = watcher->result();
|
||||
watcher->deleteLater();
|
||||
emit serverIsBusy(false);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
const ContainerConfig updatedConfig =
|
||||
m_serversController->getContainerConfig(serverId, container);
|
||||
m_protocolModel->updateModel(updatedConfig);
|
||||
|
||||
const auto defaultContainer =
|
||||
m_serversController->getDefaultContainer(serverId);
|
||||
if ((serverId == m_serversController->getDefaultServerId())
|
||||
&& (container == defaultContainer)) {
|
||||
emit currentContainerUpdated();
|
||||
} else {
|
||||
emit updateContainerFinished(tr("Settings updated successfully"), closePage);
|
||||
}
|
||||
} else {
|
||||
emit installationErrorOccurred(errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
ContainerConfig newConfigCopy = containerConfig;
|
||||
ContainerConfig oldConfigCopy = oldContainerConfig;
|
||||
InstallController *installController = m_installController;
|
||||
QFuture<ErrorCode> future =
|
||||
QtConcurrent::run([installController, serverId, container, oldConfigCopy,
|
||||
newConfigCopy]() mutable -> ErrorCode {
|
||||
return installController->updateContainer(serverId, container, oldConfigCopy, newConfigCopy);
|
||||
});
|
||||
watcher->setFuture(future);
|
||||
return;
|
||||
}
|
||||
|
||||
ErrorCode errorCode = m_installController->updateContainer(serverId, container, oldContainerConfig, containerConfig);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
ContainerConfig updatedConfig = m_serversController->getContainerConfig(serverId, container);
|
||||
m_protocolModel->updateModel(updatedConfig);
|
||||
|
||||
emit updateContainerFinished(tr("Settings updated successfully"));
|
||||
const auto defaultContainer = m_serversController->getDefaultContainer(serverId);
|
||||
if ((serverId == m_serversController->getDefaultServerId()) && (container == defaultContainer)) {
|
||||
emit currentContainerUpdated();
|
||||
} else {
|
||||
emit updateContainerFinished(tr("Settings updated successfully"), closePage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
emit installationErrorOccurred(errorCode);
|
||||
}
|
||||
|
||||
void InstallUiController::setContainerEnabled(const QString &serverId, int containerIndex, bool enabled)
|
||||
{
|
||||
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit serverIsBusy(true);
|
||||
const ErrorCode errorCode = m_installController->setDockerContainerEnabledState(serverId, container, enabled);
|
||||
emit serverIsBusy(false);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
const ContainerConfig currentConfig = m_serversController->getContainerConfig(serverId, container);
|
||||
m_protocolModel->updateModel(currentConfig);
|
||||
emit setContainerEnabledFinished(enabled);
|
||||
return;
|
||||
}
|
||||
|
||||
emit installationErrorOccurred(errorCode);
|
||||
}
|
||||
|
||||
void InstallUiController::refreshContainerStatus(const QString &serverId, int containerIndex)
|
||||
{
|
||||
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return;
|
||||
}
|
||||
|
||||
int status = 3;
|
||||
const ErrorCode errorCode = m_installController->queryDockerContainerStatus(serverId, container, status);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
emit containerStatusRefreshed(3);
|
||||
return;
|
||||
}
|
||||
emit containerStatusRefreshed(status);
|
||||
}
|
||||
|
||||
void InstallUiController::refreshContainerDiagnostics(const QString &serverId, int containerIndex, int port)
|
||||
{
|
||||
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return;
|
||||
}
|
||||
|
||||
MtProxyContainerDiagnostics diag;
|
||||
const ErrorCode errorCode = m_installController->queryMtProxyDiagnostics(serverId, container, port, diag);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
emit containerDiagnosticsRefreshed(false, false, -1, QString(), QString());
|
||||
return;
|
||||
}
|
||||
emit containerDiagnosticsRefreshed(diag.portReachable, diag.upstreamReachable, diag.clientsConnected,
|
||||
diag.lastConfigRefresh, diag.statsEndpoint);
|
||||
}
|
||||
|
||||
void InstallUiController::fetchContainerSecret(const QString &serverId, int containerIndex)
|
||||
{
|
||||
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString secret = m_installController->fetchDockerContainerSecret(serverId, container);
|
||||
emit containerSecretFetched(secret);
|
||||
}
|
||||
|
||||
void InstallUiController::rebootServer(const QString &serverId)
|
||||
{
|
||||
const QString serverName = m_serversController->notificationDisplayName(serverId);
|
||||
@@ -469,10 +592,13 @@ void InstallUiController::updateProtocolConfigModel(const QString &serverId, int
|
||||
case Proto::Awg: updateIfPresent(m_awgConfigModel, containerConfig.getAwgProtocolConfig()); break;
|
||||
case Proto::WireGuard: updateIfPresent(m_wireGuardConfigModel, containerConfig.getWireGuardProtocolConfig()); break;
|
||||
case Proto::OpenVpn: updateIfPresent(m_openVpnConfigModel, containerConfig.getOpenVpnProtocolConfig()); break;
|
||||
case Proto::Xray: updateIfPresent(m_xrayConfigModel, containerConfig.getXrayProtocolConfig()); break;
|
||||
case Proto::Xray:
|
||||
case Proto::SSXray: updateIfPresent(m_xrayConfigModel, containerConfig.getXrayProtocolConfig()); break;
|
||||
case Proto::TorWebSite: updateIfPresent(m_torConfigModel, containerConfig.getTorProtocolConfig()); break;
|
||||
case Proto::Sftp: updateIfPresent(m_sftpConfigModel, containerConfig.getSftpProtocolConfig()); break;
|
||||
case Proto::Socks5Proxy: updateIfPresent(m_socks5ConfigModel, containerConfig.getSocks5ProxyProtocolConfig()); break;
|
||||
case Proto::MtProxy: updateIfPresent(m_mtProxyConfigModel, containerConfig.getMtProxyProtocolConfig()); break;
|
||||
case Proto::Telemt: updateIfPresent(m_telemtConfigModel, containerConfig.getTelemtProtocolConfig()); break;
|
||||
#ifdef Q_OS_WINDOWS
|
||||
case Proto::Ikev2: updateIfPresent(m_ikev2ConfigModel, containerConfig.getIkev2ProtocolConfig()); break;
|
||||
#endif
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
#include "ui/models/services/torConfigModel.h"
|
||||
#include "core/models/protocols/sftpProtocolConfig.h"
|
||||
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
|
||||
#include "ui/models/services/mtProxyConfigModel.h"
|
||||
#include "ui/models/services/telemtConfigModel.h"
|
||||
|
||||
class InstallUiController : public QObject
|
||||
{
|
||||
@@ -48,6 +50,8 @@ public:
|
||||
#endif
|
||||
SftpConfigModel* sftpConfigModel,
|
||||
Socks5ProxyConfigModel* socks5ConfigModel,
|
||||
MtProxyConfigModel* mtConfigModel,
|
||||
TelemtConfigModel* telemtConfigModel,
|
||||
QObject *parent = nullptr);
|
||||
~InstallUiController();
|
||||
|
||||
@@ -58,12 +62,16 @@ public slots:
|
||||
|
||||
void scanServerForInstalledContainers(const QString &serverId);
|
||||
|
||||
void updateContainer(const QString &serverId, int containerIndex, int protocolIndex);
|
||||
void updateContainer(const QString &serverId, int containerIndex, int protocolIndex, bool closePage = true);
|
||||
|
||||
void removeServer(const QString &serverId);
|
||||
void rebootServer(const QString &serverId);
|
||||
void removeAllContainers(const QString &serverId);
|
||||
void removeContainer(const QString &serverId, int containerIndex);
|
||||
void setContainerEnabled(const QString &serverId, int containerIndex, bool enabled);
|
||||
void refreshContainerStatus(const QString &serverId, int containerIndex);
|
||||
void refreshContainerDiagnostics(const QString &serverId, int containerIndex, int port);
|
||||
void fetchContainerSecret(const QString &serverId, int containerIndex);
|
||||
|
||||
void clearCachedProfile(const QString &serverId, int containerIndex);
|
||||
|
||||
@@ -94,7 +102,7 @@ signals:
|
||||
void installContainerFinished(const QString &finishMessage, bool isServiceInstall);
|
||||
void installServerFinished(const QString &finishMessage);
|
||||
|
||||
void updateContainerFinished(const QString &message);
|
||||
void updateContainerFinished(const QString &message, bool closePage);
|
||||
|
||||
void scanServerFinished(bool isInstalledContainerFound);
|
||||
|
||||
@@ -102,6 +110,11 @@ signals:
|
||||
void removeServerFinished(const QString &finishedMessage);
|
||||
void removeAllContainersFinished(const QString &finishedMessage);
|
||||
void removeContainerFinished(const QString &finishedMessage);
|
||||
void setContainerEnabledFinished(bool enabled);
|
||||
void containerStatusRefreshed(int status);
|
||||
void containerDiagnosticsRefreshed(bool portReachable, bool upstreamReachable, int clientsConnected,
|
||||
const QString &lastConfigRefresh, const QString &statsEndpoint);
|
||||
void containerSecretFetched(const QString &secret);
|
||||
|
||||
void installationErrorOccurred(ErrorCode errorCode);
|
||||
void wrongInstallationUser(const QString &message);
|
||||
@@ -114,6 +127,8 @@ signals:
|
||||
void serverIsBusy(const bool isBusy);
|
||||
void cancelInstallation();
|
||||
|
||||
void currentContainerUpdated();
|
||||
|
||||
void cachedProfileCleared(const QString &message);
|
||||
void apiConfigRemoved(const QString &message);
|
||||
|
||||
@@ -138,6 +153,8 @@ private:
|
||||
#endif
|
||||
SftpConfigModel* m_sftpConfigModel;
|
||||
Socks5ProxyConfigModel* m_socks5ConfigModel;
|
||||
MtProxyConfigModel* m_mtProxyConfigModel;
|
||||
TelemtConfigModel* m_telemtConfigModel;
|
||||
|
||||
ServerCredentials m_processedServerCredentials;
|
||||
|
||||
|
||||
@@ -510,6 +510,10 @@ QStringList ServersUiController::getAllInstalledServicesName(int serverIndex) co
|
||||
servicesName.append("TOR");
|
||||
} else if (container == DockerContainer::Socks5Proxy) {
|
||||
servicesName.append("SOCKS5");
|
||||
} else if (container == DockerContainer::MtProxy) {
|
||||
servicesName.append("MTProxy");
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
servicesName.append("Telemt");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@ QVariant ContainersModel::data(const QModelIndex &index, int role) const
|
||||
case IsSftpRole: return container == DockerContainer::Sftp;
|
||||
case IsTorWebsiteRole: return container == DockerContainer::TorWebSite;
|
||||
case IsSocks5ProxyRole: return container == DockerContainer::Socks5Proxy;
|
||||
case IsMtProxyRole: return container == DockerContainer::MtProxy;
|
||||
case IsTelemtRole: return container == DockerContainer::Telemt;
|
||||
case InstallPageOrderRole: return ContainerUtils::installPageOrder(container);
|
||||
}
|
||||
|
||||
@@ -184,5 +186,7 @@ QHash<int, QByteArray> ContainersModel::roleNames() const
|
||||
roles[IsSftpRole] = "isSftp";
|
||||
roles[IsTorWebsiteRole] = "isTorWebsite";
|
||||
roles[IsSocks5ProxyRole] = "isSocks5Proxy";
|
||||
roles[IsMtProxyRole] = "isMtProxy";
|
||||
roles[IsTelemtRole] = "isTelemt";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,9 @@ public:
|
||||
IsDnsRole,
|
||||
IsSftpRole,
|
||||
IsTorWebsiteRole,
|
||||
IsSocks5ProxyRole
|
||||
IsSocks5ProxyRole,
|
||||
IsMtProxyRole,
|
||||
IsTelemtRole,
|
||||
};
|
||||
|
||||
Q_INVOKABLE void openContainerSettings(int containerIndex);
|
||||
|
||||
@@ -8,94 +8,575 @@
|
||||
using namespace amnezia;
|
||||
using namespace ProtocolUtils;
|
||||
|
||||
XrayConfigModel::XrayConfigModel(QObject *parent) : QAbstractListModel(parent)
|
||||
XrayConfigModel::XrayConfigModel(QObject* parent) : QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
int XrayConfigModel::rowCount(const QModelIndex &parent) const
|
||||
int XrayConfigModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool XrayConfigModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||
bool XrayConfigModel::setData(const QModelIndex& index, const QVariant& value, int role)
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= ContainerUtils::allContainers().size()) {
|
||||
// This model always has a single row (row 0). Using rowCount() avoids
|
||||
// coupling editing ability to global container list size.
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= rowCount())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QString strValue = value.toString();
|
||||
const bool wasUnsavedChanges = hasUnsavedChanges();
|
||||
|
||||
auto& srv = m_protocolConfig.serverConfig;
|
||||
auto& xhttp = srv.xhttp;
|
||||
auto& mkcp = srv.mkcp;
|
||||
auto& pad = xhttp.xPadding;
|
||||
auto& mux = xhttp.xmux;
|
||||
|
||||
QString str = value.toString();
|
||||
|
||||
switch (role)
|
||||
{
|
||||
// ── Main ──────────────────────────────────────────────────────────
|
||||
case Roles::SiteRole: srv.site = str;
|
||||
break;
|
||||
case Roles::PortRole: srv.port = str;
|
||||
break;
|
||||
case Roles::TransportRole: srv.transport = str;
|
||||
break;
|
||||
case Roles::SecurityRole: srv.security = str;
|
||||
break;
|
||||
case Roles::FlowRole: srv.flow = str;
|
||||
break;
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────────
|
||||
case Roles::FingerprintRole: srv.fingerprint = str;
|
||||
break;
|
||||
case Roles::SniRole: srv.sni = str;
|
||||
break;
|
||||
case Roles::AlpnRole: srv.alpn = str;
|
||||
break;
|
||||
|
||||
// ── XHTTP ─────────────────────────────────────────────────────────
|
||||
case Roles::XhttpModeRole: xhttp.mode = str;
|
||||
break;
|
||||
case Roles::XhttpHostRole: xhttp.host = str;
|
||||
break;
|
||||
case Roles::XhttpPathRole: xhttp.path = str;
|
||||
break;
|
||||
case Roles::XhttpHeadersTemplateRole: xhttp.headersTemplate = str;
|
||||
break;
|
||||
case Roles::XhttpUplinkMethodRole: xhttp.uplinkMethod = str;
|
||||
break;
|
||||
case Roles::XhttpDisableGrpcRole: xhttp.disableGrpc = value.toBool();
|
||||
break;
|
||||
case Roles::XhttpDisableSseRole: xhttp.disableSse = value.toBool();
|
||||
break;
|
||||
|
||||
case Roles::XhttpSessionPlacementRole: xhttp.sessionPlacement = str;
|
||||
break;
|
||||
case Roles::XhttpSessionKeyRole: xhttp.sessionKey = str;
|
||||
break;
|
||||
case Roles::XhttpSeqPlacementRole: xhttp.seqPlacement = str;
|
||||
break;
|
||||
case Roles::XhttpSeqKeyRole: xhttp.seqKey = str;
|
||||
break;
|
||||
case Roles::XhttpUplinkDataPlacementRole: xhttp.uplinkDataPlacement = str;
|
||||
break;
|
||||
case Roles::XhttpUplinkDataKeyRole: xhttp.uplinkDataKey = str;
|
||||
break;
|
||||
|
||||
case Roles::XhttpUplinkChunkSizeRole: xhttp.uplinkChunkSize = str;
|
||||
break;
|
||||
case Roles::XhttpScMaxBufferedPostsRole: xhttp.scMaxBufferedPosts = str;
|
||||
break;
|
||||
case Roles::XhttpScMaxEachPostBytesMinRole: xhttp.scMaxEachPostBytesMin = str;
|
||||
break;
|
||||
case Roles::XhttpScMaxEachPostBytesMaxRole: xhttp.scMaxEachPostBytesMax = str;
|
||||
break;
|
||||
case Roles::XhttpScMinPostsIntervalMsMinRole: xhttp.scMinPostsIntervalMsMin = str;
|
||||
break;
|
||||
case Roles::XhttpScMinPostsIntervalMsMaxRole: xhttp.scMinPostsIntervalMsMax = str;
|
||||
break;
|
||||
case Roles::XhttpScStreamUpServerSecsMinRole: xhttp.scStreamUpServerSecsMin = str;
|
||||
break;
|
||||
case Roles::XhttpScStreamUpServerSecsMaxRole: xhttp.scStreamUpServerSecsMax = str;
|
||||
break;
|
||||
|
||||
// ── mKCP ──────────────────────────────────────────────────────────
|
||||
case Roles::MkcpTtiRole: mkcp.tti = str;
|
||||
break;
|
||||
case Roles::MkcpUplinkCapacityRole: mkcp.uplinkCapacity = str;
|
||||
break;
|
||||
case Roles::MkcpDownlinkCapacityRole: mkcp.downlinkCapacity = str;
|
||||
break;
|
||||
case Roles::MkcpReadBufferSizeRole: mkcp.readBufferSize = str;
|
||||
break;
|
||||
case Roles::MkcpWriteBufferSizeRole: mkcp.writeBufferSize = str;
|
||||
break;
|
||||
case Roles::MkcpCongestionRole: mkcp.congestion = value.toBool();
|
||||
break;
|
||||
|
||||
// ── xPadding ──────────────────────────────────────────────────────
|
||||
case Roles::XPaddingBytesMinRole: pad.bytesMin = str;
|
||||
break;
|
||||
case Roles::XPaddingBytesMaxRole: pad.bytesMax = str;
|
||||
break;
|
||||
case Roles::XPaddingObfsModeRole: pad.obfsMode = value.toBool();
|
||||
break;
|
||||
case Roles::XPaddingKeyRole: pad.key = str;
|
||||
break;
|
||||
case Roles::XPaddingHeaderRole: pad.header = str;
|
||||
break;
|
||||
case Roles::XPaddingPlacementRole: pad.placement = str;
|
||||
break;
|
||||
case Roles::XPaddingMethodRole: pad.method = str;
|
||||
break;
|
||||
|
||||
// ── xmux ──────────────────────────────────────────────────────────
|
||||
case Roles::XmuxEnabledRole: mux.enabled = value.toBool();
|
||||
break;
|
||||
case Roles::XmuxMaxConcurrencyMinRole: mux.maxConcurrencyMin = str;
|
||||
break;
|
||||
case Roles::XmuxMaxConcurrencyMaxRole: mux.maxConcurrencyMax = str;
|
||||
break;
|
||||
case Roles::XmuxMaxConnectionsMinRole: mux.maxConnectionsMin = str;
|
||||
break;
|
||||
case Roles::XmuxMaxConnectionsMaxRole: mux.maxConnectionsMax = str;
|
||||
break;
|
||||
case Roles::XmuxCMaxReuseTimesMinRole: mux.cMaxReuseTimesMin = str;
|
||||
break;
|
||||
case Roles::XmuxCMaxReuseTimesMaxRole: mux.cMaxReuseTimesMax = str;
|
||||
break;
|
||||
case Roles::XmuxHMaxRequestTimesMinRole: mux.hMaxRequestTimesMin = str;
|
||||
break;
|
||||
case Roles::XmuxHMaxRequestTimesMaxRole: mux.hMaxRequestTimesMax = str;
|
||||
break;
|
||||
case Roles::XmuxHMaxReusableSecsMinRole: mux.hMaxReusableSecsMin = str;
|
||||
break;
|
||||
case Roles::XmuxHMaxReusableSecsMaxRole: mux.hMaxReusableSecsMax = str;
|
||||
break;
|
||||
case Roles::XmuxHKeepAlivePeriodRole: mux.hKeepAlivePeriod = str;
|
||||
break;
|
||||
|
||||
switch (role) {
|
||||
case Roles::SiteRole: m_protocolConfig.serverConfig.site = strValue; break;
|
||||
case Roles::PortRole: m_protocolConfig.serverConfig.port = strValue; break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
emit dataChanged(index, index, QList { role });
|
||||
emit dataChanged(index, index, QList{role});
|
||||
if (wasUnsavedChanges != hasUnsavedChanges()) {
|
||||
emit hasUnsavedChangesChanged();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QVariant XrayConfigModel::data(const QModelIndex &index, int role) const
|
||||
QVariant XrayConfigModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
switch (role) {
|
||||
case Roles::SiteRole: return m_protocolConfig.serverConfig.site;
|
||||
case Roles::PortRole: return m_protocolConfig.serverConfig.port;
|
||||
const auto& srv = m_protocolConfig.serverConfig;
|
||||
const auto& xhttp = srv.xhttp;
|
||||
const auto& mkcp = srv.mkcp;
|
||||
const auto& pad = xhttp.xPadding;
|
||||
const auto& mux = xhttp.xmux;
|
||||
|
||||
switch (role)
|
||||
{
|
||||
// ── Main ──────────────────────────────────────────────────────────
|
||||
case Roles::SiteRole: return srv.site;
|
||||
case Roles::PortRole: return srv.port;
|
||||
case Roles::TransportRole: return srv.transport;
|
||||
case Roles::SecurityRole: return srv.security;
|
||||
case Roles::FlowRole: return srv.flow;
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────────
|
||||
case Roles::FingerprintRole: return srv.fingerprint;
|
||||
case Roles::SniRole: return srv.sni;
|
||||
case Roles::AlpnRole: return srv.alpn;
|
||||
|
||||
// ── XHTTP ─────────────────────────────────────────────────────────
|
||||
case Roles::XhttpModeRole: return xhttp.mode;
|
||||
case Roles::XhttpHostRole: return xhttp.host;
|
||||
case Roles::XhttpPathRole: return xhttp.path;
|
||||
case Roles::XhttpHeadersTemplateRole: return xhttp.headersTemplate;
|
||||
case Roles::XhttpUplinkMethodRole: return xhttp.uplinkMethod;
|
||||
case Roles::XhttpDisableGrpcRole: return xhttp.disableGrpc;
|
||||
case Roles::XhttpDisableSseRole: return xhttp.disableSse;
|
||||
|
||||
case Roles::XhttpSessionPlacementRole: return xhttp.sessionPlacement;
|
||||
case Roles::XhttpSessionKeyRole: return xhttp.sessionKey;
|
||||
case Roles::XhttpSeqPlacementRole: return xhttp.seqPlacement;
|
||||
case Roles::XhttpSeqKeyRole: return xhttp.seqKey;
|
||||
case Roles::XhttpUplinkDataPlacementRole: return xhttp.uplinkDataPlacement;
|
||||
case Roles::XhttpUplinkDataKeyRole: return xhttp.uplinkDataKey;
|
||||
|
||||
case Roles::XhttpUplinkChunkSizeRole: return xhttp.uplinkChunkSize;
|
||||
case Roles::XhttpScMaxBufferedPostsRole: return xhttp.scMaxBufferedPosts;
|
||||
case Roles::XhttpScMaxEachPostBytesMinRole: return xhttp.scMaxEachPostBytesMin;
|
||||
case Roles::XhttpScMaxEachPostBytesMaxRole: return xhttp.scMaxEachPostBytesMax;
|
||||
case Roles::XhttpScMinPostsIntervalMsMinRole: return xhttp.scMinPostsIntervalMsMin;
|
||||
case Roles::XhttpScMinPostsIntervalMsMaxRole: return xhttp.scMinPostsIntervalMsMax;
|
||||
case Roles::XhttpScStreamUpServerSecsMinRole: return xhttp.scStreamUpServerSecsMin;
|
||||
case Roles::XhttpScStreamUpServerSecsMaxRole: return xhttp.scStreamUpServerSecsMax;
|
||||
|
||||
// ── mKCP ──────────────────────────────────────────────────────────
|
||||
case Roles::MkcpTtiRole: return mkcp.tti;
|
||||
case Roles::MkcpUplinkCapacityRole: return mkcp.uplinkCapacity;
|
||||
case Roles::MkcpDownlinkCapacityRole: return mkcp.downlinkCapacity;
|
||||
case Roles::MkcpReadBufferSizeRole: return mkcp.readBufferSize;
|
||||
case Roles::MkcpWriteBufferSizeRole: return mkcp.writeBufferSize;
|
||||
case Roles::MkcpCongestionRole: return mkcp.congestion;
|
||||
|
||||
// ── xPadding ──────────────────────────────────────────────────────
|
||||
case Roles::XPaddingBytesMinRole: return pad.bytesMin;
|
||||
case Roles::XPaddingBytesMaxRole: return pad.bytesMax;
|
||||
case Roles::XPaddingObfsModeRole: return pad.obfsMode;
|
||||
case Roles::XPaddingKeyRole: return pad.key;
|
||||
case Roles::XPaddingHeaderRole: return pad.header;
|
||||
case Roles::XPaddingPlacementRole: return pad.placement;
|
||||
case Roles::XPaddingMethodRole: return pad.method;
|
||||
|
||||
// ── xmux ──────────────────────────────────────────────────────────
|
||||
case Roles::XmuxEnabledRole: return mux.enabled;
|
||||
case Roles::XmuxMaxConcurrencyMinRole: return mux.maxConcurrencyMin;
|
||||
case Roles::XmuxMaxConcurrencyMaxRole: return mux.maxConcurrencyMax;
|
||||
case Roles::XmuxMaxConnectionsMinRole: return mux.maxConnectionsMin;
|
||||
case Roles::XmuxMaxConnectionsMaxRole: return mux.maxConnectionsMax;
|
||||
case Roles::XmuxCMaxReuseTimesMinRole: return mux.cMaxReuseTimesMin;
|
||||
case Roles::XmuxCMaxReuseTimesMaxRole: return mux.cMaxReuseTimesMax;
|
||||
case Roles::XmuxHMaxRequestTimesMinRole: return mux.hMaxRequestTimesMin;
|
||||
case Roles::XmuxHMaxRequestTimesMaxRole: return mux.hMaxRequestTimesMax;
|
||||
case Roles::XmuxHMaxReusableSecsMinRole: return mux.hMaxReusableSecsMin;
|
||||
case Roles::XmuxHMaxReusableSecsMaxRole: return mux.hMaxReusableSecsMax;
|
||||
case Roles::XmuxHKeepAlivePeriodRole: return mux.hKeepAlivePeriod;
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig &protocolConfig)
|
||||
void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig)
|
||||
{
|
||||
const bool wasUnsavedChanges = hasUnsavedChanges();
|
||||
|
||||
beginResetModel();
|
||||
|
||||
m_container = container;
|
||||
|
||||
|
||||
m_protocolConfig = protocolConfig;
|
||||
|
||||
|
||||
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
|
||||
|
||||
|
||||
m_originalProtocolConfig = m_protocolConfig;
|
||||
|
||||
|
||||
endResetModel();
|
||||
if (wasUnsavedChanges != hasUnsavedChanges()) {
|
||||
emit hasUnsavedChangesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig& config)
|
||||
void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &config)
|
||||
{
|
||||
if (config.port.isEmpty()) {
|
||||
config.port = protocols::xray::defaultPort;
|
||||
}
|
||||
|
||||
if (config.transportProto.isEmpty()) {
|
||||
config.transportProto = ProtocolUtils::transportProtoToString(
|
||||
ProtocolUtils::defaultTransportProto(amnezia::Proto::Xray), amnezia::Proto::Xray);
|
||||
}
|
||||
|
||||
if (config.site.isEmpty()) {
|
||||
config.site = protocols::xray::defaultSite;
|
||||
}
|
||||
|
||||
if (config.transport.isEmpty()) {
|
||||
config.transport = protocols::xray::defaultTransport;
|
||||
}
|
||||
|
||||
if (config.security.isEmpty()) {
|
||||
config.security = protocols::xray::defaultSecurity;
|
||||
}
|
||||
|
||||
if (config.flow.isEmpty()) {
|
||||
config.flow = protocols::xray::defaultFlow;
|
||||
}
|
||||
|
||||
if (config.fingerprint.isEmpty()) {
|
||||
config.fingerprint = protocols::xray::defaultFingerprint;
|
||||
} else if (config.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
config.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
}
|
||||
|
||||
if (config.sni.isEmpty()) {
|
||||
config.sni = protocols::xray::defaultSni;
|
||||
}
|
||||
|
||||
if (config.alpn.isEmpty()) {
|
||||
config.alpn = protocols::xray::defaultAlpn;
|
||||
}
|
||||
|
||||
// XHTTP transport defaults
|
||||
if (config.xhttp.host.isEmpty()) {
|
||||
config.xhttp.host = protocols::xray::defaultXhttpHost;
|
||||
}
|
||||
if (config.xhttp.mode.isEmpty()) {
|
||||
config.xhttp.mode = protocols::xray::defaultXhttpMode;
|
||||
}
|
||||
if (config.xhttp.headersTemplate.isEmpty()) {
|
||||
config.xhttp.headersTemplate = protocols::xray::defaultXhttpHeadersTemplate;
|
||||
}
|
||||
if (config.xhttp.uplinkMethod.isEmpty()) {
|
||||
config.xhttp.uplinkMethod = protocols::xray::defaultXhttpUplinkMethod;
|
||||
}
|
||||
if (config.xhttp.sessionPlacement.isEmpty()) {
|
||||
config.xhttp.sessionPlacement = protocols::xray::defaultXhttpSessionPlacement;
|
||||
}
|
||||
if (config.xhttp.sessionKey.isEmpty()) {
|
||||
config.xhttp.sessionKey = protocols::xray::defaultXhttpSessionKey;
|
||||
}
|
||||
if (config.xhttp.seqPlacement.isEmpty()) {
|
||||
config.xhttp.seqPlacement = protocols::xray::defaultXhttpSeqPlacement;
|
||||
}
|
||||
if (config.xhttp.uplinkDataPlacement.isEmpty()) {
|
||||
config.xhttp.uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement;
|
||||
}
|
||||
|
||||
// xPadding defaults
|
||||
if (config.xhttp.xPadding.placement.isEmpty()) {
|
||||
config.xhttp.xPadding.placement = protocols::xray::defaultXPaddingPlacement;
|
||||
}
|
||||
if (config.xhttp.xPadding.method.isEmpty()) {
|
||||
config.xhttp.xPadding.method = protocols::xray::defaultXPaddingMethod;
|
||||
}
|
||||
}
|
||||
|
||||
amnezia::XrayProtocolConfig XrayConfigModel::getProtocolConfig()
|
||||
{
|
||||
bool serverSettingsChanged = !m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig);
|
||||
|
||||
const bool serverSettingsChanged =
|
||||
!m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig);
|
||||
|
||||
if (serverSettingsChanged) {
|
||||
m_protocolConfig.clearClientConfig();
|
||||
}
|
||||
|
||||
return m_protocolConfig;
|
||||
}
|
||||
|
||||
bool XrayConfigModel::isServerSettingsEqual() const
|
||||
{
|
||||
return m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig);
|
||||
}
|
||||
|
||||
bool XrayConfigModel::hasUnsavedChanges() const
|
||||
{
|
||||
return !isServerSettingsEqual();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> XrayConfigModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
|
||||
// Main
|
||||
roles[SiteRole] = "site";
|
||||
roles[PortRole] = "port";
|
||||
roles[TransportRole] = "transport";
|
||||
roles[SecurityRole] = "security";
|
||||
roles[FlowRole] = "flow";
|
||||
|
||||
// Security
|
||||
roles[FingerprintRole] = "fingerprint";
|
||||
roles[SniRole] = "sni";
|
||||
roles[AlpnRole] = "alpn";
|
||||
|
||||
// XHTTP
|
||||
roles[XhttpModeRole] = "xhttpMode";
|
||||
roles[XhttpHostRole] = "xhttpHost";
|
||||
roles[XhttpPathRole] = "xhttpPath";
|
||||
roles[XhttpHeadersTemplateRole] = "xhttpHeadersTemplate";
|
||||
roles[XhttpUplinkMethodRole] = "xhttpUplinkMethod";
|
||||
roles[XhttpDisableGrpcRole] = "xhttpDisableGrpc";
|
||||
roles[XhttpDisableSseRole] = "xhttpDisableSse";
|
||||
|
||||
roles[XhttpSessionPlacementRole] = "xhttpSessionPlacement";
|
||||
roles[XhttpSessionKeyRole] = "xhttpSessionKey";
|
||||
roles[XhttpSeqPlacementRole] = "xhttpSeqPlacement";
|
||||
roles[XhttpSeqKeyRole] = "xhttpSeqKey";
|
||||
roles[XhttpUplinkDataPlacementRole] = "xhttpUplinkDataPlacement";
|
||||
roles[XhttpUplinkDataKeyRole] = "xhttpUplinkDataKey";
|
||||
|
||||
roles[XhttpUplinkChunkSizeRole] = "xhttpUplinkChunkSize";
|
||||
roles[XhttpScMaxBufferedPostsRole] = "xhttpScMaxBufferedPosts";
|
||||
roles[XhttpScMaxEachPostBytesMinRole] = "xhttpScMaxEachPostBytesMin";
|
||||
roles[XhttpScMaxEachPostBytesMaxRole] = "xhttpScMaxEachPostBytesMax";
|
||||
roles[XhttpScMinPostsIntervalMsMinRole] = "xhttpScMinPostsIntervalMsMin";
|
||||
roles[XhttpScMinPostsIntervalMsMaxRole] = "xhttpScMinPostsIntervalMsMax";
|
||||
roles[XhttpScStreamUpServerSecsMinRole] = "xhttpScStreamUpServerSecsMin";
|
||||
roles[XhttpScStreamUpServerSecsMaxRole] = "xhttpScStreamUpServerSecsMax";
|
||||
|
||||
// mKCP
|
||||
roles[MkcpTtiRole] = "mkcpTti";
|
||||
roles[MkcpUplinkCapacityRole] = "mkcpUplinkCapacity";
|
||||
roles[MkcpDownlinkCapacityRole] = "mkcpDownlinkCapacity";
|
||||
roles[MkcpReadBufferSizeRole] = "mkcpReadBufferSize";
|
||||
roles[MkcpWriteBufferSizeRole] = "mkcpWriteBufferSize";
|
||||
roles[MkcpCongestionRole] = "mkcpCongestion";
|
||||
|
||||
// xPadding
|
||||
roles[XPaddingBytesMinRole] = "xPaddingBytesMin";
|
||||
roles[XPaddingBytesMaxRole] = "xPaddingBytesMax";
|
||||
roles[XPaddingObfsModeRole] = "xPaddingObfsMode";
|
||||
roles[XPaddingKeyRole] = "xPaddingKey";
|
||||
roles[XPaddingHeaderRole] = "xPaddingHeader";
|
||||
roles[XPaddingPlacementRole] = "xPaddingPlacement";
|
||||
roles[XPaddingMethodRole] = "xPaddingMethod";
|
||||
|
||||
// xmux
|
||||
roles[XmuxEnabledRole] = "xmuxEnabled";
|
||||
roles[XmuxMaxConcurrencyMinRole] = "xmuxMaxConcurrencyMin";
|
||||
roles[XmuxMaxConcurrencyMaxRole] = "xmuxMaxConcurrencyMax";
|
||||
roles[XmuxMaxConnectionsMinRole] = "xmuxMaxConnectionsMin";
|
||||
roles[XmuxMaxConnectionsMaxRole] = "xmuxMaxConnectionsMax";
|
||||
roles[XmuxCMaxReuseTimesMinRole] = "xmuxCMaxReuseTimesMin";
|
||||
roles[XmuxCMaxReuseTimesMaxRole] = "xmuxCMaxReuseTimesMax";
|
||||
roles[XmuxHMaxRequestTimesMinRole] = "xmuxHMaxRequestTimesMin";
|
||||
roles[XmuxHMaxRequestTimesMaxRole] = "xmuxHMaxRequestTimesMax";
|
||||
roles[XmuxHMaxReusableSecsMinRole] = "xmuxHMaxReusableSecsMin";
|
||||
roles[XmuxHMaxReusableSecsMaxRole] = "xmuxHMaxReusableSecsMax";
|
||||
roles[XmuxHKeepAlivePeriodRole] = "xmuxHKeepAlivePeriod";
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
void XrayConfigModel::resetToDefaults()
|
||||
{
|
||||
const bool wasUnsavedChanges = hasUnsavedChanges();
|
||||
|
||||
beginResetModel();
|
||||
m_protocolConfig.serverConfig = amnezia::XrayServerConfig{};
|
||||
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
|
||||
endResetModel();
|
||||
|
||||
if (wasUnsavedChanges != hasUnsavedChanges()) {
|
||||
emit hasUnsavedChangesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void XrayConfigModel::applyServerConfig(const amnezia::XrayServerConfig &serverConfig)
|
||||
{
|
||||
const bool wasUnsavedChanges = hasUnsavedChanges();
|
||||
|
||||
beginResetModel();
|
||||
m_protocolConfig.serverConfig = serverConfig;
|
||||
// Clear client config since server settings changed
|
||||
m_protocolConfig.clearClientConfig();
|
||||
m_originalProtocolConfig = m_protocolConfig;
|
||||
endResetModel();
|
||||
|
||||
if (wasUnsavedChanges != hasUnsavedChanges()) {
|
||||
emit hasUnsavedChangesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::flowOptions()
|
||||
{
|
||||
return {
|
||||
"", // Empty (no flow)
|
||||
"xtls-rprx-vision",
|
||||
"xtls-rprx-vision-udp443"
|
||||
};
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::securityOptions()
|
||||
{
|
||||
return { "none", "tls", "reality" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::transportOptions()
|
||||
{
|
||||
return { "raw", "xhttp", "mkcp" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::fingerprintOptions()
|
||||
{
|
||||
return { "chrome", "firefox", "safari", "ios", "android", "edge", "360", "qq", "random" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::alpnOptions()
|
||||
{
|
||||
return { "HTTP/2", "HTTP/1.1", "HTTP/2,HTTP/1.1" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpModeOptions()
|
||||
{
|
||||
return { "Auto", "Packet-up", "Stream-up", "Stream-one" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpHeadersTemplateOptions()
|
||||
{
|
||||
return { "HTTP", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpUplinkMethodOptions()
|
||||
{
|
||||
return { "POST", "PUT", "PATCH" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpSessionPlacementOptions()
|
||||
{
|
||||
return { "Path", "Header", "Cookie", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpSessionKeyOptions()
|
||||
{
|
||||
return { "Path", "Header", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpSeqPlacementOptions()
|
||||
{
|
||||
return { "Path", "Header", "Cookie", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpUplinkDataPlacementOptions()
|
||||
{
|
||||
// Matches splithttp uplink payload placement (packet-up / advanced)
|
||||
return { "Body", "Auto", "Header", "Cookie" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xPaddingPlacementOptions()
|
||||
{
|
||||
// Xray-core: cookie | header | query | queryInHeader (not "body")
|
||||
return { "Cookie", "Header", "Query", "Query in header" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xPaddingMethodOptions()
|
||||
{
|
||||
return { "Repeat-x", "Tokenish" };
|
||||
}
|
||||
|
||||
QString XrayConfigModel::mkcpDefaultTti()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpTti);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::mkcpDefaultUplinkCapacity()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpUplinkCapacity);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::mkcpDefaultDownlinkCapacity()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpDownlinkCapacity);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::mkcpDefaultReadBufferSize()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpReadBufferSize);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::mkcpDefaultWriteBufferSize()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpWriteBufferSize);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define XRAYCONFIGMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QStringList>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
@@ -11,23 +12,122 @@
|
||||
class XrayConfigModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool hasUnsavedChanges READ hasUnsavedChanges NOTIFY hasUnsavedChangesChanged)
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
SiteRole,
|
||||
PortRole
|
||||
enum Roles
|
||||
{
|
||||
// ── Main page ─────────────────────────────────────────────────
|
||||
SiteRole = Qt::UserRole + 1,
|
||||
PortRole,
|
||||
TransportRole, // "raw" | "xhttp" | "mkcp" (display in main page row)
|
||||
SecurityRole, // "none" | "tls" | "reality" (display in main page row)
|
||||
FlowRole, // "" | "xtls-rprx-vision" | "xtls-rprx-vision-udp443"
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────
|
||||
FingerprintRole,
|
||||
SniRole,
|
||||
AlpnRole,
|
||||
|
||||
// ── Transport — XHTTP ─────────────────────────────────────────
|
||||
XhttpModeRole,
|
||||
XhttpHostRole,
|
||||
XhttpPathRole,
|
||||
XhttpHeadersTemplateRole,
|
||||
XhttpUplinkMethodRole,
|
||||
XhttpDisableGrpcRole,
|
||||
XhttpDisableSseRole,
|
||||
|
||||
// Session & Sequence
|
||||
XhttpSessionPlacementRole,
|
||||
XhttpSessionKeyRole,
|
||||
XhttpSeqPlacementRole,
|
||||
XhttpSeqKeyRole,
|
||||
XhttpUplinkDataPlacementRole,
|
||||
XhttpUplinkDataKeyRole,
|
||||
|
||||
// Traffic Shaping
|
||||
XhttpUplinkChunkSizeRole,
|
||||
XhttpScMaxBufferedPostsRole,
|
||||
XhttpScMaxEachPostBytesMinRole,
|
||||
XhttpScMaxEachPostBytesMaxRole,
|
||||
XhttpScMinPostsIntervalMsMinRole,
|
||||
XhttpScMinPostsIntervalMsMaxRole,
|
||||
XhttpScStreamUpServerSecsMinRole,
|
||||
XhttpScStreamUpServerSecsMaxRole,
|
||||
|
||||
// ── Transport — mKCP ──────────────────────────────────────────
|
||||
MkcpTtiRole,
|
||||
MkcpUplinkCapacityRole,
|
||||
MkcpDownlinkCapacityRole,
|
||||
MkcpReadBufferSizeRole,
|
||||
MkcpWriteBufferSizeRole,
|
||||
MkcpCongestionRole,
|
||||
|
||||
// ── xPadding ──────────────────────────────────────────────────
|
||||
XPaddingBytesMinRole,
|
||||
XPaddingBytesMaxRole,
|
||||
XPaddingObfsModeRole,
|
||||
XPaddingKeyRole,
|
||||
XPaddingHeaderRole,
|
||||
XPaddingPlacementRole,
|
||||
XPaddingMethodRole,
|
||||
|
||||
// ── xmux ──────────────────────────────────────────────────────
|
||||
XmuxEnabledRole,
|
||||
XmuxMaxConcurrencyMinRole,
|
||||
XmuxMaxConcurrencyMaxRole,
|
||||
XmuxMaxConnectionsMinRole,
|
||||
XmuxMaxConnectionsMaxRole,
|
||||
XmuxCMaxReuseTimesMinRole,
|
||||
XmuxCMaxReuseTimesMaxRole,
|
||||
XmuxHMaxRequestTimesMinRole,
|
||||
XmuxHMaxRequestTimesMaxRole,
|
||||
XmuxHMaxReusableSecsMinRole,
|
||||
XmuxHMaxReusableSecsMaxRole,
|
||||
XmuxHKeepAlivePeriodRole,
|
||||
};
|
||||
|
||||
explicit XrayConfigModel(QObject *parent = nullptr);
|
||||
explicit XrayConfigModel(QObject* parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
bool setData(const QModelIndex& index, const QVariant& value, int role) override;
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
// ── Static option lists (for QML DropDown models) ─────────────────
|
||||
Q_INVOKABLE static QStringList flowOptions();
|
||||
Q_INVOKABLE static QStringList securityOptions();
|
||||
Q_INVOKABLE static QStringList transportOptions();
|
||||
Q_INVOKABLE static QStringList fingerprintOptions();
|
||||
Q_INVOKABLE static QStringList alpnOptions();
|
||||
Q_INVOKABLE static QStringList xhttpModeOptions();
|
||||
Q_INVOKABLE static QStringList xhttpHeadersTemplateOptions();
|
||||
Q_INVOKABLE static QStringList xhttpUplinkMethodOptions();
|
||||
Q_INVOKABLE static QStringList xhttpSessionPlacementOptions();
|
||||
Q_INVOKABLE static QStringList xhttpSessionKeyOptions();
|
||||
Q_INVOKABLE static QStringList xhttpSeqPlacementOptions();
|
||||
Q_INVOKABLE static QStringList xhttpUplinkDataPlacementOptions();
|
||||
Q_INVOKABLE static QStringList xPaddingPlacementOptions();
|
||||
Q_INVOKABLE static QStringList xPaddingMethodOptions();
|
||||
|
||||
// mKCP display defaults (protocolConstants.h — must match xrayConfigurator empty-field behavior)
|
||||
Q_INVOKABLE static QString mkcpDefaultTti();
|
||||
Q_INVOKABLE static QString mkcpDefaultUplinkCapacity();
|
||||
Q_INVOKABLE static QString mkcpDefaultDownlinkCapacity();
|
||||
Q_INVOKABLE static QString mkcpDefaultReadBufferSize();
|
||||
Q_INVOKABLE static QString mkcpDefaultWriteBufferSize();
|
||||
|
||||
public slots:
|
||||
void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig &protocolConfig);
|
||||
void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig);
|
||||
amnezia::XrayProtocolConfig getProtocolConfig();
|
||||
bool isServerSettingsEqual() const;
|
||||
bool hasUnsavedChanges() const;
|
||||
void resetToDefaults();
|
||||
void applyServerConfig(const amnezia::XrayServerConfig &serverConfig);
|
||||
|
||||
signals:
|
||||
void hasUnsavedChangesChanged();
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
@@ -36,7 +136,7 @@ private:
|
||||
amnezia::DockerContainer m_container;
|
||||
amnezia::XrayProtocolConfig m_protocolConfig;
|
||||
amnezia::XrayProtocolConfig m_originalProtocolConfig;
|
||||
|
||||
|
||||
void applyDefaultsToServerConfig(amnezia::XrayServerConfig& config);
|
||||
};
|
||||
|
||||
|
||||
216
client/ui/models/protocols/xrayConfigSnapshotsModel.cpp
Normal file
216
client/ui/models/protocols/xrayConfigSnapshotsModel.cpp
Normal file
@@ -0,0 +1,216 @@
|
||||
#include "xrayConfigSnapshotsModel.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QUuid>
|
||||
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
|
||||
QJsonObject XrayConfigSnapshot::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj["id"] = id;
|
||||
obj["displayName"] = displayName;
|
||||
obj["createdAt"] = createdAt.toString(Qt::ISODate);
|
||||
obj["serverConfig"] = serverConfig.toJson();
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayConfigSnapshot XrayConfigSnapshot::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayConfigSnapshot s;
|
||||
s.id = json.value("id").toString();
|
||||
s.displayName = json.value("displayName").toString();
|
||||
s.createdAt = QDateTime::fromString(json.value("createdAt").toString(), Qt::ISODate);
|
||||
s.serverConfig = amnezia::XrayServerConfig::fromJson(json.value("serverConfig").toObject());
|
||||
return s;
|
||||
}
|
||||
|
||||
XrayConfigSnapshotsModel::XrayConfigSnapshotsModel(SecureAppSettingsRepository *appSettings,
|
||||
XrayConfigModel *xrayConfigModel, QObject *parent)
|
||||
: QAbstractListModel(parent), m_appSettings(appSettings), m_xrayConfigModel(xrayConfigModel)
|
||||
{
|
||||
loadAll();
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::loadAll()
|
||||
{
|
||||
m_configs.clear();
|
||||
QByteArray raw = m_appSettings->xraySavedConfigs();
|
||||
if (raw.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray arr = QJsonDocument::fromJson(raw).array();
|
||||
for (const QJsonValue &v : arr) {
|
||||
m_configs.append(XrayConfigSnapshot::fromJson(v.toObject()));
|
||||
}
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::persistAll()
|
||||
{
|
||||
QJsonArray arr;
|
||||
for (const XrayConfigSnapshot &s : m_configs) {
|
||||
arr.append(s.toJson());
|
||||
}
|
||||
m_appSettings->setXraySavedConfigs(QJsonDocument(arr).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
int XrayConfigSnapshotsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return m_configs.size();
|
||||
}
|
||||
|
||||
QVariant XrayConfigSnapshotsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= m_configs.size()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
const XrayConfigSnapshot &s = m_configs.at(index.row());
|
||||
|
||||
switch (role) {
|
||||
case IdRole: {
|
||||
return s.id;
|
||||
}
|
||||
case DisplayNameRole: {
|
||||
return s.displayName;
|
||||
}
|
||||
case CreatedAtRole: {
|
||||
return s.createdAt.toString("dd.MM.yyyy HH:mm");
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> XrayConfigSnapshotsModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[IdRole] = "configId";
|
||||
roles[DisplayNameRole] = "configName";
|
||||
roles[CreatedAtRole] = "configDate";
|
||||
return roles;
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::reload()
|
||||
{
|
||||
beginResetModel();
|
||||
loadAll();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::createFromCurrent(const amnezia::XrayServerConfig &serverConfig)
|
||||
{
|
||||
XrayConfigSnapshot snapshot;
|
||||
snapshot.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
snapshot.displayName = buildDisplayName(serverConfig);
|
||||
snapshot.createdAt = QDateTime::currentDateTime();
|
||||
snapshot.serverConfig = serverConfig;
|
||||
|
||||
beginInsertRows(QModelIndex(), m_configs.size(), m_configs.size());
|
||||
m_configs.append(snapshot);
|
||||
endInsertRows();
|
||||
|
||||
persistAll();
|
||||
}
|
||||
|
||||
amnezia::XrayServerConfig XrayConfigSnapshotsModel::applyConfig(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_configs.size()) {
|
||||
return amnezia::XrayServerConfig {};
|
||||
}
|
||||
|
||||
return m_configs.at(index).serverConfig;
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::removeConfig(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_configs.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
beginRemoveRows(QModelIndex(), index, index);
|
||||
m_configs.removeAt(index);
|
||||
endRemoveRows();
|
||||
|
||||
persistAll();
|
||||
emit configRemoved(index);
|
||||
}
|
||||
|
||||
QString XrayConfigSnapshotsModel::exportToJson(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_configs.size()) {
|
||||
return {};
|
||||
}
|
||||
return QString::fromUtf8(QJsonDocument(m_configs.at(index).toJson()).toJson(QJsonDocument::Indented));
|
||||
}
|
||||
|
||||
bool XrayConfigSnapshotsModel::importFromJson(const QString &jsonString)
|
||||
{
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
emit importFailed(tr("Invalid JSON format"));
|
||||
return false;
|
||||
}
|
||||
|
||||
XrayConfigSnapshot snapshot = XrayConfigSnapshot::fromJson(doc.object());
|
||||
if (snapshot.id.isEmpty()) {
|
||||
snapshot.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
}
|
||||
if (snapshot.displayName.isEmpty()) {
|
||||
snapshot.displayName = buildDisplayName(snapshot.serverConfig);
|
||||
}
|
||||
snapshot.createdAt = QDateTime::currentDateTime();
|
||||
|
||||
beginInsertRows(QModelIndex(), m_configs.size(), m_configs.size());
|
||||
m_configs.append(snapshot);
|
||||
endInsertRows();
|
||||
|
||||
persistAll();
|
||||
return true;
|
||||
}
|
||||
|
||||
QString XrayConfigSnapshotsModel::buildDisplayName(const amnezia::XrayServerConfig &cfg)
|
||||
{
|
||||
// Build a human-readable name: "XHTTP TLS Reality", "RAW Reality", etc.
|
||||
QString transport;
|
||||
if (cfg.transport == "xhttp") {
|
||||
transport = "XHTTP";
|
||||
} else if (cfg.transport == "mkcp") {
|
||||
transport = "mKCP";
|
||||
} else {
|
||||
transport = "RAW (TCP)";
|
||||
}
|
||||
|
||||
QString security;
|
||||
if (cfg.security == "tls") {
|
||||
security = "TLS";
|
||||
} else if (cfg.security == "reality") {
|
||||
security = "Reality";
|
||||
} else {
|
||||
security = "None";
|
||||
}
|
||||
|
||||
return QString("%1 %2").arg(transport, security).trimmed();
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::createFromCurrentModel()
|
||||
{
|
||||
if (!m_xrayConfigModel) {
|
||||
return;
|
||||
}
|
||||
createFromCurrent(m_xrayConfigModel->getProtocolConfig().serverConfig);
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::applyConfigToCurrentModel(int index)
|
||||
{
|
||||
if (!m_xrayConfigModel) {
|
||||
return;
|
||||
}
|
||||
amnezia::XrayServerConfig cfg = applyConfig(index);
|
||||
if (cfg.port.isEmpty()) {
|
||||
return; // guard against invalid index
|
||||
}
|
||||
m_xrayConfigModel->applyServerConfig(cfg);
|
||||
}
|
||||
76
client/ui/models/protocols/xrayConfigSnapshotsModel.h
Normal file
76
client/ui/models/protocols/xrayConfigSnapshotsModel.h
Normal file
@@ -0,0 +1,76 @@
|
||||
#ifndef XRAYCONFIGSMODEL_H
|
||||
#define XRAYCONFIGSMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QDateTime>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "ui/models/protocols/xrayConfigModel.h"
|
||||
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
struct XrayConfigSnapshot
|
||||
{
|
||||
QString id;
|
||||
QString displayName; // auto-generated: "XHTTP TLS Reality", "RAW Reality", etc.
|
||||
QDateTime createdAt;
|
||||
amnezia::XrayServerConfig serverConfig;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayConfigSnapshot fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
class XrayConfigSnapshotsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
IdRole = Qt::UserRole + 1,
|
||||
DisplayNameRole,
|
||||
CreatedAtRole, // "dd.MM.yyyy HH:mm"
|
||||
};
|
||||
|
||||
explicit XrayConfigSnapshotsModel(SecureAppSettingsRepository *appSettings, XrayConfigModel *xrayConfigModel,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
public slots:
|
||||
void reload();
|
||||
|
||||
Q_INVOKABLE void createFromCurrent(const amnezia::XrayServerConfig &serverConfig);
|
||||
Q_INVOKABLE amnezia::XrayServerConfig applyConfig(int index) const;
|
||||
Q_INVOKABLE void removeConfig(int index);
|
||||
|
||||
Q_INVOKABLE QString exportToJson(int index) const;
|
||||
Q_INVOKABLE bool importFromJson(const QString &jsonString);
|
||||
|
||||
// Convenience: create snapshot from live model, apply snapshot back to model
|
||||
Q_INVOKABLE void createFromCurrentModel();
|
||||
Q_INVOKABLE void applyConfigToCurrentModel(int index);
|
||||
|
||||
signals:
|
||||
void configApplied(int index);
|
||||
void configRemoved(int index);
|
||||
void importFailed(const QString &errorMessage);
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
SecureAppSettingsRepository *m_appSettings;
|
||||
XrayConfigModel *m_xrayConfigModel;
|
||||
QVector<XrayConfigSnapshot> m_configs;
|
||||
|
||||
void persistAll();
|
||||
void loadAll();
|
||||
static QString buildDisplayName(const amnezia::XrayServerConfig &cfg);
|
||||
};
|
||||
|
||||
#endif // XRAYCONFIGSMODEL_H
|
||||
@@ -42,6 +42,8 @@ QHash<int, QByteArray> ProtocolsModel::roleNames() const
|
||||
roles[IsSftpRole] = "isSftp";
|
||||
roles[IsIpsecRole] = "isIpsec";
|
||||
roles[IsSocks5ProxyRole] = "isSocks5Proxy";
|
||||
roles[IsMtProxyRole] = "isMtProxy";
|
||||
roles[IsTelemtRole] = "isTelemt";
|
||||
|
||||
return roles;
|
||||
}
|
||||
@@ -71,6 +73,8 @@ QVariant ProtocolsModel::data(const QModelIndex &index, int role) const
|
||||
case IsSftpRole: return proto == Proto::Sftp;
|
||||
case IsIpsecRole: return proto == Proto::Ikev2;
|
||||
case IsSocks5ProxyRole: return proto == Proto::Socks5Proxy;
|
||||
case IsMtProxyRole: return proto == Proto::MtProxy;
|
||||
case IsTelemtRole: return proto == Proto::Telemt;
|
||||
case RawConfigRole:
|
||||
return getRawConfig();
|
||||
case IsClientProtocolExistsRole:
|
||||
@@ -124,6 +128,8 @@ PageLoader::PageEnum ProtocolsModel::serverProtocolPage(Proto protocol) const
|
||||
case Proto::Dns: return PageLoader::PageEnum::PageServiceDnsSettings;
|
||||
case Proto::Sftp: return PageLoader::PageEnum::PageServiceSftpSettings;
|
||||
case Proto::Socks5Proxy: return PageLoader::PageEnum::PageServiceSocksProxySettings;
|
||||
case Proto::MtProxy: return PageLoader::PageEnum::PageServiceMtProxySettings;
|
||||
case Proto::Telemt: return PageLoader::PageEnum::PageServiceTelemtSettings;
|
||||
default: return PageLoader::PageEnum::PageProtocolOpenVpnSettings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ public:
|
||||
IsXrayRole,
|
||||
IsSftpRole,
|
||||
IsIpsecRole,
|
||||
IsSocks5ProxyRole
|
||||
IsSocks5ProxyRole,
|
||||
IsMtProxyRole,
|
||||
IsTelemtRole,
|
||||
};
|
||||
|
||||
explicit ProtocolsModel(QObject *parent = nullptr);
|
||||
|
||||
714
client/ui/models/services/mtProxyConfigModel.cpp
Normal file
714
client/ui/models/services/mtProxyConfigModel.cpp
Normal file
@@ -0,0 +1,714 @@
|
||||
#include "mtProxyConfigModel.h"
|
||||
|
||||
#include "ui/models/utils/mtproxy_public_host_input.h"
|
||||
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/qrCodeUtils.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "qrcodegen.hpp"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
#include <QHostAddress>
|
||||
#include <QRegExp>
|
||||
#include <QRegularExpression>
|
||||
#include <QtGlobal>
|
||||
#include <qqml.h>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
MtProxyConfigModel::MtProxyConfigModel(QObject *parent) : QAbstractListModel(parent) {
|
||||
qmlRegisterType<PublicHostInputValidator>("MtProxyConfig", 1, 0, "PublicHostInputValidator");
|
||||
}
|
||||
|
||||
int MtProxyConfigModel::rowCount(const QModelIndex &parent) const {
|
||||
Q_UNUSED(parent);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::setData(const QModelIndex &index, const QVariant &value, int role) {
|
||||
if (!index.isValid() || index.row() != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (role) {
|
||||
case Roles::PortRole: {
|
||||
m_protocolConfig.port = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::SecretRole: {
|
||||
m_protocolConfig.secret = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::TagRole: {
|
||||
const QString tag = sanitizeMtProxyTagFieldText(value.toString());
|
||||
if (!isValidMtProxyTag(tag)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.tag = tag;
|
||||
break;
|
||||
}
|
||||
case Roles::IsEnabledRole: {
|
||||
m_protocolConfig.isEnabled = value.toBool();
|
||||
break;
|
||||
}
|
||||
case Roles::PublicHostRole: {
|
||||
const QString h = value.toString().trimmed();
|
||||
if (!isValidPublicHost(h)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.publicHost = h;
|
||||
break;
|
||||
}
|
||||
case Roles::TransportModeRole: {
|
||||
m_protocolConfig.transportMode = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::TlsDomainRole: {
|
||||
const QString d = value.toString().trimmed();
|
||||
if (!isValidFakeTlsDomain(d)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.tlsDomain = d;
|
||||
break;
|
||||
}
|
||||
case Roles::AdditionalSecretsRole: {
|
||||
m_protocolConfig.additionalSecrets = value.toStringList();
|
||||
break;
|
||||
}
|
||||
case Roles::WorkersModeRole: {
|
||||
m_protocolConfig.workersMode = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::WorkersRole: {
|
||||
m_protocolConfig.workers = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::NatEnabledRole: {
|
||||
m_protocolConfig.natEnabled = value.toBool();
|
||||
break;
|
||||
}
|
||||
case Roles::NatInternalIpRole: {
|
||||
const QString ip = value.toString().trimmed();
|
||||
if (!isValidOptionalIpv4(ip)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.natInternalIp = ip;
|
||||
break;
|
||||
}
|
||||
case Roles::NatExternalIpRole: {
|
||||
const QString ip = value.toString().trimmed();
|
||||
if (!isValidOptionalIpv4(ip)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.natExternalIp = ip;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
emit dataChanged(index, index, QList{role});
|
||||
return true;
|
||||
}
|
||||
|
||||
QVariant MtProxyConfigModel::data(const QModelIndex &index, int role) const {
|
||||
if (!index.isValid() || index.row() != 0) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
switch (role) {
|
||||
case Roles::PortRole: {
|
||||
return m_protocolConfig.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : m_protocolConfig.port;
|
||||
}
|
||||
case Roles::SecretRole: {
|
||||
return m_protocolConfig.secret;
|
||||
}
|
||||
case Roles::TagRole: {
|
||||
return m_protocolConfig.tag;
|
||||
}
|
||||
case Roles::TgLinkRole: {
|
||||
return m_protocolConfig.tgLink;
|
||||
}
|
||||
case Roles::TmeLinkRole: {
|
||||
return m_protocolConfig.tmeLink;
|
||||
}
|
||||
case Roles::IsEnabledRole: {
|
||||
return m_protocolConfig.isEnabled;
|
||||
}
|
||||
case Roles::PublicHostRole: {
|
||||
return m_protocolConfig.publicHost.isEmpty()
|
||||
? m_fullConfig.value(configKey::hostName).toString()
|
||||
: m_protocolConfig.publicHost;
|
||||
}
|
||||
case Roles::TransportModeRole: {
|
||||
return m_protocolConfig.transportMode.isEmpty()
|
||||
? QString(protocols::mtProxy::transportModeStandard)
|
||||
: m_protocolConfig.transportMode;
|
||||
}
|
||||
case Roles::TlsDomainRole: {
|
||||
return m_protocolConfig.tlsDomain;
|
||||
}
|
||||
case Roles::AdditionalSecretsRole: {
|
||||
return m_protocolConfig.additionalSecrets;
|
||||
}
|
||||
case Roles::WorkersModeRole: {
|
||||
return m_protocolConfig.workersMode.isEmpty()
|
||||
? QString(protocols::mtProxy::workersModeAuto)
|
||||
: m_protocolConfig.workersMode;
|
||||
}
|
||||
case Roles::WorkersRole: {
|
||||
return m_protocolConfig.workers.isEmpty() ? QString(protocols::mtProxy::defaultWorkers)
|
||||
: m_protocolConfig.workers;
|
||||
}
|
||||
case Roles::NatEnabledRole: {
|
||||
return m_protocolConfig.natEnabled;
|
||||
}
|
||||
case Roles::NatInternalIpRole: {
|
||||
return m_protocolConfig.natInternalIp;
|
||||
}
|
||||
case Roles::NatExternalIpRole: {
|
||||
return m_protocolConfig.natExternalIp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::updateModel(amnezia::DockerContainer container,
|
||||
const amnezia::MtProxyProtocolConfig &protocolConfig) {
|
||||
beginResetModel();
|
||||
m_container = container;
|
||||
m_protocolConfig = protocolConfig;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::updateModel(const QJsonObject &config) {
|
||||
beginResetModel();
|
||||
|
||||
m_fullConfig = config;
|
||||
m_protocolConfig = MtProxyProtocolConfig::fromJson(config.value(configKey::mtproxy).toObject());
|
||||
if (m_protocolConfig.port.isEmpty()) m_protocolConfig.port = protocols::mtProxy::defaultPort;
|
||||
if (m_protocolConfig.transportMode.isEmpty()) m_protocolConfig.transportMode = protocols::mtProxy::transportModeStandard;
|
||||
if (m_protocolConfig.workersMode.isEmpty()) m_protocolConfig.workersMode = protocols::mtProxy::workersModeAuto;
|
||||
if (m_protocolConfig.workers.isEmpty()) m_protocolConfig.workers = protocols::mtProxy::defaultWorkers;
|
||||
{
|
||||
QString tagIn = sanitizeMtProxyTagFieldText(m_protocolConfig.tag);
|
||||
if (!isValidMtProxyTag(tagIn)) {
|
||||
tagIn.clear();
|
||||
}
|
||||
m_protocolConfig.tag = tagIn;
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QJsonObject MtProxyConfigModel::getConfig() {
|
||||
m_fullConfig.insert(configKey::mtproxy, m_protocolConfig.toJson());
|
||||
return m_fullConfig;
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::generateSecret() {
|
||||
// Generate 16 random bytes = 32 hex chars
|
||||
QString secret;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
quint32 byte = QRandomGenerator::global()->bounded(256);
|
||||
secret += QString("%1").arg(byte, 2, 16, QChar('0'));
|
||||
}
|
||||
|
||||
m_protocolConfig.secret = secret;
|
||||
emit dataChanged(index(0), index(0), QList<int>{SecretRole});
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setSecret(const QString &secret) {
|
||||
if (secret.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), secret, SecretRole);
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::validateAndSetSecret(const QString &rawSecret) {
|
||||
if (!QRegularExpression("^[0-9a-fA-F]{32}$").match(rawSecret).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
setData(index(0), rawSecret, SecretRole);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setPort(const QString &port) {
|
||||
setData(index(0), port, PortRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setTag(const QString &tag) {
|
||||
setData(index(0), tag, TagRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setPublicHost(const QString &host) {
|
||||
const QString t = host.trimmed();
|
||||
if (!isValidPublicHost(t)) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), t, PublicHostRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setTransportMode(const QString &mode) {
|
||||
setData(index(0), mode, TransportModeRole);
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::getTransportMode() const {
|
||||
return m_protocolConfig.transportMode.isEmpty()
|
||||
? QString(protocols::mtProxy::transportModeStandard)
|
||||
: m_protocolConfig.transportMode;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::getTlsDomain() const {
|
||||
return m_protocolConfig.tlsDomain.isEmpty()
|
||||
? QString(protocols::mtProxy::defaultTlsDomain)
|
||||
: m_protocolConfig.tlsDomain;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::getPublicHost() const {
|
||||
return m_protocolConfig.publicHost;
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setTlsDomain(const QString &domain) {
|
||||
const QString t = domain.trimmed();
|
||||
if (!isValidFakeTlsDomain(t)) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), t, TlsDomainRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setWorkersMode(const QString &mode) {
|
||||
setData(index(0), mode, WorkersModeRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setWorkers(const QString &workers) {
|
||||
setData(index(0), workers, WorkersRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setNatEnabled(bool enabled) {
|
||||
setData(index(0), enabled, NatEnabledRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setNatInternalIp(const QString &ip) {
|
||||
const QString t = ip.trimmed();
|
||||
if (!isValidOptionalIpv4(t)) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), t, NatInternalIpRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setNatExternalIp(const QString &ip) {
|
||||
const QString t = ip.trimmed();
|
||||
if (!isValidOptionalIpv4(t)) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), t, NatExternalIpRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::addAdditionalSecret() {
|
||||
QString newSecret;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
quint32 byte = QRandomGenerator::global()->bounded(256);
|
||||
newSecret += QString("%1").arg(byte, 2, 16, QChar('0'));
|
||||
}
|
||||
|
||||
m_protocolConfig.additionalSecrets.append(newSecret);
|
||||
emit dataChanged(index(0), index(0), QList<int>{AdditionalSecretsRole});
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::removeAdditionalSecret(int idx) {
|
||||
if (idx < 0 || idx >= m_protocolConfig.additionalSecrets.size()) {
|
||||
return;
|
||||
}
|
||||
m_protocolConfig.additionalSecrets.removeAt(idx);
|
||||
emit dataChanged(index(0), index(0), QList<int>{AdditionalSecretsRole});
|
||||
}
|
||||
|
||||
QVariantList MtProxyConfigModel::additionalSecretsList() const {
|
||||
QVariantList out;
|
||||
out.reserve(m_protocolConfig.additionalSecrets.size());
|
||||
for (const auto &s: m_protocolConfig.additionalSecrets) {
|
||||
if (!s.isEmpty()) {
|
||||
out.append(s);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setEnabled(bool enabled) {
|
||||
m_protocolConfig.isEnabled = enabled;
|
||||
emit dataChanged(index(0), index(0), QList<int>{IsEnabledRole});
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::generateQrCode(const QString &text) {
|
||||
if (text.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
auto qr = qrCodeUtils::generateQrCode(text.toUtf8());
|
||||
return qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::defaultTlsDomain() const {
|
||||
return protocols::mtProxy::defaultTlsDomain;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::defaultPort() const {
|
||||
return protocols::mtProxy::defaultPort;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::defaultWorkers() const {
|
||||
return protocols::mtProxy::defaultWorkers;
|
||||
}
|
||||
|
||||
int MtProxyConfigModel::maxWorkers() const {
|
||||
return protocols::mtProxy::maxWorkers;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::transportModeStandard() const {
|
||||
return protocols::mtProxy::transportModeStandard;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::transportModeFakeTLS() const {
|
||||
return protocols::mtProxy::transportModeFakeTLS;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::workersModeAuto() const {
|
||||
return protocols::mtProxy::workersModeAuto;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::workersModeManual() const {
|
||||
return protocols::mtProxy::workersModeManual;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isValidPublicHost(const QString &host) const {
|
||||
const QString t = host.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (t.length() > 253) {
|
||||
return false;
|
||||
}
|
||||
QHostAddress a(t);
|
||||
if (a.protocol() == QHostAddress::IPv4Protocol) {
|
||||
return NetworkUtilities::checkIPv4Format(t);
|
||||
}
|
||||
if (a.protocol() == QHostAddress::IPv6Protocol) {
|
||||
return true;
|
||||
}
|
||||
static const QRegularExpression onlyAsciiDigits(QStringLiteral(R"(^\d+$)"));
|
||||
if (onlyAsciiDigits.match(t).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
return NetworkUtilities::domainRegExp().exactMatch(t);
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isPublicHostInputAllowed(const QString &text) const {
|
||||
return mtproxyPublicHostInputAllowed(text);
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isPublicHostTypingIncomplete(const QString &text) const {
|
||||
const QString t = text.trimmed();
|
||||
if (isValidPublicHost(t)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static const QRegularExpression onlyDigitDot(QStringLiteral(R"(^[0-9.]+$)"));
|
||||
if (onlyDigitDot.match(t).hasMatch()) {
|
||||
if (t.endsWith(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
const QStringList parts = t.split(QLatin1Char('.'), Qt::KeepEmptyParts);
|
||||
if (parts.size() < 4) {
|
||||
return true;
|
||||
}
|
||||
for (const QString &part: parts) {
|
||||
if (part.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (t.contains(QLatin1Char(':'))) {
|
||||
if (t.contains(QLatin1String(":::"))) {
|
||||
return false;
|
||||
}
|
||||
if (t.endsWith(QLatin1Char(':'))) {
|
||||
return true;
|
||||
}
|
||||
QHostAddress a(t);
|
||||
if (a.protocol() == QHostAddress::IPv6Protocol) {
|
||||
return false;
|
||||
}
|
||||
if (!t.contains(QLatin1String("::")) && t.count(QLatin1Char(':')) < 7 && !t.contains(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!t.contains(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isValidMtProxyTag(const QString &tag) const {
|
||||
if (tag.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
static const QRegularExpression re(
|
||||
QStringLiteral("^([0-9a-fA-F]{%1})$").arg(protocols::mtProxy::botTagHexLength));
|
||||
return re.match(tag).hasMatch();
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isMtProxyTagTypingIncomplete(const QString &text) const {
|
||||
const QString t = text.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
static const QRegularExpression hexOnly(QStringLiteral(R"(^[0-9a-fA-F]*$)"));
|
||||
if (!hexOnly.match(t).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
return t.size() < protocols::mtProxy::botTagHexLength;
|
||||
}
|
||||
|
||||
int MtProxyConfigModel::mtProxyBotTagHexLength() const {
|
||||
return protocols::mtProxy::botTagHexLength;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isValidFakeTlsDomain(const QString &domain) const {
|
||||
const QString t = domain.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (t.length() > 253) {
|
||||
return false;
|
||||
}
|
||||
QHostAddress addr;
|
||||
if (addr.setAddress(t)) {
|
||||
return false;
|
||||
}
|
||||
static const QRegularExpression onlyAsciiDigits(QStringLiteral(R"(^\d+$)"));
|
||||
if (onlyAsciiDigits.match(t).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
QRegExp re(NetworkUtilities::domainRegExp());
|
||||
re.setCaseSensitivity(Qt::CaseInsensitive);
|
||||
if (!re.exactMatch(t)) {
|
||||
return false;
|
||||
}
|
||||
// ee + 32 hex (base secret) + hex(UTF-8 domain); keep headroom under typical client limits.
|
||||
if (t.toUtf8().size() > 111) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::clipboardText() const {
|
||||
if (QClipboard *c = QGuiApplication::clipboard()) {
|
||||
return c->text();
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizeFakeTlsDomainFieldText(const QString &input) const {
|
||||
const QString t = normalizeFakeTlsDomainInput(input);
|
||||
QString out;
|
||||
out.reserve(t.size());
|
||||
for (const QChar &c: t) {
|
||||
const ushort u = c.unicode();
|
||||
const bool letter = (u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z');
|
||||
const bool digit = (u >= '0' && u <= '9');
|
||||
if (letter || digit || u == '.' || u == '-') {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
if (out.size() > 253) {
|
||||
out.truncate(253);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isFakeTlsDomainInputAllowed(const QString &text) const {
|
||||
if (text.length() > 253) {
|
||||
return false;
|
||||
}
|
||||
static const QRegularExpression re(QStringLiteral(R"(^[a-zA-Z0-9.-]*$)"));
|
||||
return re.match(text).hasMatch();
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizePublicHostFieldText(const QString &input) const {
|
||||
QString out;
|
||||
const int cap = qMin(input.size(), 253);
|
||||
out.reserve(cap);
|
||||
for (const QChar &c: input) {
|
||||
if (out.size() >= 253) {
|
||||
break;
|
||||
}
|
||||
const ushort u = c.unicode();
|
||||
if ((u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9') || u == '.' || u == ':' ||
|
||||
u == '-') {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizePortFieldText(const QString &input) const {
|
||||
QString out;
|
||||
out.reserve(qMin(input.size(), 5));
|
||||
for (const QChar &c: input) {
|
||||
const ushort u = c.unicode();
|
||||
if (u >= '0' && u <= '9' && out.size() < 5) {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizeMtProxyTagFieldText(const QString &input) const {
|
||||
QString trimmed = input.trimmed();
|
||||
if (trimmed.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
|
||||
trimmed = trimmed.mid(2).trimmed();
|
||||
}
|
||||
// Prefer a contiguous 32-hex run (paste from bot message with extra text).
|
||||
static const QRegularExpression runHex(QStringLiteral(R"(([0-9a-fA-F]{32}))"));
|
||||
const QRegularExpressionMatch m = runHex.match(trimmed);
|
||||
if (m.hasMatch()) {
|
||||
return m.captured(1);
|
||||
}
|
||||
const int cap = protocols::mtProxy::botTagHexLength;
|
||||
QString out;
|
||||
out.reserve(qMin(trimmed.size(), cap));
|
||||
for (const QChar &c: trimmed) {
|
||||
if (out.size() >= cap) {
|
||||
break;
|
||||
}
|
||||
const ushort u = c.unicode();
|
||||
if ((u >= '0' && u <= '9') || (u >= 'a' && u <= 'f') || (u >= 'A' && u <= 'F')) {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizeWorkersFieldText(const QString &input) const {
|
||||
QString out;
|
||||
out.reserve(qMin(input.size(), 3));
|
||||
for (const QChar &c: input) {
|
||||
const ushort u = c.unicode();
|
||||
if (u >= '0' && u <= '9' && out.size() < 3) {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizeOptionalIpv4FieldText(const QString &input) const {
|
||||
QString out;
|
||||
out.reserve(qMin(input.size(), 15));
|
||||
for (const QChar &c: input) {
|
||||
if (out.size() >= 15) {
|
||||
break;
|
||||
}
|
||||
const ushort u = c.unicode();
|
||||
if ((u >= '0' && u <= '9') || u == '.') {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::normalizeFakeTlsDomainInput(const QString &input) const {
|
||||
QString t = input.trimmed();
|
||||
if (t.startsWith(QLatin1String("https://"), Qt::CaseInsensitive)) {
|
||||
t = t.mid(8);
|
||||
} else if (t.startsWith(QLatin1String("http://"), Qt::CaseInsensitive)) {
|
||||
t = t.mid(7);
|
||||
}
|
||||
if (const int slash = t.indexOf(QLatin1Char('/')); slash >= 0) {
|
||||
t = t.left(slash);
|
||||
}
|
||||
if (const int at = t.indexOf(QLatin1Char('@')); at >= 0) {
|
||||
t = t.mid(at + 1);
|
||||
}
|
||||
if (const int colon = t.indexOf(QLatin1Char(':')); colon >= 0) {
|
||||
t = t.left(colon);
|
||||
}
|
||||
if (t.startsWith(QLatin1String("www."), Qt::CaseInsensitive)) {
|
||||
const QString rest = t.mid(4);
|
||||
if (rest.contains(QLatin1Char('.'))) {
|
||||
t = rest;
|
||||
}
|
||||
}
|
||||
return t.trimmed();
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isFakeTlsDomainTypingIncomplete(const QString &text) const {
|
||||
const QString t = text.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (isValidFakeTlsDomain(t)) {
|
||||
return false;
|
||||
}
|
||||
if (t.contains(QLatin1Char('/')) || t.contains(QLatin1Char(':')) || t.contains(QLatin1Char('@'))
|
||||
|| t.contains(QLatin1Char(' '))) {
|
||||
return false;
|
||||
}
|
||||
if (t.contains(QLatin1String(".."))) {
|
||||
return false;
|
||||
}
|
||||
if (!t.contains(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
if (t.endsWith(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
static const QRegularExpression legalPartial(QStringLiteral(R"(^[a-zA-Z0-9.-]*$)"));
|
||||
if (!legalPartial.match(t).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isValidOptionalIpv4(const QString &ip) const {
|
||||
const QString t = ip.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return NetworkUtilities::checkIPv4Format(t);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> MtProxyConfigModel::roleNames() const {
|
||||
QHash<int, QByteArray> roles;
|
||||
|
||||
roles[PortRole] = "port";
|
||||
roles[SecretRole] = "secret";
|
||||
roles[TagRole] = "tag";
|
||||
roles[TgLinkRole] = "tgLink";
|
||||
roles[TmeLinkRole] = "tmeLink";
|
||||
roles[IsEnabledRole] = "isEnabled";
|
||||
roles[PublicHostRole] = "publicHost";
|
||||
roles[TransportModeRole] = "transportMode";
|
||||
roles[TlsDomainRole] = "tlsDomain";
|
||||
roles[AdditionalSecretsRole] = "additionalSecrets";
|
||||
roles[WorkersModeRole] = "workersMode";
|
||||
roles[WorkersRole] = "workers";
|
||||
roles[NatEnabledRole] = "natEnabled";
|
||||
roles[NatInternalIpRole] = "natInternalIp";
|
||||
roles[NatExternalIpRole] = "natExternalIp";
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
amnezia::MtProxyProtocolConfig MtProxyConfigModel::getProtocolConfig() {
|
||||
return m_protocolConfig;
|
||||
}
|
||||
156
client/ui/models/services/mtProxyConfigModel.h
Normal file
156
client/ui/models/services/mtProxyConfigModel.h
Normal file
@@ -0,0 +1,156 @@
|
||||
#ifndef MTPROXYCONFIGMODEL_H
|
||||
#define MTPROXYCONFIGMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QRandomGenerator>
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
|
||||
class MtProxyConfigModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
PortRole = Qt::UserRole + 1,
|
||||
SecretRole,
|
||||
TagRole,
|
||||
TgLinkRole,
|
||||
TmeLinkRole,
|
||||
IsEnabledRole,
|
||||
PublicHostRole,
|
||||
TransportModeRole,
|
||||
TlsDomainRole,
|
||||
AdditionalSecretsRole,
|
||||
WorkersModeRole,
|
||||
WorkersRole,
|
||||
NatEnabledRole,
|
||||
NatInternalIpRole,
|
||||
NatExternalIpRole
|
||||
};
|
||||
|
||||
explicit MtProxyConfigModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
|
||||
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
public slots:
|
||||
|
||||
void updateModel(amnezia::DockerContainer container, const amnezia::MtProxyProtocolConfig &protocolConfig);
|
||||
|
||||
void updateModel(const QJsonObject &config);
|
||||
|
||||
QJsonObject getConfig();
|
||||
|
||||
amnezia::MtProxyProtocolConfig getProtocolConfig();
|
||||
|
||||
Q_INVOKABLE void generateSecret();
|
||||
|
||||
Q_INVOKABLE void setSecret(const QString &secret);
|
||||
|
||||
Q_INVOKABLE bool validateAndSetSecret(const QString &rawSecret);
|
||||
|
||||
Q_INVOKABLE void setPort(const QString &port);
|
||||
|
||||
Q_INVOKABLE void setTag(const QString &tag);
|
||||
|
||||
Q_INVOKABLE void setPublicHost(const QString &host);
|
||||
|
||||
Q_INVOKABLE void setTransportMode(const QString &mode);
|
||||
|
||||
Q_INVOKABLE QString getTransportMode() const;
|
||||
|
||||
Q_INVOKABLE QString getTlsDomain() const;
|
||||
|
||||
Q_INVOKABLE QString getPublicHost() const;
|
||||
|
||||
Q_INVOKABLE void setTlsDomain(const QString &domain);
|
||||
|
||||
Q_INVOKABLE void setWorkersMode(const QString &mode);
|
||||
|
||||
Q_INVOKABLE void setWorkers(const QString &workers);
|
||||
|
||||
Q_INVOKABLE void setNatEnabled(bool enabled);
|
||||
|
||||
Q_INVOKABLE void setNatInternalIp(const QString &ip);
|
||||
|
||||
Q_INVOKABLE void setNatExternalIp(const QString &ip);
|
||||
|
||||
Q_INVOKABLE void addAdditionalSecret();
|
||||
|
||||
Q_INVOKABLE void removeAdditionalSecret(int idx);
|
||||
/// Current `mtproxy_additional_secrets` list from in-memory config (for QML snapshot vs. unsaved adds).
|
||||
Q_INVOKABLE QVariantList additionalSecretsList() const;
|
||||
|
||||
Q_INVOKABLE QString generateQrCode(const QString &text);
|
||||
|
||||
Q_INVOKABLE void setEnabled(bool enabled);
|
||||
|
||||
Q_INVOKABLE QString defaultTlsDomain() const;
|
||||
|
||||
Q_INVOKABLE QString defaultPort() const;
|
||||
|
||||
Q_INVOKABLE QString defaultWorkers() const;
|
||||
|
||||
Q_INVOKABLE int maxWorkers() const;
|
||||
|
||||
Q_INVOKABLE QString transportModeStandard() const;
|
||||
|
||||
Q_INVOKABLE QString transportModeFakeTLS() const;
|
||||
|
||||
Q_INVOKABLE QString workersModeAuto() const;
|
||||
|
||||
Q_INVOKABLE QString workersModeManual() const;
|
||||
|
||||
Q_INVOKABLE bool isValidPublicHost(const QString &host) const;
|
||||
|
||||
Q_INVOKABLE bool isPublicHostInputAllowed(const QString &text) const;
|
||||
|
||||
Q_INVOKABLE bool isPublicHostTypingIncomplete(const QString &text) const;
|
||||
|
||||
Q_INVOKABLE bool isValidMtProxyTag(const QString &tag) const;
|
||||
|
||||
Q_INVOKABLE bool isMtProxyTagTypingIncomplete(const QString &text) const;
|
||||
|
||||
Q_INVOKABLE int mtProxyBotTagHexLength() const;
|
||||
|
||||
Q_INVOKABLE bool isValidFakeTlsDomain(const QString &domain) const;
|
||||
|
||||
Q_INVOKABLE QString normalizeFakeTlsDomainInput(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE QString sanitizeFakeTlsDomainFieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE bool isFakeTlsDomainInputAllowed(const QString &text) const;
|
||||
|
||||
Q_INVOKABLE QString clipboardText() const;
|
||||
|
||||
Q_INVOKABLE QString sanitizePublicHostFieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE QString sanitizePortFieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE QString sanitizeMtProxyTagFieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE QString sanitizeWorkersFieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE QString sanitizeOptionalIpv4FieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE bool isFakeTlsDomainTypingIncomplete(const QString &text) const;
|
||||
|
||||
Q_INVOKABLE bool isValidOptionalIpv4(const QString &ip) const;
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
amnezia::DockerContainer m_container;
|
||||
QJsonObject m_fullConfig;
|
||||
amnezia::MtProxyProtocolConfig m_protocolConfig;
|
||||
};
|
||||
|
||||
#endif // MTPROXYCONFIGMODEL_H
|
||||
406
client/ui/models/services/telemtConfigModel.cpp
Normal file
406
client/ui/models/services/telemtConfigModel.cpp
Normal file
@@ -0,0 +1,406 @@
|
||||
#include "telemtConfigModel.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include "core/utils/qrCodeUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "qrcodegen.hpp"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
TelemtConfigModel::TelemtConfigModel(QObject *parent) : QAbstractListModel(parent) {}
|
||||
|
||||
void TelemtConfigModel::applyDefaults(TelemtProtocolConfig &c) {
|
||||
if (c.port.isEmpty()) {
|
||||
c.port = QString::fromUtf8(protocols::telemt::defaultPort);
|
||||
}
|
||||
if (c.transportMode.isEmpty()) {
|
||||
c.transportMode = QString::fromUtf8(protocols::telemt::transportModeStandard);
|
||||
}
|
||||
if (c.workersMode.isEmpty()) {
|
||||
c.workersMode = QString::fromUtf8(protocols::telemt::workersModeAuto);
|
||||
}
|
||||
if (c.workers.isEmpty()) {
|
||||
c.workers = QString::fromUtf8(protocols::telemt::defaultWorkers);
|
||||
}
|
||||
if (c.userName.isEmpty()) {
|
||||
c.userName = QString::fromUtf8(protocols::telemt::defaultUserName);
|
||||
}
|
||||
}
|
||||
|
||||
int TelemtConfigModel::rowCount(const QModelIndex &parent) const {
|
||||
Q_UNUSED(parent);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool TelemtConfigModel::setData(const QModelIndex &index, const QVariant &value, int role) {
|
||||
if (!index.isValid() || index.row() != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (role) {
|
||||
case Roles::PortRole: {
|
||||
m_protocolConfig.port = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::SecretRole: {
|
||||
m_protocolConfig.secret = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::TagRole: {
|
||||
m_protocolConfig.tag = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::IsEnabledRole: {
|
||||
m_protocolConfig.isEnabled = value.toBool();
|
||||
break;
|
||||
}
|
||||
case Roles::PublicHostRole: {
|
||||
m_protocolConfig.publicHost = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::TransportModeRole: {
|
||||
m_protocolConfig.transportMode = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::TlsDomainRole: {
|
||||
m_protocolConfig.tlsDomain = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::AdditionalSecretsRole: {
|
||||
m_protocolConfig.additionalSecrets = value.toStringList();
|
||||
break;
|
||||
}
|
||||
case Roles::WorkersModeRole: {
|
||||
m_protocolConfig.workersMode = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::WorkersRole: {
|
||||
m_protocolConfig.workers = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::NatEnabledRole: {
|
||||
m_protocolConfig.natEnabled = value.toBool();
|
||||
break;
|
||||
}
|
||||
case Roles::NatInternalIpRole: {
|
||||
m_protocolConfig.natInternalIp = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::NatExternalIpRole: {
|
||||
m_protocolConfig.natExternalIp = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::MaskEnabledRole: {
|
||||
m_protocolConfig.maskEnabled = value.toBool();
|
||||
break;
|
||||
}
|
||||
case Roles::UseMiddleProxyRole: {
|
||||
m_protocolConfig.useMiddleProxy = value.toBool();
|
||||
break;
|
||||
}
|
||||
case Roles::TlsEmulationRole: {
|
||||
m_protocolConfig.tlsEmulation = value.toBool();
|
||||
break;
|
||||
}
|
||||
case Roles::UserNameRole: {
|
||||
m_protocolConfig.userName = value.toString();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
emit dataChanged(index, index, QList{role});
|
||||
return true;
|
||||
}
|
||||
|
||||
QVariant TelemtConfigModel::data(const QModelIndex &index, int role) const {
|
||||
if (!index.isValid() || index.row() != 0) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
switch (role) {
|
||||
case Roles::PortRole: {
|
||||
return m_protocolConfig.port.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultPort)
|
||||
: m_protocolConfig.port;
|
||||
}
|
||||
case Roles::SecretRole: {
|
||||
return m_protocolConfig.secret;
|
||||
}
|
||||
case Roles::TagRole: {
|
||||
return m_protocolConfig.tag;
|
||||
}
|
||||
case Roles::TgLinkRole: {
|
||||
return m_protocolConfig.tgLink;
|
||||
}
|
||||
case Roles::TmeLinkRole: {
|
||||
return m_protocolConfig.tmeLink;
|
||||
}
|
||||
case Roles::IsEnabledRole: {
|
||||
return m_protocolConfig.isEnabled;
|
||||
}
|
||||
case Roles::PublicHostRole: {
|
||||
return m_protocolConfig.publicHost.isEmpty() ? m_fullConfig.value(QString(configKey::hostName)).toString()
|
||||
: m_protocolConfig.publicHost;
|
||||
}
|
||||
case Roles::TransportModeRole: {
|
||||
return m_protocolConfig.transportMode.isEmpty() ? QString::fromUtf8(
|
||||
protocols::telemt::transportModeStandard)
|
||||
: m_protocolConfig.transportMode;
|
||||
}
|
||||
case Roles::TlsDomainRole: {
|
||||
return m_protocolConfig.tlsDomain;
|
||||
}
|
||||
case Roles::AdditionalSecretsRole: {
|
||||
return m_protocolConfig.additionalSecrets;
|
||||
}
|
||||
case Roles::WorkersModeRole: {
|
||||
return m_protocolConfig.workersMode.isEmpty() ? QString::fromUtf8(protocols::telemt::workersModeAuto)
|
||||
: m_protocolConfig.workersMode;
|
||||
}
|
||||
case Roles::WorkersRole: {
|
||||
return m_protocolConfig.workers.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultWorkers)
|
||||
: m_protocolConfig.workers;
|
||||
}
|
||||
case Roles::NatEnabledRole: {
|
||||
return m_protocolConfig.natEnabled;
|
||||
}
|
||||
case Roles::NatInternalIpRole: {
|
||||
return m_protocolConfig.natInternalIp;
|
||||
}
|
||||
case Roles::NatExternalIpRole: {
|
||||
return m_protocolConfig.natExternalIp;
|
||||
}
|
||||
case Roles::MaskEnabledRole: {
|
||||
return m_protocolConfig.maskEnabled;
|
||||
}
|
||||
case Roles::UseMiddleProxyRole: {
|
||||
return m_protocolConfig.useMiddleProxy;
|
||||
}
|
||||
case Roles::TlsEmulationRole: {
|
||||
return m_protocolConfig.tlsEmulation;
|
||||
}
|
||||
case Roles::UserNameRole: {
|
||||
return m_protocolConfig.userName.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultUserName)
|
||||
: m_protocolConfig.userName;
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void TelemtConfigModel::updateModel(DockerContainer container, const TelemtProtocolConfig &protocolConfig) {
|
||||
beginResetModel();
|
||||
m_container = container;
|
||||
m_protocolConfig = protocolConfig;
|
||||
applyDefaults(m_protocolConfig);
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void TelemtConfigModel::updateModel(const QJsonObject &config) {
|
||||
beginResetModel();
|
||||
|
||||
m_fullConfig = config;
|
||||
m_protocolConfig = TelemtProtocolConfig::fromJson(config.value(QString(configKey::telemt)).toObject());
|
||||
applyDefaults(m_protocolConfig);
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QJsonObject TelemtConfigModel::getConfig() {
|
||||
m_fullConfig.insert(QString(configKey::telemt), m_protocolConfig.toJson());
|
||||
return m_fullConfig;
|
||||
}
|
||||
|
||||
TelemtProtocolConfig TelemtConfigModel::getProtocolConfig() {
|
||||
return m_protocolConfig;
|
||||
}
|
||||
|
||||
void TelemtConfigModel::generateSecret() {
|
||||
QString secret;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
quint32 byte = QRandomGenerator::global()->bounded(256);
|
||||
secret += QString("%1").arg(byte, 2, 16, QChar('0'));
|
||||
}
|
||||
|
||||
m_protocolConfig.secret = secret;
|
||||
emit dataChanged(index(0), index(0), QList<int>{SecretRole});
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setSecret(const QString &secret) {
|
||||
if (secret.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), secret, SecretRole);
|
||||
}
|
||||
|
||||
bool TelemtConfigModel::validateAndSetSecret(const QString &rawSecret) {
|
||||
if (!QRegularExpression(QStringLiteral("^[0-9a-fA-F]{32}$")).match(rawSecret).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
setData(index(0), rawSecret, SecretRole);
|
||||
return true;
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setPort(const QString &port) {
|
||||
setData(index(0), port, PortRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setTag(const QString &tag) {
|
||||
setData(index(0), tag, TagRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setPublicHost(const QString &host) {
|
||||
setData(index(0), host, PublicHostRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setTransportMode(const QString &mode) {
|
||||
setData(index(0), mode, TransportModeRole);
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::getTransportMode() const {
|
||||
return m_protocolConfig.transportMode.isEmpty() ? QString::fromUtf8(protocols::telemt::transportModeStandard)
|
||||
: m_protocolConfig.transportMode;
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::getTlsDomain() const {
|
||||
return m_protocolConfig.tlsDomain.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultTlsDomain)
|
||||
: m_protocolConfig.tlsDomain;
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::getPublicHost() const {
|
||||
return m_protocolConfig.publicHost;
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setTlsDomain(const QString &domain) {
|
||||
setData(index(0), domain, TlsDomainRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setWorkersMode(const QString &mode) {
|
||||
setData(index(0), mode, WorkersModeRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setWorkers(const QString &workers) {
|
||||
setData(index(0), workers, WorkersRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setNatEnabled(bool enabled) {
|
||||
setData(index(0), enabled, NatEnabledRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setNatInternalIp(const QString &ip) {
|
||||
setData(index(0), ip, NatInternalIpRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setNatExternalIp(const QString &ip) {
|
||||
setData(index(0), ip, NatExternalIpRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setMaskEnabled(bool enabled) {
|
||||
setData(index(0), enabled, MaskEnabledRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setUseMiddleProxy(bool enabled) {
|
||||
setData(index(0), enabled, UseMiddleProxyRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setTlsEmulation(bool enabled) {
|
||||
setData(index(0), enabled, TlsEmulationRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setUserName(const QString &name) {
|
||||
setData(index(0), name, UserNameRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::addAdditionalSecret() {
|
||||
QString newSecret;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
quint32 byte = QRandomGenerator::global()->bounded(256);
|
||||
newSecret += QString("%1").arg(byte, 2, 16, QChar('0'));
|
||||
}
|
||||
|
||||
m_protocolConfig.additionalSecrets.append(newSecret);
|
||||
emit dataChanged(index(0), index(0), QList<int>{AdditionalSecretsRole});
|
||||
}
|
||||
|
||||
void TelemtConfigModel::removeAdditionalSecret(int idx) {
|
||||
if (idx < 0 || idx >= m_protocolConfig.additionalSecrets.size()) {
|
||||
return;
|
||||
}
|
||||
m_protocolConfig.additionalSecrets.removeAt(idx);
|
||||
emit dataChanged(index(0), index(0), QList<int>{AdditionalSecretsRole});
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setEnabled(bool enabled) {
|
||||
m_protocolConfig.isEnabled = enabled;
|
||||
emit dataChanged(index(0), index(0), QList<int>{IsEnabledRole});
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::generateQrCode(const QString &text) {
|
||||
if (text.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
auto qr = qrCodeUtils::generateQrCode(text.toUtf8());
|
||||
return qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::defaultTlsDomain() const {
|
||||
return QString::fromUtf8(protocols::telemt::defaultTlsDomain);
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::defaultPort() const {
|
||||
return QString::fromUtf8(protocols::telemt::defaultPort);
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::defaultWorkers() const {
|
||||
return QString::fromUtf8(protocols::telemt::defaultWorkers);
|
||||
}
|
||||
|
||||
int TelemtConfigModel::maxWorkers() const {
|
||||
return protocols::telemt::maxWorkers;
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::transportModeStandard() const {
|
||||
return QString::fromUtf8(protocols::telemt::transportModeStandard);
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::transportModeFakeTLS() const {
|
||||
return QString::fromUtf8(protocols::telemt::transportModeFakeTLS);
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::workersModeAuto() const {
|
||||
return QString::fromUtf8(protocols::telemt::workersModeAuto);
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::workersModeManual() const {
|
||||
return QString::fromUtf8(protocols::telemt::workersModeManual);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> TelemtConfigModel::roleNames() const {
|
||||
QHash<int, QByteArray> roles;
|
||||
|
||||
roles[PortRole] = "port";
|
||||
roles[SecretRole] = "secret";
|
||||
roles[TagRole] = "tag";
|
||||
roles[TgLinkRole] = "tgLink";
|
||||
roles[TmeLinkRole] = "tmeLink";
|
||||
roles[IsEnabledRole] = "isEnabled";
|
||||
roles[PublicHostRole] = "publicHost";
|
||||
roles[TransportModeRole] = "transportMode";
|
||||
roles[TlsDomainRole] = "tlsDomain";
|
||||
roles[AdditionalSecretsRole] = "additionalSecrets";
|
||||
roles[WorkersModeRole] = "workersMode";
|
||||
roles[WorkersRole] = "workers";
|
||||
roles[NatEnabledRole] = "natEnabled";
|
||||
roles[NatInternalIpRole] = "natInternalIp";
|
||||
roles[NatExternalIpRole] = "natExternalIp";
|
||||
roles[MaskEnabledRole] = "maskEnabled";
|
||||
roles[UseMiddleProxyRole] = "useMiddleProxy";
|
||||
roles[TlsEmulationRole] = "tlsEmulation";
|
||||
roles[UserNameRole] = "userName";
|
||||
|
||||
return roles;
|
||||
}
|
||||
130
client/ui/models/services/telemtConfigModel.h
Normal file
130
client/ui/models/services/telemtConfigModel.h
Normal file
@@ -0,0 +1,130 @@
|
||||
#ifndef TELEMTCONFIGMODEL_H
|
||||
#define TELEMTCONFIGMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonObject>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
|
||||
class TelemtConfigModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
PortRole = Qt::UserRole + 1,
|
||||
SecretRole,
|
||||
TagRole,
|
||||
TgLinkRole,
|
||||
TmeLinkRole,
|
||||
IsEnabledRole,
|
||||
PublicHostRole,
|
||||
TransportModeRole,
|
||||
TlsDomainRole,
|
||||
AdditionalSecretsRole,
|
||||
WorkersModeRole,
|
||||
WorkersRole,
|
||||
NatEnabledRole,
|
||||
NatInternalIpRole,
|
||||
NatExternalIpRole,
|
||||
MaskEnabledRole,
|
||||
UseMiddleProxyRole,
|
||||
TlsEmulationRole,
|
||||
UserNameRole
|
||||
};
|
||||
|
||||
explicit TelemtConfigModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
|
||||
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
public slots:
|
||||
|
||||
void updateModel(amnezia::DockerContainer container, const amnezia::TelemtProtocolConfig &protocolConfig);
|
||||
|
||||
void updateModel(const QJsonObject &config);
|
||||
|
||||
QJsonObject getConfig();
|
||||
|
||||
amnezia::TelemtProtocolConfig getProtocolConfig();
|
||||
|
||||
Q_INVOKABLE void generateSecret();
|
||||
|
||||
Q_INVOKABLE void setSecret(const QString &secret);
|
||||
|
||||
Q_INVOKABLE bool validateAndSetSecret(const QString &rawSecret);
|
||||
|
||||
Q_INVOKABLE void setPort(const QString &port);
|
||||
|
||||
Q_INVOKABLE void setTag(const QString &tag);
|
||||
|
||||
Q_INVOKABLE void setPublicHost(const QString &host);
|
||||
|
||||
Q_INVOKABLE void setTransportMode(const QString &mode);
|
||||
|
||||
Q_INVOKABLE QString getTransportMode() const;
|
||||
|
||||
Q_INVOKABLE QString getTlsDomain() const;
|
||||
|
||||
Q_INVOKABLE QString getPublicHost() const;
|
||||
|
||||
Q_INVOKABLE void setTlsDomain(const QString &domain);
|
||||
|
||||
Q_INVOKABLE void setWorkersMode(const QString &mode);
|
||||
|
||||
Q_INVOKABLE void setWorkers(const QString &workers);
|
||||
|
||||
Q_INVOKABLE void setNatEnabled(bool enabled);
|
||||
|
||||
Q_INVOKABLE void setNatInternalIp(const QString &ip);
|
||||
|
||||
Q_INVOKABLE void setNatExternalIp(const QString &ip);
|
||||
|
||||
Q_INVOKABLE void addAdditionalSecret();
|
||||
|
||||
Q_INVOKABLE void removeAdditionalSecret(int idx);
|
||||
|
||||
Q_INVOKABLE QString generateQrCode(const QString &text);
|
||||
|
||||
Q_INVOKABLE void setEnabled(bool enabled);
|
||||
|
||||
Q_INVOKABLE void setMaskEnabled(bool enabled);
|
||||
|
||||
Q_INVOKABLE void setUseMiddleProxy(bool enabled);
|
||||
|
||||
Q_INVOKABLE void setTlsEmulation(bool enabled);
|
||||
|
||||
Q_INVOKABLE void setUserName(const QString &name);
|
||||
|
||||
Q_INVOKABLE QString defaultTlsDomain() const;
|
||||
|
||||
Q_INVOKABLE QString defaultPort() const;
|
||||
|
||||
Q_INVOKABLE QString defaultWorkers() const;
|
||||
|
||||
Q_INVOKABLE int maxWorkers() const;
|
||||
|
||||
Q_INVOKABLE QString transportModeStandard() const;
|
||||
|
||||
Q_INVOKABLE QString transportModeFakeTLS() const;
|
||||
|
||||
Q_INVOKABLE QString workersModeAuto() const;
|
||||
|
||||
Q_INVOKABLE QString workersModeManual() const;
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
static void applyDefaults(amnezia::TelemtProtocolConfig &c);
|
||||
|
||||
amnezia::DockerContainer m_container = amnezia::DockerContainer::None;
|
||||
QJsonObject m_fullConfig;
|
||||
amnezia::TelemtProtocolConfig m_protocolConfig;
|
||||
};
|
||||
|
||||
#endif // TELEMTCONFIGMODEL_H
|
||||
127
client/ui/models/utils/mtproxy_public_host_input.cpp
Normal file
127
client/ui/models/utils/mtproxy_public_host_input.cpp
Normal file
@@ -0,0 +1,127 @@
|
||||
#include "mtproxy_public_host_input.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
namespace {
|
||||
|
||||
bool ipv4OctetTokenOk(const QString &s) {
|
||||
static const QRegularExpression re(QStringLiteral(R"(^\d{1,3}$)"));
|
||||
if (!re.match(s).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
bool ok = false;
|
||||
const int n = s.toInt(&ok);
|
||||
return ok && n >= 0 && n <= 255;
|
||||
}
|
||||
|
||||
// Reject labels like "312edweqwe" (digits >255 then letters).
|
||||
bool labelHasInvalidOctetLikePrefixBeforeLetters(const QString &label) {
|
||||
static const QRegularExpression re(QStringLiteral(R"(^(\d+)([a-zA-Z].*)$)"));
|
||||
const QRegularExpressionMatch m = re.match(label);
|
||||
if (!m.hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
const QString digits = m.captured(1);
|
||||
if (digits.length() > 3) {
|
||||
return true;
|
||||
}
|
||||
bool ok = false;
|
||||
const int n = digits.toInt(&ok);
|
||||
if (!ok) {
|
||||
return true;
|
||||
}
|
||||
if (n > 255) {
|
||||
return true;
|
||||
}
|
||||
// Do not restrict n≤255 + letters here (e.g. "123mlkjh.example.com"); four-segment IPv4+junk is handled below.
|
||||
return false;
|
||||
}
|
||||
|
||||
// "123.123wqqweqweqweqwe" — first label is a real octet, second looks like an octet glued to letters (not "123.45").
|
||||
bool looksLikeTwoSegmentOctetThenDigitLetterGlue(const QString &text) {
|
||||
const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts);
|
||||
if (parts.size() != 2) {
|
||||
return false;
|
||||
}
|
||||
if (!ipv4OctetTokenOk(parts.at(0))) {
|
||||
return false;
|
||||
}
|
||||
const QString &p1 = parts.at(1);
|
||||
static const QRegularExpression digitThenLetter(QStringLiteral(R"(^\d+[a-zA-Z])"));
|
||||
if (!digitThenLetter.match(p1).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
return !ipv4OctetTokenOk(p1);
|
||||
}
|
||||
|
||||
// "a.b.c.djunk" where first three parts are pure octets and last part has digits then letters (e.g. "123wdqweqweqwe").
|
||||
bool looksLikeFourOctetIpv4WithGarbageInLastSegment(const QString &text) {
|
||||
const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts);
|
||||
if (parts.size() != 4) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (!ipv4OctetTokenOk(parts.at(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
static const QRegularExpression digitThenLetter(QStringLiteral(R"(^\d+[a-zA-Z])"));
|
||||
return digitThenLetter.match(parts.at(3)).hasMatch();
|
||||
}
|
||||
|
||||
bool hostLabelsRejectBrokenDigitLetterMix(const QString &text) {
|
||||
if (looksLikeTwoSegmentOctetThenDigitLetterGlue(text)) {
|
||||
return false;
|
||||
}
|
||||
if (looksLikeFourOctetIpv4WithGarbageInLastSegment(text)) {
|
||||
return false;
|
||||
}
|
||||
const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts);
|
||||
for (const QString &part: parts) {
|
||||
if (labelHasInvalidOctetLikePrefixBeforeLetters(part)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool mtproxyPublicHostInputAllowed(const QString &text) {
|
||||
if (text.length() > 253) {
|
||||
return false;
|
||||
}
|
||||
static const QRegularExpression allowed(QStringLiteral(R"(^[a-zA-Z0-9.:\-]*$)"));
|
||||
if (!allowed.match(text).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
static const QRegularExpression onlyDigits(QStringLiteral(R"(^\d+$)"));
|
||||
if (onlyDigits.match(text).hasMatch() && text.length() > 3) {
|
||||
return false;
|
||||
}
|
||||
static const QRegularExpression onlyDigitDot(QStringLiteral(R"(^[0-9.]+$)"));
|
||||
if (!text.isEmpty() && onlyDigitDot.match(text).hasMatch()) {
|
||||
static const QRegularExpression ipv4Partial(QStringLiteral(R"(^(\d{1,3}\.){0,3}\d{0,3}$)"));
|
||||
return ipv4Partial.match(text).hasMatch();
|
||||
}
|
||||
if (text.contains(QLatin1Char(':'))) {
|
||||
static const QRegularExpression ipv6Chars(QStringLiteral(R"(^[0-9a-fA-F:.]*$)"));
|
||||
if (!ipv6Chars.match(text).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
if (text.size() > 45) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!hostLabelsRejectBrokenDigitLetterMix(text)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
PublicHostInputValidator::PublicHostInputValidator(QObject *parent) : QValidator(parent) {}
|
||||
|
||||
QValidator::State PublicHostInputValidator::validate(QString &input, int &pos) const {
|
||||
Q_UNUSED(pos)
|
||||
return mtproxyPublicHostInputAllowed(input) ? Acceptable : Invalid;
|
||||
}
|
||||
20
client/ui/models/utils/mtproxy_public_host_input.h
Normal file
20
client/ui/models/utils/mtproxy_public_host_input.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef MTPROXY_PUBLIC_HOST_INPUT_H
|
||||
#define MTPROXY_PUBLIC_HOST_INPUT_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <QValidator>
|
||||
|
||||
/// Shared rules for public host field (IPv4 dotted partial, IPv6 hex, FQDN ASCII).
|
||||
bool mtproxyPublicHostInputAllowed(const QString &text);
|
||||
|
||||
class PublicHostInputValidator : public QValidator {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PublicHostInputValidator(QObject *parent = nullptr);
|
||||
|
||||
QValidator::State validate(QString &input, int &pos) const override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -45,6 +45,12 @@ ListViewType {
|
||||
PageController.goToPage(PageEnum.PageProtocolRaw)
|
||||
} else if (isDns) {
|
||||
PageController.goToPage(PageEnum.PageServiceDnsSettings)
|
||||
} else if (isMtProxy) {
|
||||
MtProxyConfigModel.updateModel(config)
|
||||
PageController.goToPage(PageEnum.PageServiceMtProxySettings)
|
||||
} else if (isTelemt) {
|
||||
TelemtConfigModel.updateModel(config)
|
||||
PageController.goToPage(PageEnum.PageServiceTelemtSettings)
|
||||
} else {
|
||||
InstallController.updateProtocols(ServersUiController.getServerId(ServersUiController.processedServerIndex), containerIndex)
|
||||
PageController.goToPage(PageEnum.PageSettingsServerProtocol)
|
||||
|
||||
@@ -31,6 +31,9 @@ ListViewType {
|
||||
|
||||
function triggerCurrentItem() {
|
||||
var item = root.itemAtIndex(selectedIndex)
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
item.selectable.clicked()
|
||||
}
|
||||
|
||||
|
||||
61
client/ui/qml/Controls2/MinMaxRowType.qml
Normal file
61
client/ui/qml/Controls2/MinMaxRowType.qml
Normal file
@@ -0,0 +1,61 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Style 1.0
|
||||
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
|
||||
// MinMaxRowType — two side-by-side labeled text fields: Min / Max
|
||||
// Usage:
|
||||
// MinMaxRowType {
|
||||
// minValue: "0"
|
||||
// maxValue: "0"
|
||||
// onMinChanged: someProperty = val
|
||||
// onMaxChanged: someProperty = val
|
||||
// }
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string minValue: "0"
|
||||
property string maxValue: "0"
|
||||
|
||||
signal minChanged(string val)
|
||||
signal maxChanged(string val)
|
||||
|
||||
implicitHeight: row.implicitHeight
|
||||
implicitWidth: row.implicitWidth
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
// Min field
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
headerText: qsTr("Min")
|
||||
textField.text: root.minValue
|
||||
textField.validator: IntValidator { bottom: 0 }
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== root.minValue) {
|
||||
root.minChanged(textField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Max field
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
headerText: qsTr("Max")
|
||||
textField.text: root.maxValue
|
||||
textField.validator: IntValidator { bottom: 0 }
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== root.maxValue) {
|
||||
root.maxChanged(textField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ Item {
|
||||
id: root
|
||||
|
||||
property string headerText
|
||||
property string subtitleText // optional line under header (e.g. default value hint)
|
||||
property string headerTextDisabledColor: AmneziaStyle.color.charcoalGray
|
||||
property string headerTextColor: AmneziaStyle.color.mutedGray
|
||||
|
||||
@@ -84,6 +85,15 @@ Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SmallTextType {
|
||||
text: root.subtitleText
|
||||
visible: root.subtitleText !== ""
|
||||
color: AmneziaStyle.color.charcoalGray
|
||||
font.pixelSize: 13
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: visible ? 2 : 0
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: textField
|
||||
|
||||
|
||||
125
client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml
Normal file
125
client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml
Normal file
@@ -0,0 +1,125 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("Flow")
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("Empty")
|
||||
checked: flow === ""
|
||||
onClicked: flow = ""
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: "xtls-rprx-vision"
|
||||
checked: flow === "xtls-rprx-vision"
|
||||
onClicked: flow = "xtls-rprx-vision"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: "xtls-rprx-vision-udp443"
|
||||
checked: flow === "xtls-rprx-vision-udp443"
|
||||
onClicked: flow = "xtls-rprx-vision-udp443"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
|
||||
return
|
||||
}
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
||||
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
|
||||
}
|
||||
var noButtonFunction = function () {
|
||||
if (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
292
client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml
Normal file
292
client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml
Normal file
@@ -0,0 +1,292 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("Security")
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("None")
|
||||
checked: security === "none"
|
||||
onClicked: security = "none"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("TLS")
|
||||
checked: security === "tls"
|
||||
onClicked: security = "tls"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("Reality")
|
||||
checked: security === "reality"
|
||||
onClicked: security = "reality"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── TLS fields ────────────────────────────────────────────
|
||||
ColumnLayout {
|
||||
visible: security === "tls"
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
DropDownType {
|
||||
id: tlsAlpnDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: alpn
|
||||
descriptionText: qsTr("ALPN")
|
||||
headerText: qsTr("ALPN")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.alpnOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
alpn = selectedText
|
||||
tlsAlpnDropDown.text = selectedText
|
||||
tlsAlpnDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === alpn) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
tlsAlpnDropDown.text = alpn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: tlsFingerprintDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: fingerprint
|
||||
descriptionText: qsTr("Fingerprint")
|
||||
headerText: qsTr("Fingerprint")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.fingerprintOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
fingerprint = selectedText
|
||||
tlsFingerprintDropDown.text = selectedText
|
||||
tlsFingerprintDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === fingerprint) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
tlsFingerprintDropDown.text = fingerprint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Server Name (SNI)")
|
||||
textField.text: sni
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== sni) sni = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reality fields ────────────────────────────────────────
|
||||
ColumnLayout {
|
||||
visible: security === "reality"
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
DropDownType {
|
||||
id: realityFingerprintDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: fingerprint
|
||||
descriptionText: qsTr("Fingerprint")
|
||||
headerText: qsTr("Fingerprint")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.fingerprintOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
fingerprint = selectedText
|
||||
realityFingerprintDropDown.text = selectedText
|
||||
realityFingerprintDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === fingerprint) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
realityFingerprintDropDown.text = fingerprint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Server Name (SNI)")
|
||||
textField.text: sni
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== sni) sni = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
|
||||
return
|
||||
}
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
||||
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
|
||||
}
|
||||
var noButtonFunction = function () {
|
||||
if (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,20 @@ import "../Components"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
function formatTransport(value) {
|
||||
if (value === "raw") return "RAW (TCP)"
|
||||
if (value === "xhttp") return "XHTTP"
|
||||
if (value === "mkcp") return "mKCP"
|
||||
return value
|
||||
}
|
||||
|
||||
function formatSecurity(value) {
|
||||
if (value === "none") return "None"
|
||||
if (value === "tls") return "TLS"
|
||||
if (value === "reality") return "Reality"
|
||||
return value
|
||||
}
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
@@ -50,88 +64,125 @@ PageType {
|
||||
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
headerText: qsTr("XRay settings")
|
||||
Layout.topMargin: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
headerText: qsTr("XRay VLESS settings")
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignRight
|
||||
implicitWidth: 40
|
||||
implicitHeight: 40
|
||||
image: "qrc:/images/controls/more-vertical.svg"
|
||||
imageColor: AmneziaStyle.color.paleGray
|
||||
onClicked: PageController.goToPage(PageEnum.PageProtocolXraySnapshots)
|
||||
}
|
||||
}
|
||||
|
||||
LabelTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 4
|
||||
text: qsTr("More about settings")
|
||||
color: AmneziaStyle.color.goldenApricot
|
||||
font.pixelSize: 16
|
||||
lineHeight: 24 + LanguageUiController.getLineHeightAppend()
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: Qt.openUrlExternally("https://docs.amnezia.org")
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: textFieldWithHeaderType
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 32
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
enabled: listView.enabled
|
||||
|
||||
headerText: qsTr("Disguised as traffic from")
|
||||
textField.text: site
|
||||
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== site) {
|
||||
var tmpText = textField.text
|
||||
tmpText = tmpText.toLocaleLowerCase()
|
||||
|
||||
if (tmpText.startsWith("https://")) {
|
||||
tmpText = textField.text.substring(8)
|
||||
site = tmpText
|
||||
} else {
|
||||
site = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkEmptyText: true
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: portTextField
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
enabled: listView.enabled
|
||||
|
||||
headerText: qsTr("Port")
|
||||
textField.text: port
|
||||
textField.maximumLength: 5
|
||||
textField.validator: IntValidator { bottom: 1; top: 65535 }
|
||||
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== port) {
|
||||
port = textField.text
|
||||
}
|
||||
textField.validator: IntValidator {
|
||||
bottom: 1; top: 65535
|
||||
}
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== port) port = textField.text
|
||||
}
|
||||
|
||||
checkEmptyText: true
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
text: qsTr("Transport")
|
||||
descriptionText: root.formatTransport(transport)
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
enabled: listView.enabled
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayTransportSettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Security")
|
||||
descriptionText: root.formatSecurity(security)
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
enabled: listView.enabled
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageProtocolXraySecuritySettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Flow")
|
||||
descriptionText: flow
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
enabled: listView.enabled
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayFlowSettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true; Layout.preferredHeight: 24
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
enabled: portTextField.errorText === ""
|
||||
|
||||
// Show Save immediately while user edits port, even before focus loss.
|
||||
visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || textFieldWithHeaderType.textField.text !== port)
|
||||
enabled: visible && textFieldWithHeaderType.errorText === ""
|
||||
text: qsTr("Save")
|
||||
|
||||
onClicked: function() {
|
||||
forceActiveFocus()
|
||||
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
|
||||
var yesButtonFunction = function() {
|
||||
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
|
||||
@@ -142,16 +193,32 @@ PageType {
|
||||
InstallController.updateContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
|
||||
}
|
||||
var noButtonFunction = function() {
|
||||
if (!GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
if (!GC.isMobile()) saveButton.forceActiveFocus()
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: saveButton.clicked()
|
||||
Keys.onReturnPressed: saveButton.clicked()
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Reset settings")
|
||||
textColor: AmneziaStyle.color.vibrantRed
|
||||
visible: listView.enabled
|
||||
clickedFunction: function() {
|
||||
var yesButtonFunction = function() {
|
||||
XrayConfigModel.resetToDefaults()
|
||||
}
|
||||
showQuestionDrawer(qsTr("Reset settings?"), qsTr("All XRay settings will be restored to defaults."),
|
||||
qsTr("Reset"), qsTr("Cancel"), yesButtonFunction, function() {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true; Layout.preferredHeight: 32
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
291
client/ui/qml/Pages2/PageProtocolXraySnapshots.qml
Normal file
291
client/ui/qml/Pages2/PageProtocolXraySnapshots.qml
Normal file
@@ -0,0 +1,291 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
import Qt.labs.platform 1.1
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property string selectedConfigName: ""
|
||||
property int selectedConfigIndex: -1
|
||||
|
||||
// Reload the list every time we open this page
|
||||
Component.onCompleted: XrayConfigSnapshotsModel.reload()
|
||||
|
||||
// ── Save xray config snapshot to file ────────────────────────────
|
||||
function saveConfigToFile(json) {
|
||||
var fileName = ""
|
||||
if (GC.isMobile()) {
|
||||
fileName = "amnezia_xray_config.json"
|
||||
} else {
|
||||
fileName = SystemController.getFileName(
|
||||
qsTr("Save XRay configuration"),
|
||||
qsTr("JSON files (*.json)"),
|
||||
StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/amnezia_xray_config",
|
||||
true,
|
||||
".json")
|
||||
}
|
||||
if (fileName !== "") {
|
||||
PageController.showBusyIndicator(true)
|
||||
ExportController.setConfigFromString(json, fileName)
|
||||
PageController.showBusyIndicator(false)
|
||||
PageController.showNotificationMessage(qsTr("Configuration saved"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
model: XrayConfigSnapshotsModel
|
||||
|
||||
header: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("XRay Configurations")
|
||||
}
|
||||
|
||||
// ── Create from current settings ──────────────────────────
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Create configuration based on current settings")
|
||||
textMaximumLineCount: 2
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
XrayConfigSnapshotsModel.createFromCurrentModel()
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Export ────────────────────────────────────────────────
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Export settings")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
var idx = root.selectedConfigIndex >= 0 ? root.selectedConfigIndex : 0
|
||||
if (listView.count > 0) {
|
||||
var json = XrayConfigSnapshotsModel.exportToJson(idx)
|
||||
saveConfigToFile(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Import ────────────────────────────────────────────────
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Import settings")
|
||||
descriptionText: qsTr("In JSON format")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
var filePath = SystemController.getFileName(
|
||||
qsTr("Open XRay configuration"),
|
||||
qsTr("JSON files (*.json)"))
|
||||
if (filePath !== "") {
|
||||
var jsonContent = ImportController.readTextFile(filePath)
|
||||
if (jsonContent !== "") {
|
||||
if (!XrayConfigSnapshotsModel.importFromJson(jsonContent)) {
|
||||
PageController.showNotificationMessage(qsTr("Failed to import configuration"))
|
||||
} else {
|
||||
PageController.showNotificationMessage(qsTr("Configuration imported successfully"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Section label ─────────────────────────────────────────
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("Configurations")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
visible: listView.count > 0
|
||||
}
|
||||
}
|
||||
|
||||
// ── Empty state ───────────────────────────────────────────────
|
||||
footer: ColumnLayout {
|
||||
width: listView.width
|
||||
visible: listView.count === 0
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 32
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("No saved configurations yet.\nCreate one from the current settings.")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
// ── Config list items ─────────────────────────────────────────
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: configName
|
||||
descriptionText: configDate
|
||||
rightImageSource: "qrc:/images/controls/more-vertical.svg"
|
||||
clickedFunction: function () {
|
||||
root.selectedConfigName = configName
|
||||
root.selectedConfigIndex = index
|
||||
configActionsDrawer.openTriggered()
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Import result handler ─────────────────────────────────────────
|
||||
Connections {
|
||||
target: XrayConfigSnapshotsModel
|
||||
|
||||
function onImportFailed(errorMessage) {
|
||||
PageController.showNotificationMessage(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Per-config actions drawer ─────────────────────────────────────
|
||||
DrawerType2 {
|
||||
id: configActionsDrawer
|
||||
parent: root
|
||||
anchors.fill: parent
|
||||
expandedHeight: root.height * 0.35
|
||||
|
||||
expandedStateContent: ColumnLayout {
|
||||
id: drawerContent
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: 0
|
||||
|
||||
onImplicitHeightChanged: {
|
||||
configActionsDrawer.expandedHeight = drawerContent.implicitHeight + 32
|
||||
}
|
||||
|
||||
BackButtonType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
backButtonFunction: function () {
|
||||
configActionsDrawer.closeTriggered()
|
||||
}
|
||||
}
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
Layout.bottomMargin: 16
|
||||
headerText: root.selectedConfigName
|
||||
}
|
||||
|
||||
// Apply
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Apply configuration")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
configActionsDrawer.closeTriggered()
|
||||
XrayConfigSnapshotsModel.applyConfigToCurrentModel(root.selectedConfigIndex)
|
||||
PageController.closePage()
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// Export this config
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Export configuration")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
configActionsDrawer.closeTriggered()
|
||||
var json = XrayConfigSnapshotsModel.exportToJson(root.selectedConfigIndex)
|
||||
saveConfigToFile(json)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// Delete
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Delete configuration")
|
||||
textColor: AmneziaStyle.color.vibrantRed
|
||||
clickedFunction: function () {
|
||||
configActionsDrawer.closeTriggered()
|
||||
var yesButtonFunction = function () {
|
||||
XrayConfigSnapshotsModel.removeConfig(root.selectedConfigIndex)
|
||||
root.selectedConfigIndex = -1
|
||||
root.selectedConfigName = ""
|
||||
}
|
||||
showQuestionDrawer(
|
||||
qsTr("Delete configuration?"),
|
||||
qsTr("This action cannot be undone."),
|
||||
qsTr("Delete"), qsTr("Cancel"),
|
||||
yesButtonFunction, function () {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
755
client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml
Normal file
755
client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml
Normal file
@@ -0,0 +1,755 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("Transport")
|
||||
}
|
||||
|
||||
// ── Radio buttons ─────────────────────────────────────────
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("RAW (TCP)")
|
||||
checked: transport === "raw"
|
||||
onToggled: if (checked && transport !== "raw") transport = "raw"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("XHTTP")
|
||||
descriptionText: qsTr("Advanced users")
|
||||
checked: transport === "xhttp"
|
||||
onToggled: if (checked && transport !== "xhttp") transport = "xhttp"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("mKCP")
|
||||
checked: transport === "mkcp"
|
||||
onToggled: if (checked && transport !== "mkcp") transport = "mkcp"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// mKCP Settings
|
||||
// ══════════════════════════════════════════════════════════
|
||||
ColumnLayout {
|
||||
visible: transport === "mkcp"
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("mKCP Settings")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("TTI")
|
||||
subtitleText: qsTr("Default: %1 ms", "mKCP TTI").arg(XrayConfigModel.mkcpDefaultTti())
|
||||
textField.text: mkcpTti
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpTti) mkcpTti = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("uplinkCapacity")
|
||||
subtitleText: qsTr("Default: %1 Mbit/s", "mKCP uplink").arg(XrayConfigModel.mkcpDefaultUplinkCapacity())
|
||||
textField.text: mkcpUplinkCapacity
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpUplinkCapacity) mkcpUplinkCapacity = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("downlinkCapacity")
|
||||
subtitleText: qsTr("Default: %1 Mbit/s", "mKCP downlink").arg(XrayConfigModel.mkcpDefaultDownlinkCapacity())
|
||||
textField.text: mkcpDownlinkCapacity
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpDownlinkCapacity) mkcpDownlinkCapacity = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("readBufferSize")
|
||||
subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultReadBufferSize())
|
||||
textField.text: mkcpReadBufferSize
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpReadBufferSize) mkcpReadBufferSize = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("writeBufferSize")
|
||||
subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize())
|
||||
textField.text: mkcpWriteBufferSize
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpWriteBufferSize) mkcpWriteBufferSize = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
Layout.topMargin: 8
|
||||
text: qsTr("Congestion")
|
||||
checked: mkcpCongestion
|
||||
onToggled: mkcpCongestion = checked
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// XHTTP Settings
|
||||
// ══════════════════════════════════════════════════════════
|
||||
ColumnLayout {
|
||||
visible: transport === "xhttp"
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
DropDownType {
|
||||
id: modeDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpMode
|
||||
descriptionText: qsTr("Mode")
|
||||
headerText: qsTr("Mode")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpModeOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpMode = selectedText
|
||||
modeDropDown.text = selectedText
|
||||
modeDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpMode) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
modeDropDown.text = xhttpMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("HTTP Profile")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Host")
|
||||
textField.text: xhttpHost
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpHost) xhttpHost = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Path")
|
||||
textField.text: xhttpPath
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpPath) xhttpPath = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: headersDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpHeadersTemplate
|
||||
descriptionText: qsTr("Headers template")
|
||||
headerText: qsTr("Headers template")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpHeadersTemplateOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpHeadersTemplate = selectedText
|
||||
headersDropDown.text = selectedText
|
||||
headersDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpHeadersTemplate) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
headersDropDown.text = xhttpHeadersTemplate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: uplinkMethodDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpUplinkMethod
|
||||
descriptionText: qsTr("UplinkHTTPMethod")
|
||||
headerText: qsTr("UplinkHTTPMethod")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpUplinkMethodOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpUplinkMethod = selectedText
|
||||
uplinkMethodDropDown.text = selectedText
|
||||
uplinkMethodDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpUplinkMethod) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
uplinkMethodDropDown.text = xhttpUplinkMethod
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
Layout.topMargin: 16
|
||||
text: qsTr("Disable gRPC Header")
|
||||
descriptionText: qsTr("noGRPCHeader")
|
||||
checked: xhttpDisableGrpc
|
||||
onToggled: xhttpDisableGrpc = checked
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
text: qsTr("Disable SSE Header")
|
||||
descriptionText: qsTr("noSSEHeader")
|
||||
checked: xhttpDisableSse
|
||||
onToggled: xhttpDisableSse = checked
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Session & Sequence ────────────────────────────────
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("Session & Sequence")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: sessionPlacementDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpSessionPlacement
|
||||
descriptionText: qsTr("SessionPlacement")
|
||||
headerText: qsTr("SessionPlacement")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpSessionPlacementOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpSessionPlacement = selectedText
|
||||
sessionPlacementDropDown.text = selectedText
|
||||
sessionPlacementDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpSessionPlacement) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
sessionPlacementDropDown.text = xhttpSessionPlacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: sessionKeyDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpSessionKey
|
||||
descriptionText: qsTr("SessionKey")
|
||||
headerText: qsTr("SessionKey")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpSessionKeyOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpSessionKey = selectedText
|
||||
sessionKeyDropDown.text = selectedText
|
||||
sessionKeyDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpSessionKey) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
sessionKeyDropDown.text = xhttpSessionKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: seqPlacementDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpSeqPlacement
|
||||
descriptionText: qsTr("SeqPlacement")
|
||||
headerText: qsTr("SeqPlacement")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpSeqPlacementOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpSeqPlacement = selectedText
|
||||
seqPlacementDropDown.text = selectedText
|
||||
seqPlacementDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpSeqPlacement) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
seqPlacementDropDown.text = xhttpSeqPlacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("SeqKey")
|
||||
textField.text: xhttpSeqKey
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpSeqKey) xhttpSeqKey = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: uplinkDataPlacementDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpUplinkDataPlacement
|
||||
descriptionText: qsTr("UplinkDataPlacement")
|
||||
headerText: qsTr("UplinkDataPlacement")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpUplinkDataPlacementOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpUplinkDataPlacement = selectedText
|
||||
uplinkDataPlacementDropDown.text = selectedText
|
||||
uplinkDataPlacementDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpUplinkDataPlacement) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
uplinkDataPlacementDropDown.text = xhttpUplinkDataPlacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("UplinkDataKey")
|
||||
textField.text: xhttpUplinkDataKey
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpUplinkDataKey) xhttpUplinkDataKey = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
// ── Traffic Shaping ───────────────────────────────────
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("Traffic Shaping")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("UplinkChunkSize")
|
||||
textField.text: xhttpUplinkChunkSize
|
||||
textField.validator: IntValidator {
|
||||
bottom: 0
|
||||
}
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpUplinkChunkSize) xhttpUplinkChunkSize = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("scMaxBufferedPosts")
|
||||
textField.text: xhttpScMaxBufferedPosts
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpScMaxBufferedPosts) xhttpScMaxBufferedPosts = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("scMaxEachPostBytes")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xhttpScMaxEachPostBytesMin
|
||||
maxValue: xhttpScMaxEachPostBytesMax
|
||||
onMinChanged: xhttpScMaxEachPostBytesMin = val
|
||||
onMaxChanged: xhttpScMaxEachPostBytesMax = val
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("scStreamUpServerSecs")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xhttpScStreamUpServerSecsMin
|
||||
maxValue: xhttpScStreamUpServerSecsMax
|
||||
onMinChanged: xhttpScStreamUpServerSecsMin = val
|
||||
onMaxChanged: xhttpScStreamUpServerSecsMax = val
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("scMinPostsIntervalMs")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xhttpScMinPostsIntervalMsMin
|
||||
maxValue: xhttpScMinPostsIntervalMsMax
|
||||
onMinChanged: xhttpScMinPostsIntervalMsMin = val
|
||||
onMaxChanged: xhttpScMinPostsIntervalMsMax = val
|
||||
}
|
||||
|
||||
// ── Padding and multiplexing ──────────────────────────
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("Padding and multiplexing")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("xPadding")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayXPaddingSettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("XMux")
|
||||
descriptionText: xmuxEnabled ? qsTr("On") : qsTr("Off")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayXmuxSettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
|
||||
return
|
||||
}
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
||||
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
|
||||
}
|
||||
var noButtonFunction = function () {
|
||||
if (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
108
client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml
Normal file
108
client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml
Normal file
@@ -0,0 +1,108 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("xPaddingBytes")
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("Range")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xPaddingBytesMin
|
||||
maxValue: xPaddingBytesMax
|
||||
onMinChanged: xPaddingBytesMin = val
|
||||
onMaxChanged: xPaddingBytesMax = val
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
|
||||
return
|
||||
}
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
||||
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
|
||||
}
|
||||
var noButtonFunction = function () {
|
||||
if (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
224
client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml
Normal file
224
client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml
Normal file
@@ -0,0 +1,224 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("xPadding")
|
||||
}
|
||||
|
||||
// xPaddingBytes — min/max display row
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("xPaddingBytes")
|
||||
descriptionText: (xPaddingBytesMin !== "" ? xPaddingBytesMin : "0") + "—" + (xPaddingBytesMax !== "" ? xPaddingBytesMax : "0")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayXPaddingBytesSettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
text: qsTr("xPaddingObfsMode")
|
||||
checked: xPaddingObfsMode
|
||||
onToggled: xPaddingObfsMode = checked
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
headerText: qsTr("xPaddingKey")
|
||||
textField.text: xPaddingKey
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xPaddingKey) xPaddingKey = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("xPaddingHeader")
|
||||
textField.text: xPaddingHeader
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xPaddingHeader) xPaddingHeader = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: placementDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xPaddingPlacement
|
||||
descriptionText: qsTr("xPaddingPlacement")
|
||||
headerText: qsTr("xPaddingPlacement")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xPaddingPlacementOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xPaddingPlacement = selectedText
|
||||
placementDropDown.text = selectedText
|
||||
placementDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xPaddingPlacement) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
placementDropDown.text = xPaddingPlacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: methodDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xPaddingMethod
|
||||
descriptionText: qsTr("xPaddingMethod")
|
||||
headerText: qsTr("xPaddingMethod")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xPaddingMethodOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xPaddingMethod = selectedText
|
||||
methodDropDown.text = selectedText
|
||||
methodDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xPaddingMethod) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
methodDropDown.text = xPaddingMethod
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
|
||||
return
|
||||
}
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
||||
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
|
||||
}
|
||||
var noButtonFunction = function () {
|
||||
if (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
222
client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml
Normal file
222
client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml
Normal file
@@ -0,0 +1,222 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("xmux")
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
text: qsTr("xmux")
|
||||
checked: xmuxEnabled
|
||||
onToggled: xmuxEnabled = checked
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
enabled: xmuxEnabled
|
||||
|
||||
// maxConcurrency
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("maxConcurrency")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxMaxConcurrencyMin
|
||||
maxValue: xmuxMaxConcurrencyMax
|
||||
onMinChanged: xmuxMaxConcurrencyMin = val
|
||||
onMaxChanged: xmuxMaxConcurrencyMax = val
|
||||
}
|
||||
|
||||
// maxConnections
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("maxConnections")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxMaxConnectionsMin
|
||||
maxValue: xmuxMaxConnectionsMax
|
||||
onMinChanged: xmuxMaxConnectionsMin = val
|
||||
onMaxChanged: xmuxMaxConnectionsMax = val
|
||||
}
|
||||
|
||||
// cMaxReuseTimes
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("cMaxReuseTimes")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxCMaxReuseTimesMin
|
||||
maxValue: xmuxCMaxReuseTimesMax
|
||||
onMinChanged: xmuxCMaxReuseTimesMin = val
|
||||
onMaxChanged: xmuxCMaxReuseTimesMax = val
|
||||
}
|
||||
|
||||
// hMaxRequestTimes
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("hMaxRequestTimes")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxHMaxRequestTimesMin
|
||||
maxValue: xmuxHMaxRequestTimesMax
|
||||
onMinChanged: xmuxHMaxRequestTimesMin = val
|
||||
onMaxChanged: xmuxHMaxRequestTimesMax = val
|
||||
}
|
||||
|
||||
// hMaxReusableSecs
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("hMaxReusableSecs")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxHMaxReusableSecsMin
|
||||
maxValue: xmuxHMaxReusableSecsMax
|
||||
onMinChanged: xmuxHMaxReusableSecsMin = val
|
||||
onMaxChanged: xmuxHMaxReusableSecsMax = val
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
headerText: qsTr("hKeepAlivePeriod")
|
||||
textField.text: xmuxHKeepAlivePeriod
|
||||
textField.validator: IntValidator {
|
||||
bottom: 0
|
||||
}
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xmuxHKeepAlivePeriod) xmuxHKeepAlivePeriod = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
|
||||
return
|
||||
}
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
||||
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
|
||||
}
|
||||
var noButtonFunction = function () {
|
||||
if (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1885
client/ui/qml/Pages2/PageServiceMtProxySettings.qml
Normal file
1885
client/ui/qml/Pages2/PageServiceMtProxySettings.qml
Normal file
File diff suppressed because it is too large
Load Diff
1447
client/ui/qml/Pages2/PageServiceTelemtSettings.qml
Normal file
1447
client/ui/qml/Pages2/PageServiceTelemtSettings.qml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -132,9 +132,11 @@ PageType {
|
||||
onInstallationErrorOccurred(message)
|
||||
}
|
||||
|
||||
function onUpdateContainerFinished(message) {
|
||||
function onUpdateContainerFinished(message, closePage) {
|
||||
PageController.showNotificationMessage(message)
|
||||
PageController.closePage()
|
||||
if (closePage) {
|
||||
PageController.closePage()
|
||||
}
|
||||
}
|
||||
|
||||
function onCachedProfileCleared(message) {
|
||||
|
||||
@@ -77,7 +77,19 @@
|
||||
<file>Pages2/PageProtocolRaw.qml</file>
|
||||
<file>Pages2/PageProtocolWireGuardSettings.qml</file>
|
||||
<file>Pages2/PageProtocolXraySettings.qml</file>
|
||||
|
||||
<file>Pages2/PageProtocolXraySnapshots.qml</file>
|
||||
<file>Pages2/PageProtocolXrayFlowSettings.qml</file>
|
||||
<file>Pages2/PageProtocolXraySecuritySettings.qml</file>
|
||||
<file>Pages2/PageProtocolXrayTransportSettings.qml</file>
|
||||
<file>Pages2/PageProtocolXrayXmuxSettings.qml</file>
|
||||
<file>Pages2/PageProtocolXrayXPaddingSettings.qml</file>
|
||||
<file>Pages2/PageProtocolXrayXPaddingBytesSettings.qml</file>
|
||||
<file>Controls2/MinMaxRowType.qml</file>
|
||||
|
||||
<file>Pages2/PageServiceDnsSettings.qml</file>
|
||||
<file>Pages2/PageServiceMtProxySettings.qml</file>
|
||||
<file>Pages2/PageServiceTelemtSettings.qml</file>
|
||||
<file>Pages2/PageServiceSftpSettings.qml</file>
|
||||
<file>Pages2/PageServiceSocksProxySettings.qml</file>
|
||||
<file>Pages2/PageServiceTorWebsiteSettings.qml</file>
|
||||
|
||||
Reference in New Issue
Block a user