Compare commits

..

10 Commits

Author SHA1 Message Date
dranik d1e8fc5f89 merge dev 2026-05-18 15:04:57 +03:00
dranik 39e4c52fb3 merge last integrate-mtproxy 2026-05-18 14:27:14 +03:00
vkamn 72196484a7 Merge branch 'dev' of github-amnezia:amnezia-vpn/amnezia-client into HEAD 2026-05-15 21:53:30 +08:00
vkamn 05ce813c23 refactor: move logic from ui to core 2026-05-15 21:43:47 +08:00
vkamn 0945366587 Merge branch 'dev' of github-amnezia:amnezia-vpn/amnezia-client into integrate-mtproxy 2026-05-15 21:33:04 +08:00
dranik b7c502e7ed remove old path 2026-05-04 23:25:56 +03:00
dranik 4b8ddf7236 fixed secret & enum 2026-05-04 23:17:57 +03:00
dranik a0145475f2 Feat: Add Telemt (MtProxy) 2026-05-04 22:58:26 +03:00
dranik 9f3359e1e8 add path files 2026-05-04 19:27:13 +03:00
dranik 485d0c848a Feat: Add MtProxy (Telegram) 2026-05-04 19:16:48 +03:00
54 changed files with 249 additions and 4604 deletions
-3
View File
@@ -24,8 +24,5 @@
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
<string name="openNotificationSettings">Открыть настройки уведомлений</string>
<string name="vpnStateEventChannelName">Уведомления о VPN</string>
<string name="vpnStateEventChannelDescription">Краткие оповещения при подключении и отключении VPN</string>
<string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string>
</resources>
-3
View File
@@ -24,8 +24,5 @@
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string>
<string name="openNotificationSettings">Open notification settings</string>
<string name="vpnStateEventChannelName">VPN connection alerts</string>
<string name="vpnStateEventChannelDescription">Brief alerts when VPN connects or disconnects</string>
<string name="tvNoFileBrowser">Please install a file management utility to browse files</string>
</resources>
@@ -1003,11 +1003,6 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused")
fun isNotificationPermissionGranted(): Boolean = applicationContext.isNotificationPermissionGranted()
@Suppress("unused")
fun showVpnStateNotification(title: String, message: String) {
ServiceNotification.showVpnStateEvent(applicationContext, title, message)
}
@Suppress("unused")
fun requestNotificationPermission() {
val shouldShowPreRequest = shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
@@ -26,9 +26,6 @@ private const val OLD_NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notific
private const val NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notifications"
const val NOTIFICATION_ID = 1337
const val VPN_STATE_EVENT_NOTIFICATION_ID = 1338
private const val VPN_STATE_EVENT_CHANNEL_ID = "org.amnezia.vpn.vpn_state_events"
private const val GET_ACTIVITY_REQUEST_CODE = 0
private const val CONNECT_REQUEST_CODE = 1
private const val DISCONNECT_REQUEST_CODE = 2
@@ -165,42 +162,8 @@ class ServiceNotification(private val context: Context) {
.setDescription(context.resources.getString(R.string.notificationChannelDescription))
.build()
)
createNotificationChannel(
Builder(VPN_STATE_EVENT_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setShowBadge(false)
.setSound(null, null)
.setVibrationEnabled(false)
.setLightsEnabled(false)
.setName(context.getString(R.string.vpnStateEventChannelName))
.setDescription(context.getString(R.string.vpnStateEventChannelDescription))
.build()
)
}
}
/** Brief alert when VPN connects or disconnects (invoked from Qt via AmneziaActivity). */
fun showVpnStateEvent(context: Context, title: String, message: String) {
if (!context.isNotificationPermissionGranted()) return
val nm = NotificationManagerCompat.from(context)
val notification = NotificationCompat.Builder(context, VPN_STATE_EVENT_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_amnezia_round)
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setContentIntent(
PendingIntent.getActivity(
context,
GET_ACTIVITY_REQUEST_CODE,
Intent(context, AmneziaActivity::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
.build()
nm.notify(VPN_STATE_EVENT_NOTIFICATION_ID, notification)
}
}
}
-2
View File
@@ -31,7 +31,6 @@ set(LIBS ${LIBS}
set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macos_ne_vpn_notification.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
@@ -43,7 +42,6 @@ set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_contro
set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macos_ne_vpn_notification.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
-10
View File
@@ -85,11 +85,6 @@ if(NOT ANDROID)
set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/ui/utils/notificationHandler.h
)
else()
set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/ui/utils/notificationHandler.h
${CLIENT_ROOT_DIR}/platforms/android/android_notificationhandler.h
)
endif()
set(SOURCES ${SOURCES}
@@ -183,11 +178,6 @@ if(NOT ANDROID)
set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/ui/utils/notificationHandler.cpp
)
else()
set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/ui/utils/notificationHandler.cpp
${CLIENT_ROOT_DIR}/platforms/android/android_notificationhandler.cpp
)
endif()
set(COMMON_FILES_H
+66 -397
View File
@@ -20,123 +20,14 @@
#include "core/models/protocols/xrayProtocolConfig.h"
namespace {
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
Logger logger("XrayConfigurator");
}
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,
@@ -144,19 +35,11 @@ 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 "";
@@ -171,7 +54,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";
@@ -185,7 +68,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";
@@ -201,29 +84,26 @@ 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
@@ -236,7 +116,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))
);
@@ -248,286 +128,75 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
return clientId;
}
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
{
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 ContainerConfig &containerConfig,
const DnsSettings &dnsSettings,
ErrorCode &errorCode)
{
const XrayServerConfig *serverConfig = nullptr;
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
serverConfig = &xrayCfg->serverConfig;
const XrayServerConfig* serverConfig = nullptr;
if (auto* xrayConfig = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
serverConfig = &xrayConfig->serverConfig;
}
if (!serverConfig) {
logger.error() << "No XrayProtocolConfig found";
QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, dnsSettings, errorCode);
if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) {
logger.error() << "Failed to prepare server config";
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";
if (errorCode == ErrorCode::NoError) {
errorCode = ErrorCode::InternalError;
}
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;
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 (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", "");
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;
protocolConfig.serverConfig = srv;
if (serverConfig) {
protocolConfig.serverConfig = *serverConfig;
}
XrayClientConfig clientConfig;
clientConfig.nativeConfig = config;
qDebug() << "config:" << config;
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
clientConfig.localPort = "";
clientConfig.id = xrayClientId;
protocolConfig.setClientConfig(clientConfig);
return protocolConfig;
}
}
@@ -2,13 +2,11 @@
#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
{
@@ -20,17 +18,10 @@ 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,9 +86,6 @@ 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);
+6 -4
View File
@@ -65,7 +65,6 @@
#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"
@@ -77,7 +76,9 @@
#include "ui/models/ipSplitTunnelingModel.h"
#include "ui/models/newsModel.h"
class NotificationHandler;
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "ui/utils/notificationHandler.h"
#endif
class CoreSignalHandlers;
class TestMultipleImports;
@@ -142,7 +143,9 @@ private:
SecureServersRepository* m_serversRepository;
SecureAppSettingsRepository* m_appSettingsRepository;
NotificationHandler* m_notificationHandler {};
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
NotificationHandler* m_notificationHandler;
#endif
QMetaObject::Connection m_reloadConfigErrorOccurredConnection;
@@ -202,7 +205,6 @@ private:
OpenVpnConfigModel* m_openVpnConfigModel;
XrayConfigModel* m_xrayConfigModel;
XrayConfigSnapshotsModel* m_xrayConfigSnapshotsModel;
TorConfigModel* m_torConfigModel;
WireGuardConfigModel* m_wireGuardConfigModel;
AwgConfigModel* m_awgConfigModel;
@@ -37,8 +37,8 @@
#include "ui/models/containersModel.h"
#include "core/utils/containerEnum.h"
#include "ui/utils/notificationHandler.h"
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "ui/utils/notificationHandler.h"
#include "ui/utils/systemTrayNotificationHandler.h"
#endif
@@ -410,23 +410,22 @@ void CoreSignalHandlers::initIosSettingsHandler()
void CoreSignalHandlers::initNotificationHandler()
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
m_coreController->m_notificationHandler = NotificationHandler::create(m_coreController);
connect(m_coreController->m_connectionController, &ConnectionController::connectionStateChanged, m_coreController->m_notificationHandler,
&NotificationHandler::setConnectionState);
connect(m_coreController, &CoreController::translationsUpdated, m_coreController->m_notificationHandler, &NotificationHandler::onTranslationsUpdated);
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
connect(m_coreController->m_notificationHandler, &NotificationHandler::raiseRequested, m_coreController->m_pageController, &PageController::raiseMainWindow);
connect(m_coreController->m_notificationHandler, &NotificationHandler::connectRequested, m_coreController->m_connectionUiController,
static_cast<void (ConnectionUiController::*)()>(&ConnectionUiController::openConnection));
connect(m_coreController->m_notificationHandler, &NotificationHandler::disconnectRequested, m_coreController->m_connectionUiController,
&ConnectionUiController::closeConnection);
connect(m_coreController, &CoreController::translationsUpdated, m_coreController->m_notificationHandler, &NotificationHandler::onTranslationsUpdated);
if (auto *trayHandler = qobject_cast<SystemTrayNotificationHandler *>(m_coreController->m_notificationHandler)) {
connect(m_coreController, &CoreController::websiteUrlChanged, trayHandler, &SystemTrayNotificationHandler::updateWebsiteUrl);
}
#endif
auto* trayHandler = qobject_cast<SystemTrayNotificationHandler*>(m_coreController->m_notificationHandler);
connect(m_coreController, &CoreController::websiteUrlChanged, trayHandler, &SystemTrayNotificationHandler::updateWebsiteUrl);
#endif
}
void CoreSignalHandlers::initUpdateFoundHandler()
@@ -323,18 +323,6 @@ 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");
+8 -251
View File
@@ -14,18 +14,8 @@
#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;
@@ -73,251 +63,18 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
}
QJsonObject streamSettings = inbound[protocols::xray::streamSettings].toObject();
auto *xrayConfig = config.getXrayProtocolConfig();
if (!xrayConfig) {
logger.error() << "No XrayProtocolConfig in ContainerConfig";
QJsonObject realitySettings = streamSettings[protocols::xray::realitySettings].toObject();
if (!realitySettings.contains(protocols::xray::serverNames)) {
logger.error() << "Settings missing 'serverNames' field";
return ErrorCode::InternalError;
}
XrayServerConfig &srv = xrayConfig->serverConfig;
QString siteName = realitySettings[protocols::xray::serverNames][0].toString();
// ── Port ─────────────────────────────────────────────────────────
if (inbound.contains(protocols::xray::port)) {
srv.port = QString::number(inbound[protocols::xray::port].toInt());
if (auto* xrayConfig = config.getXrayProtocolConfig()) {
xrayConfig->serverConfig.site = siteName;
}
// ── 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;
}
@@ -3,173 +3,20 @@
#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;
}
@@ -182,96 +29,60 @@ 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 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;
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;
}
bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) const
bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig& other) const
{
return port == other.port
&& site == other.site
&& security == other.security
&& flow == other.flow
&& transport == other.transport
&& fingerprint == other.fingerprint
&& sni == other.sni;
return port == other.port && site == other.site;
}
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 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());
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());
if (!doc.isNull() && doc.isObject()) {
QJsonObject configObj = doc.object();
if (configObj.contains(protocols::xray::outbounds)) {
@@ -289,7 +100,7 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject &json)
if (!users.isEmpty()) {
QJsonObject user = users[0].toObject();
if (user.contains(protocols::xray::id)) {
c.id = user[protocols::xray::id].toString();
config.id = user[protocols::xray::id].toString();
}
}
}
@@ -300,15 +111,16 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject &json)
}
}
}
return c;
return config;
}
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)) {
@@ -318,20 +130,22 @@ 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 c;
c.serverConfig = XrayServerConfig::fromJson(json);
XrayProtocolConfig config;
config.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;
@@ -344,14 +158,14 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json)
}
}
}
c.clientConfig = clientCfg;
config.clientConfig = clientCfg;
} else {
c.clientConfig = XrayClientConfig::fromJson(parsed);
config.clientConfig = XrayClientConfig::fromJson(parsed);
}
}
}
return c;
return config;
}
bool XrayProtocolConfig::hasClientConfig() const
@@ -359,7 +173,7 @@ bool XrayProtocolConfig::hasClientConfig() const
return clientConfig.has_value();
}
void XrayProtocolConfig::setClientConfig(const XrayClientConfig &config)
void XrayProtocolConfig::setClientConfig(const XrayClientConfig& config)
{
clientConfig = config;
}
@@ -370,3 +184,4 @@ void XrayProtocolConfig::clearClientConfig()
}
} // namespace amnezia
+11 -109
View File
@@ -2,145 +2,47 @@
#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
+1 -47
View File
@@ -2,7 +2,6 @@
#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"
@@ -10,7 +9,6 @@
#include <QCryptographicHash>
#include <QJsonDocument>
#include <QTimer>
#include <QJsonObject>
#include <QNetworkInterface>
#include <QtCore/qlogging.h>
@@ -81,29 +79,12 @@ ErrorCode XrayProtocol::start()
m_socksPassword = creds.password;
m_socksPort = creds.port;
QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact);
const 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);
@@ -207,33 +188,6 @@ 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 {
-3
View File
@@ -35,9 +35,6 @@ 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,12 +451,4 @@ 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,9 +92,6 @@ public:
QString nextAvailableServerName() const;
QByteArray xraySavedConfigs() const;
void setXraySavedConfigs(const QByteArray &data);
signals:
void appLanguageChanged(QLocale locale);
void allowedDnsServersChanged(const QStringList &servers);
-70
View File
@@ -126,76 +126,6 @@ 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");
}
}
@@ -58,40 +58,6 @@ 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";
@@ -307,16 +307,6 @@ void AndroidController::requestNotificationPermission()
callActivityMethod("requestNotificationPermission", "()V");
}
void AndroidController::showVpnStateNotification(const QString &title, const QString &message)
{
if (!isNotificationPermissionGranted()) {
return;
}
callActivityMethod("showVpnStateNotification", "(Ljava/lang/String;Ljava/lang/String;)V",
QJniObject::fromString(title).object<jstring>(),
QJniObject::fromString(message).object<jstring>());
}
bool AndroidController::requestAuthentication()
{
QEventLoop wait;
@@ -53,7 +53,6 @@ public:
QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize);
bool isNotificationPermissionGranted();
void requestNotificationPermission();
void showVpnStateNotification(const QString &title, const QString &message);
bool requestAuthentication();
void sendTouch(float x, float y);
@@ -1,19 +0,0 @@
#include "android_notificationhandler.h"
#include "android_controller.h"
AndroidNotificationHandler::AndroidNotificationHandler(QObject *parent)
: NotificationHandler(parent)
{
}
void AndroidNotificationHandler::notify(Message type, const QString &title, const QString &message, int timerMsec)
{
Q_UNUSED(type);
Q_UNUSED(timerMsec);
// Permission is checked on the Kotlin side as well; avoid JNI if already denied.
if (!AndroidController::instance()->isNotificationPermissionGranted()) {
return;
}
AndroidController::instance()->showVpnStateNotification(title, message);
}
@@ -1,16 +0,0 @@
#ifndef ANDROID_NOTIFICATIONHANDLER_H
#define ANDROID_NOTIFICATIONHANDLER_H
#include "ui/utils/notificationHandler.h"
class AndroidNotificationHandler final : public NotificationHandler {
Q_OBJECT
public:
explicit AndroidNotificationHandler(QObject *parent = nullptr);
protected:
void notify(Message type, const QString &title, const QString &message, int timerMsec) override;
};
#endif // ANDROID_NOTIFICATIONHANDLER_H
+1 -1
View File
@@ -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(configKey::description).toString().trimmed();
const QString serverDescription = configuration.value(config_key::description).toString().trimmed();
QString tunnelName;
if (serverDescription.isEmpty()) {
tunnelName = ProtocolUtils::protoToString(proto);
+6 -12
View File
@@ -61,7 +61,6 @@ IOSNotificationHandler::~IOSNotificationHandler() { }
void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title,
const QString& message, int timerMsec) {
Q_UNUSED(type);
Q_UNUSED(timerMsec);
if (!m_delegate) {
return;
@@ -72,13 +71,11 @@ void IOSNotificationHandler::notify(NotificationHandler::Message type, const QSt
content.body = message.toNSString();
content.sound = [UNNotificationSound defaultSound];
NSTimeInterval delay = 0.1;
int timerSec = timerMsec / 1000;
UNTimeIntervalNotificationTrigger* trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:delay repeats:NO];
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:timerSec repeats:NO];
NSString* requestId = [NSString stringWithFormat:@"amneziavpn.vpnstate.%lld",
(long long)([[NSDate date] timeIntervalSince1970] * 1000.0)];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:requestId
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"amneziavpn"
content:content
trigger:trigger];
@@ -146,7 +143,6 @@ IOSNotificationHandler::~IOSNotificationHandler() { }
void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title,
const QString& message, int timerMsec) {
Q_UNUSED(type);
Q_UNUSED(timerMsec);
if (!m_delegate) {
return;
@@ -157,13 +153,11 @@ void IOSNotificationHandler::notify(NotificationHandler::Message type, const QSt
content.body = message.toNSString();
content.sound = [UNNotificationSound defaultSound];
NSTimeInterval delay = 0.1;
int timerSec = timerMsec / 1000;
UNTimeIntervalNotificationTrigger* trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:delay repeats:NO];
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:timerSec repeats:NO];
NSString* requestId = [NSString stringWithFormat:@"amneziavpn.vpnstate.%lld",
(long long)([[NSDate date] timeIntervalSince1970] * 1000.0)];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:requestId
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"amneziavpn"
content:content
trigger:trigger];
@@ -1,8 +0,0 @@
#ifndef MACOS_NE_VPN_NOTIFICATION_H
#define MACOS_NE_VPN_NOTIFICATION_H
class QString;
void macosNePostVpnStateNotification(const QString &title, const QString &message);
#endif
@@ -1,91 +0,0 @@
#include "macos_ne_vpn_notification.h"
#include <QtGlobal>
#include <QString>
#import <Foundation/Foundation.h>
#import <UserNotifications/UserNotifications.h>
namespace {
@interface MacosNeVpnNotificationDelegate : NSObject <UNUserNotificationCenterDelegate>
@end
@implementation MacosNeVpnNotificationDelegate
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
Q_UNUSED(center)
Q_UNUSED(notification)
completionHandler(UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner);
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))completionHandler
{
Q_UNUSED(center)
Q_UNUSED(response)
completionHandler();
}
@end
MacosNeVpnNotificationDelegate *delegateInstance()
{
static MacosNeVpnNotificationDelegate *d;
static dispatch_once_t once;
dispatch_once(&once, ^{
d = [[MacosNeVpnNotificationDelegate alloc] init];
});
return d;
}
void ensureNotificationCenterSetup()
{
static dispatch_once_t once;
dispatch_once(&once, ^{
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert |
UNAuthorizationOptionBadge)
completionHandler:^(BOOL granted, NSError *_Nullable error) {
Q_UNUSED(granted);
if (!error) {
center.delegate = delegateInstance();
}
}];
});
}
} // namespace
void macosNePostVpnStateNotification(const QString &title, const QString &message)
{
ensureNotificationCenterSetup();
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = title.toNSString();
content.body = message.toNSString();
content.sound = nil;
NSTimeInterval delay = 0.1;
UNTimeIntervalNotificationTrigger *trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:delay repeats:NO];
NSString *identifier =
[NSString stringWithFormat:@"amneziavpn.vpnstate.%lld", (long long)([[NSDate date] timeIntervalSince1970] * 1000.0)];
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier
content:content
trigger:trigger];
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center addNotificationRequest:request
withCompletionHandler:^(NSError *_Nullable error) {
if (error) {
NSLog(@"macosNePostVpnStateNotification failed: %@", error);
}
}];
}
-15
View File
@@ -1312,21 +1312,6 @@ 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>
@@ -16,10 +16,6 @@
#include <QFutureWatcher>
#include <QTimer>
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#endif
namespace
{
namespace configKey
@@ -201,12 +201,3 @@ 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,7 +28,6 @@ public slots:
QString getMaliciousWarningText();
bool isNativeWireGuardConfig();
void processNativeWireGuardConfig();
QString readTextFile(const QString &fileName);
#if defined Q_OS_ANDROID || defined Q_OS_IOS
void startDecodingQr();
+1 -9
View File
@@ -82,15 +82,7 @@ namespace PageLoader
PageSetupWizardApiPremiumInfo,
PageSetupWizardApiTrialEmail,
PageDevMenu,
PageProtocolXraySnapshots,
PageProtocolXrayTransportSettings,
PageProtocolXrayXmuxSettings,
PageProtocolXrayXPaddingSettings,
PageProtocolXrayFlowSettings,
PageProtocolXraySecuritySettings,
PageProtocolXrayXPaddingBytesSettings,
PageDevMenu
};
Q_ENUM_NS(PageEnum)
@@ -127,13 +127,3 @@ 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);
}
}
@@ -32,7 +32,6 @@ public slots:
QList<QString> getQrCodes();
void exportConfig(const QString &fileName);
void setConfigFromString(const QString &config, const QString &fileName);
void updateClientManagementModel(const QString &serverId, int containerIndex);
+1 -2
View File
@@ -592,8 +592,7 @@ 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:
case Proto::SSXray: updateIfPresent(m_xrayConfigModel, containerConfig.getXrayProtocolConfig()); break;
case Proto::Xray: 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;
+22 -503
View File
@@ -8,575 +8,94 @@
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)
{
// 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())
{
if (!index.isValid() || index.row() < 0 || index.row() >= ContainerUtils::allContainers().size()) {
return false;
}
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;
QString strValue = value.toString();
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});
if (wasUnsavedChanges != hasUnsavedChanges()) {
emit hasUnsavedChangesChanged();
}
emit dataChanged(index, index, QList { role });
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();
}
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;
switch (role) {
case Roles::SiteRole: return m_protocolConfig.serverConfig.site;
case Roles::PortRole: return m_protocolConfig.serverConfig.port;
}
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()
{
const bool serverSettingsChanged =
!m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig);
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);
}
+9 -109
View File
@@ -2,7 +2,6 @@
#define XRAYCONFIGMODEL_H
#include <QAbstractListModel>
#include <QStringList>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
@@ -12,122 +11,23 @@
class XrayConfigModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(bool hasUnsavedChanges READ hasUnsavedChanges NOTIFY hasUnsavedChangesChanged)
public:
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,
enum Roles {
SiteRole,
PortRole
};
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;
// ── 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();
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::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;
@@ -136,7 +36,7 @@ private:
amnezia::DockerContainer m_container;
amnezia::XrayProtocolConfig m_protocolConfig;
amnezia::XrayProtocolConfig m_originalProtocolConfig;
void applyDefaultsToServerConfig(amnezia::XrayServerConfig& config);
};
@@ -1,216 +0,0 @@
#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);
}
@@ -1,76 +0,0 @@
#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
-61
View File
@@ -1,61 +0,0 @@
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,7 +10,6 @@ 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
@@ -85,15 +84,6 @@ 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
@@ -1,125 +0,0 @@
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)
}
}
}
@@ -1,292 +0,0 @@
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)
}
}
}
+50 -117
View File
@@ -17,20 +17,6 @@ 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
@@ -64,125 +50,88 @@ PageType {
spacing: 0
RowLayout {
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
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")
}
headerText: qsTr("XRay settings")
}
TextFieldWithHeaderType {
id: textFieldWithHeaderType
Layout.fillWidth: true
Layout.topMargin: 32
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
}
headerText: qsTr("Disguised as traffic from")
textField.text: site
textField.onEditingFinished: {
if (textField.text !== port) port = textField.text
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
}
LabelWithButtonType {
TextFieldWithHeaderType {
id: portTextField
Layout.fillWidth: true
Layout.topMargin: 16
text: qsTr("Transport")
descriptionText: root.formatTransport(transport)
rightImageSource: "qrc:/images/controls/chevron-right.svg"
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: listView.enabled
clickedFunction: function() {
PageController.goToPage(PageEnum.PageProtocolXrayTransportSettings)
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
}
}
}
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
checkEmptyText: true
}
BasicButtonType {
id: saveButton
Layout.fillWidth: true
Layout.bottomMargin: 8
Layout.topMargin: 24
Layout.bottomMargin: 24
Layout.leftMargin: 16
Layout.rightMargin: 16
// Show Save immediately while user edits port, even before focus loss.
visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || textFieldWithHeaderType.textField.text !== port)
enabled: visible && textFieldWithHeaderType.errorText === ""
enabled: portTextField.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"))
@@ -193,32 +142,16 @@ 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
}
}
}
}
@@ -1,291 +0,0 @@
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
}
}
}
}
@@ -1,755 +0,0 @@
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)
}
}
}
@@ -1,108 +0,0 @@
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)
}
}
}
@@ -1,224 +0,0 @@
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)
}
}
}
@@ -1,222 +0,0 @@
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)
}
}
}
-10
View File
@@ -77,16 +77,6 @@
<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>
+3 -6
View File
@@ -5,19 +5,16 @@
#include <QDebug>
#include "notificationHandler.h"
#if defined(Q_OS_ANDROID)
# include "platforms/android/android_notificationhandler.h"
#elif defined(Q_OS_IOS)
#if defined(Q_OS_IOS)
# include "platforms/ios/iosnotificationhandler.h"
#else
# include "systemTrayNotificationHandler.h"
#endif
// static
NotificationHandler* NotificationHandler::create(QObject* parent) {
#if defined(Q_OS_ANDROID)
return new AndroidNotificationHandler(parent);
#elif defined(Q_OS_IOS)
#if defined(Q_OS_IOS)
return new IOSNotificationHandler(parent);
#else
return new SystemTrayNotificationHandler(parent);
@@ -10,10 +10,6 @@
# include "platforms/macos/macosutils.h"
#endif
#ifdef MACOS_NE
# include "platforms/macos/macos_ne_vpn_notification.h"
#endif
#include <QApplication>
#include <QDesktopServices>
#include <QIcon>
@@ -156,12 +152,6 @@ void SystemTrayNotificationHandler::notify(NotificationHandler::Message type,
int timerMsec) {
Q_UNUSED(type);
#ifdef MACOS_NE
Q_UNUSED(timerMsec);
macosNePostVpnStateNotification(title, message);
return;
#endif
QIcon icon(ConnectedTrayIconName);
m_systemTrayIcon.showMessage(title, message, icon, timerMsec);
}