mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-21 02:15:51 +03:00
Compare commits
13 Commits
fix/androi
...
fix/init-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a198cce87 | ||
|
|
4890dd1d74 | ||
|
|
564630827e | ||
|
|
fbe15d965b | ||
|
|
b29515c380 | ||
|
|
0658a8f565 | ||
|
|
482ec04b4a | ||
|
|
d40d24fcf9 | ||
|
|
fb5666057b | ||
|
|
a49892c7e7 | ||
|
|
277b295fd8 | ||
|
|
8c33779fc3 | ||
|
|
f0299ca9fe |
66
.github/workflows/deploy.yml
vendored
66
.github/workflows/deploy.yml
vendored
@@ -18,7 +18,6 @@ jobs:
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
base: ${{ github.event.before }}
|
||||
filters: |
|
||||
recipes:
|
||||
- 'recipes/**'
|
||||
@@ -40,7 +39,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build dependencies'
|
||||
shell: bash
|
||||
@@ -50,9 +49,11 @@ jobs:
|
||||
done
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
@@ -98,7 +99,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Install system packages'
|
||||
run: sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev
|
||||
@@ -118,7 +119,7 @@ jobs:
|
||||
- name: 'Upload installer artifact'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/AmneziaVPN-*-Linux.run
|
||||
path: deploy/build/AmneziaVPN_*_linux_x64.run
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
@@ -149,15 +150,17 @@ jobs:
|
||||
- uses: ilammy/msvc-dev-cmd@v1
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build dependencies'
|
||||
run: cmake -S . -B build -G "Visual Studio 17 2022" -DPREBUILTS_ONLY=1
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
@@ -229,7 +232,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build project'
|
||||
shell: cmd
|
||||
@@ -242,27 +245,31 @@ jobs:
|
||||
- name: 'Upload WIX installer artifact'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/AmneziaVPN-*-win64.msi
|
||||
path: deploy/build/AmneziaVPN_*_windows_x64.msi
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload IFW installer artifact'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/AmneziaVPN-*-win64.exe
|
||||
path: deploy/build/AmneziaVPN_*_windows_x64.exe
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
Bake-Prebuilts-iOS:
|
||||
runs-on: macos-latest
|
||||
needs: Detect-Changes
|
||||
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
xcode-version: [26.0]
|
||||
xcode-version: [26.0, 26.4]
|
||||
include:
|
||||
- xcode-version: 26.4
|
||||
os: macos-26
|
||||
|
||||
runs-on: ${{ matrix.os || 'macos-latest' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -279,15 +286,17 @@ jobs:
|
||||
xcode-version: ${{ matrix.xcode-version }}
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build dependencies'
|
||||
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1 -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_SYSROOT=iphoneos
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
@@ -344,7 +353,7 @@ jobs:
|
||||
- name: 'Setup xcode'
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '26.1'
|
||||
xcode-version: '26.0'
|
||||
|
||||
- name: 'Install desktop Qt'
|
||||
uses: jurplel/install-qt-action@v3
|
||||
@@ -376,7 +385,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install deps'
|
||||
run: pip install "conan==2.26.2" jsonschema jinja2
|
||||
run: pip install "conan==2.28.0" jsonschema jinja2
|
||||
|
||||
- name: 'Build project'
|
||||
env:
|
||||
@@ -394,14 +403,17 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Bake-Prebuilts-MacOS:
|
||||
runs-on: macos-latest
|
||||
|
||||
needs: Detect-Changes
|
||||
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
xcode-version: [16.2, 16.4]
|
||||
xcode-version: [16.2, 16.4, 26.4]
|
||||
include:
|
||||
- xcode-version: 26.4
|
||||
os: macos-26
|
||||
|
||||
runs-on: ${{ matrix.os || 'macos-latest' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -418,15 +430,17 @@ jobs:
|
||||
xcode-version: ${{ matrix.xcode-version }}
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build dependencies'
|
||||
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
@@ -502,7 +516,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build project'
|
||||
env:
|
||||
@@ -518,7 +532,7 @@ jobs:
|
||||
- name: 'Upload installer artifact'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/AmneziaVPN-*-Darwin.pkg
|
||||
path: deploy/build/AmneziaVPN_*_macos_x64.pkg
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
@@ -548,15 +562,17 @@ jobs:
|
||||
xcode-version: ${{ matrix.xcode-version }}
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build dependencies'
|
||||
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1 -DMACOS_NE=TRUE
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
@@ -635,7 +651,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build project'
|
||||
run: |
|
||||
@@ -671,7 +687,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Setup Android SDK'
|
||||
uses: android-actions/setup-android@v4
|
||||
@@ -696,9 +712,11 @@ jobs:
|
||||
done
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
@@ -712,7 +730,7 @@ jobs:
|
||||
env:
|
||||
ANDROID_PLATFORM: android-28
|
||||
NDK_VERSION: 27.0.11718014
|
||||
QT_VERSION: 6.10.1
|
||||
QT_VERSION: 6.10.3
|
||||
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
@@ -806,7 +824,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Decode keystore secret to file'
|
||||
env:
|
||||
|
||||
@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 4.8.15.4)
|
||||
set(AMNEZIAVPN_VERSION 4.8.9.0)
|
||||
|
||||
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
|
||||
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
|
||||
@@ -28,7 +28,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
|
||||
set(RELEASE_DATE "${CURRENT_DATE}")
|
||||
|
||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||
set(APP_ANDROID_VERSION_CODE 2120)
|
||||
set(APP_ANDROID_VERSION_CODE 2122)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
@@ -35,6 +35,8 @@ set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/installers/torInstaller.h
|
||||
${CLIENT_ROOT_DIR}/core/installers/sftpInstaller.h
|
||||
${CLIENT_ROOT_DIR}/core/installers/socks5Installer.h
|
||||
${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.h
|
||||
${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.h
|
||||
@@ -110,6 +112,8 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/installers/torInstaller.cpp
|
||||
${CLIENT_ROOT_DIR}/core/installers/sftpInstaller.cpp
|
||||
${CLIENT_ROOT_DIR}/core/installers/socks5Installer.cpp
|
||||
${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.cpp
|
||||
${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.cpp
|
||||
@@ -201,12 +205,14 @@ file(GLOB UI_MODELS_H CONFIGURE_DEPENDS
|
||||
${CLIENT_ROOT_DIR}/ui/models/*.h
|
||||
${CLIENT_ROOT_DIR}/ui/models/protocols/*.h
|
||||
${CLIENT_ROOT_DIR}/ui/models/services/*.h
|
||||
${CLIENT_ROOT_DIR}/ui/models/utils/*.h
|
||||
${CLIENT_ROOT_DIR}/ui/models/api/*.h
|
||||
)
|
||||
file(GLOB UI_MODELS_CPP CONFIGURE_DEPENDS
|
||||
${CLIENT_ROOT_DIR}/ui/models/*.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/models/protocols/*.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/models/services/*.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/models/utils/*.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/models/api/*.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -20,14 +20,123 @@
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
|
||||
namespace {
|
||||
Logger logger("XrayConfigurator");
|
||||
}
|
||||
Logger logger("XrayConfigurator");
|
||||
|
||||
QString normalizeXhttpMode(const QString &m) {
|
||||
const QString t = m.trimmed();
|
||||
if (t.isEmpty() || t.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("auto");
|
||||
}
|
||||
if (t.compare(QLatin1String("Packet-up"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("packet-up");
|
||||
if (t.compare(QLatin1String("Stream-up"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("stream-up");
|
||||
if (t.compare(QLatin1String("Stream-one"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("stream-one");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
// Xray-core: empty → path; "None" in UI → omit (core default path)
|
||||
QString normalizeSessionSeqPlacement(const QString &p)
|
||||
{
|
||||
if (p.isEmpty() || p.compare(QLatin1String("None"), Qt::CaseInsensitive) == 0)
|
||||
return {};
|
||||
return p.toLower();
|
||||
}
|
||||
|
||||
QString normalizeUplinkDataPlacement(const QString &p)
|
||||
{
|
||||
if (p.isEmpty() || p.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("body");
|
||||
if (p.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("auto");
|
||||
if (p.compare(QLatin1String("Query"), Qt::CaseInsensitive) == 0)
|
||||
// "Query" is not valid for uplink payload in splithttp; closest documented mode
|
||||
return QStringLiteral("header");
|
||||
return p.toLower();
|
||||
}
|
||||
|
||||
// splithttp: cookie | header | query | queryInHeader (not "body")
|
||||
QString normalizeXPaddingPlacement(const QString &p)
|
||||
{
|
||||
QString t = p.trimmed();
|
||||
if (t.isEmpty())
|
||||
return QString::fromLatin1(amnezia::protocols::xray::defaultXPaddingPlacement).toLower();
|
||||
if (t.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("queryInHeader");
|
||||
if (t.contains(QLatin1String("queryInHeader"), Qt::CaseInsensitive)
|
||||
|| t.compare(QLatin1String("Query in header"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("queryInHeader");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
// splithttp: repeat-x | tokenish
|
||||
QString normalizeXPaddingMethod(const QString &m)
|
||||
{
|
||||
QString t = m.trimmed();
|
||||
if (t.isEmpty() || t.compare(QLatin1String("Repeat-x"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("repeat-x");
|
||||
if (t.compare(QLatin1String("Tokenish"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("tokenish");
|
||||
if (t.compare(QLatin1String("Random"), Qt::CaseInsensitive) == 0
|
||||
|| t.compare(QLatin1String("Zero"), Qt::CaseInsensitive) == 0)
|
||||
return QStringLiteral("repeat-x");
|
||||
return t.toLower();
|
||||
}
|
||||
|
||||
void putIntRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin,
|
||||
const char *fallbackMax)
|
||||
{
|
||||
if (minV.isEmpty() && maxV.isEmpty())
|
||||
return;
|
||||
if (minV.isEmpty())
|
||||
minV = QString::fromLatin1(fallbackMin);
|
||||
if (maxV.isEmpty())
|
||||
maxV = QString::fromLatin1(fallbackMax);
|
||||
QJsonObject r;
|
||||
r[QStringLiteral("from")] = minV.toInt();
|
||||
r[QStringLiteral("to")] = maxV.toInt();
|
||||
obj[QString::fromUtf8(key)] = r;
|
||||
}
|
||||
|
||||
// Desktop applies this in XrayProtocol::start(); iOS/Android pass JSON straight to libxray — same fixes here.
|
||||
void sanitizeXrayNativeConfig(amnezia::ProtocolConfig &pc)
|
||||
{
|
||||
QString c = pc.nativeConfig();
|
||||
if (c.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
bool changed = false;
|
||||
if (c.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
c.replace(QLatin1String("Mozilla/5.0"), QString::fromLatin1(amnezia::protocols::xray::defaultFingerprint),
|
||||
Qt::CaseInsensitive);
|
||||
changed = true;
|
||||
}
|
||||
const QString legacyListen = QString::fromLatin1(amnezia::protocols::xray::defaultLocalAddr);
|
||||
const QString listenOk = QString::fromLatin1(amnezia::protocols::xray::defaultLocalListenAddr);
|
||||
if (c.contains(legacyListen)) {
|
||||
c.replace(legacyListen, listenOk);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
pc.setNativeConfig(c);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
XrayConfigurator::XrayConfigurator(SshSession* sshSession, QObject *parent)
|
||||
: ConfiguratorBase(sshSession, parent)
|
||||
{
|
||||
}
|
||||
|
||||
amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
|
||||
amnezia::ProtocolConfig protocolConfig)
|
||||
{
|
||||
applyDnsToNativeConfig(settings.dns, protocolConfig);
|
||||
sanitizeXrayNativeConfig(protocolConfig);
|
||||
return protocolConfig;
|
||||
}
|
||||
|
||||
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
@@ -35,11 +144,19 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
{
|
||||
// Generate new UUID for client
|
||||
QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
|
||||
|
||||
// Get flow value from settings (default xtls-rprx-vision)
|
||||
QString flowValue = "xtls-rprx-vision";
|
||||
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
if (!xrayCfg->serverConfig.flow.isEmpty()) {
|
||||
flowValue = xrayCfg->serverConfig.flow;
|
||||
}
|
||||
}
|
||||
|
||||
// Get current server config
|
||||
QString currentConfig = m_sshSession->getTextFileFromContainer(
|
||||
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
|
||||
|
||||
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to get server config file";
|
||||
return "";
|
||||
@@ -54,7 +171,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
}
|
||||
|
||||
QJsonObject serverConfig = doc.object();
|
||||
|
||||
|
||||
// Validate server config structure
|
||||
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
|
||||
logger.error() << "Server config missing 'inbounds' field";
|
||||
@@ -68,7 +185,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
QJsonObject inbound = inbounds[0].toObject();
|
||||
if (!inbound.contains(amnezia::protocols::xray::settings)) {
|
||||
logger.error() << "Inbound missing 'settings' field";
|
||||
@@ -84,26 +201,29 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
}
|
||||
|
||||
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
|
||||
|
||||
|
||||
// Create configuration for new client
|
||||
QJsonObject clientConfig {
|
||||
{amnezia::protocols::xray::id, clientId},
|
||||
{amnezia::protocols::xray::flow, "xtls-rprx-vision"}
|
||||
};
|
||||
|
||||
clientConfig[amnezia::protocols::xray::id] = clientId;
|
||||
if (!flowValue.isEmpty()) {
|
||||
clientConfig[amnezia::protocols::xray::flow] = flowValue;
|
||||
}
|
||||
|
||||
clients.append(clientConfig);
|
||||
|
||||
|
||||
// Update config
|
||||
settings[amnezia::protocols::xray::clients] = clients;
|
||||
inbound[amnezia::protocols::xray::settings] = settings;
|
||||
inbounds[0] = inbound;
|
||||
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
|
||||
|
||||
|
||||
// Save updated config to server
|
||||
QString updatedConfig = QJsonDocument(serverConfig).toJson();
|
||||
errorCode = m_sshSession->uploadTextFileToContainer(
|
||||
container,
|
||||
credentials,
|
||||
container,
|
||||
credentials,
|
||||
updatedConfig,
|
||||
amnezia::protocols::xray::serverConfigPath,
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting
|
||||
@@ -116,7 +236,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
// Restart container
|
||||
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
|
||||
errorCode = m_sshSession->runScript(
|
||||
credentials,
|
||||
credentials,
|
||||
m_sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns))
|
||||
);
|
||||
|
||||
@@ -128,75 +248,286 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
return clientId;
|
||||
}
|
||||
|
||||
ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
ErrorCode &errorCode)
|
||||
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
|
||||
{
|
||||
const XrayServerConfig* serverConfig = nullptr;
|
||||
if (auto* xrayConfig = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
serverConfig = &xrayConfig->serverConfig;
|
||||
QJsonObject streamSettings;
|
||||
const auto &xhttp = srv.xhttp;
|
||||
const auto &mkcp = srv.mkcp;
|
||||
namespace px = amnezia::protocols::xray;
|
||||
|
||||
QString networkValue = QStringLiteral("tcp");
|
||||
if (srv.transport == QLatin1String("xhttp"))
|
||||
networkValue = QStringLiteral("xhttp");
|
||||
else if (srv.transport == QLatin1String("mkcp"))
|
||||
networkValue = QStringLiteral("kcp");
|
||||
streamSettings[px::network] = networkValue;
|
||||
|
||||
streamSettings[px::security] = srv.security;
|
||||
|
||||
if (srv.security == QLatin1String("tls")) {
|
||||
QJsonObject tlsSettings;
|
||||
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
|
||||
tlsSettings[px::serverName] = sniEff;
|
||||
const QString alpnEff = srv.alpn.isEmpty() ? QString::fromLatin1(px::defaultAlpn) : srv.alpn;
|
||||
QJsonArray alpnArray;
|
||||
for (const QString &a : alpnEff.split(QLatin1Char(','))) {
|
||||
const QString t = a.trimmed();
|
||||
if (!t.isEmpty())
|
||||
alpnArray.append(t);
|
||||
}
|
||||
if (!alpnArray.isEmpty())
|
||||
tlsSettings[QStringLiteral("alpn")] = alpnArray;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
tlsSettings[px::fingerprint] = fpEff;
|
||||
streamSettings[QStringLiteral("tlsSettings")] = tlsSettings;
|
||||
}
|
||||
|
||||
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
QJsonObject realSettings;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
realSettings[px::fingerprint] = fpEff;
|
||||
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
|
||||
realSettings[px::serverName] = sniEff;
|
||||
streamSettings[px::realitySettings] = realSettings;
|
||||
}
|
||||
|
||||
// XHTTP — JSON must match Xray-core SplitHTTPConfig (flat xPadding fields, see transport_internet.go)
|
||||
if (srv.transport == QLatin1String("xhttp")) {
|
||||
QJsonObject xo;
|
||||
const QString hostEff = xhttp.host.isEmpty() ? QString::fromLatin1(px::defaultXhttpHost) : xhttp.host;
|
||||
xo[QStringLiteral("host")] = hostEff;
|
||||
if (!xhttp.path.isEmpty())
|
||||
xo[QStringLiteral("path")] = xhttp.path;
|
||||
xo[QStringLiteral("mode")] = normalizeXhttpMode(xhttp.mode);
|
||||
|
||||
if (xhttp.headersTemplate.compare(QLatin1String("HTTP"), Qt::CaseInsensitive) == 0) {
|
||||
QJsonObject headers;
|
||||
headers[QStringLiteral("Host")] = hostEff;
|
||||
xo[QStringLiteral("headers")] = headers;
|
||||
}
|
||||
|
||||
const QString methodEff =
|
||||
xhttp.uplinkMethod.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkMethod) : xhttp.uplinkMethod;
|
||||
xo[QStringLiteral("uplinkHTTPMethod")] = methodEff.toUpper();
|
||||
|
||||
xo[QStringLiteral("noGRPCHeader")] = xhttp.disableGrpc;
|
||||
xo[QStringLiteral("noSSEHeader")] = xhttp.disableSse;
|
||||
|
||||
const QString sessPl = normalizeSessionSeqPlacement(xhttp.sessionPlacement);
|
||||
if (!sessPl.isEmpty())
|
||||
xo[QStringLiteral("sessionPlacement")] = sessPl;
|
||||
const QString seqPl = normalizeSessionSeqPlacement(xhttp.seqPlacement);
|
||||
if (!seqPl.isEmpty())
|
||||
xo[QStringLiteral("seqPlacement")] = seqPl;
|
||||
if (!xhttp.sessionKey.isEmpty())
|
||||
xo[QStringLiteral("sessionKey")] = xhttp.sessionKey;
|
||||
if (!xhttp.seqKey.isEmpty())
|
||||
xo[QStringLiteral("seqKey")] = xhttp.seqKey;
|
||||
|
||||
xo[QStringLiteral("uplinkDataPlacement")] = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement);
|
||||
if (!xhttp.uplinkDataKey.isEmpty())
|
||||
xo[QStringLiteral("uplinkDataKey")] = xhttp.uplinkDataKey;
|
||||
|
||||
const QString ucs = xhttp.uplinkChunkSize.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkChunkSize)
|
||||
: xhttp.uplinkChunkSize;
|
||||
if (!ucs.isEmpty() && ucs != QLatin1String("0")) {
|
||||
const int v = ucs.toInt();
|
||||
QJsonObject chunkR;
|
||||
chunkR[QStringLiteral("from")] = v;
|
||||
chunkR[QStringLiteral("to")] = v;
|
||||
xo[QStringLiteral("uplinkChunkSize")] = chunkR;
|
||||
}
|
||||
|
||||
if (!xhttp.scMaxBufferedPosts.isEmpty())
|
||||
xo[QStringLiteral("scMaxBufferedPosts")] = xhttp.scMaxBufferedPosts.toLongLong();
|
||||
|
||||
putIntRangeIfAny(xo, "scMaxEachPostBytes", xhttp.scMaxEachPostBytesMin, xhttp.scMaxEachPostBytesMax,
|
||||
px::defaultXhttpScMaxEachPostBytesMin, px::defaultXhttpScMaxEachPostBytesMax);
|
||||
putIntRangeIfAny(xo, "scMinPostsIntervalMs", xhttp.scMinPostsIntervalMsMin, xhttp.scMinPostsIntervalMsMax,
|
||||
px::defaultXhttpScMinPostsIntervalMsMin, px::defaultXhttpScMinPostsIntervalMsMax);
|
||||
putIntRangeIfAny(xo, "scStreamUpServerSecs", xhttp.scStreamUpServerSecsMin, xhttp.scStreamUpServerSecsMax,
|
||||
px::defaultXhttpScStreamUpServerSecsMin, px::defaultXhttpScStreamUpServerSecsMax);
|
||||
|
||||
const auto &pad = xhttp.xPadding;
|
||||
xo[QStringLiteral("xPaddingObfsMode")] = pad.obfsMode;
|
||||
if (pad.obfsMode) {
|
||||
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
|
||||
QJsonObject br;
|
||||
br[QStringLiteral("from")] = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt();
|
||||
br[QStringLiteral("to")] = pad.bytesMax.isEmpty() ? (pad.bytesMin.isEmpty() ? 256 : pad.bytesMin.toInt())
|
||||
: pad.bytesMax.toInt();
|
||||
xo[QStringLiteral("xPaddingBytes")] = br;
|
||||
}
|
||||
xo[QStringLiteral("xPaddingKey")] = pad.key.isEmpty() ? QStringLiteral("x_padding") : pad.key;
|
||||
xo[QStringLiteral("xPaddingHeader")] = pad.header.isEmpty() ? QStringLiteral("X-Padding") : pad.header;
|
||||
xo[QStringLiteral("xPaddingPlacement")] = normalizeXPaddingPlacement(
|
||||
pad.placement.isEmpty() ? QString::fromLatin1(px::defaultXPaddingPlacement) : pad.placement);
|
||||
xo[QStringLiteral("xPaddingMethod")] = normalizeXPaddingMethod(
|
||||
pad.method.isEmpty() ? QString::fromLatin1(px::defaultXPaddingMethod) : pad.method);
|
||||
}
|
||||
|
||||
// xmux: Xray has no "enabled" flag; omit object when UI disables multiplex tuning.
|
||||
if (xhttp.xmux.enabled) {
|
||||
QJsonObject mux;
|
||||
auto addMuxRange = [&](const char *key, const QString &a, const QString &b) {
|
||||
if (a.isEmpty() && b.isEmpty())
|
||||
return;
|
||||
QJsonObject r;
|
||||
r[QStringLiteral("from")] = a.isEmpty() ? 0 : a.toInt();
|
||||
r[QStringLiteral("to")] = b.isEmpty() ? 0 : b.toInt();
|
||||
mux[QString::fromUtf8(key)] = r;
|
||||
};
|
||||
addMuxRange("maxConcurrency", xhttp.xmux.maxConcurrencyMin, xhttp.xmux.maxConcurrencyMax);
|
||||
addMuxRange("maxConnections", xhttp.xmux.maxConnectionsMin, xhttp.xmux.maxConnectionsMax);
|
||||
addMuxRange("cMaxReuseTimes", xhttp.xmux.cMaxReuseTimesMin, xhttp.xmux.cMaxReuseTimesMax);
|
||||
addMuxRange("hMaxRequestTimes", xhttp.xmux.hMaxRequestTimesMin, xhttp.xmux.hMaxRequestTimesMax);
|
||||
addMuxRange("hMaxReusableSecs", xhttp.xmux.hMaxReusableSecsMin, xhttp.xmux.hMaxReusableSecsMax);
|
||||
if (!xhttp.xmux.hKeepAlivePeriod.isEmpty())
|
||||
mux[QStringLiteral("hKeepAlivePeriod")] = xhttp.xmux.hKeepAlivePeriod.toLongLong();
|
||||
if (!mux.isEmpty())
|
||||
xo[QStringLiteral("xmux")] = mux;
|
||||
}
|
||||
|
||||
streamSettings[QStringLiteral("xhttpSettings")] = xo;
|
||||
}
|
||||
|
||||
if (srv.transport == QLatin1String("mkcp")) {
|
||||
QJsonObject kcpObj;
|
||||
const QString ttiEff = mkcp.tti.isEmpty() ? QString::fromLatin1(px::defaultMkcpTti) : mkcp.tti;
|
||||
const QString upEff = mkcp.uplinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpUplinkCapacity)
|
||||
: mkcp.uplinkCapacity;
|
||||
const QString downEff = mkcp.downlinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpDownlinkCapacity)
|
||||
: mkcp.downlinkCapacity;
|
||||
const QString rbufEff = mkcp.readBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpReadBufferSize)
|
||||
: mkcp.readBufferSize;
|
||||
const QString wbufEff = mkcp.writeBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpWriteBufferSize)
|
||||
: mkcp.writeBufferSize;
|
||||
kcpObj[QStringLiteral("tti")] = ttiEff.toInt();
|
||||
kcpObj[QStringLiteral("uplinkCapacity")] = upEff.toInt();
|
||||
kcpObj[QStringLiteral("downlinkCapacity")] = downEff.toInt();
|
||||
kcpObj[QStringLiteral("readBufferSize")] = rbufEff.toInt();
|
||||
kcpObj[QStringLiteral("writeBufferSize")] = wbufEff.toInt();
|
||||
kcpObj[QStringLiteral("congestion")] = mkcp.congestion;
|
||||
streamSettings[QStringLiteral("kcpSettings")] = kcpObj;
|
||||
}
|
||||
|
||||
return streamSettings;
|
||||
}
|
||||
|
||||
ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
ErrorCode &errorCode)
|
||||
{
|
||||
const XrayServerConfig *serverConfig = nullptr;
|
||||
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
serverConfig = &xrayCfg->serverConfig;
|
||||
}
|
||||
|
||||
if (!serverConfig) {
|
||||
logger.error() << "No XrayProtocolConfig found";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
|
||||
const XrayServerConfig &srv = *serverConfig;
|
||||
|
||||
QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, dnsSettings, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) {
|
||||
logger.error() << "Failed to prepare server config";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = ErrorCode::InternalError;
|
||||
}
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
|
||||
amnezia::ScriptVars vars = amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns);
|
||||
vars.append(amnezia::genProtocolVarsForContainer(container, containerConfig));
|
||||
QString config = m_sshSession->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), vars);
|
||||
|
||||
if (config.isEmpty()) {
|
||||
logger.error() << "Failed to get config template";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
// Fetch server keys (Reality only)
|
||||
QString xrayPublicKey;
|
||||
QString xrayShortId;
|
||||
|
||||
if (srv.security == "reality") {
|
||||
xrayPublicKey = m_sshSession->getTextFileFromContainer(container, credentials,
|
||||
amnezia::protocols::xray::PublicKeyPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
|
||||
logger.error() << "Failed to get public key";
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = ErrorCode::InternalError;
|
||||
}
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
xrayPublicKey.replace("\n", "");
|
||||
|
||||
xrayShortId = m_sshSession->getTextFileFromContainer(container, credentials,
|
||||
amnezia::protocols::xray::shortidPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
|
||||
logger.error() << "Failed to get short ID";
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = ErrorCode::InternalError;
|
||||
}
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
xrayShortId.replace("\n", "");
|
||||
}
|
||||
|
||||
QString xrayPublicKey =
|
||||
m_sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
|
||||
logger.error() << "Failed to get public key";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
xrayPublicKey.replace("\n", "");
|
||||
|
||||
QString xrayShortId =
|
||||
m_sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
|
||||
logger.error() << "Failed to get short ID";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
xrayShortId.replace("\n", "");
|
||||
|
||||
if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) {
|
||||
logger.error() << "Config template missing required variables:"
|
||||
<< "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID")
|
||||
<< "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY")
|
||||
<< "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID");
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
// Build outbound
|
||||
QJsonObject userObj;
|
||||
userObj[amnezia::protocols::xray::id] = xrayClientId;
|
||||
userObj[amnezia::protocols::xray::encryption] = "none";
|
||||
if (!srv.flow.isEmpty()) {
|
||||
userObj[amnezia::protocols::xray::flow] = srv.flow;
|
||||
}
|
||||
|
||||
config.replace("$XRAY_CLIENT_ID", xrayClientId);
|
||||
config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey);
|
||||
config.replace("$XRAY_SHORT_ID", xrayShortId);
|
||||
QJsonObject vnextEntry;
|
||||
vnextEntry[amnezia::protocols::xray::address] = credentials.hostName;
|
||||
vnextEntry[amnezia::protocols::xray::port] = srv.port.toInt();
|
||||
vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj };
|
||||
|
||||
QJsonObject outboundSettings;
|
||||
outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry };
|
||||
|
||||
QJsonObject outbound;
|
||||
outbound["protocol"] = "vless";
|
||||
outbound[amnezia::protocols::xray::settings] = outboundSettings;
|
||||
|
||||
// Build streamSettings
|
||||
QJsonObject streamObj = buildStreamSettings(srv, xrayClientId);
|
||||
|
||||
// Inject Reality keys
|
||||
if (srv.security == "reality") {
|
||||
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
|
||||
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
|
||||
rs[amnezia::protocols::xray::shortId] = xrayShortId;
|
||||
rs[amnezia::protocols::xray::spiderX] = "";
|
||||
streamObj[amnezia::protocols::xray::realitySettings] = rs;
|
||||
}
|
||||
|
||||
outbound[amnezia::protocols::xray::streamSettings] = streamObj;
|
||||
|
||||
// Build full client config
|
||||
QJsonObject inboundObj;
|
||||
inboundObj["listen"] = amnezia::protocols::xray::defaultLocalListenAddr;
|
||||
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort;
|
||||
inboundObj["protocol"] = "socks";
|
||||
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { "udp", true } };
|
||||
|
||||
QJsonObject clientJson;
|
||||
clientJson["log"] = QJsonObject { { "loglevel", "error" } };
|
||||
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj };
|
||||
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound };
|
||||
|
||||
QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
|
||||
|
||||
// Return
|
||||
XrayProtocolConfig protocolConfig;
|
||||
if (serverConfig) {
|
||||
protocolConfig.serverConfig = *serverConfig;
|
||||
}
|
||||
|
||||
protocolConfig.serverConfig = srv;
|
||||
|
||||
XrayClientConfig clientConfig;
|
||||
clientConfig.nativeConfig = config;
|
||||
clientConfig.localPort = "";
|
||||
qDebug() << "config:" << config;
|
||||
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
clientConfig.id = xrayClientId;
|
||||
|
||||
|
||||
protocolConfig.setClientConfig(clientConfig);
|
||||
|
||||
|
||||
return protocolConfig;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@
|
||||
#define XRAY_CONFIGURATOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "configuratorBase.h"
|
||||
#include "core/utils/errorCodes.h"
|
||||
#include "core/utils/routeModes.h"
|
||||
#include "core/utils/commonStructs.h"
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
|
||||
class XrayConfigurator : public ConfiguratorBase
|
||||
{
|
||||
@@ -18,10 +20,17 @@ public:
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
amnezia::ErrorCode &errorCode) override;
|
||||
|
||||
amnezia::ProtocolConfig processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
|
||||
amnezia::ProtocolConfig protocolConfig) override;
|
||||
|
||||
private:
|
||||
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
amnezia::ErrorCode &errorCode);
|
||||
|
||||
// Builds the native xray "streamSettings" JSON object from XrayServerConfig
|
||||
QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
|
||||
const QString &clientId) const;
|
||||
};
|
||||
|
||||
#endif // XRAY_CONFIGURATOR_H
|
||||
|
||||
@@ -86,6 +86,9 @@ void CoreController::initModels()
|
||||
m_xrayConfigModel = new XrayConfigModel(this);
|
||||
setQmlContextProperty("XrayConfigModel", m_xrayConfigModel);
|
||||
|
||||
m_xrayConfigSnapshotsModel = new XrayConfigSnapshotsModel(m_appSettingsRepository, m_xrayConfigModel, this);
|
||||
setQmlContextProperty("XrayConfigSnapshotsModel", m_xrayConfigSnapshotsModel);
|
||||
|
||||
m_torConfigModel = new TorConfigModel(this);
|
||||
setQmlContextProperty("TorConfigModel", m_torConfigModel);
|
||||
|
||||
@@ -100,6 +103,12 @@ void CoreController::initModels()
|
||||
m_socks5ConfigModel = new Socks5ProxyConfigModel(this);
|
||||
setQmlContextProperty("Socks5ProxyConfigModel", m_socks5ConfigModel);
|
||||
|
||||
m_mtProxyConfigModel = new MtProxyConfigModel(this);
|
||||
setQmlContextProperty("MtProxyConfigModel", m_mtProxyConfigModel);
|
||||
|
||||
m_telemtConfigModel = new TelemtConfigModel(this);
|
||||
setQmlContextProperty("TelemtConfigModel", m_telemtConfigModel);
|
||||
|
||||
m_clientManagementModel = new ClientManagementModel(this);
|
||||
setQmlContextProperty("ClientManagementModel", m_clientManagementModel);
|
||||
|
||||
@@ -169,7 +178,7 @@ void CoreController::initControllers()
|
||||
#ifdef Q_OS_WINDOWS
|
||||
m_ikev2ConfigModel,
|
||||
#endif
|
||||
m_sftpConfigModel, m_socks5ConfigModel, this);
|
||||
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel, this);
|
||||
setQmlContextProperty("InstallController", m_installUiController);
|
||||
|
||||
m_importController = new ImportUiController(m_importCoreController, this);
|
||||
@@ -202,6 +211,10 @@ void CoreController::initControllers()
|
||||
m_systemController = new SystemController(this);
|
||||
setQmlContextProperty("SystemController", m_systemController);
|
||||
|
||||
m_networkReachabilityController = new NetworkReachabilityController(this);
|
||||
m_engine->rootContext()->setContextProperty("NetworkReachabilityController", m_networkReachabilityController);
|
||||
m_engine->rootContext()->setContextProperty("NetworkReachability", m_networkReachabilityController);
|
||||
|
||||
m_servicesCatalogUiController = new ServicesCatalogUiController(m_servicesCatalogController, m_apiServicesModel, this);
|
||||
setQmlContextProperty("ServicesCatalogUiController", m_servicesCatalogUiController);
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "ui/controllers/languageUiController.h"
|
||||
#include "ui/controllers/updateUiController.h"
|
||||
#include "ui/controllers/api/servicesCatalogUiController.h"
|
||||
#include "ui/controllers/networkReachabilityController.h"
|
||||
|
||||
#include "core/controllers/serversController.h"
|
||||
#include "core/controllers/selfhosted/usersController.h"
|
||||
@@ -64,11 +65,15 @@
|
||||
#include "ui/models/protocols/openvpnConfigModel.h"
|
||||
#include "ui/models/protocols/wireguardConfigModel.h"
|
||||
#include "ui/models/protocols/xrayConfigModel.h"
|
||||
#include "ui/models/protocols/xrayConfigSnapshotsModel.h"
|
||||
#include "ui/models/protocolsModel.h"
|
||||
#include "ui/models/services/torConfigModel.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "ui/models/services/sftpConfigModel.h"
|
||||
#include "ui/models/services/socks5ProxyConfigModel.h"
|
||||
#include "ui/models/services/mtProxyConfigModel.h"
|
||||
#include "ui/models/services/telemtConfigModel.h"
|
||||
|
||||
#include "ui/models/ipSplitTunnelingModel.h"
|
||||
#include "ui/models/newsModel.h"
|
||||
|
||||
@@ -156,6 +161,7 @@ private:
|
||||
ServersUiController* m_serversUiController;
|
||||
IpSplitTunnelingUiController* m_ipSplitTunnelingUiController;
|
||||
SystemController* m_systemController;
|
||||
NetworkReachabilityController* m_networkReachabilityController;
|
||||
AppSplitTunnelingUiController* m_appSplitTunnelingUiController;
|
||||
AllowedDnsUiController* m_allowedDnsUiController;
|
||||
LanguageUiController* m_languageUiController;
|
||||
@@ -200,6 +206,7 @@ private:
|
||||
|
||||
OpenVpnConfigModel* m_openVpnConfigModel;
|
||||
XrayConfigModel* m_xrayConfigModel;
|
||||
XrayConfigSnapshotsModel* m_xrayConfigSnapshotsModel;
|
||||
TorConfigModel* m_torConfigModel;
|
||||
WireGuardConfigModel* m_wireGuardConfigModel;
|
||||
AwgConfigModel* m_awgConfigModel;
|
||||
@@ -208,6 +215,8 @@ private:
|
||||
#endif
|
||||
SftpConfigModel* m_sftpConfigModel;
|
||||
Socks5ProxyConfigModel* m_socks5ConfigModel;
|
||||
MtProxyConfigModel* m_mtProxyConfigModel;
|
||||
TelemtConfigModel* m_telemtConfigModel;
|
||||
|
||||
CoreSignalHandlers* m_signalHandlers;
|
||||
};
|
||||
|
||||
@@ -323,6 +323,18 @@ ExportController::ExportResult ExportController::generateXrayConfig(const QStrin
|
||||
vlessServer.shortId = realitySettings.value(amnezia::protocols::xray::shortId).toString();
|
||||
vlessServer.fingerprint = realitySettings.value(amnezia::protocols::xray::fingerprint).toString("chrome");
|
||||
vlessServer.spiderX = realitySettings.value(amnezia::protocols::xray::spiderX).toString("");
|
||||
} else if (vlessServer.security == "tls") {
|
||||
QJsonObject tlsSettings = streamSettings.value("tlsSettings").toObject();
|
||||
vlessServer.serverName = tlsSettings.value(amnezia::protocols::xray::serverName).toString();
|
||||
vlessServer.fingerprint = tlsSettings.value(amnezia::protocols::xray::fingerprint).toString();
|
||||
// alpn: serialize array back to comma-separated for VLESS URI
|
||||
QJsonArray alpnArr = tlsSettings.value("alpn").toArray();
|
||||
QStringList alpnList;
|
||||
for (const QJsonValue &v : alpnArr) {
|
||||
alpnList << v.toString();
|
||||
}
|
||||
// alpn goes into vless URI query param — handled by Serialize via serverName/alpn fields
|
||||
// VlessServerObject doesn't have alpn field, so we embed in serverName if needed
|
||||
}
|
||||
|
||||
result.nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, "AmneziaVPN");
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
#include "core/installers/openvpnInstaller.h"
|
||||
#include "core/installers/sftpInstaller.h"
|
||||
#include "core/installers/socks5Installer.h"
|
||||
#include "core/installers/mtProxyInstaller.h"
|
||||
#include "core/installers/telemtInstaller.h"
|
||||
#include "core/installers/torInstaller.h"
|
||||
#include "core/installers/wireguardInstaller.h"
|
||||
#include "core/installers/xrayInstaller.h"
|
||||
@@ -34,6 +36,7 @@
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/awgProtocolConfig.h"
|
||||
#include "ui/models/protocols/wireguardConfigModel.h"
|
||||
#include "core/utils/utilities.h"
|
||||
@@ -53,6 +56,21 @@ using namespace ProtocolUtils;
|
||||
namespace
|
||||
{
|
||||
Logger logger("InstallController");
|
||||
|
||||
bool dockerDaemonContainerMissing(const QString &out, const QString &containerDockerName)
|
||||
{
|
||||
if (!out.contains(QLatin1String("Error response from daemon"), Qt::CaseInsensitive)) {
|
||||
return false;
|
||||
}
|
||||
if (out.contains(QLatin1String("No such container"), Qt::CaseInsensitive)
|
||||
&& out.contains(containerDockerName, Qt::CaseInsensitive)) {
|
||||
return true;
|
||||
}
|
||||
if (out.size() < 700 && out.contains(QLatin1String("is not running"), Qt::CaseInsensitive)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
InstallController::InstallController(SecureServersRepository *serversRepository,
|
||||
@@ -136,6 +154,15 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession(this);
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession(this);
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
}
|
||||
adminConfig->updateContainerConfig(container, newConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
return ErrorCode::NoError;
|
||||
@@ -165,6 +192,11 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
|
||||
}
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
}
|
||||
clearCachedProfile(serverId, container);
|
||||
adminConfig->updateContainerConfig(container, newConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
@@ -194,38 +226,75 @@ void InstallController::clearCachedProfile(const QString &serverId, DockerContai
|
||||
|
||||
ErrorCode InstallController::validateAndPrepareConfig(const QString &serverId)
|
||||
{
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
const auto kind = m_serversRepository->serverKind(serverId);
|
||||
|
||||
DockerContainer container = DockerContainer::None;
|
||||
ContainerConfig containerConfig;
|
||||
|
||||
switch (kind) {
|
||||
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
|
||||
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!cfg.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
container = cfg->defaultContainer;
|
||||
containerConfig = cfg->containerConfig(container);
|
||||
break;
|
||||
}
|
||||
case serverConfigUtils::ConfigType::SelfHostedUser: {
|
||||
const auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
|
||||
if (!cfg.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
container = cfg->defaultContainer;
|
||||
containerConfig = cfg->containerConfig(container);
|
||||
break;
|
||||
}
|
||||
case serverConfigUtils::ConfigType::Native: {
|
||||
const auto cfg = m_serversRepository->nativeConfig(serverId);
|
||||
if (!cfg.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
container = cfg->defaultContainer;
|
||||
containerConfig = cfg->containerConfig(container);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
DockerContainer container = adminConfig->defaultContainer;
|
||||
|
||||
if (container == DockerContainer::None) {
|
||||
return ErrorCode::NoInstalledContainersError;
|
||||
}
|
||||
|
||||
ContainerConfig containerConfig = adminConfig->containerConfig(container);
|
||||
if (containerConfig.protocolConfig.hasClientConfig()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
if (kind != serverConfigUtils::ConfigType::SelfHostedAdmin) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
SshSession sshSession;
|
||||
|
||||
auto isProtocolConfigExists = [](const ContainerConfig &cfg) {
|
||||
return cfg.protocolConfig.hasClientConfig();
|
||||
};
|
||||
|
||||
if (!isProtocolConfigExists(containerConfig)) {
|
||||
QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName());
|
||||
ErrorCode errorCode = processContainerForAdmin(container, containerConfig, credentials, sshSession, serverId, clientName);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
adminConfig->updateContainerConfig(container, containerConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
const QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName());
|
||||
const ErrorCode errorCode = processContainerForAdmin(container, containerConfig, credentials, sshSession, serverId, clientName);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
adminConfig->updateContainerConfig(container, containerConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
@@ -408,9 +477,24 @@ ErrorCode InstallController::configureContainerWorker(const ServerCredentials &c
|
||||
sshSession.replaceVars(amnezia::scriptData(ProtocolScriptType::configure_container, container), baseVars),
|
||||
cbReadStdOut, cbReadStdErr);
|
||||
|
||||
if (e != ErrorCode::NoError) {
|
||||
return e;
|
||||
}
|
||||
|
||||
if (dockerDaemonContainerMissing(stdOut, ContainerUtils::containerToString(container))) {
|
||||
qDebug() << "configureContainerWorker: Docker daemon reports container missing/stopped, output:" << stdOut;
|
||||
return ErrorCode::ServerContainerMissingError;
|
||||
}
|
||||
|
||||
updateContainerConfigAfterInstallation(container, config, stdOut);
|
||||
|
||||
return e;
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config);
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::startupContainerWorker(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession)
|
||||
@@ -563,6 +647,79 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::MtProxy) {
|
||||
const auto *oldMt = oldConfig.getMtProxyProtocolConfig();
|
||||
const auto *newMt = newConfig.getMtProxyProtocolConfig();
|
||||
if (oldMt && newMt) {
|
||||
const QString oldPort =
|
||||
oldMt->port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : oldMt->port;
|
||||
const QString newPort =
|
||||
newMt->port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : newMt->port;
|
||||
if (oldPort != newPort) {
|
||||
return true;
|
||||
}
|
||||
const QString oldTransport = oldMt->transportMode.isEmpty() ? QString(
|
||||
protocols::mtProxy::transportModeStandard)
|
||||
: oldMt->transportMode;
|
||||
const QString newTransport = newMt->transportMode.isEmpty() ? QString(
|
||||
protocols::mtProxy::transportModeStandard)
|
||||
: newMt->transportMode;
|
||||
if (oldTransport != newTransport) {
|
||||
return true;
|
||||
}
|
||||
if (oldMt->tlsDomain != newMt->tlsDomain) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Telemt) {
|
||||
const auto *oldT = oldConfig.getTelemtProtocolConfig();
|
||||
const auto *newT = newConfig.getTelemtProtocolConfig();
|
||||
if (oldT && newT) {
|
||||
const QString oldPort =
|
||||
oldT->port.isEmpty() ? QString(protocols::telemt::defaultPort) : oldT->port;
|
||||
const QString newPort =
|
||||
newT->port.isEmpty() ? QString(protocols::telemt::defaultPort) : newT->port;
|
||||
if (oldPort != newPort) {
|
||||
return true;
|
||||
}
|
||||
const QString oldTransport = oldT->transportMode.isEmpty()
|
||||
? QString(protocols::telemt::transportModeStandard)
|
||||
: oldT->transportMode;
|
||||
const QString newTransport = newT->transportMode.isEmpty()
|
||||
? QString(protocols::telemt::transportModeStandard)
|
||||
: newT->transportMode;
|
||||
if (oldTransport != newTransport) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->tlsDomain != newT->tlsDomain) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->maskEnabled != newT->maskEnabled) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->tlsEmulation != newT->tlsEmulation) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->useMiddleProxy != newT->useMiddleProxy) {
|
||||
return true;
|
||||
}
|
||||
if (oldT->tag != newT->tag) {
|
||||
return true;
|
||||
}
|
||||
const QString oldUser = oldT->userName.isEmpty()
|
||||
? QString::fromUtf8(protocols::telemt::defaultUserName)
|
||||
: oldT->userName;
|
||||
const QString newUser = newT->userName.isEmpty()
|
||||
? QString::fromUtf8(protocols::telemt::defaultUserName)
|
||||
: newT->userName;
|
||||
if (oldUser != newUser) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Socks5Proxy) {
|
||||
return true;
|
||||
}
|
||||
@@ -654,7 +811,7 @@ ErrorCode InstallController::isUserInSudo(const ServerCredentials &credentials,
|
||||
return ErrorCode::ServerUserDirectoryNotAccessible;
|
||||
if (stdOut.contains("sudoers") || stdOut.contains("is not allowed to run sudo on"))
|
||||
return ErrorCode::ServerUserNotAllowedInSudoers;
|
||||
if (stdOut.contains("password is required"))
|
||||
if (stdOut.contains("password is required") || stdOut.contains("authentication is required"))
|
||||
return ErrorCode::ServerUserPasswordRequired;
|
||||
|
||||
return error;
|
||||
@@ -823,6 +980,8 @@ QScopedPointer<InstallerBase> InstallController::createInstaller(DockerContainer
|
||||
case DockerContainer::TorWebSite: return QScopedPointer<InstallerBase>(new TorInstaller(this));
|
||||
case DockerContainer::Sftp: return QScopedPointer<InstallerBase>(new SftpInstaller(this));
|
||||
case DockerContainer::Socks5Proxy: return QScopedPointer<InstallerBase>(new Socks5Installer(this));
|
||||
case DockerContainer::MtProxy: return QScopedPointer<InstallerBase>(new MtProxyInstaller(this));
|
||||
case DockerContainer::Telemt: return QScopedPointer<InstallerBase>(new TelemtInstaller(this));
|
||||
default: return QScopedPointer<InstallerBase>(new InstallerBase(this));
|
||||
}
|
||||
}
|
||||
@@ -861,6 +1020,20 @@ bool InstallController::isUpdateDockerContainerRequired(DockerContainer containe
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (container == DockerContainer::MtProxy) {
|
||||
const auto *oldMt = oldConfig.getMtProxyProtocolConfig();
|
||||
const auto *newMt = newConfig.getMtProxyProtocolConfig();
|
||||
if (!oldMt || !newMt) {
|
||||
return true;
|
||||
}
|
||||
return !oldMt->equalsDockerDeploymentSettings(*newMt);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
const auto *oldT = oldConfig.getTelemtProtocolConfig();
|
||||
const auto *newT = newConfig.getTelemtProtocolConfig();
|
||||
if (!oldT || !newT) {
|
||||
return true;
|
||||
}
|
||||
return !oldT->equalsDockerDeploymentSettings(*newT);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -1164,6 +1337,56 @@ void InstallController::updateContainerConfigAfterInstallation(DockerContainer c
|
||||
onion.replace("\n", "");
|
||||
torProtocolConfig->serverConfig.site = onion;
|
||||
}
|
||||
} else if (container == DockerContainer::MtProxy) {
|
||||
if (auto* mtProxyConfig = containerConfig.getMtProxyProtocolConfig()) {
|
||||
qDebug() << "amnezia mtproxy" << stdOut;
|
||||
|
||||
static const QRegularExpression reSecret(
|
||||
QStringLiteral(R"(\[\*\]\s+Secret:\s+([0-9a-fA-F]{32}))"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
static const QRegularExpression reTgLink(QStringLiteral(R"(\[\*\]\s+tg://\s+link:\s+(tg://proxy\?[^\s]+))"));
|
||||
static const QRegularExpression reTmeLink(
|
||||
QStringLiteral(R"(\[\*\]\s+t\.me\s+link:\s+(https://t\.me/proxy\?[^\s]+))"));
|
||||
|
||||
const QRegularExpressionMatch mSecret = reSecret.match(stdOut);
|
||||
const QRegularExpressionMatch mTgLink = reTgLink.match(stdOut);
|
||||
const QRegularExpressionMatch mTmeLink = reTmeLink.match(stdOut);
|
||||
|
||||
if (mSecret.hasMatch()) {
|
||||
mtProxyConfig->secret = mSecret.captured(1);
|
||||
}
|
||||
if (mTgLink.hasMatch()) {
|
||||
mtProxyConfig->tgLink = mTgLink.captured(1);
|
||||
}
|
||||
if (mTmeLink.hasMatch()) {
|
||||
mtProxyConfig->tmeLink = mTmeLink.captured(1);
|
||||
}
|
||||
}
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
if (auto *telemtConfig = containerConfig.getTelemtProtocolConfig()) {
|
||||
qDebug() << "amnezia-telemt configure stdout" << stdOut;
|
||||
|
||||
static const QRegularExpression reSecret(
|
||||
QStringLiteral(R"(\[\*\]\s+Secret:\s+([0-9a-fA-F]{32}))"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
static const QRegularExpression reTgLink(QStringLiteral(R"(\[\*\]\s+tg://\s+link:\s+(tg://proxy\?[^\s]+))"));
|
||||
static const QRegularExpression reTmeLink(
|
||||
QStringLiteral(R"(\[\*\]\s+t\.me\s+link:\s+(https://t\.me/proxy\?[^\s]+))"));
|
||||
|
||||
const QRegularExpressionMatch mSecret = reSecret.match(stdOut);
|
||||
const QRegularExpressionMatch mTgLink = reTgLink.match(stdOut);
|
||||
const QRegularExpressionMatch mTmeLink = reTmeLink.match(stdOut);
|
||||
|
||||
if (mSecret.hasMatch()) {
|
||||
telemtConfig->secret = mSecret.captured(1);
|
||||
}
|
||||
if (mTgLink.hasMatch()) {
|
||||
telemtConfig->tgLink = mTgLink.captured(1);
|
||||
}
|
||||
if (mTmeLink.hasMatch()) {
|
||||
telemtConfig->tmeLink = mTmeLink.captured(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1248,3 +1471,126 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::setDockerContainerEnabledState(const QString &serverId, DockerContainer container, bool enabled)
|
||||
{
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
SshSession sshSession(this);
|
||||
const QString script = enabled ? QStringLiteral("sudo docker start %1").arg(containerName)
|
||||
: QStringLiteral("sudo docker stop %1").arg(containerName);
|
||||
const ErrorCode runError = sshSession.runScript(credentials, script);
|
||||
if (runError != ErrorCode::NoError) {
|
||||
return runError;
|
||||
}
|
||||
ContainerConfig currentConfig = adminConfig->containerConfig(container);
|
||||
bool persist = false;
|
||||
if (auto *mtConfig = currentConfig.getMtProxyProtocolConfig()) {
|
||||
mtConfig->isEnabled = enabled;
|
||||
persist = true;
|
||||
} else if (auto *telemtConfig = currentConfig.getTelemtProtocolConfig()) {
|
||||
telemtConfig->isEnabled = enabled;
|
||||
persist = true;
|
||||
}
|
||||
if (persist) {
|
||||
adminConfig->updateContainerConfig(container, currentConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::queryDockerContainerStatus(const QString &serverId, DockerContainer container, int &statusOut)
|
||||
{
|
||||
statusOut = 3;
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
SshSession sshSession(this);
|
||||
const QString script = QStringLiteral(
|
||||
"sudo docker inspect --format '{{.State.Status}}' %1 2>/dev/null || echo 'not_found'")
|
||||
.arg(containerName);
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
const QString status = stdOut.trimmed();
|
||||
if (status == QLatin1String("running")) {
|
||||
statusOut = 1;
|
||||
} else if (status == QLatin1String("not_found") || status.isEmpty()) {
|
||||
statusOut = 0;
|
||||
} else if (status == QLatin1String("exited") || status == QLatin1String("created")
|
||||
|| status == QLatin1String("paused")) {
|
||||
statusOut = 2;
|
||||
} else {
|
||||
statusOut = 3;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode InstallController::queryMtProxyDiagnostics(const QString &serverId, DockerContainer container, int listenPort,
|
||||
MtProxyContainerDiagnostics &out)
|
||||
{
|
||||
out = {};
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession(this);
|
||||
return MtProxyInstaller::queryDiagnostics(sshSession, credentials, container, listenPort, out);
|
||||
}
|
||||
|
||||
QString InstallController::fetchDockerContainerSecret(const QString &serverId, DockerContainer container)
|
||||
{
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return {};
|
||||
}
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return {};
|
||||
}
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
if (!credentials.isValid()) {
|
||||
return {};
|
||||
}
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
SshSession sshSession(this);
|
||||
const QString path = QStringLiteral("/data/secret");
|
||||
const QString cmd = QStringLiteral("sudo docker exec %1 cat %2").arg(containerName, path);
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, cmd, cbReadStdOut);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return {};
|
||||
}
|
||||
const QString secret = stdOut.trimmed();
|
||||
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
|
||||
return hex32.match(secret).hasMatch() ? secret : QString();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/installers/mtProxyInstaller.h"
|
||||
|
||||
class SshSession;
|
||||
class InstallerBase;
|
||||
@@ -39,6 +40,16 @@ public:
|
||||
ErrorCode removeAllContainers(const QString &serverId);
|
||||
ErrorCode removeContainer(const QString &serverId, DockerContainer container);
|
||||
|
||||
ErrorCode setDockerContainerEnabledState(const QString &serverId, DockerContainer container, bool enabled);
|
||||
|
||||
/// statusOut: 0 = not deployed, 1 = running, 2 = stopped, 3 = error
|
||||
ErrorCode queryDockerContainerStatus(const QString &serverId, DockerContainer container, int &statusOut);
|
||||
|
||||
ErrorCode queryMtProxyDiagnostics(const QString &serverId, DockerContainer container, int listenPort,
|
||||
MtProxyContainerDiagnostics &out);
|
||||
|
||||
QString fetchDockerContainerSecret(const QString &serverId, DockerContainer container);
|
||||
|
||||
ContainerConfig generateConfig(DockerContainer container, int port, TransportProto transportProto);
|
||||
ErrorCode getAlreadyInstalledContainers(const ServerCredentials &credentials, QMap<DockerContainer, ContainerConfig> &installedContainers, SshSession &sshSession);
|
||||
|
||||
|
||||
@@ -21,13 +21,13 @@ namespace
|
||||
Logger logger("UpdateController");
|
||||
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-win64.exe");
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_windows_x64.exe");
|
||||
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN_installer.exe";
|
||||
#elif defined(Q_OS_MACOS)
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-Darwin.pkg");
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_macos_x64.pkg");
|
||||
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.pkg";
|
||||
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-Linux.run");
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_linux_x64.run");
|
||||
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.run";
|
||||
#endif
|
||||
}
|
||||
@@ -106,7 +106,7 @@ void UpdateController::fetchGatewayUrl()
|
||||
// Workaround: wait before contacting gateway to avoid rate limit triggered by other requests (news etc.)
|
||||
QTimer::singleShot(1000, this, [this, gatewayController, apiPayload]() {
|
||||
gatewayController->postAsync(QStringLiteral("%1v1/updater_endpoint"), apiPayload)
|
||||
.then(this, [this](QPair<ErrorCode, QByteArray> result) {
|
||||
.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {
|
||||
auto [err, gatewayResponse] = result;
|
||||
if (err != ErrorCode::NoError) {
|
||||
logger.error() << errorString(err);
|
||||
|
||||
16
client/core/diagnostics/containerDiagnostics.h
Normal file
16
client/core/diagnostics/containerDiagnostics.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#ifndef CONTAINERDIAGNOSTICS_H
|
||||
#define CONTAINERDIAGNOSTICS_H
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
struct ContainerDiagnostics
|
||||
{
|
||||
bool available = false;
|
||||
bool portReachable = false;
|
||||
|
||||
virtual ~ContainerDiagnostics() = default;
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // CONTAINERDIAGNOSTICS_H
|
||||
18
client/core/diagnostics/mtProxyDiagnostics.h
Normal file
18
client/core/diagnostics/mtProxyDiagnostics.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#ifndef MTPROXYDIAGNOSTICS_H
|
||||
#define MTPROXYDIAGNOSTICS_H
|
||||
|
||||
#include "containerDiagnostics.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace amnezia {
|
||||
struct MtProxyDiagnostics : ContainerDiagnostics {
|
||||
bool upstreamReachable = false;
|
||||
int clientsConnected = -1;
|
||||
QString lastConfigRefresh;
|
||||
QString statsEndpoint;
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // MTPROXYDIAGNOSTICS_H
|
||||
20
client/core/diagnostics/telemtDiagnostics.h
Normal file
20
client/core/diagnostics/telemtDiagnostics.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef TELEMTDIAGNOSTICS_H
|
||||
#define TELEMTDIAGNOSTICS_H
|
||||
|
||||
#include "containerDiagnostics.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
struct TelemtDiagnostics : ContainerDiagnostics
|
||||
{
|
||||
bool upstreamReachable = false;
|
||||
int clientsConnected = -1;
|
||||
QString lastConfigRefresh;
|
||||
QString statsEndpoint;
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // TELEMTDIAGNOSTICS_H
|
||||
@@ -14,6 +14,8 @@
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "core/models/protocols/sftpProtocolConfig.h"
|
||||
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
#include "core/models/protocols/ikev2ProtocolConfig.h"
|
||||
#include "core/models/protocols/torProtocolConfig.h"
|
||||
|
||||
@@ -91,6 +93,18 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
|
||||
config.protocolConfig = socks5Config;
|
||||
break;
|
||||
}
|
||||
case Proto::MtProxy: {
|
||||
MtProxyProtocolConfig mtConfig;
|
||||
mtConfig.port = portStr;
|
||||
config.protocolConfig = mtConfig;
|
||||
break;
|
||||
}
|
||||
case Proto::Telemt: {
|
||||
TelemtProtocolConfig telemtConfig;
|
||||
telemtConfig.port = portStr;
|
||||
config.protocolConfig = telemtConfig;
|
||||
break;
|
||||
}
|
||||
case Proto::Ikev2: {
|
||||
Ikev2ProtocolConfig ikev2Config;
|
||||
config.protocolConfig = ikev2Config;
|
||||
|
||||
130
client/core/installers/mtProxyInstaller.cpp
Normal file
130
client/core/installers/mtProxyInstaller.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include "mtProxyInstaller.h"
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace {
|
||||
constexpr QLatin1String kMtProxyClientJsonPath("/data/amnezia-mtproxy-client.json");
|
||||
constexpr QLatin1String kMtProxyClientJsonUploadPath("data/amnezia-mtproxy-client.json");
|
||||
constexpr QLatin1String kMtProxySecretPath("/data/secret");
|
||||
}
|
||||
|
||||
MtProxyInstaller::MtProxyInstaller(QObject *parent)
|
||||
: InstallerBase(parent) {
|
||||
}
|
||||
|
||||
ErrorCode MtProxyInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession *sshSession, ContainerConfig &config) {
|
||||
if (container != DockerContainer::MtProxy || !sshSession) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
MtProxyProtocolConfig *mt = config.getMtProxyProtocolConfig();
|
||||
if (!mt) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode jsonErr = ErrorCode::NoError;
|
||||
const QByteArray jsonRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyClientJsonPath), jsonErr);
|
||||
if (jsonErr == ErrorCode::NoError && !jsonRaw.trimmed().isEmpty()) {
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(jsonRaw.trimmed(), &parseError);
|
||||
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
|
||||
QJsonObject merged = mt->toJson();
|
||||
const QJsonObject snap = doc.object();
|
||||
for (auto it = snap.constBegin(); it != snap.constEnd(); ++it) {
|
||||
merged.insert(it.key(), it.value());
|
||||
}
|
||||
*mt = MtProxyProtocolConfig::fromJson(merged);
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCode secretErr = ErrorCode::NoError;
|
||||
const QByteArray secretRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxySecretPath), secretErr);
|
||||
const QString sec = QString::fromUtf8(secretRaw).trimmed();
|
||||
if (sec.length() == 32) {
|
||||
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
|
||||
if (hex32.match(sec).hasMatch()) {
|
||||
mt->secret = sec;
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode MtProxyInstaller::queryDiagnostics(SshSession &sshSession, const ServerCredentials &credentials,
|
||||
DockerContainer container, int listenPort,
|
||||
MtProxyContainerDiagnostics &out)
|
||||
{
|
||||
out = {};
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
const QString script =
|
||||
QStringLiteral(
|
||||
"PORT_OK=$(sudo docker exec %1 sh -c 'ss -tlnp 2>/dev/null | grep -q :%2 && echo yes || echo no' 2>/dev/null || echo no); "
|
||||
"TG_OK=$(curl -s --max-time 5 -o /dev/null -w '%%{http_code}' https://core.telegram.org/getProxySecret 2>/dev/null | grep -q '200' && echo yes || echo no); "
|
||||
"CLIENTS=$(sudo docker exec amnezia-mtproxy sh -c 'curl -s --max-time 3 http://localhost:2398/stats 2>/dev/null | grep -o \"total_special_connections:[0-9]*\" | cut -d: -f2' 2>/dev/null); "
|
||||
"CONF_TIME=$(sudo docker exec amnezia-mtproxy sh -c 'stat -c \"%%y\" /data/proxy-multi.conf 2>/dev/null | cut -d. -f1' 2>/dev/null || echo unknown); "
|
||||
"echo \"PORT_OK=${PORT_OK}\"; "
|
||||
"echo \"TG_OK=${TG_OK}\"; "
|
||||
"echo \"CLIENTS=${CLIENTS:-0}\"; "
|
||||
"echo \"CONF_TIME=${CONF_TIME}\"; "
|
||||
"echo \"STATS=http://localhost:2398/stats\";")
|
||||
.arg(containerName)
|
||||
.arg(listenPort);
|
||||
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
stdOut += data;
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
for (const QString &line : stdOut.split('\n', Qt::SkipEmptyParts)) {
|
||||
if (line.startsWith(QLatin1String("PORT_OK="))) {
|
||||
out.portReachable = line.mid(8).trimmed() == QLatin1String("yes");
|
||||
} else if (line.startsWith(QLatin1String("TG_OK="))) {
|
||||
out.upstreamReachable = line.mid(6).trimmed() == QLatin1String("yes");
|
||||
} else if (line.startsWith(QLatin1String("CLIENTS="))) {
|
||||
out.clientsConnected = line.mid(8).trimmed().toInt();
|
||||
} else if (line.startsWith(QLatin1String("CONF_TIME="))) {
|
||||
out.lastConfigRefresh = line.mid(10).trimmed();
|
||||
} else if (line.startsWith(QLatin1String("STATS="))) {
|
||||
out.statsEndpoint = line.mid(6).trimmed();
|
||||
}
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
void MtProxyInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,
|
||||
DockerContainer container, const ContainerConfig &config) {
|
||||
const MtProxyProtocolConfig *mt = config.getMtProxyProtocolConfig();
|
||||
if (!mt) {
|
||||
return;
|
||||
}
|
||||
const QByteArray payload = QJsonDocument(mt->toJson()).toJson(QJsonDocument::Compact);
|
||||
const ErrorCode err = sshSession.uploadTextFileToContainer(container, credentials, QString::fromUtf8(payload),
|
||||
QString(kMtProxyClientJsonUploadPath));
|
||||
if (err != ErrorCode::NoError) {
|
||||
qWarning() << "MtProxyInstaller::uploadClientSettingsSnapshot failed" << err;
|
||||
}
|
||||
}
|
||||
34
client/core/installers/mtProxyInstaller.h
Normal file
34
client/core/installers/mtProxyInstaller.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#ifndef MTPROXYINSTALLER_H
|
||||
#define MTPROXYINSTALLER_H
|
||||
|
||||
#include "installerBase.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
struct MtProxyContainerDiagnostics {
|
||||
bool portReachable = false;
|
||||
bool upstreamReachable = false;
|
||||
int clientsConnected = -1;
|
||||
QString lastConfigRefresh;
|
||||
QString statsEndpoint;
|
||||
};
|
||||
|
||||
class MtProxyInstaller : public InstallerBase {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MtProxyInstaller(QObject *parent = nullptr);
|
||||
|
||||
amnezia::ErrorCode
|
||||
extractConfigFromContainer(amnezia::DockerContainer container, const amnezia::ServerCredentials &credentials,
|
||||
SshSession *sshSession, amnezia::ContainerConfig &config) override;
|
||||
|
||||
static void uploadClientSettingsSnapshot(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
const amnezia::ContainerConfig &config);
|
||||
|
||||
static amnezia::ErrorCode queryDiagnostics(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container, int listenPort,
|
||||
MtProxyContainerDiagnostics &out);
|
||||
};
|
||||
|
||||
#endif // MTPROXYINSTALLER_H
|
||||
79
client/core/installers/telemtInstaller.cpp
Normal file
79
client/core/installers/telemtInstaller.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#include "telemtInstaller.h"
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace {
|
||||
constexpr QLatin1String kTelemtClientJsonPath("/data/amnezia-telemt-client.json");
|
||||
constexpr QLatin1String kTelemtClientJsonUploadPath("data/amnezia-telemt-client.json");
|
||||
constexpr QLatin1String kTelemtSecretPath("/data/secret");
|
||||
}
|
||||
|
||||
TelemtInstaller::TelemtInstaller(QObject *parent) : InstallerBase(parent) {}
|
||||
|
||||
ErrorCode TelemtInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession *sshSession, ContainerConfig &config) {
|
||||
if (container != DockerContainer::Telemt || !sshSession) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
TelemtProtocolConfig *tc = config.getTelemtProtocolConfig();
|
||||
if (!tc) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode jsonErr = ErrorCode::NoError;
|
||||
const QByteArray jsonRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtClientJsonPath), jsonErr);
|
||||
if (jsonErr == ErrorCode::NoError && !jsonRaw.trimmed().isEmpty()) {
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(jsonRaw.trimmed(), &parseError);
|
||||
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
|
||||
QJsonObject merged = tc->toJson();
|
||||
const QJsonObject snap = doc.object();
|
||||
for (auto it = snap.constBegin(); it != snap.constEnd(); ++it) {
|
||||
merged.insert(it.key(), it.value());
|
||||
}
|
||||
*tc = TelemtProtocolConfig::fromJson(merged);
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCode secretErr = ErrorCode::NoError;
|
||||
const QByteArray secretRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtSecretPath), secretErr);
|
||||
const QString sec = QString::fromUtf8(secretRaw).trimmed();
|
||||
if (sec.length() == 32) {
|
||||
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
|
||||
if (hex32.match(sec).hasMatch()) {
|
||||
tc->secret = sec;
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
void TelemtInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,
|
||||
DockerContainer container, const ContainerConfig &config) {
|
||||
const TelemtProtocolConfig *tc = config.getTelemtProtocolConfig();
|
||||
if (!tc) {
|
||||
return;
|
||||
}
|
||||
const QByteArray payload = QJsonDocument(tc->toJson()).toJson(QJsonDocument::Compact);
|
||||
const ErrorCode err = sshSession.uploadTextFileToContainer(container, credentials, QString::fromUtf8(payload),
|
||||
QString(kTelemtClientJsonUploadPath));
|
||||
if (err != ErrorCode::NoError) {
|
||||
qWarning() << "TelemtInstaller::uploadClientSettingsSnapshot failed" << err;
|
||||
}
|
||||
}
|
||||
20
client/core/installers/telemtInstaller.h
Normal file
20
client/core/installers/telemtInstaller.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef TELEMTINSTALLER_H
|
||||
#define TELEMTINSTALLER_H
|
||||
|
||||
#include "installerBase.h"
|
||||
|
||||
class TelemtInstaller : public InstallerBase {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TelemtInstaller(QObject *parent = nullptr);
|
||||
|
||||
amnezia::ErrorCode
|
||||
extractConfigFromContainer(amnezia::DockerContainer container, const amnezia::ServerCredentials &credentials,
|
||||
SshSession *sshSession, amnezia::ContainerConfig &config) override;
|
||||
|
||||
static void uploadClientSettingsSnapshot(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
const amnezia::ContainerConfig &config);
|
||||
};
|
||||
|
||||
#endif // TELEMTINSTALLER_H
|
||||
@@ -14,8 +14,18 @@
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "logger.h"
|
||||
|
||||
namespace {
|
||||
namespace
|
||||
{
|
||||
Logger logger("XrayInstaller");
|
||||
|
||||
// Xray expects uTLS preset names (chrome, firefox, …). Old Amnezia/server templates used "Mozilla/5.0".
|
||||
QString normalizeXrayFingerprint(const QString &fp)
|
||||
{
|
||||
if (fp.isEmpty() || fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
return QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
}
|
||||
return fp;
|
||||
}
|
||||
}
|
||||
|
||||
using namespace amnezia;
|
||||
@@ -63,18 +73,251 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
}
|
||||
|
||||
QJsonObject streamSettings = inbound[protocols::xray::streamSettings].toObject();
|
||||
QJsonObject realitySettings = streamSettings[protocols::xray::realitySettings].toObject();
|
||||
if (!realitySettings.contains(protocols::xray::serverNames)) {
|
||||
logger.error() << "Settings missing 'serverNames' field";
|
||||
auto *xrayConfig = config.getXrayProtocolConfig();
|
||||
if (!xrayConfig) {
|
||||
logger.error() << "No XrayProtocolConfig in ContainerConfig";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QString siteName = realitySettings[protocols::xray::serverNames][0].toString();
|
||||
XrayServerConfig &srv = xrayConfig->serverConfig;
|
||||
|
||||
if (auto* xrayConfig = config.getXrayProtocolConfig()) {
|
||||
xrayConfig->serverConfig.site = siteName;
|
||||
// ── Port ─────────────────────────────────────────────────────────
|
||||
if (inbound.contains(protocols::xray::port)) {
|
||||
srv.port = QString::number(inbound[protocols::xray::port].toInt());
|
||||
}
|
||||
|
||||
|
||||
// ── Network (transport) ───────────────────────────────────────────
|
||||
QString networkVal = streamSettings.value(protocols::xray::network).toString("tcp");
|
||||
if (networkVal == "xhttp") {
|
||||
srv.transport = "xhttp";
|
||||
} else if (networkVal == "kcp") {
|
||||
srv.transport = "mkcp";
|
||||
} else {
|
||||
srv.transport = "raw";
|
||||
}
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────────
|
||||
srv.security = streamSettings.value(protocols::xray::security).toString("reality");
|
||||
|
||||
// ── Reality settings ──────────────────────────────────────────────
|
||||
if (srv.security == "reality") {
|
||||
QJsonObject rs = streamSettings.value(protocols::xray::realitySettings).toObject();
|
||||
|
||||
// serverNames array → site + sni
|
||||
if (rs.contains(protocols::xray::serverNames)) {
|
||||
QString sniVal = rs[protocols::xray::serverNames].toArray().first().toString();
|
||||
srv.sni = sniVal;
|
||||
srv.site = sniVal;
|
||||
} else if (rs.contains(protocols::xray::serverName)) {
|
||||
srv.sni = rs[protocols::xray::serverName].toString();
|
||||
srv.site = srv.sni;
|
||||
}
|
||||
|
||||
srv.fingerprint = normalizeXrayFingerprint(rs.value(protocols::xray::fingerprint).toString());
|
||||
}
|
||||
|
||||
// ── TLS settings ──────────────────────────────────────────────────
|
||||
if (srv.security == "tls") {
|
||||
QJsonObject tls = streamSettings.value("tlsSettings").toObject();
|
||||
srv.sni = tls.value(protocols::xray::serverName).toString();
|
||||
srv.fingerprint = normalizeXrayFingerprint(tls.value(protocols::xray::fingerprint).toString());
|
||||
|
||||
QJsonArray alpnArr = tls.value("alpn").toArray();
|
||||
QStringList alpnList;
|
||||
for (const QJsonValue &v : alpnArr) {
|
||||
alpnList << v.toString();
|
||||
}
|
||||
srv.alpn = alpnList.join(",");
|
||||
}
|
||||
|
||||
// ── Flow (from users array) ───────────────────────────────────────
|
||||
if (inbound.contains(protocols::xray::settings)) {
|
||||
QJsonObject s = inbound[protocols::xray::settings].toObject();
|
||||
QJsonArray clientsArr = s.value(protocols::xray::clients).toArray();
|
||||
if (!clientsArr.isEmpty()) {
|
||||
srv.flow = clientsArr[0].toObject().value(protocols::xray::flow).toString();
|
||||
}
|
||||
}
|
||||
|
||||
// ── XHTTP settings (Xray-core SplitHTTPConfig + legacy Amnezia keys) ──
|
||||
if (srv.transport == "xhttp") {
|
||||
QJsonObject xhttpObj = streamSettings.value("xhttpSettings").toObject();
|
||||
{
|
||||
const QString m = xhttpObj.value("mode").toString();
|
||||
if (m.isEmpty() || m == QLatin1String("auto"))
|
||||
srv.xhttp.mode = QStringLiteral("Auto");
|
||||
else if (m == QLatin1String("packet-up"))
|
||||
srv.xhttp.mode = QStringLiteral("Packet-up");
|
||||
else if (m == QLatin1String("stream-up"))
|
||||
srv.xhttp.mode = QStringLiteral("Stream-up");
|
||||
else if (m == QLatin1String("stream-one"))
|
||||
srv.xhttp.mode = QStringLiteral("Stream-one");
|
||||
else
|
||||
srv.xhttp.mode = m;
|
||||
}
|
||||
|
||||
srv.xhttp.host = xhttpObj.value("host").toString();
|
||||
srv.xhttp.path = xhttpObj.value("path").toString();
|
||||
|
||||
{
|
||||
const QJsonObject hdrs = xhttpObj.value("headers").toObject();
|
||||
if (hdrs.contains(QLatin1String("Host")) || !hdrs.isEmpty())
|
||||
srv.xhttp.headersTemplate = QStringLiteral("HTTP");
|
||||
}
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("uplinkHTTPMethod")))
|
||||
srv.xhttp.uplinkMethod = xhttpObj.value("uplinkHTTPMethod").toString();
|
||||
else
|
||||
srv.xhttp.uplinkMethod = xhttpObj.value("method").toString();
|
||||
|
||||
srv.xhttp.disableGrpc = xhttpObj.value("noGRPCHeader").toBool(true);
|
||||
srv.xhttp.disableSse = xhttpObj.value("noSSEHeader").toBool(true);
|
||||
|
||||
auto sessionSeqUi = [](const QString &core) -> QString {
|
||||
if (core.isEmpty() || core == QLatin1String("path"))
|
||||
return QStringLiteral("Path");
|
||||
if (core == QLatin1String("cookie"))
|
||||
return QStringLiteral("Cookie");
|
||||
if (core == QLatin1String("header"))
|
||||
return QStringLiteral("Header");
|
||||
if (core == QLatin1String("query"))
|
||||
return QStringLiteral("Query");
|
||||
return core;
|
||||
};
|
||||
QString sess = xhttpObj.value("sessionPlacement").toString();
|
||||
if (sess.isEmpty())
|
||||
sess = xhttpObj.value("scSessionPlacement").toString();
|
||||
srv.xhttp.sessionPlacement = sessionSeqUi(sess);
|
||||
|
||||
QString seq = xhttpObj.value("seqPlacement").toString();
|
||||
if (seq.isEmpty())
|
||||
seq = xhttpObj.value("scSeqPlacement").toString();
|
||||
srv.xhttp.seqPlacement = sessionSeqUi(seq);
|
||||
|
||||
auto uplinkDataUi = [](const QString &core) -> QString {
|
||||
if (core.isEmpty() || core == QLatin1String("body"))
|
||||
return QStringLiteral("Body");
|
||||
if (core == QLatin1String("auto"))
|
||||
return QStringLiteral("Auto");
|
||||
if (core == QLatin1String("header"))
|
||||
return QStringLiteral("Header");
|
||||
if (core == QLatin1String("cookie"))
|
||||
return QStringLiteral("Cookie");
|
||||
return core;
|
||||
};
|
||||
QString udata = xhttpObj.value("uplinkDataPlacement").toString();
|
||||
if (udata.isEmpty())
|
||||
udata = xhttpObj.value("scUplinkDataPlacement").toString();
|
||||
srv.xhttp.uplinkDataPlacement = uplinkDataUi(udata);
|
||||
|
||||
srv.xhttp.sessionKey = xhttpObj.value("sessionKey").toString();
|
||||
srv.xhttp.seqKey = xhttpObj.value("seqKey").toString();
|
||||
srv.xhttp.uplinkDataKey = xhttpObj.value("uplinkDataKey").toString();
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("uplinkChunkSize"))) {
|
||||
QJsonObject uc = xhttpObj.value("uplinkChunkSize").toObject();
|
||||
if (!uc.isEmpty())
|
||||
srv.xhttp.uplinkChunkSize = QString::number(uc.value("from").toInt());
|
||||
} else if (xhttpObj.contains(QLatin1String("xhttpUplinkChunkSize"))) {
|
||||
srv.xhttp.uplinkChunkSize = QString::number(xhttpObj.value("xhttpUplinkChunkSize").toInt());
|
||||
}
|
||||
if (xhttpObj.contains(QLatin1String("scMaxBufferedPosts"))) {
|
||||
srv.xhttp.scMaxBufferedPosts = QString::number(xhttpObj.value("scMaxBufferedPosts").toVariant().toLongLong());
|
||||
}
|
||||
|
||||
auto readRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
QJsonObject r = xhttpObj.value(QLatin1String(key)).toObject();
|
||||
if (!r.isEmpty()) {
|
||||
minOut = QString::number(r.value("from").toInt());
|
||||
maxOut = QString::number(r.value("to").toInt());
|
||||
}
|
||||
};
|
||||
readRange("scMaxEachPostBytes", srv.xhttp.scMaxEachPostBytesMin, srv.xhttp.scMaxEachPostBytesMax);
|
||||
readRange("scMinPostsIntervalMs", srv.xhttp.scMinPostsIntervalMsMin, srv.xhttp.scMinPostsIntervalMsMax);
|
||||
readRange("scStreamUpServerSecs", srv.xhttp.scStreamUpServerSecsMin, srv.xhttp.scStreamUpServerSecsMax);
|
||||
|
||||
auto loadPaddingFromObject = [&](const QJsonObject &pad) {
|
||||
if (pad.contains(QLatin1String("xPaddingObfsMode")))
|
||||
srv.xhttp.xPadding.obfsMode = pad.value("xPaddingObfsMode").toBool(true);
|
||||
srv.xhttp.xPadding.key = pad.value("xPaddingKey").toString();
|
||||
srv.xhttp.xPadding.header = pad.value("xPaddingHeader").toString();
|
||||
srv.xhttp.xPadding.placement = pad.value("xPaddingPlacement").toString();
|
||||
srv.xhttp.xPadding.method = pad.value("xPaddingMethod").toString();
|
||||
QJsonObject bytesRange = pad.value("xPaddingBytes").toObject();
|
||||
if (!bytesRange.isEmpty()) {
|
||||
srv.xhttp.xPadding.bytesMin = QString::number(bytesRange.value("from").toInt());
|
||||
srv.xhttp.xPadding.bytesMax = QString::number(bytesRange.value("to").toInt());
|
||||
}
|
||||
QString pl = srv.xhttp.xPadding.placement.toLower();
|
||||
if (pl == QLatin1String("cookie"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Cookie");
|
||||
else if (pl == QLatin1String("header"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Header");
|
||||
else if (pl == QLatin1String("query"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Query");
|
||||
else if (pl == QLatin1String("queryinheader"))
|
||||
srv.xhttp.xPadding.placement = QStringLiteral("Query in header");
|
||||
QString met = srv.xhttp.xPadding.method.toLower();
|
||||
if (met == QLatin1String("repeat-x"))
|
||||
srv.xhttp.xPadding.method = QStringLiteral("Repeat-x");
|
||||
else if (met == QLatin1String("tokenish"))
|
||||
srv.xhttp.xPadding.method = QStringLiteral("Tokenish");
|
||||
};
|
||||
if (xhttpObj.contains(QLatin1String("xPaddingObfsMode")) || xhttpObj.contains(QLatin1String("xPaddingKey"))
|
||||
|| !xhttpObj.value("xPaddingBytes").toObject().isEmpty()) {
|
||||
loadPaddingFromObject(xhttpObj);
|
||||
} else if (xhttpObj.contains(QLatin1String("xPadding")) && xhttpObj.value("xPadding").isObject()) {
|
||||
const QJsonObject nested = xhttpObj.value("xPadding").toObject();
|
||||
if (!nested.isEmpty()) {
|
||||
loadPaddingFromObject(nested);
|
||||
if (!nested.contains(QLatin1String("xPaddingObfsMode")))
|
||||
srv.xhttp.xPadding.obfsMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (xhttpObj.contains(QLatin1String("xmux"))) {
|
||||
QJsonObject mux = xhttpObj.value("xmux").toObject();
|
||||
srv.xhttp.xmux.enabled = true;
|
||||
|
||||
auto readMuxRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
QJsonObject r = mux.value(QLatin1String(key)).toObject();
|
||||
if (!r.isEmpty()) {
|
||||
minOut = QString::number(r.value("from").toInt());
|
||||
maxOut = QString::number(r.value("to").toInt());
|
||||
}
|
||||
};
|
||||
readMuxRange("maxConcurrency", srv.xhttp.xmux.maxConcurrencyMin, srv.xhttp.xmux.maxConcurrencyMax);
|
||||
readMuxRange("maxConnections", srv.xhttp.xmux.maxConnectionsMin, srv.xhttp.xmux.maxConnectionsMax);
|
||||
readMuxRange("cMaxReuseTimes", srv.xhttp.xmux.cMaxReuseTimesMin, srv.xhttp.xmux.cMaxReuseTimesMax);
|
||||
readMuxRange("hMaxRequestTimes", srv.xhttp.xmux.hMaxRequestTimesMin, srv.xhttp.xmux.hMaxRequestTimesMax);
|
||||
readMuxRange("hMaxReusableSecs", srv.xhttp.xmux.hMaxReusableSecsMin, srv.xhttp.xmux.hMaxReusableSecsMax);
|
||||
|
||||
if (mux.contains(QLatin1String("hKeepAlivePeriod")))
|
||||
srv.xhttp.xmux.hKeepAlivePeriod = QString::number(mux.value("hKeepAlivePeriod").toVariant().toLongLong());
|
||||
}
|
||||
}
|
||||
|
||||
// ── mKCP settings ─────────────────────────────────────────────────
|
||||
if (srv.transport == "mkcp") {
|
||||
QJsonObject kcp = streamSettings.value("kcpSettings").toObject();
|
||||
if (kcp.contains("tti")) {
|
||||
srv.mkcp.tti = QString::number(kcp["tti"].toInt());
|
||||
}
|
||||
if (kcp.contains("uplinkCapacity")) {
|
||||
srv.mkcp.uplinkCapacity = QString::number(kcp["uplinkCapacity"].toInt());
|
||||
}
|
||||
if (kcp.contains("downlinkCapacity")) {
|
||||
srv.mkcp.downlinkCapacity = QString::number(kcp["downlinkCapacity"].toInt());
|
||||
}
|
||||
if (kcp.contains("readBufferSize")) {
|
||||
srv.mkcp.readBufferSize = QString::number(kcp["readBufferSize"].toInt());
|
||||
}
|
||||
if (kcp.contains("writeBufferSize")) {
|
||||
srv.mkcp.writeBufferSize = QString::number(kcp["writeBufferSize"].toInt());
|
||||
}
|
||||
srv.mkcp.congestion = kcp.value("congestion").toBool(true);
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,26 @@ const Socks5ProxyProtocolConfig* ContainerConfig::getSocks5ProxyProtocolConfig()
|
||||
return protocolConfig.as<Socks5ProxyProtocolConfig>();
|
||||
}
|
||||
|
||||
MtProxyProtocolConfig* ContainerConfig::getMtProxyProtocolConfig()
|
||||
{
|
||||
return protocolConfig.as<MtProxyProtocolConfig>();
|
||||
}
|
||||
|
||||
const MtProxyProtocolConfig* ContainerConfig::getMtProxyProtocolConfig() const
|
||||
{
|
||||
return protocolConfig.as<MtProxyProtocolConfig>();
|
||||
}
|
||||
|
||||
TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig()
|
||||
{
|
||||
return protocolConfig.as<TelemtProtocolConfig>();
|
||||
}
|
||||
|
||||
const TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig() const
|
||||
{
|
||||
return protocolConfig.as<TelemtProtocolConfig>();
|
||||
}
|
||||
|
||||
Ikev2ProtocolConfig* ContainerConfig::getIkev2ProtocolConfig()
|
||||
{
|
||||
return protocolConfig.as<Ikev2ProtocolConfig>();
|
||||
|
||||
@@ -57,6 +57,12 @@ struct ContainerConfig {
|
||||
Socks5ProxyProtocolConfig* getSocks5ProxyProtocolConfig();
|
||||
const Socks5ProxyProtocolConfig* getSocks5ProxyProtocolConfig() const;
|
||||
|
||||
MtProxyProtocolConfig* getMtProxyProtocolConfig();
|
||||
const MtProxyProtocolConfig* getMtProxyProtocolConfig() const;
|
||||
|
||||
TelemtProtocolConfig* getTelemtProtocolConfig();
|
||||
const TelemtProtocolConfig* getTelemtProtocolConfig() const;
|
||||
|
||||
Ikev2ProtocolConfig* getIkev2ProtocolConfig();
|
||||
const Ikev2ProtocolConfig* getIkev2ProtocolConfig() const;
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/models/protocols/ikev2ProtocolConfig.h"
|
||||
#include "core/models/protocols/dnsProtocolConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -38,6 +40,10 @@ Proto ProtocolConfig::type() const
|
||||
return Proto::TorWebSite;
|
||||
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
|
||||
return Proto::Dns;
|
||||
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
|
||||
return Proto::MtProxy;
|
||||
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
|
||||
return Proto::Telemt;
|
||||
}
|
||||
return Proto::Unknown;
|
||||
}, data);
|
||||
@@ -65,6 +71,10 @@ QString ProtocolConfig::port() const
|
||||
return QString();
|
||||
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
|
||||
return QString();
|
||||
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
|
||||
return arg.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : arg.port;
|
||||
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
|
||||
return arg.port.isEmpty() ? QString(protocols::telemt::defaultPort) : arg.port;
|
||||
}
|
||||
return QString();
|
||||
}, data);
|
||||
@@ -88,6 +98,10 @@ QString ProtocolConfig::transportProto() const
|
||||
return QString();
|
||||
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
|
||||
return QString();
|
||||
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
|
||||
return QStringLiteral("tcp");
|
||||
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
|
||||
return QStringLiteral("tcp");
|
||||
}
|
||||
return QString();
|
||||
}, data);
|
||||
@@ -299,6 +313,10 @@ ProtocolConfig ProtocolConfig::fromJson(const QJsonObject& json, Proto type)
|
||||
return ProtocolConfig{TorProtocolConfig::fromJson(json)};
|
||||
case Proto::Dns:
|
||||
return ProtocolConfig{DnsProtocolConfig::fromJson(json)};
|
||||
case Proto::MtProxy:
|
||||
return ProtocolConfig{MtProxyProtocolConfig::fromJson(json)};
|
||||
case Proto::Telemt:
|
||||
return ProtocolConfig{TelemtProtocolConfig::fromJson(json)};
|
||||
default:
|
||||
return ProtocolConfig{AwgProtocolConfig{}};
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include "core/models/protocols/ikev2ProtocolConfig.h"
|
||||
#include "core/models/protocols/torProtocolConfig.h"
|
||||
#include "core/models/protocols/dnsProtocolConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -36,6 +38,8 @@ struct ProtocolConfig {
|
||||
XrayProtocolConfig,
|
||||
SftpProtocolConfig,
|
||||
Socks5ProxyProtocolConfig,
|
||||
MtProxyProtocolConfig,
|
||||
TelemtProtocolConfig,
|
||||
Ikev2ProtocolConfig,
|
||||
TorProtocolConfig,
|
||||
DnsProtocolConfig
|
||||
|
||||
147
client/core/models/protocols/mtProxyProtocolConfig.cpp
Normal file
147
client/core/models/protocols/mtProxyProtocolConfig.cpp
Normal file
@@ -0,0 +1,147 @@
|
||||
#include "mtProxyProtocolConfig.h"
|
||||
|
||||
#include "../../../core/utils/protocolEnum.h"
|
||||
#include "../../../core/protocols/protocolUtils.h"
|
||||
#include "../../../core/utils/constants/configKeys.h"
|
||||
#include "../../../core/utils/constants/protocolConstants.h"
|
||||
#include <QJsonArray>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace amnezia {
|
||||
|
||||
QJsonObject MtProxyProtocolConfig::toJson() const {
|
||||
QJsonObject obj;
|
||||
|
||||
if (!port.isEmpty()) {
|
||||
obj[configKey::port] = port;
|
||||
}
|
||||
if (!secret.isEmpty()) {
|
||||
obj[protocols::mtProxy::secretKey] = secret;
|
||||
}
|
||||
if (!tag.isEmpty()) {
|
||||
obj[protocols::mtProxy::tagKey] = tag;
|
||||
}
|
||||
if (!tgLink.isEmpty()) {
|
||||
obj[protocols::mtProxy::tgLinkKey] = tgLink;
|
||||
}
|
||||
if (!tmeLink.isEmpty()) {
|
||||
obj[protocols::mtProxy::tmeLinkKey] = tmeLink;
|
||||
}
|
||||
obj[protocols::mtProxy::isEnabledKey] = isEnabled;
|
||||
if (!publicHost.isEmpty()) {
|
||||
obj[protocols::mtProxy::publicHostKey] = publicHost;
|
||||
}
|
||||
if (!transportMode.isEmpty()) {
|
||||
obj[protocols::mtProxy::transportModeKey] = transportMode;
|
||||
}
|
||||
if (!tlsDomain.isEmpty()) {
|
||||
obj[protocols::mtProxy::tlsDomainKey] = tlsDomain;
|
||||
}
|
||||
if (!additionalSecrets.isEmpty()) {
|
||||
obj[protocols::mtProxy::additionalSecretsKey] = QJsonArray::fromStringList(additionalSecrets);
|
||||
}
|
||||
if (!workersMode.isEmpty()) {
|
||||
obj[protocols::mtProxy::workersModeKey] = workersMode;
|
||||
}
|
||||
if (!workers.isEmpty()) {
|
||||
obj[protocols::mtProxy::workersKey] = workers;
|
||||
}
|
||||
obj[protocols::mtProxy::natEnabledKey] = natEnabled;
|
||||
if (!natInternalIp.isEmpty()) {
|
||||
obj[protocols::mtProxy::natInternalIpKey] = natInternalIp;
|
||||
}
|
||||
if (!natExternalIp.isEmpty()) {
|
||||
obj[protocols::mtProxy::natExternalIpKey] = natExternalIp;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
MtProxyProtocolConfig MtProxyProtocolConfig::fromJson(const QJsonObject &json) {
|
||||
MtProxyProtocolConfig config;
|
||||
|
||||
config.port = json.value(configKey::port).toString();
|
||||
config.secret = json.value(protocols::mtProxy::secretKey).toString();
|
||||
config.tag = json.value(protocols::mtProxy::tagKey).toString();
|
||||
config.tgLink = json.value(protocols::mtProxy::tgLinkKey).toString();
|
||||
config.tmeLink = json.value(protocols::mtProxy::tmeLinkKey).toString();
|
||||
config.isEnabled = json.value(protocols::mtProxy::isEnabledKey).toBool(true);
|
||||
config.publicHost = json.value(protocols::mtProxy::publicHostKey).toString();
|
||||
config.transportMode = json.value(protocols::mtProxy::transportModeKey).toString();
|
||||
config.tlsDomain = json.value(protocols::mtProxy::tlsDomainKey).toString();
|
||||
for (const auto &v: json.value(protocols::mtProxy::additionalSecretsKey).toArray()) {
|
||||
const QString s = v.toString();
|
||||
if (!s.isEmpty()) {
|
||||
config.additionalSecrets.append(s);
|
||||
}
|
||||
}
|
||||
config.workersMode = json.value(protocols::mtProxy::workersModeKey).toString();
|
||||
config.workers = json.value(protocols::mtProxy::workersKey).toString();
|
||||
config.natEnabled = json.value(protocols::mtProxy::natEnabledKey).toBool(false);
|
||||
config.natInternalIp = json.value(protocols::mtProxy::natInternalIpKey).toString();
|
||||
config.natExternalIp = json.value(protocols::mtProxy::natExternalIpKey).toString();
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
bool MtProxyProtocolConfig::equalsDockerDeploymentSettings(const MtProxyProtocolConfig &other) const {
|
||||
const auto normPort = [](const QString &p) {
|
||||
return p.isEmpty() ? QString(protocols::mtProxy::defaultPort) : p;
|
||||
};
|
||||
const auto normTransport = [](const QString &t) {
|
||||
return t.isEmpty() ? QString(protocols::mtProxy::transportModeStandard) : t;
|
||||
};
|
||||
const auto normWorkersMode = [](const QString &m) {
|
||||
return m.isEmpty() ? QString(protocols::mtProxy::workersModeAuto) : m;
|
||||
};
|
||||
|
||||
if (normPort(port) != normPort(other.port)) {
|
||||
return false;
|
||||
}
|
||||
if (normTransport(transportMode) != normTransport(other.transportMode)) {
|
||||
return false;
|
||||
}
|
||||
if (tlsDomain != other.tlsDomain) {
|
||||
return false;
|
||||
}
|
||||
if (secret != other.secret) {
|
||||
return false;
|
||||
}
|
||||
if (tag != other.tag) {
|
||||
return false;
|
||||
}
|
||||
if (publicHost != other.publicHost) {
|
||||
return false;
|
||||
}
|
||||
if (normWorkersMode(workersMode) != normWorkersMode(other.workersMode)) {
|
||||
return false;
|
||||
}
|
||||
if (workers != other.workers) {
|
||||
return false;
|
||||
}
|
||||
if (natEnabled != other.natEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (natInternalIp != other.natInternalIp) {
|
||||
return false;
|
||||
}
|
||||
if (natExternalIp != other.natExternalIp) {
|
||||
return false;
|
||||
}
|
||||
if (isEnabled != other.isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QStringList aa = additionalSecrets;
|
||||
QStringList bb = other.additionalSecrets;
|
||||
aa.removeAll(QString());
|
||||
bb.removeAll(QString());
|
||||
std::sort(aa.begin(), aa.end());
|
||||
std::sort(bb.begin(), bb.end());
|
||||
return aa == bb;
|
||||
}
|
||||
|
||||
} // namespace amnezia
|
||||
38
client/core/models/protocols/mtProxyProtocolConfig.h
Normal file
38
client/core/models/protocols/mtProxyProtocolConfig.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef MTPROXYPROTOCOLCONFIG_H
|
||||
#define MTPROXYPROTOCOLCONFIG_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace amnezia {
|
||||
|
||||
struct MtProxyProtocolConfig {
|
||||
QString port;
|
||||
QString secret;
|
||||
QString tag;
|
||||
QString tgLink;
|
||||
QString tmeLink;
|
||||
bool isEnabled = true;
|
||||
QString publicHost;
|
||||
QString transportMode;
|
||||
QString tlsDomain;
|
||||
QStringList additionalSecrets;
|
||||
QString workersMode;
|
||||
QString workers;
|
||||
bool natEnabled = false;
|
||||
QString natInternalIp;
|
||||
QString natExternalIp;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
|
||||
static MtProxyProtocolConfig fromJson(const QJsonObject &json);
|
||||
|
||||
// Port, transport, TLS, secrets, NAT, workers, isEnabled, additionalSecrets (order-independent).
|
||||
// Ignores tgLink / tmeLink (derived / display).
|
||||
bool equalsDockerDeploymentSettings(const MtProxyProtocolConfig &other) const;
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // MTPROXYPROTOCOLCONFIG_H
|
||||
162
client/core/models/protocols/telemtProtocolConfig.cpp
Normal file
162
client/core/models/protocols/telemtProtocolConfig.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
#include "telemtProtocolConfig.h"
|
||||
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <algorithm>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
QJsonObject TelemtProtocolConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
if (!port.isEmpty()) {
|
||||
obj[QString(configKey::port)] = port;
|
||||
}
|
||||
if (!secret.isEmpty()) {
|
||||
obj[protocols::telemt::secretKey] = secret;
|
||||
}
|
||||
if (!tag.isEmpty()) {
|
||||
obj[protocols::telemt::tagKey] = tag;
|
||||
}
|
||||
if (!tgLink.isEmpty()) {
|
||||
obj[protocols::telemt::tgLinkKey] = tgLink;
|
||||
}
|
||||
if (!tmeLink.isEmpty()) {
|
||||
obj[protocols::telemt::tmeLinkKey] = tmeLink;
|
||||
}
|
||||
obj[protocols::telemt::isEnabledKey] = isEnabled;
|
||||
if (!publicHost.isEmpty()) {
|
||||
obj[protocols::telemt::publicHostKey] = publicHost;
|
||||
}
|
||||
if (!transportMode.isEmpty()) {
|
||||
obj[protocols::telemt::transportModeKey] = transportMode;
|
||||
}
|
||||
if (!tlsDomain.isEmpty()) {
|
||||
obj[protocols::telemt::tlsDomainKey] = tlsDomain;
|
||||
}
|
||||
obj[protocols::telemt::maskEnabledKey] = maskEnabled;
|
||||
obj[protocols::telemt::tlsEmulationKey] = tlsEmulation;
|
||||
obj[protocols::telemt::useMiddleProxyKey] = useMiddleProxy;
|
||||
if (!userName.isEmpty()) {
|
||||
obj[protocols::telemt::userNameKey] = userName;
|
||||
}
|
||||
if (!additionalSecrets.isEmpty()) {
|
||||
obj[protocols::telemt::additionalSecretsKey] = QJsonArray::fromStringList(additionalSecrets);
|
||||
}
|
||||
if (!workersMode.isEmpty()) {
|
||||
obj[protocols::telemt::workersModeKey] = workersMode;
|
||||
}
|
||||
if (!workers.isEmpty()) {
|
||||
obj[protocols::telemt::workersKey] = workers;
|
||||
}
|
||||
obj[protocols::telemt::natEnabledKey] = natEnabled;
|
||||
if (!natInternalIp.isEmpty()) {
|
||||
obj[protocols::telemt::natInternalIpKey] = natInternalIp;
|
||||
}
|
||||
if (!natExternalIp.isEmpty()) {
|
||||
obj[protocols::telemt::natExternalIpKey] = natExternalIp;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
TelemtProtocolConfig TelemtProtocolConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
TelemtProtocolConfig c;
|
||||
c.port = json.value(QString(configKey::port)).toString();
|
||||
c.secret = json.value(protocols::telemt::secretKey).toString();
|
||||
c.tag = json.value(protocols::telemt::tagKey).toString();
|
||||
c.tgLink = json.value(protocols::telemt::tgLinkKey).toString();
|
||||
c.tmeLink = json.value(protocols::telemt::tmeLinkKey).toString();
|
||||
c.isEnabled = json.value(protocols::telemt::isEnabledKey).toBool(true);
|
||||
c.publicHost = json.value(protocols::telemt::publicHostKey).toString();
|
||||
c.transportMode = json.value(protocols::telemt::transportModeKey).toString();
|
||||
c.tlsDomain = json.value(protocols::telemt::tlsDomainKey).toString();
|
||||
c.maskEnabled = json.value(protocols::telemt::maskEnabledKey).toBool(true);
|
||||
c.tlsEmulation = json.value(protocols::telemt::tlsEmulationKey).toBool(false);
|
||||
c.useMiddleProxy = json.value(protocols::telemt::useMiddleProxyKey).toBool(true);
|
||||
c.userName = json.value(protocols::telemt::userNameKey).toString();
|
||||
for (const auto &v : json.value(protocols::telemt::additionalSecretsKey).toArray()) {
|
||||
const QString s = v.toString();
|
||||
if (!s.isEmpty()) {
|
||||
c.additionalSecrets.append(s);
|
||||
}
|
||||
}
|
||||
c.workersMode = json.value(protocols::telemt::workersModeKey).toString();
|
||||
c.workers = json.value(protocols::telemt::workersKey).toString();
|
||||
c.natEnabled = json.value(protocols::telemt::natEnabledKey).toBool(false);
|
||||
c.natInternalIp = json.value(protocols::telemt::natInternalIpKey).toString();
|
||||
c.natExternalIp = json.value(protocols::telemt::natExternalIpKey).toString();
|
||||
return c;
|
||||
}
|
||||
|
||||
bool TelemtProtocolConfig::equalsDockerDeploymentSettings(const TelemtProtocolConfig &other) const
|
||||
{
|
||||
const auto normPort = [](const QString &p) {
|
||||
return p.isEmpty() ? QString(protocols::telemt::defaultPort) : p;
|
||||
};
|
||||
const auto normTransport = [](const QString &t) {
|
||||
return t.isEmpty() ? QString(protocols::telemt::transportModeStandard) : t;
|
||||
};
|
||||
const auto normWorkersMode = [](const QString &m) {
|
||||
return m.isEmpty() ? QString(protocols::telemt::workersModeAuto) : m;
|
||||
};
|
||||
|
||||
if (normPort(port) != normPort(other.port)) {
|
||||
return false;
|
||||
}
|
||||
if (normTransport(transportMode) != normTransport(other.transportMode)) {
|
||||
return false;
|
||||
}
|
||||
if (tlsDomain != other.tlsDomain) {
|
||||
return false;
|
||||
}
|
||||
if (secret != other.secret) {
|
||||
return false;
|
||||
}
|
||||
if (tag != other.tag) {
|
||||
return false;
|
||||
}
|
||||
if (publicHost != other.publicHost) {
|
||||
return false;
|
||||
}
|
||||
if (maskEnabled != other.maskEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (tlsEmulation != other.tlsEmulation) {
|
||||
return false;
|
||||
}
|
||||
if (useMiddleProxy != other.useMiddleProxy) {
|
||||
return false;
|
||||
}
|
||||
if (userName != other.userName) {
|
||||
return false;
|
||||
}
|
||||
if (normWorkersMode(workersMode) != normWorkersMode(other.workersMode)) {
|
||||
return false;
|
||||
}
|
||||
if (workers != other.workers) {
|
||||
return false;
|
||||
}
|
||||
if (natEnabled != other.natEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (natInternalIp != other.natInternalIp) {
|
||||
return false;
|
||||
}
|
||||
if (natExternalIp != other.natExternalIp) {
|
||||
return false;
|
||||
}
|
||||
if (isEnabled != other.isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QStringList aa = additionalSecrets;
|
||||
QStringList bb = other.additionalSecrets;
|
||||
aa.removeAll(QString());
|
||||
bb.removeAll(QString());
|
||||
std::sort(aa.begin(), aa.end());
|
||||
std::sort(bb.begin(), bb.end());
|
||||
return aa == bb;
|
||||
}
|
||||
38
client/core/models/protocols/telemtProtocolConfig.h
Normal file
38
client/core/models/protocols/telemtProtocolConfig.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef TELEMTPROTOCOLCONFIG_H
|
||||
#define TELEMTPROTOCOLCONFIG_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace amnezia {
|
||||
|
||||
struct TelemtProtocolConfig {
|
||||
QString port;
|
||||
QString secret;
|
||||
QString tag;
|
||||
QString tgLink;
|
||||
QString tmeLink;
|
||||
bool isEnabled = true;
|
||||
QString publicHost;
|
||||
QString transportMode;
|
||||
QString tlsDomain;
|
||||
bool maskEnabled = true;
|
||||
bool tlsEmulation = false;
|
||||
bool useMiddleProxy = true;
|
||||
QString userName;
|
||||
QStringList additionalSecrets;
|
||||
QString workersMode;
|
||||
QString workers;
|
||||
bool natEnabled = false;
|
||||
QString natInternalIp;
|
||||
QString natExternalIp;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static TelemtProtocolConfig fromJson(const QJsonObject &json);
|
||||
bool equalsDockerDeploymentSettings(const TelemtProtocolConfig &other) const;
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // TELEMTPROTOCOLCONFIG_H
|
||||
@@ -3,20 +3,173 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
|
||||
#include "../../../core/utils/protocolEnum.h"
|
||||
#include "../../../core/protocols/protocolUtils.h"
|
||||
#include "../../../core/utils/constants/configKeys.h"
|
||||
#include "../../../core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
|
||||
using namespace amnezia;
|
||||
using namespace ProtocolUtils;
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
QJsonObject XrayXPaddingConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
if (!bytesMin.isEmpty()) obj[configKey::xPaddingBytesMin] = bytesMin;
|
||||
if (!bytesMax.isEmpty()) obj[configKey::xPaddingBytesMax] = bytesMax;
|
||||
obj[configKey::xPaddingObfsMode] = obfsMode;
|
||||
if (!key.isEmpty()) obj[configKey::xPaddingKey] = key;
|
||||
if (!header.isEmpty()) obj[configKey::xPaddingHeader] = header;
|
||||
if (!placement.isEmpty()) obj[configKey::xPaddingPlacement] = placement;
|
||||
if (!method.isEmpty()) obj[configKey::xPaddingMethod] = method;
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayXPaddingConfig XrayXPaddingConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayXPaddingConfig c;
|
||||
c.bytesMin = json.value(configKey::xPaddingBytesMin).toString();
|
||||
c.bytesMax = json.value(configKey::xPaddingBytesMax).toString();
|
||||
c.obfsMode = json.value(configKey::xPaddingObfsMode).toBool(true);
|
||||
c.key = json.value(configKey::xPaddingKey).toString(protocols::xray::defaultSite);
|
||||
c.header = json.value(configKey::xPaddingHeader).toString();
|
||||
c.placement = json.value(configKey::xPaddingPlacement).toString(protocols::xray::defaultXPaddingPlacement);
|
||||
c.method = json.value(configKey::xPaddingMethod).toString(protocols::xray::defaultXPaddingMethod);
|
||||
return c;
|
||||
}
|
||||
|
||||
QJsonObject XrayXmuxConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj[configKey::xmuxEnabled] = enabled;
|
||||
if (!maxConcurrencyMin.isEmpty()) obj[configKey::xmuxMaxConcurrencyMin] = maxConcurrencyMin;
|
||||
if (!maxConcurrencyMax.isEmpty()) obj[configKey::xmuxMaxConcurrencyMax] = maxConcurrencyMax;
|
||||
if (!maxConnectionsMin.isEmpty()) obj[configKey::xmuxMaxConnectionsMin] = maxConnectionsMin;
|
||||
if (!maxConnectionsMax.isEmpty()) obj[configKey::xmuxMaxConnectionsMax] = maxConnectionsMax;
|
||||
if (!cMaxReuseTimesMin.isEmpty()) obj[configKey::xmuxCMaxReuseTimesMin] = cMaxReuseTimesMin;
|
||||
if (!cMaxReuseTimesMax.isEmpty()) obj[configKey::xmuxCMaxReuseTimesMax] = cMaxReuseTimesMax;
|
||||
if (!hMaxRequestTimesMin.isEmpty()) obj[configKey::xmuxHMaxRequestTimesMin] = hMaxRequestTimesMin;
|
||||
if (!hMaxRequestTimesMax.isEmpty()) obj[configKey::xmuxHMaxRequestTimesMax] = hMaxRequestTimesMax;
|
||||
if (!hMaxReusableSecsMin.isEmpty()) obj[configKey::xmuxHMaxReusableSecsMin] = hMaxReusableSecsMin;
|
||||
if (!hMaxReusableSecsMax.isEmpty()) obj[configKey::xmuxHMaxReusableSecsMax] = hMaxReusableSecsMax;
|
||||
if (!hKeepAlivePeriod.isEmpty()) obj[configKey::xmuxHKeepAlivePeriod] = hKeepAlivePeriod;
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayXmuxConfig XrayXmuxConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayXmuxConfig c;
|
||||
c.enabled = json.value(configKey::xmuxEnabled).toBool(true);
|
||||
c.maxConcurrencyMin = json.value(configKey::xmuxMaxConcurrencyMin).toString("0");
|
||||
c.maxConcurrencyMax = json.value(configKey::xmuxMaxConcurrencyMax).toString("0");
|
||||
c.maxConnectionsMin = json.value(configKey::xmuxMaxConnectionsMin).toString("0");
|
||||
c.maxConnectionsMax = json.value(configKey::xmuxMaxConnectionsMax).toString("0");
|
||||
c.cMaxReuseTimesMin = json.value(configKey::xmuxCMaxReuseTimesMin).toString("0");
|
||||
c.cMaxReuseTimesMax = json.value(configKey::xmuxCMaxReuseTimesMax).toString("0");
|
||||
c.hMaxRequestTimesMin = json.value(configKey::xmuxHMaxRequestTimesMin).toString("0");
|
||||
c.hMaxRequestTimesMax = json.value(configKey::xmuxHMaxRequestTimesMax).toString("0");
|
||||
c.hMaxReusableSecsMin = json.value(configKey::xmuxHMaxReusableSecsMin).toString("0");
|
||||
c.hMaxReusableSecsMax = json.value(configKey::xmuxHMaxReusableSecsMax).toString("0");
|
||||
c.hKeepAlivePeriod = json.value(configKey::xmuxHKeepAlivePeriod).toString();
|
||||
return c;
|
||||
}
|
||||
|
||||
QJsonObject XrayXhttpConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
if (!mode.isEmpty()) obj[configKey::xhttpMode] = mode;
|
||||
if (!host.isEmpty()) obj[configKey::xhttpHost] = host;
|
||||
if (!path.isEmpty()) obj[configKey::xhttpPath] = path;
|
||||
if (!headersTemplate.isEmpty()) obj[configKey::xhttpHeadersTemplate] = headersTemplate;
|
||||
if (!uplinkMethod.isEmpty()) obj[configKey::xhttpUplinkMethod] = uplinkMethod;
|
||||
obj[configKey::xhttpDisableGrpc] = disableGrpc;
|
||||
obj[configKey::xhttpDisableSse] = disableSse;
|
||||
|
||||
if (!sessionPlacement.isEmpty()) obj[configKey::xhttpSessionPlacement] = sessionPlacement;
|
||||
if (!sessionKey.isEmpty()) obj[configKey::xhttpSessionKey] = sessionKey;
|
||||
if (!seqPlacement.isEmpty()) obj[configKey::xhttpSeqPlacement] = seqPlacement;
|
||||
if (!seqKey.isEmpty()) obj[configKey::xhttpSeqKey] = seqKey;
|
||||
if (!uplinkDataPlacement.isEmpty()) obj[configKey::xhttpUplinkDataPlacement] = uplinkDataPlacement;
|
||||
if (!uplinkDataKey.isEmpty()) obj[configKey::xhttpUplinkDataKey] = uplinkDataKey;
|
||||
|
||||
if (!uplinkChunkSize.isEmpty()) obj[configKey::xhttpUplinkChunkSize] = uplinkChunkSize;
|
||||
if (!scMaxBufferedPosts.isEmpty()) obj[configKey::xhttpScMaxBufferedPosts] = scMaxBufferedPosts;
|
||||
if (!scMaxEachPostBytesMin.isEmpty()) obj[configKey::xhttpScMaxEachPostBytesMin] = scMaxEachPostBytesMin;
|
||||
if (!scMaxEachPostBytesMax.isEmpty()) obj[configKey::xhttpScMaxEachPostBytesMax] = scMaxEachPostBytesMax;
|
||||
if (!scMinPostsIntervalMsMin.isEmpty()) obj[configKey::xhttpScMinPostsIntervalMsMin] = scMinPostsIntervalMsMin;
|
||||
if (!scMinPostsIntervalMsMax.isEmpty()) obj[configKey::xhttpScMinPostsIntervalMsMax] = scMinPostsIntervalMsMax;
|
||||
if (!scStreamUpServerSecsMin.isEmpty()) obj[configKey::xhttpScStreamUpServerSecsMin] = scStreamUpServerSecsMin;
|
||||
if (!scStreamUpServerSecsMax.isEmpty()) obj[configKey::xhttpScStreamUpServerSecsMax] = scStreamUpServerSecsMax;
|
||||
|
||||
obj["xPadding"] = xPadding.toJson();
|
||||
obj["xmux"] = xmux.toJson();
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayXhttpConfig c;
|
||||
c.mode = json.value(configKey::xhttpMode).toString(protocols::xray::defaultXhttpMode);
|
||||
c.host = json.value(configKey::xhttpHost).toString(protocols::xray::defaultSite);
|
||||
c.path = json.value(configKey::xhttpPath).toString();
|
||||
c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString(protocols::xray::defaultXhttpHeadersTemplate);
|
||||
c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString(protocols::xray::defaultXhttpUplinkMethod);
|
||||
c.disableGrpc = json.value(configKey::xhttpDisableGrpc).toBool(true);
|
||||
c.disableSse = json.value(configKey::xhttpDisableSse).toBool(true);
|
||||
|
||||
c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString(protocols::xray::defaultXhttpSessionPlacement);
|
||||
c.sessionKey = json.value(configKey::xhttpSessionKey).toString();
|
||||
c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString(protocols::xray::defaultXhttpSessionPlacement);
|
||||
c.seqKey = json.value(configKey::xhttpSeqKey).toString();
|
||||
c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString(protocols::xray::defaultXhttpUplinkDataPlacement);
|
||||
c.uplinkDataKey = json.value(configKey::xhttpUplinkDataKey).toString();
|
||||
|
||||
c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString("0");
|
||||
c.scMaxBufferedPosts = json.value(configKey::xhttpScMaxBufferedPosts).toString();
|
||||
c.scMaxEachPostBytesMin = json.value(configKey::xhttpScMaxEachPostBytesMin).toString("1");
|
||||
c.scMaxEachPostBytesMax = json.value(configKey::xhttpScMaxEachPostBytesMax).toString("100");
|
||||
c.scMinPostsIntervalMsMin = json.value(configKey::xhttpScMinPostsIntervalMsMin).toString("100");
|
||||
c.scMinPostsIntervalMsMax = json.value(configKey::xhttpScMinPostsIntervalMsMax).toString("800");
|
||||
c.scStreamUpServerSecsMin = json.value(configKey::xhttpScStreamUpServerSecsMin).toString("1");
|
||||
c.scStreamUpServerSecsMax = json.value(configKey::xhttpScStreamUpServerSecsMax).toString("100");
|
||||
|
||||
c.xPadding = XrayXPaddingConfig::fromJson(json.value("xPadding").toObject());
|
||||
c.xmux = XrayXmuxConfig::fromJson(json.value("xmux").toObject());
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
QJsonObject XrayMkcpConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
if (!tti.isEmpty()) obj[configKey::mkcpTti] = tti;
|
||||
if (!uplinkCapacity.isEmpty()) obj[configKey::mkcpUplinkCapacity] = uplinkCapacity;
|
||||
if (!downlinkCapacity.isEmpty()) obj[configKey::mkcpDownlinkCapacity] = downlinkCapacity;
|
||||
if (!readBufferSize.isEmpty()) obj[configKey::mkcpReadBufferSize] = readBufferSize;
|
||||
if (!writeBufferSize.isEmpty()) obj[configKey::mkcpWriteBufferSize] = writeBufferSize;
|
||||
obj[configKey::mkcpCongestion] = congestion;
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayMkcpConfig XrayMkcpConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayMkcpConfig c;
|
||||
c.tti = json.value(configKey::mkcpTti).toString();
|
||||
c.uplinkCapacity = json.value(configKey::mkcpUplinkCapacity).toString();
|
||||
c.downlinkCapacity = json.value(configKey::mkcpDownlinkCapacity).toString();
|
||||
c.readBufferSize = json.value(configKey::mkcpReadBufferSize).toString();
|
||||
c.writeBufferSize = json.value(configKey::mkcpWriteBufferSize).toString();
|
||||
c.congestion = json.value(configKey::mkcpCongestion).toBool(true);
|
||||
return c;
|
||||
}
|
||||
|
||||
QJsonObject XrayServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
|
||||
|
||||
// Existing fields
|
||||
if (!port.isEmpty()) {
|
||||
obj[configKey::port] = port;
|
||||
}
|
||||
@@ -29,60 +182,96 @@ QJsonObject XrayServerConfig::toJson() const
|
||||
if (!site.isEmpty()) {
|
||||
obj[configKey::site] = site;
|
||||
}
|
||||
|
||||
|
||||
if (isThirdPartyConfig) {
|
||||
obj[configKey::isThirdPartyConfig] = isThirdPartyConfig;
|
||||
}
|
||||
|
||||
|
||||
// New: Security
|
||||
if (!security.isEmpty()) {
|
||||
obj[configKey::xraySecurity] = security;
|
||||
}
|
||||
if (!flow.isEmpty()) {
|
||||
obj[configKey::xrayFlow] = flow;
|
||||
}
|
||||
if (!fingerprint.isEmpty()) {
|
||||
obj[configKey::xrayFingerprint] = fingerprint;
|
||||
}
|
||||
if (!sni.isEmpty()) {
|
||||
obj[configKey::xraySni] = sni;
|
||||
}
|
||||
if (!alpn.isEmpty()) {
|
||||
obj[configKey::xrayAlpn] = alpn;
|
||||
}
|
||||
|
||||
// New: Transport
|
||||
if (!transport.isEmpty()) {
|
||||
obj[configKey::xrayTransport] = transport;
|
||||
}
|
||||
obj["xhttp"] = xhttp.toJson();
|
||||
obj["mkcp"] = mkcp.toJson();
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayServerConfig XrayServerConfig::fromJson(const QJsonObject& json)
|
||||
XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayServerConfig config;
|
||||
|
||||
config.port = json.value(configKey::port).toString();
|
||||
config.transportProto = json.value(configKey::transportProto).toString();
|
||||
config.subnetAddress = json.value(configKey::subnetAddress).toString();
|
||||
config.site = json.value(configKey::site).toString();
|
||||
|
||||
config.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
|
||||
|
||||
return config;
|
||||
XrayServerConfig c;
|
||||
|
||||
// Existing fields
|
||||
c.port = json.value(configKey::port).toString();
|
||||
c.transportProto = json.value(configKey::transportProto).toString();
|
||||
c.subnetAddress = json.value(configKey::subnetAddress).toString();
|
||||
c.site = json.value(configKey::site).toString();
|
||||
c.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
|
||||
|
||||
// New: Security
|
||||
c.security = json.value(configKey::xraySecurity).toString(protocols::xray::defaultSecurity);
|
||||
c.flow = json.value(configKey::xrayFlow).toString(protocols::xray::defaultFlow);
|
||||
c.fingerprint = json.value(configKey::xrayFingerprint).toString(protocols::xray::defaultFingerprint);
|
||||
if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
}
|
||||
c.sni = json.value(configKey::xraySni).toString(protocols::xray::defaultSni);
|
||||
c.alpn = json.value(configKey::xrayAlpn).toString(protocols::xray::defaultAlpn);
|
||||
|
||||
// New: Transport
|
||||
c.transport = json.value(configKey::xrayTransport).toString(protocols::xray::defaultTransport);
|
||||
c.xhttp = XrayXhttpConfig::fromJson(json.value("xhttp").toObject());
|
||||
c.mkcp = XrayMkcpConfig::fromJson(json.value("mkcp").toObject());
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig& other) const
|
||||
bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) const
|
||||
{
|
||||
return port == other.port && site == other.site;
|
||||
return port == other.port
|
||||
&& site == other.site
|
||||
&& security == other.security
|
||||
&& flow == other.flow
|
||||
&& transport == other.transport
|
||||
&& fingerprint == other.fingerprint
|
||||
&& sni == other.sni;
|
||||
}
|
||||
|
||||
QJsonObject XrayClientConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
|
||||
if (!nativeConfig.isEmpty()) {
|
||||
obj[configKey::config] = nativeConfig;
|
||||
}
|
||||
if (!localPort.isEmpty()) {
|
||||
obj[configKey::localPort] = localPort;
|
||||
}
|
||||
if (!id.isEmpty()) {
|
||||
obj[configKey::clientId] = id;
|
||||
}
|
||||
|
||||
if (!nativeConfig.isEmpty()) obj[configKey::config] = nativeConfig;
|
||||
if (!localPort.isEmpty()) obj[configKey::localPort] = localPort;
|
||||
if (!id.isEmpty()) obj[configKey::clientId] = id;
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json)
|
||||
XrayClientConfig XrayClientConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayClientConfig config;
|
||||
|
||||
config.nativeConfig = json.value(configKey::config).toString();
|
||||
config.localPort = json.value(configKey::localPort).toString();
|
||||
config.id = json.value(configKey::clientId).toString();
|
||||
|
||||
if (config.id.isEmpty() && !config.nativeConfig.isEmpty()) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(config.nativeConfig.toUtf8());
|
||||
XrayClientConfig c;
|
||||
c.nativeConfig = json.value(configKey::config).toString();
|
||||
c.localPort = json.value(configKey::localPort).toString();
|
||||
c.id = json.value(configKey::clientId).toString();
|
||||
|
||||
if (c.id.isEmpty() && !c.nativeConfig.isEmpty()) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(c.nativeConfig.toUtf8());
|
||||
if (!doc.isNull() && doc.isObject()) {
|
||||
QJsonObject configObj = doc.object();
|
||||
if (configObj.contains(protocols::xray::outbounds)) {
|
||||
@@ -100,7 +289,7 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json)
|
||||
if (!users.isEmpty()) {
|
||||
QJsonObject user = users[0].toObject();
|
||||
if (user.contains(protocols::xray::id)) {
|
||||
config.id = user[protocols::xray::id].toString();
|
||||
c.id = user[protocols::xray::id].toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,16 +300,15 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
QJsonObject XrayProtocolConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj = serverConfig.toJson();
|
||||
|
||||
|
||||
if (clientConfig.has_value()) {
|
||||
// Third-party import: nativeConfig is raw Xray JSON (inbounds/outbounds)
|
||||
QJsonDocument doc = QJsonDocument::fromJson(clientConfig->nativeConfig.toUtf8());
|
||||
if (!doc.isNull() && doc.isObject() && doc.object().contains(protocols::xray::outbounds)
|
||||
&& !doc.object().contains(configKey::config)) {
|
||||
@@ -130,22 +318,20 @@ QJsonObject XrayProtocolConfig::toJson() const
|
||||
obj[configKey::lastConfig] = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject& json)
|
||||
XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayProtocolConfig config;
|
||||
|
||||
config.serverConfig = XrayServerConfig::fromJson(json);
|
||||
|
||||
XrayProtocolConfig c;
|
||||
c.serverConfig = XrayServerConfig::fromJson(json);
|
||||
|
||||
QString lastConfigStr = json.value(configKey::lastConfig).toString();
|
||||
if (!lastConfigStr.isEmpty()) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(lastConfigStr.toUtf8());
|
||||
if (doc.isObject()) {
|
||||
QJsonObject parsed = doc.object();
|
||||
// Third-party import stores raw Xray config (inbounds/outbounds) directly
|
||||
if (parsed.contains(protocols::xray::outbounds) && !parsed.contains(configKey::config)) {
|
||||
XrayClientConfig clientCfg;
|
||||
clientCfg.nativeConfig = lastConfigStr;
|
||||
@@ -158,14 +344,14 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject& json)
|
||||
}
|
||||
}
|
||||
}
|
||||
config.clientConfig = clientCfg;
|
||||
c.clientConfig = clientCfg;
|
||||
} else {
|
||||
config.clientConfig = XrayClientConfig::fromJson(parsed);
|
||||
c.clientConfig = XrayClientConfig::fromJson(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
bool XrayProtocolConfig::hasClientConfig() const
|
||||
@@ -173,7 +359,7 @@ bool XrayProtocolConfig::hasClientConfig() const
|
||||
return clientConfig.has_value();
|
||||
}
|
||||
|
||||
void XrayProtocolConfig::setClientConfig(const XrayClientConfig& config)
|
||||
void XrayProtocolConfig::setClientConfig(const XrayClientConfig &config)
|
||||
{
|
||||
clientConfig = config;
|
||||
}
|
||||
@@ -184,4 +370,3 @@ void XrayProtocolConfig::clearClientConfig()
|
||||
}
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
|
||||
@@ -2,47 +2,145 @@
|
||||
#define XRAYPROTOCOLCONFIG_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include <QString>
|
||||
#include <optional>
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
|
||||
// ── xPadding ─────────────────────────────────────────────────────────────────
|
||||
struct XrayXPaddingConfig {
|
||||
QString bytesMin; // xPaddingBytes min
|
||||
QString bytesMax; // xPaddingBytes max
|
||||
bool obfsMode = true; // xPaddingObfsMode
|
||||
QString key; // xPaddingKey
|
||||
QString header; // xPaddingHeader
|
||||
QString placement = protocols::xray::defaultXPaddingPlacement; // xPaddingPlacement: Cookie|Header|Query|Body
|
||||
QString method = protocols::xray::defaultXPaddingMethod; // xPaddingMethod: Repeat-x|Random|Zero
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayXPaddingConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
// ── xmux ─────────────────────────────────────────────────────────────────────
|
||||
struct XrayXmuxConfig {
|
||||
bool enabled = true;
|
||||
|
||||
QString maxConcurrencyMin = "0";
|
||||
QString maxConcurrencyMax = "0";
|
||||
QString maxConnectionsMin = "0";
|
||||
QString maxConnectionsMax = "0";
|
||||
QString cMaxReuseTimesMin = "0";
|
||||
QString cMaxReuseTimesMax = "0";
|
||||
QString hMaxRequestTimesMin = "0";
|
||||
QString hMaxRequestTimesMax = "0";
|
||||
QString hMaxReusableSecsMin = "0";
|
||||
QString hMaxReusableSecsMax = "0";
|
||||
QString hKeepAlivePeriod;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayXmuxConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
// ── XHTTP transport ───────────────────────────────────────────────────────────
|
||||
struct XrayXhttpConfig {
|
||||
QString mode = protocols::xray::defaultXhttpMode; // Auto|Packet-up|Stream-up|Stream-one
|
||||
QString host = protocols::xray::defaultXhttpHost;
|
||||
QString path;
|
||||
QString headersTemplate = protocols::xray::defaultXhttpHeadersTemplate; // HTTP|None
|
||||
QString uplinkMethod = protocols::xray::defaultXhttpUplinkMethod; // POST|PUT|PATCH
|
||||
bool disableGrpc = true;
|
||||
bool disableSse = true;
|
||||
|
||||
// Session & Sequence
|
||||
QString sessionPlacement = protocols::xray::defaultXhttpSessionPlacement;
|
||||
QString sessionKey = protocols::xray::defaultXhttpSessionKey;
|
||||
QString seqPlacement = protocols::xray::defaultXhttpSeqPlacement;
|
||||
QString seqKey;
|
||||
QString uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement;
|
||||
QString uplinkDataKey;
|
||||
|
||||
// Traffic Shaping
|
||||
QString uplinkChunkSize = protocols::xray::defaultXhttpUplinkChunkSize;
|
||||
QString scMaxBufferedPosts;
|
||||
QString scMaxEachPostBytesMin = protocols::xray::defaultXhttpScMaxEachPostBytesMin;
|
||||
QString scMaxEachPostBytesMax = protocols::xray::defaultXhttpScMaxEachPostBytesMax;
|
||||
QString scMinPostsIntervalMsMin = protocols::xray::defaultXhttpScMinPostsIntervalMsMin;
|
||||
QString scMinPostsIntervalMsMax = protocols::xray::defaultXhttpScMinPostsIntervalMsMax;
|
||||
QString scStreamUpServerSecsMin = protocols::xray::defaultXhttpScStreamUpServerSecsMin;
|
||||
QString scStreamUpServerSecsMax = protocols::xray::defaultXhttpScStreamUpServerSecsMax;
|
||||
|
||||
XrayXPaddingConfig xPadding;
|
||||
XrayXmuxConfig xmux;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayXhttpConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
// ── mKCP transport ────────────────────────────────────────────────────────────
|
||||
struct XrayMkcpConfig {
|
||||
QString tti;
|
||||
QString uplinkCapacity;
|
||||
QString downlinkCapacity;
|
||||
QString readBufferSize;
|
||||
QString writeBufferSize;
|
||||
bool congestion = true;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayMkcpConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
// ── Server config (settings editable by user) ─────────────────────────────────
|
||||
struct XrayServerConfig {
|
||||
QString port;
|
||||
QString transportProto;
|
||||
QString subnetAddress;
|
||||
QString site;
|
||||
bool isThirdPartyConfig = false;
|
||||
|
||||
|
||||
// New: Security
|
||||
QString security = protocols::xray::defaultSecurity;
|
||||
QString flow = protocols::xray::defaultFlow;
|
||||
QString fingerprint = protocols::xray::defaultFingerprint;
|
||||
QString sni = protocols::xray::defaultSni;
|
||||
QString alpn = protocols::xray::defaultAlpn;
|
||||
|
||||
// New: Transport
|
||||
QString transport = protocols::xray::defaultTransport;
|
||||
XrayXhttpConfig xhttp;
|
||||
XrayMkcpConfig mkcp;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayServerConfig fromJson(const QJsonObject& json);
|
||||
|
||||
bool hasEqualServerSettings(const XrayServerConfig& other) const;
|
||||
|
||||
static XrayServerConfig fromJson(const QJsonObject &json);
|
||||
|
||||
bool hasEqualServerSettings(const XrayServerConfig &other) const;
|
||||
};
|
||||
|
||||
// ── Client config (generated, not edited by user) ─────────────────────────────
|
||||
struct XrayClientConfig {
|
||||
QString nativeConfig;
|
||||
QString localPort;
|
||||
QString id;
|
||||
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayClientConfig fromJson(const QJsonObject& json);
|
||||
static XrayClientConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
// ── Top-level protocol config ──────────────────────────────────────────────────
|
||||
struct XrayProtocolConfig {
|
||||
XrayServerConfig serverConfig;
|
||||
std::optional<XrayClientConfig> clientConfig;
|
||||
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayProtocolConfig fromJson(const QJsonObject& json);
|
||||
|
||||
static XrayProtocolConfig fromJson(const QJsonObject &json);
|
||||
|
||||
bool hasClientConfig() const;
|
||||
void setClientConfig(const XrayClientConfig& config);
|
||||
void setClientConfig(const XrayClientConfig &config);
|
||||
void clearClientConfig();
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // XRAYPROTOCOLCONFIG_H
|
||||
|
||||
|
||||
@@ -68,7 +68,10 @@ QMap<Proto, QString> ProtocolUtils::protocolHumanNames()
|
||||
{ Proto::TorWebSite, "Website in Tor network" },
|
||||
{ Proto::Dns, "DNS Service" },
|
||||
{ Proto::Sftp, QObject::tr("SFTP service") },
|
||||
{ Proto::Socks5Proxy, QObject::tr("SOCKS5 proxy server") } };
|
||||
{ Proto::Socks5Proxy, QObject::tr("SOCKS5 proxy server") },
|
||||
{ Proto::MtProxy, QObject::tr("MTProxy (Telegram)") },
|
||||
{ Proto::Telemt, QObject::tr("Telemt (Telegram)") },
|
||||
};
|
||||
}
|
||||
|
||||
QMap<Proto, QString> ProtocolUtils::protocolDescriptions()
|
||||
@@ -92,6 +95,8 @@ ServiceType ProtocolUtils::protocolService(Proto p)
|
||||
case Proto::Dns: return ServiceType::Other;
|
||||
case Proto::Sftp: return ServiceType::Other;
|
||||
case Proto::Socks5Proxy: return ServiceType::Other;
|
||||
case Proto::MtProxy: return ServiceType::Other;
|
||||
case Proto::Telemt: return ServiceType::Other;
|
||||
default: return ServiceType::Other;
|
||||
}
|
||||
}
|
||||
@@ -104,6 +109,8 @@ int ProtocolUtils::getPortForInstall(Proto p)
|
||||
case OpenVpn:
|
||||
case Socks5Proxy:
|
||||
return QRandomGenerator::global()->bounded(30000, 50000);
|
||||
case MtProxy:
|
||||
case Telemt:
|
||||
default:
|
||||
return defaultPort(p);
|
||||
}
|
||||
@@ -123,6 +130,8 @@ int ProtocolUtils::defaultPort(Proto p)
|
||||
case Proto::Dns: return 53;
|
||||
case Proto::Sftp: return 222;
|
||||
case Proto::Socks5Proxy: return 38080;
|
||||
case Proto::MtProxy: return QString(protocols::mtProxy::defaultPort).toInt();
|
||||
case Proto::Telemt: return QString(protocols::telemt::defaultPort).toInt();
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
@@ -141,6 +150,8 @@ bool ProtocolUtils::defaultPortChangeable(Proto p)
|
||||
case Proto::Dns: return false;
|
||||
case Proto::Sftp: return true;
|
||||
case Proto::Socks5Proxy: return true;
|
||||
case Proto::MtProxy: return true;
|
||||
case Proto::Telemt: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
@@ -161,6 +172,8 @@ TransportProto ProtocolUtils::defaultTransportProto(Proto p)
|
||||
case Proto::Dns: return TransportProto::Udp;
|
||||
case Proto::Sftp: return TransportProto::Tcp;
|
||||
case Proto::Socks5Proxy: return TransportProto::Tcp;
|
||||
case Proto::MtProxy: return TransportProto::Tcp;
|
||||
case Proto::Telemt: return TransportProto::Tcp;
|
||||
default: return TransportProto::Udp;
|
||||
}
|
||||
}
|
||||
@@ -180,9 +193,10 @@ bool ProtocolUtils::defaultTransportProtoChangeable(Proto p)
|
||||
case Proto::Dns: return false;
|
||||
case Proto::Sftp: return false;
|
||||
case Proto::Socks5Proxy: return false;
|
||||
case Proto::MtProxy: return false;
|
||||
case Proto::Telemt: return false;
|
||||
default: return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QString ProtocolUtils::key_proto_config_data(Proto p)
|
||||
@@ -208,4 +222,3 @@ QString ProtocolUtils::getProtocolVersionString(const QJsonObject &protocolConfi
|
||||
if (version == protocols::awg::awgV1_5) return QObject::tr(" (version 1.5)");
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
48
client/core/protocols/xrayProtocol.cpp
Executable file → Normal file
48
client/core/protocols/xrayProtocol.cpp
Executable file → Normal file
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/ipcClient.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/serialization/serialization.h"
|
||||
@@ -9,6 +10,7 @@
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QJsonDocument>
|
||||
#include <QTimer>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkInterface>
|
||||
#include <QtCore/qlogging.h>
|
||||
@@ -79,12 +81,29 @@ ErrorCode XrayProtocol::start()
|
||||
m_socksPassword = creds.password;
|
||||
m_socksPort = creds.port;
|
||||
|
||||
const QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact);
|
||||
QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact);
|
||||
if (xrayConfigStr.isEmpty()) {
|
||||
qCritical() << "Xray config is empty";
|
||||
return ErrorCode::XrayExecutableCrashed;
|
||||
}
|
||||
|
||||
// Fix fingerprint: old configs may contain "Mozilla/5.0" which xray-core rejects.
|
||||
// Replace with the correct default at runtime so stale stored configs still work.
|
||||
if (xrayConfigStr.contains("Mozilla/5.0", Qt::CaseInsensitive)) {
|
||||
xrayConfigStr.replace("Mozilla/5.0", amnezia::protocols::xray::defaultFingerprint,
|
||||
Qt::CaseInsensitive);
|
||||
qDebug() << "XrayProtocol: patched legacy fingerprint to"
|
||||
<< amnezia::protocols::xray::defaultFingerprint;
|
||||
}
|
||||
|
||||
// Fix inbound listen address: old configs may use "10.33.0.2" which doesn't exist
|
||||
// until TUN is created. xray must listen on 127.0.0.1 so tun2socks can connect.
|
||||
if (xrayConfigStr.contains(amnezia::protocols::xray::defaultLocalAddr)) {
|
||||
xrayConfigStr.replace(amnezia::protocols::xray::defaultLocalAddr,
|
||||
amnezia::protocols::xray::defaultLocalListenAddr);
|
||||
qDebug() << "XrayProtocol: patched legacy inbound listen address to 127.0.0.1";
|
||||
}
|
||||
|
||||
return IpcClient::withInterface(
|
||||
[&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStart = iface->xrayStart(xrayConfigStr);
|
||||
@@ -188,6 +207,33 @@ ErrorCode XrayProtocol::startTun2Socks()
|
||||
connect(
|
||||
m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this,
|
||||
[this](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||
// Check stdout for "resource busy" — the TUN device was not yet released
|
||||
// by the previous tun2socks instance. Retry after a short delay.
|
||||
bool resourceBusy = false;
|
||||
if (m_tun2socksProcess) {
|
||||
auto readOut = m_tun2socksProcess->readAllStandardOutput();
|
||||
if (readOut.waitForFinished()) {
|
||||
resourceBusy = readOut.returnValue().contains("resource busy");
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceBusy && m_tun2socksRetryCount < maxTun2SocksRetries) {
|
||||
m_tun2socksRetryCount++;
|
||||
qWarning() << QString("Tun2socks: TUN resource busy, retrying (%1/%2) in %3ms...")
|
||||
.arg(m_tun2socksRetryCount)
|
||||
.arg(maxTun2SocksRetries)
|
||||
.arg(tun2socksRetryDelayMs);
|
||||
QTimer::singleShot(tun2socksRetryDelayMs, this, [this]() {
|
||||
if (ErrorCode err = startTun2Socks(); err != ErrorCode::NoError) {
|
||||
stop();
|
||||
setLastError(err);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
m_tun2socksRetryCount = 0;
|
||||
|
||||
if (exitStatus == QProcess::ExitStatus::CrashExit) {
|
||||
qCritical() << "Tun2socks process crashed!";
|
||||
} else {
|
||||
|
||||
@@ -35,6 +35,9 @@ private:
|
||||
int m_socksPort = 10808;
|
||||
|
||||
QSharedPointer<IpcProcessInterfaceReplica> m_tun2socksProcess;
|
||||
int m_tun2socksRetryCount = 0;
|
||||
static constexpr int maxTun2SocksRetries = 5;
|
||||
static constexpr int tun2socksRetryDelayMs = 400;
|
||||
};
|
||||
|
||||
#endif // XRAYPROTOCOL_H
|
||||
|
||||
@@ -451,4 +451,12 @@ void SecureAppSettingsRepository::setInstallationUuid(const QString &uuid)
|
||||
m_settings->setValue("Conf/installationUuid", uuid);
|
||||
}
|
||||
|
||||
QByteArray SecureAppSettingsRepository::xraySavedConfigs() const
|
||||
{
|
||||
return value("Xray/savedConfigs").toByteArray();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setXraySavedConfigs(const QByteArray &data)
|
||||
{
|
||||
setValue("Xray/savedConfigs", data);
|
||||
}
|
||||
|
||||
@@ -92,6 +92,9 @@ public:
|
||||
|
||||
QString nextAvailableServerName() const;
|
||||
|
||||
QByteArray xraySavedConfigs() const;
|
||||
void setXraySavedConfigs(const QByteArray &data);
|
||||
|
||||
signals:
|
||||
void appLanguageChanged(QLocale locale);
|
||||
void allowedDnsServersChanged(const QStringList &servers);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include <QLatin1Char>
|
||||
#include <QDateTime>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
@@ -104,10 +103,6 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
}
|
||||
|
||||
qDebug() << QString::fromUtf8(responseBody);
|
||||
qDebug() << replyError;
|
||||
qDebug() << httpStatusCode;
|
||||
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
@@ -233,18 +228,3 @@ QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
|
||||
|
||||
return vpnKeyText;
|
||||
}
|
||||
|
||||
QString apiUtils::countryCodeBaseForFlag(const QString &fullCountryCode)
|
||||
{
|
||||
const QString trimmed = fullCountryCode.trimmed();
|
||||
if (trimmed.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
const int dashIdx = trimmed.indexOf(QLatin1Char('-'));
|
||||
const QString base = dashIdx < 0 ? trimmed : trimmed.left(dashIdx);
|
||||
const QString normalized = base.trimmed();
|
||||
if (normalized.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
return normalized.toUpper();
|
||||
}
|
||||
|
||||
@@ -25,9 +25,6 @@ namespace apiUtils
|
||||
|
||||
QString getPremiumV1VpnKey(const QJsonObject &serverConfigObject);
|
||||
QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject);
|
||||
|
||||
// ISO2-style segment for flagKit assets (e.g. US-WEST -> US). Do not use in API request bodies.
|
||||
QString countryCodeBaseForFlag(const QString &fullCountryCode);
|
||||
}
|
||||
|
||||
#endif // APIUTILS_H
|
||||
|
||||
@@ -93,6 +93,8 @@ namespace amnezia
|
||||
constexpr QLatin1String xray("xray");
|
||||
constexpr QLatin1String ssxray("ssxray");
|
||||
constexpr QLatin1String socks5proxy("socks5proxy");
|
||||
constexpr QLatin1String mtproxy("mtproxy");
|
||||
constexpr QLatin1String telemt("telemt");
|
||||
|
||||
constexpr QLatin1String splitTunnelSites("splitTunnelSites");
|
||||
constexpr QLatin1String splitTunnelType("splitTunnelType");
|
||||
@@ -124,6 +126,76 @@ namespace amnezia
|
||||
constexpr QLatin1String dataSent("dataSent");
|
||||
|
||||
constexpr QLatin1String storageServerId("storageServerId");
|
||||
|
||||
// ── Xray-specific keys ────────────────────────────────────────
|
||||
|
||||
// Security
|
||||
constexpr QLatin1String xraySecurity("xray_security"); // none | tls | reality
|
||||
constexpr QLatin1String xrayFlow("xray_flow"); // "" | xtls-rprx-vision | xtls-rprx-vision-udp443
|
||||
constexpr QLatin1String xrayFingerprint("xray_fingerprint"); // Mozilla/5.0 | chrome | firefox | ...
|
||||
constexpr QLatin1String xraySni("xray_sni"); // Server Name (SNI)
|
||||
constexpr QLatin1String xrayAlpn("xray_alpn"); // HTTP/2 | HTTP/1.1 | HTTP/2,HTTP/1.1
|
||||
|
||||
// Transport — common
|
||||
constexpr QLatin1String xrayTransport("xray_transport"); // raw | xhttp | mkcp
|
||||
|
||||
// Transport — XHTTP
|
||||
constexpr QLatin1String xhttpMode("xhttp_mode"); // Auto | Packet-up | Stream-up | Stream-one
|
||||
constexpr QLatin1String xhttpHost("xhttp_host");
|
||||
constexpr QLatin1String xhttpPath("xhttp_path");
|
||||
constexpr QLatin1String xhttpHeadersTemplate("xhttp_headers_template"); // HTTP | None
|
||||
constexpr QLatin1String xhttpUplinkMethod("xhttp_uplink_method"); // POST | PUT | PATCH
|
||||
constexpr QLatin1String xhttpDisableGrpc("xhttp_disable_grpc"); // bool
|
||||
constexpr QLatin1String xhttpDisableSse("xhttp_disable_sse"); // bool
|
||||
|
||||
// Transport — XHTTP Session & Sequence
|
||||
constexpr QLatin1String xhttpSessionPlacement("xhttp_session_placement"); // Path | Header | Cookie | None
|
||||
constexpr QLatin1String xhttpSessionKey("xhttp_session_key");
|
||||
constexpr QLatin1String xhttpSeqPlacement("xhttp_seq_placement");
|
||||
constexpr QLatin1String xhttpSeqKey("xhttp_seq_key");
|
||||
constexpr QLatin1String xhttpUplinkDataPlacement("xhttp_uplink_data_placement"); // Body | Query
|
||||
constexpr QLatin1String xhttpUplinkDataKey("xhttp_uplink_data_key");
|
||||
|
||||
// Transport — XHTTP Traffic Shaping
|
||||
constexpr QLatin1String xhttpUplinkChunkSize("xhttp_uplink_chunk_size");
|
||||
constexpr QLatin1String xhttpScMaxBufferedPosts("xhttp_sc_max_buffered_posts");
|
||||
constexpr QLatin1String xhttpScMaxEachPostBytesMin("xhttp_sc_max_each_post_bytes_min");
|
||||
constexpr QLatin1String xhttpScMaxEachPostBytesMax("xhttp_sc_max_each_post_bytes_max");
|
||||
constexpr QLatin1String xhttpScMinPostsIntervalMsMin("xhttp_sc_min_posts_interval_ms_min");
|
||||
constexpr QLatin1String xhttpScMinPostsIntervalMsMax("xhttp_sc_min_posts_interval_ms_max");
|
||||
constexpr QLatin1String xhttpScStreamUpServerSecsMin("xhttp_sc_stream_up_server_secs_min");
|
||||
constexpr QLatin1String xhttpScStreamUpServerSecsMax("xhttp_sc_stream_up_server_secs_max");
|
||||
|
||||
// Transport — mKCP
|
||||
constexpr QLatin1String mkcpTti("mkcp_tti");
|
||||
constexpr QLatin1String mkcpUplinkCapacity("mkcp_uplink_capacity");
|
||||
constexpr QLatin1String mkcpDownlinkCapacity("mkcp_downlink_capacity");
|
||||
constexpr QLatin1String mkcpReadBufferSize("mkcp_read_buffer_size");
|
||||
constexpr QLatin1String mkcpWriteBufferSize("mkcp_write_buffer_size");
|
||||
constexpr QLatin1String mkcpCongestion("mkcp_congestion"); // bool
|
||||
|
||||
// xPadding
|
||||
constexpr QLatin1String xPaddingBytesMin("xpadding_bytes_min");
|
||||
constexpr QLatin1String xPaddingBytesMax("xpadding_bytes_max");
|
||||
constexpr QLatin1String xPaddingObfsMode("xpadding_obfs_mode"); // bool
|
||||
constexpr QLatin1String xPaddingKey("xpadding_key");
|
||||
constexpr QLatin1String xPaddingHeader("xpadding_header");
|
||||
constexpr QLatin1String xPaddingPlacement("xpadding_placement"); // Cookie | Header | Query | Body
|
||||
constexpr QLatin1String xPaddingMethod("xpadding_method"); // Repeat-x | Random | Zero
|
||||
|
||||
// xmux
|
||||
constexpr QLatin1String xmuxEnabled("xmux_enabled"); // bool
|
||||
constexpr QLatin1String xmuxMaxConcurrencyMin("xmux_max_concurrency_min");
|
||||
constexpr QLatin1String xmuxMaxConcurrencyMax("xmux_max_concurrency_max");
|
||||
constexpr QLatin1String xmuxMaxConnectionsMin("xmux_max_connections_min");
|
||||
constexpr QLatin1String xmuxMaxConnectionsMax("xmux_max_connections_max");
|
||||
constexpr QLatin1String xmuxCMaxReuseTimesMin("xmux_c_max_reuse_times_min");
|
||||
constexpr QLatin1String xmuxCMaxReuseTimesMax("xmux_c_max_reuse_times_max");
|
||||
constexpr QLatin1String xmuxHMaxRequestTimesMin("xmux_h_max_request_times_min");
|
||||
constexpr QLatin1String xmuxHMaxRequestTimesMax("xmux_h_max_request_times_max");
|
||||
constexpr QLatin1String xmuxHMaxReusableSecsMin("xmux_h_max_reusable_secs_min");
|
||||
constexpr QLatin1String xmuxHMaxReusableSecsMax("xmux_h_max_reusable_secs_max");
|
||||
constexpr QLatin1String xmuxHKeepAlivePeriod("xmux_h_keep_alive_period");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
|
||||
namespace protocols
|
||||
{
|
||||
|
||||
@@ -57,6 +58,40 @@ namespace amnezia
|
||||
constexpr char defaultPort[] = "443";
|
||||
constexpr char defaultLocalProxyPort[] = "10808";
|
||||
constexpr char defaultLocalAddr[] = "10.33.0.2";
|
||||
constexpr char defaultLocalListenAddr[] = "127.0.0.1";
|
||||
|
||||
constexpr char defaultSecurity[] = "reality";
|
||||
constexpr char defaultFlow[] = "xtls-rprx-vision";
|
||||
constexpr char defaultTransport[] = "raw";
|
||||
constexpr char defaultFingerprint[] = "chrome";
|
||||
constexpr char defaultSni[] = "cdn.example.com";
|
||||
constexpr char defaultAlpn[] = "HTTP/2";
|
||||
|
||||
constexpr char defaultXhttpMode[] = "Auto";
|
||||
constexpr char defaultXhttpHeadersTemplate[] = "HTTP";
|
||||
constexpr char defaultXhttpUplinkMethod[] = "POST";
|
||||
constexpr char defaultXhttpSessionPlacement[] = "Path";
|
||||
constexpr char defaultXhttpSessionKey[] = "Path";
|
||||
constexpr char defaultXhttpSeqPlacement[] = "Path";
|
||||
constexpr char defaultXhttpUplinkDataPlacement[] = "Body";
|
||||
|
||||
constexpr char defaultXhttpHost[] = "www.googletagmanager.com";
|
||||
constexpr char defaultXhttpUplinkChunkSize[] = "0";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMin[] = "1";
|
||||
constexpr char defaultXhttpScMaxEachPostBytesMax[] = "100";
|
||||
constexpr char defaultXhttpScMinPostsIntervalMsMin[] = "100";
|
||||
constexpr char defaultXhttpScMinPostsIntervalMsMax[] = "800";
|
||||
constexpr char defaultXhttpScStreamUpServerSecsMin[] = "1";
|
||||
constexpr char defaultXhttpScStreamUpServerSecsMax[] = "100";
|
||||
|
||||
constexpr char defaultXPaddingPlacement[] = "Cookie";
|
||||
constexpr char defaultXPaddingMethod[] = "Repeat-x";
|
||||
|
||||
constexpr char defaultMkcpTti[] = "50";
|
||||
constexpr char defaultMkcpUplinkCapacity[] = "5";
|
||||
constexpr char defaultMkcpDownlinkCapacity[] = "20";
|
||||
constexpr char defaultMkcpReadBufferSize[] = "2";
|
||||
constexpr char defaultMkcpWriteBufferSize[] = "2";
|
||||
|
||||
constexpr char outbounds[] = "outbounds";
|
||||
constexpr char inbounds[] = "inbounds";
|
||||
@@ -174,9 +209,71 @@ namespace amnezia
|
||||
constexpr char proxyConfigPath[] = "/usr/local/3proxy/conf/3proxy.cfg";
|
||||
}
|
||||
|
||||
namespace mtProxy
|
||||
{
|
||||
constexpr char secretKey[] = "mtproxy_secret";
|
||||
constexpr char tagKey[] = "mtproxy_tag";
|
||||
constexpr char tgLinkKey[] = "mtproxy_tg_link";
|
||||
constexpr char tmeLinkKey[] = "mtproxy_tme_link";
|
||||
constexpr char isEnabledKey[] = "mtproxy_is_enabled";
|
||||
constexpr char publicHostKey[] = "mtproxy_public_host";
|
||||
constexpr char transportModeKey[] = "mtproxy_transport_mode";
|
||||
constexpr char tlsDomainKey[] = "mtproxy_tls_domain";
|
||||
constexpr char additionalSecretsKey[] = "mtproxy_additional_secrets";
|
||||
constexpr char workersKey[] = "mtproxy_workers";
|
||||
constexpr char workersModeKey[] = "mtproxy_workers_mode";
|
||||
constexpr char natEnabledKey[] = "mtproxy_nat_enabled";
|
||||
constexpr char natInternalIpKey[] = "mtproxy_nat_internal_ip";
|
||||
constexpr char natExternalIpKey[] = "mtproxy_nat_external_ip";
|
||||
|
||||
constexpr char transportModeStandard[] = "standard";
|
||||
constexpr char transportModeFakeTLS[] = "faketls";
|
||||
|
||||
constexpr char workersModeAuto[] = "auto";
|
||||
constexpr char workersModeManual[] = "manual";
|
||||
|
||||
constexpr char defaultPort[] = "443";
|
||||
constexpr char defaultWorkers[] = "2";
|
||||
constexpr int maxWorkers = 32;
|
||||
constexpr int botTagHexLength = 32;
|
||||
constexpr char defaultTlsDomain[] = "googletagmanager.com";
|
||||
}
|
||||
|
||||
namespace telemt
|
||||
{
|
||||
constexpr char secretKey[] = "telemt_secret";
|
||||
constexpr char tagKey[] = "telemt_tag";
|
||||
constexpr char tgLinkKey[] = "telemt_tg_link";
|
||||
constexpr char tmeLinkKey[] = "telemt_tme_link";
|
||||
constexpr char isEnabledKey[] = "telemt_is_enabled";
|
||||
constexpr char publicHostKey[] = "telemt_public_host";
|
||||
constexpr char transportModeKey[] = "telemt_transport_mode";
|
||||
constexpr char tlsDomainKey[] = "telemt_tls_domain";
|
||||
constexpr char maskEnabledKey[] = "telemt_mask_enabled";
|
||||
constexpr char tlsEmulationKey[] = "telemt_tls_emulation";
|
||||
constexpr char useMiddleProxyKey[] = "telemt_use_middle_proxy";
|
||||
constexpr char userNameKey[] = "telemt_user_name";
|
||||
// Stored for UI only (Telemt server ignores these; same controls as MTProxy page)
|
||||
constexpr char additionalSecretsKey[] = "telemt_additional_secrets";
|
||||
constexpr char workersKey[] = "telemt_workers";
|
||||
constexpr char workersModeKey[] = "telemt_workers_mode";
|
||||
constexpr char natEnabledKey[] = "telemt_nat_enabled";
|
||||
constexpr char natInternalIpKey[] = "telemt_nat_internal_ip";
|
||||
constexpr char natExternalIpKey[] = "telemt_nat_external_ip";
|
||||
|
||||
constexpr char transportModeStandard[] = "standard";
|
||||
constexpr char transportModeFakeTLS[] = "faketls";
|
||||
|
||||
constexpr char defaultPort[] = "443";
|
||||
constexpr char defaultTlsDomain[] = "googletagmanager.com";
|
||||
constexpr char defaultUserName[] = "amnezia";
|
||||
constexpr char defaultWorkers[] = "2";
|
||||
constexpr char workersModeAuto[] = "auto";
|
||||
constexpr char workersModeManual[] = "manual";
|
||||
constexpr int maxWorkers = 32;
|
||||
}
|
||||
|
||||
} // namespace protocols
|
||||
}
|
||||
|
||||
#endif // PROTOCOLCONSTANTS_H
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ namespace amnezia
|
||||
TorWebSite,
|
||||
Dns,
|
||||
Sftp,
|
||||
Socks5Proxy
|
||||
Socks5Proxy,
|
||||
MtProxy,
|
||||
Telemt,
|
||||
};
|
||||
Q_ENUM_NS(DockerContainer)
|
||||
} // namespace ContainerEnumNS
|
||||
|
||||
@@ -72,7 +72,10 @@ QMap<DockerContainer, QString> ContainerUtils::containerHumanNames()
|
||||
{ DockerContainer::TorWebSite, QObject::tr("Website in Tor network") },
|
||||
{ DockerContainer::Dns, QObject::tr("AmneziaDNS") },
|
||||
{ DockerContainer::Sftp, QObject::tr("SFTP file sharing service") },
|
||||
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") } };
|
||||
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") },
|
||||
{ DockerContainer::MtProxy, QObject::tr("MTProxy (Telegram)") },
|
||||
{ DockerContainer::Telemt, QObject::tr("Telemt (Telegram)") },
|
||||
};
|
||||
}
|
||||
|
||||
QMap<DockerContainer, QString> ContainerUtils::containerDescriptions()
|
||||
@@ -102,7 +105,12 @@ QMap<DockerContainer, QString> ContainerUtils::containerDescriptions()
|
||||
{ DockerContainer::Sftp,
|
||||
QObject::tr("Create a file vault on your server to securely store and transfer files.") },
|
||||
{ DockerContainer::Socks5Proxy,
|
||||
QObject::tr("") } };
|
||||
QObject::tr("") },
|
||||
{ DockerContainer::MtProxy,
|
||||
QObject::tr("Telegram MTProto proxy server") },
|
||||
{ DockerContainer::Telemt,
|
||||
QObject::tr("Telegram MTProto proxy (Telemt, Rust)") },
|
||||
};
|
||||
}
|
||||
|
||||
QMap<DockerContainer, QString> ContainerUtils::containerDetailedDescriptions()
|
||||
@@ -172,7 +180,15 @@ QMap<DockerContainer, QString> ContainerUtils::containerDetailedDescriptions()
|
||||
"You will be able to access it using\n FileZilla or other SFTP clients, "
|
||||
"as well as mount the disk on your device to access\n it directly from your device.\n\n"
|
||||
"For more detailed information, you can\n find it in the support section under \"Create SFTP file storage.\" ") },
|
||||
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") }
|
||||
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") },
|
||||
{ DockerContainer::MtProxy,
|
||||
QObject::tr("Telegram MTProto proxy server. "
|
||||
"Allows Telegram clients to connect through your server "
|
||||
"using the MTProto protocol. Supports FakeTLS mode for "
|
||||
"bypassing DPI-based blocking.") },
|
||||
{ DockerContainer::Telemt,
|
||||
QObject::tr("Telegram MTProto proxy powered by Telemt (Rust). "
|
||||
"Supports secure and TLS fronting modes with optional traffic masking.") },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -197,6 +213,8 @@ Proto ContainerUtils::defaultProtocol(DockerContainer c)
|
||||
case DockerContainer::Dns: return Proto::Dns;
|
||||
case DockerContainer::Sftp: return Proto::Sftp;
|
||||
case DockerContainer::Socks5Proxy: return Proto::Socks5Proxy;
|
||||
case DockerContainer::MtProxy: return Proto::MtProxy;
|
||||
case DockerContainer::Telemt: return Proto::Telemt;
|
||||
default: return Proto::Unknown;
|
||||
}
|
||||
}
|
||||
@@ -224,6 +242,8 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Xray: return true;
|
||||
case DockerContainer::SSXray: return true;
|
||||
case DockerContainer::MtProxy: return true;
|
||||
case DockerContainer::Telemt: return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -237,7 +257,8 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Xray: return true;
|
||||
case DockerContainer::SSXray: return true;
|
||||
return false;
|
||||
case DockerContainer::MtProxy: return true;
|
||||
case DockerContainer::Telemt: return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -256,6 +277,8 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Xray: return true;
|
||||
case DockerContainer::SSXray: return true;
|
||||
case DockerContainer::MtProxy: return true;
|
||||
case DockerContainer::Telemt: return true;
|
||||
default: return false;
|
||||
}
|
||||
|
||||
@@ -318,6 +341,8 @@ bool ContainerUtils::isShareable(DockerContainer container)
|
||||
case DockerContainer::Dns: return false;
|
||||
case DockerContainer::Sftp: return false;
|
||||
case DockerContainer::Socks5Proxy: return false;
|
||||
case DockerContainer::MtProxy: return false;
|
||||
case DockerContainer::Telemt: return false;
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
@@ -346,8 +371,10 @@ int ContainerUtils::installPageOrder(DockerContainer container)
|
||||
case DockerContainer::Xray: return 3;
|
||||
case DockerContainer::Ipsec: return 7;
|
||||
case DockerContainer::SSXray: return 8;
|
||||
case DockerContainer::MtProxy:
|
||||
case DockerContainer::Telemt:
|
||||
return 20;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,9 @@ namespace amnezia
|
||||
TorWebSite,
|
||||
Dns,
|
||||
Sftp,
|
||||
Socks5Proxy
|
||||
Socks5Proxy,
|
||||
MtProxy,
|
||||
Telemt,
|
||||
};
|
||||
Q_ENUM_NS(Proto)
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
@@ -20,6 +19,8 @@
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "core/models/protocols/sftpProtocolConfig.h"
|
||||
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
|
||||
using namespace amnezia;
|
||||
using namespace ProtocolUtils;
|
||||
@@ -38,6 +39,8 @@ QString amnezia::scriptFolder(amnezia::DockerContainer container)
|
||||
case DockerContainer::Dns: return QLatin1String("dns");
|
||||
case DockerContainer::Sftp: return QLatin1String("sftp");
|
||||
case DockerContainer::Socks5Proxy: return QLatin1String("socks5_proxy");
|
||||
case DockerContainer::MtProxy: return QLatin1String("mtproxy");
|
||||
case DockerContainer::Telemt: return QLatin1String("telemt");
|
||||
default: return QString();
|
||||
}
|
||||
}
|
||||
@@ -284,6 +287,86 @@ amnezia::ScriptVars amnezia::genSocks5ProxyVars(const ContainerConfig &container
|
||||
return vars;
|
||||
}
|
||||
|
||||
amnezia::ScriptVars amnezia::genMtProxyVars(const ContainerConfig &containerConfig) {
|
||||
ScriptVars vars;
|
||||
|
||||
if (auto *mtProxyProtocolConfig = containerConfig.getMtProxyProtocolConfig()) {
|
||||
const MtProxyProtocolConfig &c = *mtProxyProtocolConfig;
|
||||
|
||||
vars.append({{"$MTPROXY_PORT", c.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : c.port}});
|
||||
vars.append({{"$MTPROXY_SECRET", c.secret}});
|
||||
vars.append({{"$MTPROXY_TAG", c.tag}});
|
||||
vars.append({{"$MTPROXY_TRANSPORT_MODE",
|
||||
c.transportMode.isEmpty() ? QString(protocols::mtProxy::transportModeStandard)
|
||||
: c.transportMode}});
|
||||
|
||||
QString tlsDomain = c.tlsDomain;
|
||||
if (tlsDomain.isEmpty()) {
|
||||
tlsDomain = QString(protocols::mtProxy::defaultTlsDomain);
|
||||
}
|
||||
vars.append({{"$MTPROXY_TLS_DOMAIN", tlsDomain}});
|
||||
vars.append({{"$MTPROXY_PUBLIC_HOST", c.publicHost}});
|
||||
|
||||
QStringList additionalList;
|
||||
for (const QString &s: c.additionalSecrets) {
|
||||
if (!s.isEmpty()) {
|
||||
additionalList << s;
|
||||
}
|
||||
}
|
||||
vars.append({{"$MTPROXY_ADDITIONAL_SECRETS", additionalList.join(QLatin1Char(','))}});
|
||||
|
||||
const QString workersMode = c.workersMode.isEmpty() ? QString(protocols::mtProxy::workersModeAuto)
|
||||
: c.workersMode;
|
||||
QString workers;
|
||||
if (workersMode == QLatin1String(protocols::mtProxy::workersModeManual)) {
|
||||
workers = c.workers.isEmpty() ? QString(protocols::mtProxy::defaultWorkers) : c.workers;
|
||||
} else {
|
||||
const QString transportMode =
|
||||
c.transportMode.isEmpty() ? QString(protocols::mtProxy::transportModeStandard) : c.transportMode;
|
||||
workers = (transportMode == QLatin1String(protocols::mtProxy::transportModeFakeTLS)) ? QStringLiteral("0")
|
||||
: QStringLiteral("2");
|
||||
}
|
||||
vars.append({{"$MTPROXY_WORKERS", workers}});
|
||||
|
||||
vars.append({{"$MTPROXY_NAT_ENABLED", c.natEnabled ? QStringLiteral("1") : QStringLiteral("0")}});
|
||||
vars.append({{"$MTPROXY_NAT_INTERNAL_IP", c.natInternalIp}});
|
||||
vars.append({{"$MTPROXY_NAT_EXTERNAL_IP", c.natExternalIp}});
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
amnezia::ScriptVars amnezia::genTelemtVars(const ContainerConfig &containerConfig)
|
||||
{
|
||||
ScriptVars vars;
|
||||
|
||||
if (auto *telemtProtocolConfig = containerConfig.getTelemtProtocolConfig()) {
|
||||
const TelemtProtocolConfig &c = *telemtProtocolConfig;
|
||||
|
||||
const QString transport = c.transportMode.isEmpty() ? QString(protocols::telemt::transportModeStandard)
|
||||
: c.transportMode;
|
||||
const bool faketls = (transport == QLatin1String(protocols::telemt::transportModeFakeTLS));
|
||||
vars.append({ { "$TELEMT_TOML_SECURE", faketls ? QLatin1String("false") : QLatin1String("true") } });
|
||||
vars.append({ { "$TELEMT_TOML_TLS", faketls ? QLatin1String("true") : QLatin1String("false") } });
|
||||
vars.append({ { "$TELEMT_PORT", c.port.isEmpty() ? QString(protocols::telemt::defaultPort) : c.port } });
|
||||
vars.append({ { "$TELEMT_SECRET", c.secret } });
|
||||
vars.append({ { "$TELEMT_TAG", c.tag } });
|
||||
QString tlsDomain = c.tlsDomain;
|
||||
if (tlsDomain.isEmpty()) {
|
||||
tlsDomain = QString(protocols::telemt::defaultTlsDomain);
|
||||
}
|
||||
vars.append({ { "$TELEMT_TLS_DOMAIN", tlsDomain } });
|
||||
vars.append({ { "$TELEMT_PUBLIC_HOST", c.publicHost } });
|
||||
vars.append({ { "$TELEMT_USER_NAME",
|
||||
c.userName.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultUserName) : c.userName } });
|
||||
vars.append({ { "$TELEMT_USE_MIDDLE_PROXY", c.useMiddleProxy ? QLatin1String("true") : QLatin1String("false") } });
|
||||
vars.append({ { "$TELEMT_MASK", c.maskEnabled ? QLatin1String("true") : QLatin1String("false") } });
|
||||
vars.append({ { "$TELEMT_TLS_EMULATION", c.tlsEmulation ? QLatin1String("true") : QLatin1String("false") } });
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig)
|
||||
{
|
||||
ScriptVars vars;
|
||||
@@ -308,6 +391,12 @@ amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer contain
|
||||
case Proto::Socks5Proxy:
|
||||
vars.append(genSocks5ProxyVars(containerConfig));
|
||||
break;
|
||||
case Proto::MtProxy:
|
||||
vars.append(genMtProxyVars(containerConfig));
|
||||
break;
|
||||
case Proto::Telemt:
|
||||
vars.append(genTelemtVars(containerConfig));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ ScriptVars genWireGuardVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genAwgVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genSftpVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genSocks5ProxyVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genMtProxyVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genTelemtVars(const ContainerConfig &containerConfig);
|
||||
|
||||
ScriptVars genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace libssh {
|
||||
QEventLoop wait;
|
||||
connect(&watcher, &QFutureWatcher<ErrorCode>::finished, &wait, &QEventLoop::quit);
|
||||
watcher.setFuture(future);
|
||||
wait.exec();
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
int connectionResult = watcher.result();
|
||||
|
||||
@@ -189,7 +189,7 @@ namespace libssh {
|
||||
|
||||
QEventLoop wait;
|
||||
QObject::connect(this, &Client::writeToChannelFinished, &wait, &QEventLoop::quit);
|
||||
wait.exec();
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
return watcher.result();
|
||||
}
|
||||
@@ -284,7 +284,7 @@ namespace libssh {
|
||||
|
||||
QEventLoop wait;
|
||||
QObject::connect(this, &Client::scpFileCopyFinished, &wait, &QEventLoop::quit);
|
||||
wait.exec();
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
closeScpSession();
|
||||
return watcher.result();
|
||||
|
||||
@@ -103,8 +103,8 @@ ErrorCode SshSession::runContainerScript(const ServerCredentials &credentials, D
|
||||
if (e)
|
||||
return e;
|
||||
|
||||
QString runner =
|
||||
QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, (container == DockerContainer::Socks5Proxy ? "sh" : "bash"));
|
||||
const bool useSh = container == DockerContainer::Socks5Proxy || container == DockerContainer::MtProxy || container == DockerContainer::Telemt;
|
||||
QString runner = QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, useSh ? "sh" : "bash");
|
||||
e = runScript(credentials, replaceVars(runner, amnezia::genBaseVars(credentials, container, QString(), QString())), cbReadStdOut, cbReadStdErr);
|
||||
|
||||
QString remover = QString("sudo docker exec -i $CONTAINER_NAME rm %1 ").arg(fileName);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
#include <QDebug>
|
||||
#include <QTimer>
|
||||
#include <libssh/libssh.h>
|
||||
|
||||
#include "amneziaApplication.h"
|
||||
#include "core/utils/osSignalHandler.h"
|
||||
#include "core/utils/migrations.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include "Windows.h"
|
||||
#endif
|
||||
@@ -47,6 +46,11 @@ int main(int argc, char *argv[])
|
||||
AmneziaApplication app(argc, argv);
|
||||
OsSignalHandler::setup();
|
||||
|
||||
ssh_init();
|
||||
QObject::connect(&app, &QCoreApplication::aboutToQuit, []() {
|
||||
ssh_finalize();
|
||||
});
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
if (isAnotherInstanceRunning()) {
|
||||
QTimer::singleShot(1000, &app, [&]() { app.quit(); });
|
||||
|
||||
@@ -220,7 +220,7 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur
|
||||
m_rawConfig = configuration;
|
||||
m_serverAddress = configuration.value(configKey::hostName).toString().toNSString();
|
||||
|
||||
const QString serverDescription = configuration.value(config_key::description).toString().trimmed();
|
||||
const QString serverDescription = configuration.value(configKey::description).toString().trimmed();
|
||||
QString tunnelName;
|
||||
if (serverDescription.isEmpty()) {
|
||||
tunnelName = ProtocolUtils::protoToString(proto);
|
||||
|
||||
@@ -97,8 +97,8 @@ void SecureQSettings::setValue(const QString &key, const QVariant &value)
|
||||
QByteArray encryptedValue = encryptText(decryptedValue);
|
||||
m_settings.setValue(key, magicString + encryptedValue);
|
||||
} else {
|
||||
qCritical() << "SecureQSettings::setValue Keystore unavailable, storing unencrypted";
|
||||
m_settings.setValue(key, value);
|
||||
qCritical() << "SecureQSettings::setValue Encryption required, but key is empty";
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -211,7 +211,7 @@ bool SecureQSettings::encryptionRequired() const
|
||||
// QtKeyChain failing on Linux
|
||||
return false;
|
||||
#endif
|
||||
return m_keystoreAvailable;
|
||||
return true;
|
||||
}
|
||||
|
||||
QByteArray SecureQSettings::getEncKey() const
|
||||
@@ -237,7 +237,6 @@ QByteArray SecureQSettings::getEncKey() const
|
||||
m_key = getSecTag(settingsKeyTag);
|
||||
if (key != m_key) {
|
||||
qCritical() << "SecureQSettings::getEncKey Unable to store key in keychain" << key.size() << m_key.size();
|
||||
m_keystoreAvailable = false;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -266,7 +265,6 @@ QByteArray SecureQSettings::getEncIv() const
|
||||
m_iv = getSecTag(settingsIvTag);
|
||||
if (iv != m_iv) {
|
||||
qCritical() << "SecureQSettings::getEncIv Unable to store IV in keychain" << iv.size() << m_iv.size();
|
||||
m_keystoreAvailable = false;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ private:
|
||||
|
||||
mutable QByteArray m_key;
|
||||
mutable QByteArray m_iv;
|
||||
mutable bool m_keystoreAvailable = true;
|
||||
|
||||
const QByteArray magicString { "EncData" }; // Magic keyword used for mark encrypted QByteArray
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); silent_inst="-yq install"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\
|
||||
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); silent_inst="-yq install --install-recommends"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\
|
||||
elif which dnf > /dev/null 2>&1; then pm=$(which dnf); silent_inst="-yq install"; check_pkgs="-yq check-update"; docker_pkg="docker"; dist="fedora";\
|
||||
elif which yum > /dev/null 2>&1; then pm=$(which yum); silent_inst="-y -q install"; check_pkgs="-y -q check-update"; docker_pkg="docker"; dist="centos";\
|
||||
elif which zypper > /dev/null 2>&1; then pm=$(which zypper); silent_inst="-nq install"; check_pkgs="-nq refresh"; docker_pkg="docker"; dist="opensuse";\
|
||||
|
||||
9
client/server_scripts/mtproxy/Dockerfile
Normal file
9
client/server_scripts/mtproxy/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM amneziavpn/mtproxy:latest
|
||||
|
||||
RUN mkdir -p /opt/amnezia /data
|
||||
RUN printf '#!/bin/sh\ntail -f /dev/null\n' > /opt/amnezia/start.sh && \
|
||||
chmod a+x /opt/amnezia/start.sh
|
||||
|
||||
VOLUME /data
|
||||
ENTRYPOINT ["/bin/sh", "/opt/amnezia/start.sh"]
|
||||
CMD [""]
|
||||
60
client/server_scripts/mtproxy/configure_container.sh
Normal file
60
client/server_scripts/mtproxy/configure_container.sh
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Download Telegram config files
|
||||
curl -s https://core.telegram.org/getProxySecret -o /data/proxy-secret
|
||||
curl -s https://core.telegram.org/getProxyConfig -o /data/proxy-multi.conf
|
||||
|
||||
# Determine secret: env var -> saved file -> generate new
|
||||
if [ -n "$MTPROXY_SECRET" ]; then
|
||||
SECRET="$MTPROXY_SECRET"
|
||||
elif [ -f /data/secret ]; then
|
||||
SECRET=$(cat /data/secret)
|
||||
else
|
||||
SECRET=$(openssl rand -hex 16)
|
||||
fi
|
||||
|
||||
# Validate: must be exactly 32 hex chars
|
||||
echo "$SECRET" | grep -qE '^[0-9a-fA-F]{32}$' || SECRET=$(openssl rand -hex 16)
|
||||
|
||||
# Persist secret for start.sh restarts
|
||||
echo "$SECRET" > /data/secret
|
||||
|
||||
# Detect external IP
|
||||
IP=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null)
|
||||
[ -z "$IP" ] && IP=$(curl -s --max-time 5 https://ifconfig.me 2>/dev/null)
|
||||
[ -z "$IP" ] && IP=$(curl -s --max-time 5 https://icanhazip.com 2>/dev/null)
|
||||
|
||||
# Use custom public host/domain if provided, otherwise fall back to detected IP
|
||||
if [ -n "$MTPROXY_PUBLIC_HOST" ]; then
|
||||
LINK_HOST="$MTPROXY_PUBLIC_HOST"
|
||||
else
|
||||
LINK_HOST="$IP"
|
||||
fi
|
||||
|
||||
PORT=$MTPROXY_PORT
|
||||
|
||||
# Transport mode is substituted by replaceVars — plain variable, no curly braces
|
||||
TRANSPORT_MODE=$MTPROXY_TRANSPORT_MODE
|
||||
|
||||
PADDED_SECRET="dd${SECRET}"
|
||||
|
||||
if [ "$TRANSPORT_MODE" = "faketls" ] && [ -n "$MTPROXY_TLS_DOMAIN" ]; then
|
||||
DOMAIN_HEX=$(echo -n "$MTPROXY_TLS_DOMAIN" | od -A n -t x1 | tr -d ' \n')
|
||||
FAKETLS_SECRET="ee${SECRET}${DOMAIN_HEX}"
|
||||
else
|
||||
FAKETLS_SECRET=""
|
||||
fi
|
||||
|
||||
# Active link secret depends on transport mode
|
||||
if [ "$TRANSPORT_MODE" = "faketls" ] && [ -n "$FAKETLS_SECRET" ]; then
|
||||
LINK_SECRET="$FAKETLS_SECRET"
|
||||
else
|
||||
LINK_SECRET="$PADDED_SECRET"
|
||||
fi
|
||||
|
||||
# Output stable markers — parsed by updateContainerConfigAfterInstallation()
|
||||
echo "[*] MTProxy configuration"
|
||||
echo "[*] Secret: ${SECRET}"
|
||||
echo "[*] FakeTLS: ${FAKETLS_SECRET}"
|
||||
echo "[*] tg:// link: tg://proxy?server=${LINK_HOST}&port=${PORT}&secret=${LINK_SECRET}"
|
||||
echo "[*] t.me link: https://t.me/proxy?server=${LINK_HOST}&port=${PORT}&secret=${LINK_SECRET}"
|
||||
9
client/server_scripts/mtproxy/run_container.sh
Normal file
9
client/server_scripts/mtproxy/run_container.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
# Run container
|
||||
sudo docker run -d \
|
||||
--log-driver none \
|
||||
--restart always \
|
||||
-p $MTPROXY_PORT:$MTPROXY_PORT/tcp \
|
||||
-v amnezia-mtproxy-data:/data \
|
||||
--name $CONTAINER_NAME \
|
||||
$CONTAINER_NAME
|
||||
|
||||
71
client/server_scripts/mtproxy/start.sh
Normal file
71
client/server_scripts/mtproxy/start.sh
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Container startup"
|
||||
|
||||
# Read persisted secret
|
||||
SECRET=""
|
||||
if [ -f /data/secret ]; then
|
||||
SECRET=$(cat /data/secret)
|
||||
fi
|
||||
|
||||
if [ -z "$SECRET" ]; then
|
||||
echo "ERROR: /data/secret not found — run configure_container first"
|
||||
tail -f /dev/null
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build tag argument
|
||||
TAG_ARG=""
|
||||
if [ -n "$MTPROXY_TAG" ]; then
|
||||
TAG_ARG="-P $MTPROXY_TAG"
|
||||
fi
|
||||
|
||||
# Build domain argument for FakeTLS mode
|
||||
DOMAIN_ARG=""
|
||||
if [ "$MTPROXY_TRANSPORT_MODE" = "faketls" ] && [ -n "$MTPROXY_TLS_DOMAIN" ]; then
|
||||
DOMAIN_ARG="--domain $MTPROXY_TLS_DOMAIN"
|
||||
fi
|
||||
|
||||
WORKERS=$MTPROXY_WORKERS
|
||||
STATS_PORT=2398
|
||||
LISTEN_PORT=$MTPROXY_PORT
|
||||
|
||||
NAT_FLAG=""
|
||||
NAT_VALUE=""
|
||||
if [ "$MTPROXY_NAT_ENABLED" = "1" ] && [ -n "$MTPROXY_NAT_INTERNAL_IP" ] && [ -n "$MTPROXY_NAT_EXTERNAL_IP" ]; then
|
||||
NAT_FLAG="--nat-info"
|
||||
NAT_VALUE="$MTPROXY_NAT_INTERNAL_IP:$MTPROXY_NAT_EXTERNAL_IP"
|
||||
else
|
||||
INTERNAL_IP=$(hostname -i 2>/dev/null | awk '{print $1}')
|
||||
EXTERNAL_IP=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null)
|
||||
[ -z "$EXTERNAL_IP" ] && EXTERNAL_IP=$(curl -s --max-time 5 https://ifconfig.me 2>/dev/null)
|
||||
|
||||
if [ -n "$INTERNAL_IP" ] && [ -n "$EXTERNAL_IP" ] && [ "$INTERNAL_IP" != "$EXTERNAL_IP" ]; then
|
||||
NAT_FLAG="--nat-info"
|
||||
NAT_VALUE="${INTERNAL_IP}:${EXTERNAL_IP}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build additional secrets arguments
|
||||
ADDITIONAL_SECRETS_ARG=""
|
||||
if [ -n "$MTPROXY_ADDITIONAL_SECRETS" ]; then
|
||||
for S in $(echo "$MTPROXY_ADDITIONAL_SECRETS" | tr ',' ' '); do
|
||||
ADDITIONAL_SECRETS_ARG="$ADDITIONAL_SECRETS_ARG -S $S"
|
||||
done
|
||||
fi
|
||||
|
||||
# Start proxy (foreground)
|
||||
exec mtproto-proxy \
|
||||
-u root \
|
||||
-p ${STATS_PORT} \
|
||||
-H ${LISTEN_PORT} \
|
||||
-S ${SECRET} \
|
||||
${ADDITIONAL_SECRETS_ARG} \
|
||||
--aes-pwd /data/proxy-secret \
|
||||
-M ${WORKERS} \
|
||||
-C 60000 \
|
||||
--allow-skip-dh \
|
||||
${NAT_FLAG:+${NAT_FLAG} ${NAT_VALUE}} \
|
||||
${TAG_ARG} \
|
||||
${DOMAIN_ARG} \
|
||||
/data/proxy-multi.conf
|
||||
@@ -24,6 +24,14 @@
|
||||
<file>ipsec/run_container.sh</file>
|
||||
<file>ipsec/start.sh</file>
|
||||
<file>ipsec/strongswan.profile</file>
|
||||
<file>mtproxy/configure_container.sh</file>
|
||||
<file>mtproxy/Dockerfile</file>
|
||||
<file>mtproxy/run_container.sh</file>
|
||||
<file>mtproxy/start.sh</file>
|
||||
<file>telemt/configure_container.sh</file>
|
||||
<file>telemt/Dockerfile</file>
|
||||
<file>telemt/run_container.sh</file>
|
||||
<file>telemt/start.sh</file>
|
||||
<file>openvpn/configure_container.sh</file>
|
||||
<file>openvpn/Dockerfile</file>
|
||||
<file>openvpn/run_container.sh</file>
|
||||
@@ -55,4 +63,3 @@
|
||||
<file>xray/template.json</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
|
||||
42
client/server_scripts/telemt/Dockerfile
Normal file
42
client/server_scripts/telemt/Dockerfile
Normal file
@@ -0,0 +1,42 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Debian-based image with Telemt binary (shell + jq for Amnezia configure scripts).
|
||||
# Binary from https://github.com/telemt/telemt releases (same pattern as upstream Dockerfile minimal stage).
|
||||
|
||||
FROM debian:12-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
binutils \
|
||||
ca-certificates \
|
||||
curl \
|
||||
jq \
|
||||
openssl \
|
||||
tar \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Use machine arch (works with classic `docker build`; TARGETARCH is only set with BuildKit).
|
||||
RUN set -eux; \
|
||||
ARCH="$(uname -m)"; \
|
||||
case "$ARCH" in \
|
||||
x86_64) ASSET="telemt-x86_64-linux-musl.tar.gz" ;; \
|
||||
aarch64|arm64) ASSET="telemt-aarch64-linux-musl.tar.gz" ;; \
|
||||
*) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
curl -fL --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 120 \
|
||||
-o "/tmp/${ASSET}" "https://github.com/telemt/telemt/releases/latest/download/${ASSET}"; \
|
||||
curl -fL --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 120 \
|
||||
-o "/tmp/${ASSET}.sha256" "https://github.com/telemt/telemt/releases/latest/download/${ASSET}.sha256"; \
|
||||
cd /tmp && sha256sum -c "${ASSET}.sha256"; \
|
||||
tar -xzf "${ASSET}" -C /tmp; \
|
||||
test -f /tmp/telemt; \
|
||||
install -m 0755 /tmp/telemt /usr/local/bin/telemt; \
|
||||
strip --strip-unneeded /usr/local/bin/telemt || true; \
|
||||
rm -f "/tmp/${ASSET}" "/tmp/${ASSET}.sha256" /tmp/telemt
|
||||
|
||||
RUN mkdir -p /opt/amnezia /data
|
||||
RUN printf '#!/bin/sh\ntail -f /dev/null\n' > /opt/amnezia/start.sh && \
|
||||
chmod a+x /opt/amnezia/start.sh
|
||||
|
||||
VOLUME /data
|
||||
ENTRYPOINT ["/bin/sh", "/opt/amnezia/start.sh"]
|
||||
CMD [""]
|
||||
73
client/server_scripts/telemt/configure_container.sh
Normal file
73
client/server_scripts/telemt/configure_container.sh
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/bin/sh
|
||||
# Do not use set -e: Telemt / curl / kill edge cases should not abort the whole configure step.
|
||||
|
||||
echo "[*] Amnezia Telemt: configure script start"
|
||||
mkdir -p /data/tlsfront
|
||||
|
||||
# Secret: substituted $TELEMT_SECRET -> saved file -> openssl (same rules as MTProxy configure)
|
||||
if [ -n "$TELEMT_SECRET" ]; then
|
||||
SECRET="$TELEMT_SECRET"
|
||||
elif [ -f /data/secret ]; then
|
||||
SECRET=$(cat /data/secret)
|
||||
else
|
||||
SECRET=$(openssl rand -hex 16)
|
||||
fi
|
||||
# Must be exactly 32 hex chars
|
||||
echo "$SECRET" | grep -qE '^[0-9a-fA-F]{32}$' || SECRET=$(openssl rand -hex 16)
|
||||
|
||||
# Build config.toml (other variables substituted on the host by Amnezia before upload)
|
||||
rm -f /data/config.toml
|
||||
|
||||
{
|
||||
echo "### Amnezia Telemt — generated"
|
||||
echo "[general]"
|
||||
echo "use_middle_proxy = $TELEMT_USE_MIDDLE_PROXY"
|
||||
echo "log_level = \"normal\""
|
||||
if [ -n "$TELEMT_TAG" ]; then
|
||||
echo "ad_tag = \"$TELEMT_TAG\""
|
||||
fi
|
||||
echo ""
|
||||
echo "[general.modes]"
|
||||
echo "classic = false"
|
||||
echo "secure = $TELEMT_TOML_SECURE"
|
||||
echo "tls = $TELEMT_TOML_TLS"
|
||||
echo ""
|
||||
echo "[general.links]"
|
||||
echo "show = \"*\""
|
||||
if [ -n "$TELEMT_PUBLIC_HOST" ]; then
|
||||
echo "public_host = \"$TELEMT_PUBLIC_HOST\""
|
||||
fi
|
||||
echo "public_port = $TELEMT_PORT"
|
||||
echo ""
|
||||
echo "[server]"
|
||||
echo "port = $TELEMT_PORT"
|
||||
echo ""
|
||||
echo "[server.api]"
|
||||
echo "enabled = true"
|
||||
echo "listen = \"0.0.0.0:9091\""
|
||||
# Match upstream Telemt default: localhost API only (curl in this script uses 127.0.0.1).
|
||||
echo "whitelist = [\"127.0.0.0/8\"]"
|
||||
echo ""
|
||||
echo "[[server.listeners]]"
|
||||
echo "ip = \"0.0.0.0\""
|
||||
echo ""
|
||||
echo "[censorship]"
|
||||
echo "tls_domain = \"$TELEMT_TLS_DOMAIN\""
|
||||
echo "mask = $TELEMT_MASK"
|
||||
echo "tls_emulation = $TELEMT_TLS_EMULATION"
|
||||
echo "tls_front_dir = \"/data/tlsfront\""
|
||||
echo ""
|
||||
echo "[access.users]"
|
||||
echo "$TELEMT_USER_NAME = \"$SECRET\""
|
||||
} > /data/config.toml
|
||||
|
||||
echo "$SECRET" > /data/secret
|
||||
chmod 600 /data/secret 2>/dev/null || true
|
||||
|
||||
# Do not start telemt here: a long-lived process + curl loop inside `docker exec` can confuse SSH/Docker
|
||||
# timing and is unnecessary — start.sh runs telemt after configure. Links can be empty until the service
|
||||
# is up; the client still parses Secret below.
|
||||
echo "[*] Telemt configuration"
|
||||
echo "[*] Secret: $SECRET"
|
||||
echo "[*] tg:// link: "
|
||||
echo "[*] t.me link: "
|
||||
9
client/server_scripts/telemt/run_container.sh
Normal file
9
client/server_scripts/telemt/run_container.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
# Run container (ulimit per Telemt docs — avoids "Too many open files" under load)
|
||||
sudo docker run -d \
|
||||
--log-driver none \
|
||||
--restart always \
|
||||
--ulimit nofile=65536:65536 \
|
||||
-p $TELEMT_PORT:$TELEMT_PORT/tcp \
|
||||
-v amnezia-telemt-data:/data \
|
||||
--name $CONTAINER_NAME \
|
||||
$CONTAINER_NAME
|
||||
12
client/server_scripts/telemt/start.sh
Normal file
12
client/server_scripts/telemt/start.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Container startup (Telemt)"
|
||||
|
||||
if [ ! -f /data/config.toml ]; then
|
||||
echo "ERROR: /data/config.toml not found — run configure_container first"
|
||||
tail -f /dev/null
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p /data/tlsfront
|
||||
exec /usr/local/bin/telemt /data/config.toml
|
||||
@@ -1312,6 +1312,21 @@ Thank you for staying with us!</source>
|
||||
</context>
|
||||
<context>
|
||||
<name>PageProtocolXraySettings</name>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageProtocolXraySettings.qml" line="61"/>
|
||||
<source>XRay VLESS settings</source>
|
||||
<translation>Настройки XRay VLESS</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageProtocolXraySettings.qml" line="80"/>
|
||||
<source>More about settings</source>
|
||||
<translation>Подробнее о настройках</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageProtocolXraySettings.qml" line="188"/>
|
||||
<source>Reset settings</source>
|
||||
<translation>Сбросить настройки</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageProtocolXraySettings.qml" line="57"/>
|
||||
<source>XRay settings</source>
|
||||
|
||||
@@ -201,3 +201,12 @@ bool ImportUiController::decodeQrCode(const QString &code)
|
||||
return mInstance->parseQrCodeChunk(code);
|
||||
}
|
||||
#endif
|
||||
|
||||
QString ImportUiController::readTextFile(const QString &fileName)
|
||||
{
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return {};
|
||||
}
|
||||
return QString::fromUtf8(file.readAll());
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public slots:
|
||||
QString getMaliciousWarningText();
|
||||
bool isNativeWireGuardConfig();
|
||||
void processNativeWireGuardConfig();
|
||||
QString readTextFile(const QString &fileName);
|
||||
|
||||
#if defined Q_OS_ANDROID || defined Q_OS_IOS
|
||||
void startDecodingQr();
|
||||
|
||||
46
client/ui/controllers/networkReachabilityController.cpp
Normal file
46
client/ui/controllers/networkReachabilityController.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#include "networkReachabilityController.h"
|
||||
|
||||
#include <QNetworkInformation>
|
||||
|
||||
namespace {
|
||||
|
||||
bool reachabilityAllowsRemoteOperations(QNetworkInformation::Reachability r) {
|
||||
using R = QNetworkInformation::Reachability;
|
||||
// Unknown: no backend or not yet determined — do not block UI.
|
||||
return r == R::Online || r == R::Unknown;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NetworkReachabilityController::NetworkReachabilityController(QObject *parent) : QObject(parent) {
|
||||
attachToNetworkInformation();
|
||||
}
|
||||
|
||||
bool NetworkReachabilityController::hasInternetAccess() const {
|
||||
return m_hasInternetAccess;
|
||||
}
|
||||
|
||||
void NetworkReachabilityController::attachToNetworkInformation() {
|
||||
if (!QNetworkInformation::loadDefaultBackend()) {
|
||||
return;
|
||||
}
|
||||
QNetworkInformation *ni = QNetworkInformation::instance();
|
||||
if (!ni) {
|
||||
return;
|
||||
}
|
||||
const bool initial = reachabilityAllowsRemoteOperations(ni->reachability());
|
||||
const bool previous = m_hasInternetAccess;
|
||||
m_hasInternetAccess = initial;
|
||||
if (previous != m_hasInternetAccess) {
|
||||
emit hasInternetAccessChanged();
|
||||
}
|
||||
connect(ni, &QNetworkInformation::reachabilityChanged, this,
|
||||
[this](QNetworkInformation::Reachability r) {
|
||||
const bool ok = reachabilityAllowsRemoteOperations(r);
|
||||
if (ok == m_hasInternetAccess) {
|
||||
return;
|
||||
}
|
||||
m_hasInternetAccess = ok;
|
||||
emit hasInternetAccessChanged();
|
||||
});
|
||||
}
|
||||
30
client/ui/controllers/networkReachabilityController.h
Normal file
30
client/ui/controllers/networkReachabilityController.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef NETWORKREACHABILITYCONTROLLER_H
|
||||
#define NETWORKREACHABILITYCONTROLLER_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
// Exposes QNetworkInformation to QML for UI that must not run remote operations offline.
|
||||
// Note: mozilla/networkwatcher.h has NetworkWatcher::getReachability() using the same API,
|
||||
// but networkwatcher.cpp is not linked into the desktop client (only the service process).
|
||||
|
||||
class NetworkReachabilityController final : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(bool hasInternetAccess READ hasInternetAccess NOTIFY hasInternetAccessChanged)
|
||||
|
||||
public:
|
||||
explicit NetworkReachabilityController(QObject *parent = nullptr);
|
||||
|
||||
bool hasInternetAccess() const;
|
||||
|
||||
signals:
|
||||
|
||||
void hasInternetAccessChanged();
|
||||
|
||||
private:
|
||||
void attachToNetworkInformation();
|
||||
|
||||
bool m_hasInternetAccess = true;
|
||||
};
|
||||
|
||||
#endif // NETWORKREACHABILITYCONTROLLER_H
|
||||
@@ -50,6 +50,8 @@ namespace PageLoader
|
||||
PageServiceTorWebsiteSettings,
|
||||
PageServiceDnsSettings,
|
||||
PageServiceSocksProxySettings,
|
||||
PageServiceMtProxySettings,
|
||||
PageServiceTelemtSettings,
|
||||
|
||||
PageSetupWizardStart,
|
||||
PageSetupWizardCredentials,
|
||||
@@ -80,7 +82,15 @@ namespace PageLoader
|
||||
PageSetupWizardApiPremiumInfo,
|
||||
PageSetupWizardApiTrialEmail,
|
||||
|
||||
PageDevMenu
|
||||
PageDevMenu,
|
||||
|
||||
PageProtocolXraySnapshots,
|
||||
PageProtocolXrayTransportSettings,
|
||||
PageProtocolXrayXmuxSettings,
|
||||
PageProtocolXrayXPaddingSettings,
|
||||
PageProtocolXrayFlowSettings,
|
||||
PageProtocolXraySecuritySettings,
|
||||
PageProtocolXrayXPaddingBytesSettings,
|
||||
};
|
||||
Q_ENUM_NS(PageEnum)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <QDebug>
|
||||
|
||||
#include "../systemController.h"
|
||||
#include "core/utils/qrCodeUtils.h"
|
||||
|
||||
ExportUiController::ExportUiController(ExportController* exportController, QObject *parent)
|
||||
: QObject(parent),
|
||||
@@ -53,6 +54,14 @@ void ExportUiController::generateXrayConfig(const QString &serverId, const QStri
|
||||
applyExportResult(result);
|
||||
}
|
||||
|
||||
void ExportUiController::generateQrFromString(const QString &text)
|
||||
{
|
||||
clearPreviousConfig();
|
||||
m_config = text;
|
||||
m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(text.toUtf8());
|
||||
emit exportConfigChanged();
|
||||
}
|
||||
|
||||
QString ExportUiController::getConfig()
|
||||
{
|
||||
return m_config;
|
||||
@@ -118,3 +127,13 @@ void ExportUiController::applyExportResult(const ExportController::ExportResult
|
||||
|
||||
emit exportConfigChanged();
|
||||
}
|
||||
|
||||
void ExportUiController::setConfigFromString(const QString &config, const QString &fileName)
|
||||
{
|
||||
clearPreviousConfig();
|
||||
m_config = config;
|
||||
emit exportConfigChanged();
|
||||
if (!fileName.isEmpty()) {
|
||||
SystemController::saveFile(fileName, m_config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,14 @@ public slots:
|
||||
void generateWireGuardConfig(const QString &serverId, const QString &clientName);
|
||||
void generateAwgConfig(const QString &serverId, int containerIndex, const QString &clientName);
|
||||
void generateXrayConfig(const QString &serverId, const QString &clientName);
|
||||
void generateQrFromString(const QString &text);
|
||||
|
||||
QString getConfig();
|
||||
QString getNativeConfigString();
|
||||
QList<QString> getQrCodes();
|
||||
|
||||
void exportConfig(const QString &fileName);
|
||||
void setConfigFromString(const QString &config, const QString &fileName);
|
||||
|
||||
void updateClientManagementModel(const QString &serverId, int containerIndex);
|
||||
|
||||
|
||||
136
client/ui/controllers/selfhosted/installUiController.cpp
Executable file → Normal file
136
client/ui/controllers/selfhosted/installUiController.cpp
Executable file → Normal file
@@ -5,11 +5,13 @@
|
||||
#include <QEventLoop>
|
||||
#include <QJsonObject>
|
||||
#include <QRandomGenerator>
|
||||
#include <QRegularExpression>
|
||||
#include <QStandardPaths>
|
||||
#include <QFutureWatcher>
|
||||
#include <QtConcurrent>
|
||||
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/controllers/selfhosted/installController.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
@@ -47,6 +49,8 @@ InstallUiController::InstallUiController(InstallController *installController,
|
||||
#endif
|
||||
SftpConfigModel *sftpConfigModel,
|
||||
Socks5ProxyConfigModel *socks5ConfigModel,
|
||||
MtProxyConfigModel* mtConfigModel,
|
||||
TelemtConfigModel *telemtConfigModel,
|
||||
QObject *parent)
|
||||
: QObject(parent),
|
||||
m_installController(installController),
|
||||
@@ -63,7 +67,9 @@ InstallUiController::InstallUiController(InstallController *installController,
|
||||
m_ikev2ConfigModel(ikev2ConfigModel),
|
||||
#endif
|
||||
m_sftpConfigModel(sftpConfigModel),
|
||||
m_socks5ConfigModel(socks5ConfigModel)
|
||||
m_socks5ConfigModel(socks5ConfigModel),
|
||||
m_mtProxyConfigModel(mtConfigModel),
|
||||
m_telemtConfigModel(telemtConfigModel)
|
||||
{
|
||||
connect(m_installController, &InstallController::configValidated, this, &InstallUiController::configValidated);
|
||||
connect(m_installController, &InstallController::validationErrorOccurred, this, [this](ErrorCode errorCode) {
|
||||
@@ -199,7 +205,7 @@ void InstallUiController::scanServerForInstalledContainers(const QString &server
|
||||
emit installationErrorOccurred(errorCode);
|
||||
}
|
||||
|
||||
void InstallUiController::updateContainer(const QString &serverId, int containerIndex, int protocolIndex)
|
||||
void InstallUiController::updateContainer(const QString &serverId, int containerIndex, int protocolIndex, bool closePage)
|
||||
{
|
||||
DockerContainer container = static_cast<DockerContainer>(containerIndex);
|
||||
|
||||
@@ -238,6 +244,14 @@ void InstallUiController::updateContainer(const QString &serverId, int container
|
||||
containerConfig.protocolConfig = m_socks5ConfigModel->getProtocolConfig();
|
||||
break;
|
||||
}
|
||||
case Proto::MtProxy: {
|
||||
containerConfig.protocolConfig = m_mtProxyConfigModel->getProtocolConfig();
|
||||
break;
|
||||
}
|
||||
case Proto::Telemt: {
|
||||
containerConfig.protocolConfig = m_telemtConfigModel->getProtocolConfig();
|
||||
break;
|
||||
}
|
||||
#ifdef Q_OS_WINDOWS
|
||||
case Proto::Ikev2: {
|
||||
containerConfig.protocolConfig = m_ikev2ConfigModel->getProtocolConfig();
|
||||
@@ -249,19 +263,128 @@ void InstallUiController::updateContainer(const QString &serverId, int container
|
||||
}
|
||||
ContainerConfig oldContainerConfig = m_serversController->getContainerConfig(serverId, container);
|
||||
|
||||
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
|
||||
emit serverIsBusy(true);
|
||||
auto *watcher = new QFutureWatcher<ErrorCode>(this);
|
||||
QObject::connect(watcher, &QFutureWatcher<ErrorCode>::finished, this,
|
||||
[this, watcher, serverId, container, closePage]() {
|
||||
const ErrorCode errorCode = watcher->result();
|
||||
watcher->deleteLater();
|
||||
emit serverIsBusy(false);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
const ContainerConfig updatedConfig =
|
||||
m_serversController->getContainerConfig(serverId, container);
|
||||
m_protocolModel->updateModel(updatedConfig);
|
||||
|
||||
const auto defaultContainer =
|
||||
m_serversController->getDefaultContainer(serverId);
|
||||
if ((serverId == m_serversController->getDefaultServerId())
|
||||
&& (container == defaultContainer)) {
|
||||
emit currentContainerUpdated();
|
||||
} else {
|
||||
emit updateContainerFinished(tr("Settings updated successfully"), closePage);
|
||||
}
|
||||
} else {
|
||||
emit installationErrorOccurred(errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
ContainerConfig newConfigCopy = containerConfig;
|
||||
ContainerConfig oldConfigCopy = oldContainerConfig;
|
||||
InstallController *installController = m_installController;
|
||||
QFuture<ErrorCode> future =
|
||||
QtConcurrent::run([installController, serverId, container, oldConfigCopy,
|
||||
newConfigCopy]() mutable -> ErrorCode {
|
||||
return installController->updateContainer(serverId, container, oldConfigCopy, newConfigCopy);
|
||||
});
|
||||
watcher->setFuture(future);
|
||||
return;
|
||||
}
|
||||
|
||||
ErrorCode errorCode = m_installController->updateContainer(serverId, container, oldContainerConfig, containerConfig);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
ContainerConfig updatedConfig = m_serversController->getContainerConfig(serverId, container);
|
||||
m_protocolModel->updateModel(updatedConfig);
|
||||
|
||||
emit updateContainerFinished(tr("Settings updated successfully"));
|
||||
const auto defaultContainer = m_serversController->getDefaultContainer(serverId);
|
||||
if ((serverId == m_serversController->getDefaultServerId()) && (container == defaultContainer)) {
|
||||
emit currentContainerUpdated();
|
||||
} else {
|
||||
emit updateContainerFinished(tr("Settings updated successfully"), closePage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
emit installationErrorOccurred(errorCode);
|
||||
}
|
||||
|
||||
void InstallUiController::setContainerEnabled(const QString &serverId, int containerIndex, bool enabled)
|
||||
{
|
||||
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit serverIsBusy(true);
|
||||
const ErrorCode errorCode = m_installController->setDockerContainerEnabledState(serverId, container, enabled);
|
||||
emit serverIsBusy(false);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
const ContainerConfig currentConfig = m_serversController->getContainerConfig(serverId, container);
|
||||
m_protocolModel->updateModel(currentConfig);
|
||||
emit setContainerEnabledFinished(enabled);
|
||||
return;
|
||||
}
|
||||
|
||||
emit installationErrorOccurred(errorCode);
|
||||
}
|
||||
|
||||
void InstallUiController::refreshContainerStatus(const QString &serverId, int containerIndex)
|
||||
{
|
||||
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return;
|
||||
}
|
||||
|
||||
int status = 3;
|
||||
const ErrorCode errorCode = m_installController->queryDockerContainerStatus(serverId, container, status);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
emit containerStatusRefreshed(3);
|
||||
return;
|
||||
}
|
||||
emit containerStatusRefreshed(status);
|
||||
}
|
||||
|
||||
void InstallUiController::refreshContainerDiagnostics(const QString &serverId, int containerIndex, int port)
|
||||
{
|
||||
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return;
|
||||
}
|
||||
|
||||
MtProxyContainerDiagnostics diag;
|
||||
const ErrorCode errorCode = m_installController->queryMtProxyDiagnostics(serverId, container, port, diag);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
emit containerDiagnosticsRefreshed(false, false, -1, QString(), QString());
|
||||
return;
|
||||
}
|
||||
emit containerDiagnosticsRefreshed(diag.portReachable, diag.upstreamReachable, diag.clientsConnected,
|
||||
diag.lastConfigRefresh, diag.statsEndpoint);
|
||||
}
|
||||
|
||||
void InstallUiController::fetchContainerSecret(const QString &serverId, int containerIndex)
|
||||
{
|
||||
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString secret = m_installController->fetchDockerContainerSecret(serverId, container);
|
||||
emit containerSecretFetched(secret);
|
||||
}
|
||||
|
||||
void InstallUiController::rebootServer(const QString &serverId)
|
||||
{
|
||||
const QString serverName = m_serversController->notificationDisplayName(serverId);
|
||||
@@ -469,10 +592,13 @@ void InstallUiController::updateProtocolConfigModel(const QString &serverId, int
|
||||
case Proto::Awg: updateIfPresent(m_awgConfigModel, containerConfig.getAwgProtocolConfig()); break;
|
||||
case Proto::WireGuard: updateIfPresent(m_wireGuardConfigModel, containerConfig.getWireGuardProtocolConfig()); break;
|
||||
case Proto::OpenVpn: updateIfPresent(m_openVpnConfigModel, containerConfig.getOpenVpnProtocolConfig()); break;
|
||||
case Proto::Xray: updateIfPresent(m_xrayConfigModel, containerConfig.getXrayProtocolConfig()); break;
|
||||
case Proto::Xray:
|
||||
case Proto::SSXray: updateIfPresent(m_xrayConfigModel, containerConfig.getXrayProtocolConfig()); break;
|
||||
case Proto::TorWebSite: updateIfPresent(m_torConfigModel, containerConfig.getTorProtocolConfig()); break;
|
||||
case Proto::Sftp: updateIfPresent(m_sftpConfigModel, containerConfig.getSftpProtocolConfig()); break;
|
||||
case Proto::Socks5Proxy: updateIfPresent(m_socks5ConfigModel, containerConfig.getSocks5ProxyProtocolConfig()); break;
|
||||
case Proto::MtProxy: updateIfPresent(m_mtProxyConfigModel, containerConfig.getMtProxyProtocolConfig()); break;
|
||||
case Proto::Telemt: updateIfPresent(m_telemtConfigModel, containerConfig.getTelemtProtocolConfig()); break;
|
||||
#ifdef Q_OS_WINDOWS
|
||||
case Proto::Ikev2: updateIfPresent(m_ikev2ConfigModel, containerConfig.getIkev2ProtocolConfig()); break;
|
||||
#endif
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
#include "ui/models/services/torConfigModel.h"
|
||||
#include "core/models/protocols/sftpProtocolConfig.h"
|
||||
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
|
||||
#include "ui/models/services/mtProxyConfigModel.h"
|
||||
#include "ui/models/services/telemtConfigModel.h"
|
||||
|
||||
class InstallUiController : public QObject
|
||||
{
|
||||
@@ -48,6 +50,8 @@ public:
|
||||
#endif
|
||||
SftpConfigModel* sftpConfigModel,
|
||||
Socks5ProxyConfigModel* socks5ConfigModel,
|
||||
MtProxyConfigModel* mtConfigModel,
|
||||
TelemtConfigModel* telemtConfigModel,
|
||||
QObject *parent = nullptr);
|
||||
~InstallUiController();
|
||||
|
||||
@@ -58,12 +62,16 @@ public slots:
|
||||
|
||||
void scanServerForInstalledContainers(const QString &serverId);
|
||||
|
||||
void updateContainer(const QString &serverId, int containerIndex, int protocolIndex);
|
||||
void updateContainer(const QString &serverId, int containerIndex, int protocolIndex, bool closePage = true);
|
||||
|
||||
void removeServer(const QString &serverId);
|
||||
void rebootServer(const QString &serverId);
|
||||
void removeAllContainers(const QString &serverId);
|
||||
void removeContainer(const QString &serverId, int containerIndex);
|
||||
void setContainerEnabled(const QString &serverId, int containerIndex, bool enabled);
|
||||
void refreshContainerStatus(const QString &serverId, int containerIndex);
|
||||
void refreshContainerDiagnostics(const QString &serverId, int containerIndex, int port);
|
||||
void fetchContainerSecret(const QString &serverId, int containerIndex);
|
||||
|
||||
void clearCachedProfile(const QString &serverId, int containerIndex);
|
||||
|
||||
@@ -94,7 +102,7 @@ signals:
|
||||
void installContainerFinished(const QString &finishMessage, bool isServiceInstall);
|
||||
void installServerFinished(const QString &finishMessage);
|
||||
|
||||
void updateContainerFinished(const QString &message);
|
||||
void updateContainerFinished(const QString &message, bool closePage);
|
||||
|
||||
void scanServerFinished(bool isInstalledContainerFound);
|
||||
|
||||
@@ -102,6 +110,11 @@ signals:
|
||||
void removeServerFinished(const QString &finishedMessage);
|
||||
void removeAllContainersFinished(const QString &finishedMessage);
|
||||
void removeContainerFinished(const QString &finishedMessage);
|
||||
void setContainerEnabledFinished(bool enabled);
|
||||
void containerStatusRefreshed(int status);
|
||||
void containerDiagnosticsRefreshed(bool portReachable, bool upstreamReachable, int clientsConnected,
|
||||
const QString &lastConfigRefresh, const QString &statsEndpoint);
|
||||
void containerSecretFetched(const QString &secret);
|
||||
|
||||
void installationErrorOccurred(ErrorCode errorCode);
|
||||
void wrongInstallationUser(const QString &message);
|
||||
@@ -114,6 +127,8 @@ signals:
|
||||
void serverIsBusy(const bool isBusy);
|
||||
void cancelInstallation();
|
||||
|
||||
void currentContainerUpdated();
|
||||
|
||||
void cachedProfileCleared(const QString &message);
|
||||
void apiConfigRemoved(const QString &message);
|
||||
|
||||
@@ -138,6 +153,8 @@ private:
|
||||
#endif
|
||||
SftpConfigModel* m_sftpConfigModel;
|
||||
Socks5ProxyConfigModel* m_socks5ConfigModel;
|
||||
MtProxyConfigModel* m_mtProxyConfigModel;
|
||||
TelemtConfigModel* m_telemtConfigModel;
|
||||
|
||||
ServerCredentials m_processedServerCredentials;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "serversUiController.h"
|
||||
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
@@ -216,11 +215,7 @@ QString ServersUiController::getDefaultServerImagePathCollapsed() const
|
||||
if (!description.isApiV2 || description.apiServerCountryCode.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
const QString imageCode = apiUtils::countryCodeBaseForFlag(description.apiServerCountryCode.toUpper());
|
||||
if (imageCode.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(imageCode);
|
||||
return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(description.apiServerCountryCode.toUpper());
|
||||
}
|
||||
}
|
||||
return "";
|
||||
@@ -510,6 +505,10 @@ QStringList ServersUiController::getAllInstalledServicesName(int serverIndex) co
|
||||
servicesName.append("TOR");
|
||||
} else if (container == DockerContainer::Socks5Proxy) {
|
||||
servicesName.append("SOCKS5");
|
||||
} else if (container == DockerContainer::MtProxy) {
|
||||
servicesName.append("MTProxy");
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
servicesName.append("Telemt");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "logger.h"
|
||||
|
||||
namespace
|
||||
@@ -42,7 +41,7 @@ QVariant ApiCountryModel::data(const QModelIndex &index, int role) const
|
||||
return countryInfo.countryName;
|
||||
}
|
||||
case CountryImageCodeRole: {
|
||||
return apiUtils::countryCodeBaseForFlag(countryInfo.countryCode);
|
||||
return countryInfo.countryCode.toUpper();
|
||||
}
|
||||
case IsIssuedRole: {
|
||||
return isIssued;
|
||||
|
||||
@@ -74,6 +74,8 @@ QVariant ContainersModel::data(const QModelIndex &index, int role) const
|
||||
case IsSftpRole: return container == DockerContainer::Sftp;
|
||||
case IsTorWebsiteRole: return container == DockerContainer::TorWebSite;
|
||||
case IsSocks5ProxyRole: return container == DockerContainer::Socks5Proxy;
|
||||
case IsMtProxyRole: return container == DockerContainer::MtProxy;
|
||||
case IsTelemtRole: return container == DockerContainer::Telemt;
|
||||
case InstallPageOrderRole: return ContainerUtils::installPageOrder(container);
|
||||
}
|
||||
|
||||
@@ -184,5 +186,7 @@ QHash<int, QByteArray> ContainersModel::roleNames() const
|
||||
roles[IsSftpRole] = "isSftp";
|
||||
roles[IsTorWebsiteRole] = "isTorWebsite";
|
||||
roles[IsSocks5ProxyRole] = "isSocks5Proxy";
|
||||
roles[IsMtProxyRole] = "isMtProxy";
|
||||
roles[IsTelemtRole] = "isTelemt";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,9 @@ public:
|
||||
IsDnsRole,
|
||||
IsSftpRole,
|
||||
IsTorWebsiteRole,
|
||||
IsSocks5ProxyRole
|
||||
IsSocks5ProxyRole,
|
||||
IsMtProxyRole,
|
||||
IsTelemtRole,
|
||||
};
|
||||
|
||||
Q_INVOKABLE void openContainerSettings(int containerIndex);
|
||||
|
||||
@@ -8,94 +8,575 @@
|
||||
using namespace amnezia;
|
||||
using namespace ProtocolUtils;
|
||||
|
||||
XrayConfigModel::XrayConfigModel(QObject *parent) : QAbstractListModel(parent)
|
||||
XrayConfigModel::XrayConfigModel(QObject* parent) : QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
int XrayConfigModel::rowCount(const QModelIndex &parent) const
|
||||
int XrayConfigModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool XrayConfigModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||
bool XrayConfigModel::setData(const QModelIndex& index, const QVariant& value, int role)
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= ContainerUtils::allContainers().size()) {
|
||||
// This model always has a single row (row 0). Using rowCount() avoids
|
||||
// coupling editing ability to global container list size.
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= rowCount())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QString strValue = value.toString();
|
||||
const bool wasUnsavedChanges = hasUnsavedChanges();
|
||||
|
||||
auto& srv = m_protocolConfig.serverConfig;
|
||||
auto& xhttp = srv.xhttp;
|
||||
auto& mkcp = srv.mkcp;
|
||||
auto& pad = xhttp.xPadding;
|
||||
auto& mux = xhttp.xmux;
|
||||
|
||||
QString str = value.toString();
|
||||
|
||||
switch (role)
|
||||
{
|
||||
// ── Main ──────────────────────────────────────────────────────────
|
||||
case Roles::SiteRole: srv.site = str;
|
||||
break;
|
||||
case Roles::PortRole: srv.port = str;
|
||||
break;
|
||||
case Roles::TransportRole: srv.transport = str;
|
||||
break;
|
||||
case Roles::SecurityRole: srv.security = str;
|
||||
break;
|
||||
case Roles::FlowRole: srv.flow = str;
|
||||
break;
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────────
|
||||
case Roles::FingerprintRole: srv.fingerprint = str;
|
||||
break;
|
||||
case Roles::SniRole: srv.sni = str;
|
||||
break;
|
||||
case Roles::AlpnRole: srv.alpn = str;
|
||||
break;
|
||||
|
||||
// ── XHTTP ─────────────────────────────────────────────────────────
|
||||
case Roles::XhttpModeRole: xhttp.mode = str;
|
||||
break;
|
||||
case Roles::XhttpHostRole: xhttp.host = str;
|
||||
break;
|
||||
case Roles::XhttpPathRole: xhttp.path = str;
|
||||
break;
|
||||
case Roles::XhttpHeadersTemplateRole: xhttp.headersTemplate = str;
|
||||
break;
|
||||
case Roles::XhttpUplinkMethodRole: xhttp.uplinkMethod = str;
|
||||
break;
|
||||
case Roles::XhttpDisableGrpcRole: xhttp.disableGrpc = value.toBool();
|
||||
break;
|
||||
case Roles::XhttpDisableSseRole: xhttp.disableSse = value.toBool();
|
||||
break;
|
||||
|
||||
case Roles::XhttpSessionPlacementRole: xhttp.sessionPlacement = str;
|
||||
break;
|
||||
case Roles::XhttpSessionKeyRole: xhttp.sessionKey = str;
|
||||
break;
|
||||
case Roles::XhttpSeqPlacementRole: xhttp.seqPlacement = str;
|
||||
break;
|
||||
case Roles::XhttpSeqKeyRole: xhttp.seqKey = str;
|
||||
break;
|
||||
case Roles::XhttpUplinkDataPlacementRole: xhttp.uplinkDataPlacement = str;
|
||||
break;
|
||||
case Roles::XhttpUplinkDataKeyRole: xhttp.uplinkDataKey = str;
|
||||
break;
|
||||
|
||||
case Roles::XhttpUplinkChunkSizeRole: xhttp.uplinkChunkSize = str;
|
||||
break;
|
||||
case Roles::XhttpScMaxBufferedPostsRole: xhttp.scMaxBufferedPosts = str;
|
||||
break;
|
||||
case Roles::XhttpScMaxEachPostBytesMinRole: xhttp.scMaxEachPostBytesMin = str;
|
||||
break;
|
||||
case Roles::XhttpScMaxEachPostBytesMaxRole: xhttp.scMaxEachPostBytesMax = str;
|
||||
break;
|
||||
case Roles::XhttpScMinPostsIntervalMsMinRole: xhttp.scMinPostsIntervalMsMin = str;
|
||||
break;
|
||||
case Roles::XhttpScMinPostsIntervalMsMaxRole: xhttp.scMinPostsIntervalMsMax = str;
|
||||
break;
|
||||
case Roles::XhttpScStreamUpServerSecsMinRole: xhttp.scStreamUpServerSecsMin = str;
|
||||
break;
|
||||
case Roles::XhttpScStreamUpServerSecsMaxRole: xhttp.scStreamUpServerSecsMax = str;
|
||||
break;
|
||||
|
||||
// ── mKCP ──────────────────────────────────────────────────────────
|
||||
case Roles::MkcpTtiRole: mkcp.tti = str;
|
||||
break;
|
||||
case Roles::MkcpUplinkCapacityRole: mkcp.uplinkCapacity = str;
|
||||
break;
|
||||
case Roles::MkcpDownlinkCapacityRole: mkcp.downlinkCapacity = str;
|
||||
break;
|
||||
case Roles::MkcpReadBufferSizeRole: mkcp.readBufferSize = str;
|
||||
break;
|
||||
case Roles::MkcpWriteBufferSizeRole: mkcp.writeBufferSize = str;
|
||||
break;
|
||||
case Roles::MkcpCongestionRole: mkcp.congestion = value.toBool();
|
||||
break;
|
||||
|
||||
// ── xPadding ──────────────────────────────────────────────────────
|
||||
case Roles::XPaddingBytesMinRole: pad.bytesMin = str;
|
||||
break;
|
||||
case Roles::XPaddingBytesMaxRole: pad.bytesMax = str;
|
||||
break;
|
||||
case Roles::XPaddingObfsModeRole: pad.obfsMode = value.toBool();
|
||||
break;
|
||||
case Roles::XPaddingKeyRole: pad.key = str;
|
||||
break;
|
||||
case Roles::XPaddingHeaderRole: pad.header = str;
|
||||
break;
|
||||
case Roles::XPaddingPlacementRole: pad.placement = str;
|
||||
break;
|
||||
case Roles::XPaddingMethodRole: pad.method = str;
|
||||
break;
|
||||
|
||||
// ── xmux ──────────────────────────────────────────────────────────
|
||||
case Roles::XmuxEnabledRole: mux.enabled = value.toBool();
|
||||
break;
|
||||
case Roles::XmuxMaxConcurrencyMinRole: mux.maxConcurrencyMin = str;
|
||||
break;
|
||||
case Roles::XmuxMaxConcurrencyMaxRole: mux.maxConcurrencyMax = str;
|
||||
break;
|
||||
case Roles::XmuxMaxConnectionsMinRole: mux.maxConnectionsMin = str;
|
||||
break;
|
||||
case Roles::XmuxMaxConnectionsMaxRole: mux.maxConnectionsMax = str;
|
||||
break;
|
||||
case Roles::XmuxCMaxReuseTimesMinRole: mux.cMaxReuseTimesMin = str;
|
||||
break;
|
||||
case Roles::XmuxCMaxReuseTimesMaxRole: mux.cMaxReuseTimesMax = str;
|
||||
break;
|
||||
case Roles::XmuxHMaxRequestTimesMinRole: mux.hMaxRequestTimesMin = str;
|
||||
break;
|
||||
case Roles::XmuxHMaxRequestTimesMaxRole: mux.hMaxRequestTimesMax = str;
|
||||
break;
|
||||
case Roles::XmuxHMaxReusableSecsMinRole: mux.hMaxReusableSecsMin = str;
|
||||
break;
|
||||
case Roles::XmuxHMaxReusableSecsMaxRole: mux.hMaxReusableSecsMax = str;
|
||||
break;
|
||||
case Roles::XmuxHKeepAlivePeriodRole: mux.hKeepAlivePeriod = str;
|
||||
break;
|
||||
|
||||
switch (role) {
|
||||
case Roles::SiteRole: m_protocolConfig.serverConfig.site = strValue; break;
|
||||
case Roles::PortRole: m_protocolConfig.serverConfig.port = strValue; break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
emit dataChanged(index, index, QList { role });
|
||||
emit dataChanged(index, index, QList{role});
|
||||
if (wasUnsavedChanges != hasUnsavedChanges()) {
|
||||
emit hasUnsavedChangesChanged();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QVariant XrayConfigModel::data(const QModelIndex &index, int role) const
|
||||
QVariant XrayConfigModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
switch (role) {
|
||||
case Roles::SiteRole: return m_protocolConfig.serverConfig.site;
|
||||
case Roles::PortRole: return m_protocolConfig.serverConfig.port;
|
||||
const auto& srv = m_protocolConfig.serverConfig;
|
||||
const auto& xhttp = srv.xhttp;
|
||||
const auto& mkcp = srv.mkcp;
|
||||
const auto& pad = xhttp.xPadding;
|
||||
const auto& mux = xhttp.xmux;
|
||||
|
||||
switch (role)
|
||||
{
|
||||
// ── Main ──────────────────────────────────────────────────────────
|
||||
case Roles::SiteRole: return srv.site;
|
||||
case Roles::PortRole: return srv.port;
|
||||
case Roles::TransportRole: return srv.transport;
|
||||
case Roles::SecurityRole: return srv.security;
|
||||
case Roles::FlowRole: return srv.flow;
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────────
|
||||
case Roles::FingerprintRole: return srv.fingerprint;
|
||||
case Roles::SniRole: return srv.sni;
|
||||
case Roles::AlpnRole: return srv.alpn;
|
||||
|
||||
// ── XHTTP ─────────────────────────────────────────────────────────
|
||||
case Roles::XhttpModeRole: return xhttp.mode;
|
||||
case Roles::XhttpHostRole: return xhttp.host;
|
||||
case Roles::XhttpPathRole: return xhttp.path;
|
||||
case Roles::XhttpHeadersTemplateRole: return xhttp.headersTemplate;
|
||||
case Roles::XhttpUplinkMethodRole: return xhttp.uplinkMethod;
|
||||
case Roles::XhttpDisableGrpcRole: return xhttp.disableGrpc;
|
||||
case Roles::XhttpDisableSseRole: return xhttp.disableSse;
|
||||
|
||||
case Roles::XhttpSessionPlacementRole: return xhttp.sessionPlacement;
|
||||
case Roles::XhttpSessionKeyRole: return xhttp.sessionKey;
|
||||
case Roles::XhttpSeqPlacementRole: return xhttp.seqPlacement;
|
||||
case Roles::XhttpSeqKeyRole: return xhttp.seqKey;
|
||||
case Roles::XhttpUplinkDataPlacementRole: return xhttp.uplinkDataPlacement;
|
||||
case Roles::XhttpUplinkDataKeyRole: return xhttp.uplinkDataKey;
|
||||
|
||||
case Roles::XhttpUplinkChunkSizeRole: return xhttp.uplinkChunkSize;
|
||||
case Roles::XhttpScMaxBufferedPostsRole: return xhttp.scMaxBufferedPosts;
|
||||
case Roles::XhttpScMaxEachPostBytesMinRole: return xhttp.scMaxEachPostBytesMin;
|
||||
case Roles::XhttpScMaxEachPostBytesMaxRole: return xhttp.scMaxEachPostBytesMax;
|
||||
case Roles::XhttpScMinPostsIntervalMsMinRole: return xhttp.scMinPostsIntervalMsMin;
|
||||
case Roles::XhttpScMinPostsIntervalMsMaxRole: return xhttp.scMinPostsIntervalMsMax;
|
||||
case Roles::XhttpScStreamUpServerSecsMinRole: return xhttp.scStreamUpServerSecsMin;
|
||||
case Roles::XhttpScStreamUpServerSecsMaxRole: return xhttp.scStreamUpServerSecsMax;
|
||||
|
||||
// ── mKCP ──────────────────────────────────────────────────────────
|
||||
case Roles::MkcpTtiRole: return mkcp.tti;
|
||||
case Roles::MkcpUplinkCapacityRole: return mkcp.uplinkCapacity;
|
||||
case Roles::MkcpDownlinkCapacityRole: return mkcp.downlinkCapacity;
|
||||
case Roles::MkcpReadBufferSizeRole: return mkcp.readBufferSize;
|
||||
case Roles::MkcpWriteBufferSizeRole: return mkcp.writeBufferSize;
|
||||
case Roles::MkcpCongestionRole: return mkcp.congestion;
|
||||
|
||||
// ── xPadding ──────────────────────────────────────────────────────
|
||||
case Roles::XPaddingBytesMinRole: return pad.bytesMin;
|
||||
case Roles::XPaddingBytesMaxRole: return pad.bytesMax;
|
||||
case Roles::XPaddingObfsModeRole: return pad.obfsMode;
|
||||
case Roles::XPaddingKeyRole: return pad.key;
|
||||
case Roles::XPaddingHeaderRole: return pad.header;
|
||||
case Roles::XPaddingPlacementRole: return pad.placement;
|
||||
case Roles::XPaddingMethodRole: return pad.method;
|
||||
|
||||
// ── xmux ──────────────────────────────────────────────────────────
|
||||
case Roles::XmuxEnabledRole: return mux.enabled;
|
||||
case Roles::XmuxMaxConcurrencyMinRole: return mux.maxConcurrencyMin;
|
||||
case Roles::XmuxMaxConcurrencyMaxRole: return mux.maxConcurrencyMax;
|
||||
case Roles::XmuxMaxConnectionsMinRole: return mux.maxConnectionsMin;
|
||||
case Roles::XmuxMaxConnectionsMaxRole: return mux.maxConnectionsMax;
|
||||
case Roles::XmuxCMaxReuseTimesMinRole: return mux.cMaxReuseTimesMin;
|
||||
case Roles::XmuxCMaxReuseTimesMaxRole: return mux.cMaxReuseTimesMax;
|
||||
case Roles::XmuxHMaxRequestTimesMinRole: return mux.hMaxRequestTimesMin;
|
||||
case Roles::XmuxHMaxRequestTimesMaxRole: return mux.hMaxRequestTimesMax;
|
||||
case Roles::XmuxHMaxReusableSecsMinRole: return mux.hMaxReusableSecsMin;
|
||||
case Roles::XmuxHMaxReusableSecsMaxRole: return mux.hMaxReusableSecsMax;
|
||||
case Roles::XmuxHKeepAlivePeriodRole: return mux.hKeepAlivePeriod;
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig &protocolConfig)
|
||||
void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig)
|
||||
{
|
||||
const bool wasUnsavedChanges = hasUnsavedChanges();
|
||||
|
||||
beginResetModel();
|
||||
|
||||
m_container = container;
|
||||
|
||||
|
||||
m_protocolConfig = protocolConfig;
|
||||
|
||||
|
||||
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
|
||||
|
||||
|
||||
m_originalProtocolConfig = m_protocolConfig;
|
||||
|
||||
|
||||
endResetModel();
|
||||
if (wasUnsavedChanges != hasUnsavedChanges()) {
|
||||
emit hasUnsavedChangesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig& config)
|
||||
void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &config)
|
||||
{
|
||||
if (config.port.isEmpty()) {
|
||||
config.port = protocols::xray::defaultPort;
|
||||
}
|
||||
|
||||
if (config.transportProto.isEmpty()) {
|
||||
config.transportProto = ProtocolUtils::transportProtoToString(
|
||||
ProtocolUtils::defaultTransportProto(amnezia::Proto::Xray), amnezia::Proto::Xray);
|
||||
}
|
||||
|
||||
if (config.site.isEmpty()) {
|
||||
config.site = protocols::xray::defaultSite;
|
||||
}
|
||||
|
||||
if (config.transport.isEmpty()) {
|
||||
config.transport = protocols::xray::defaultTransport;
|
||||
}
|
||||
|
||||
if (config.security.isEmpty()) {
|
||||
config.security = protocols::xray::defaultSecurity;
|
||||
}
|
||||
|
||||
if (config.flow.isEmpty()) {
|
||||
config.flow = protocols::xray::defaultFlow;
|
||||
}
|
||||
|
||||
if (config.fingerprint.isEmpty()) {
|
||||
config.fingerprint = protocols::xray::defaultFingerprint;
|
||||
} else if (config.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
config.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
}
|
||||
|
||||
if (config.sni.isEmpty()) {
|
||||
config.sni = protocols::xray::defaultSni;
|
||||
}
|
||||
|
||||
if (config.alpn.isEmpty()) {
|
||||
config.alpn = protocols::xray::defaultAlpn;
|
||||
}
|
||||
|
||||
// XHTTP transport defaults
|
||||
if (config.xhttp.host.isEmpty()) {
|
||||
config.xhttp.host = protocols::xray::defaultXhttpHost;
|
||||
}
|
||||
if (config.xhttp.mode.isEmpty()) {
|
||||
config.xhttp.mode = protocols::xray::defaultXhttpMode;
|
||||
}
|
||||
if (config.xhttp.headersTemplate.isEmpty()) {
|
||||
config.xhttp.headersTemplate = protocols::xray::defaultXhttpHeadersTemplate;
|
||||
}
|
||||
if (config.xhttp.uplinkMethod.isEmpty()) {
|
||||
config.xhttp.uplinkMethod = protocols::xray::defaultXhttpUplinkMethod;
|
||||
}
|
||||
if (config.xhttp.sessionPlacement.isEmpty()) {
|
||||
config.xhttp.sessionPlacement = protocols::xray::defaultXhttpSessionPlacement;
|
||||
}
|
||||
if (config.xhttp.sessionKey.isEmpty()) {
|
||||
config.xhttp.sessionKey = protocols::xray::defaultXhttpSessionKey;
|
||||
}
|
||||
if (config.xhttp.seqPlacement.isEmpty()) {
|
||||
config.xhttp.seqPlacement = protocols::xray::defaultXhttpSeqPlacement;
|
||||
}
|
||||
if (config.xhttp.uplinkDataPlacement.isEmpty()) {
|
||||
config.xhttp.uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement;
|
||||
}
|
||||
|
||||
// xPadding defaults
|
||||
if (config.xhttp.xPadding.placement.isEmpty()) {
|
||||
config.xhttp.xPadding.placement = protocols::xray::defaultXPaddingPlacement;
|
||||
}
|
||||
if (config.xhttp.xPadding.method.isEmpty()) {
|
||||
config.xhttp.xPadding.method = protocols::xray::defaultXPaddingMethod;
|
||||
}
|
||||
}
|
||||
|
||||
amnezia::XrayProtocolConfig XrayConfigModel::getProtocolConfig()
|
||||
{
|
||||
bool serverSettingsChanged = !m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig);
|
||||
|
||||
const bool serverSettingsChanged =
|
||||
!m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig);
|
||||
|
||||
if (serverSettingsChanged) {
|
||||
m_protocolConfig.clearClientConfig();
|
||||
}
|
||||
|
||||
return m_protocolConfig;
|
||||
}
|
||||
|
||||
bool XrayConfigModel::isServerSettingsEqual() const
|
||||
{
|
||||
return m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig);
|
||||
}
|
||||
|
||||
bool XrayConfigModel::hasUnsavedChanges() const
|
||||
{
|
||||
return !isServerSettingsEqual();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> XrayConfigModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
|
||||
// Main
|
||||
roles[SiteRole] = "site";
|
||||
roles[PortRole] = "port";
|
||||
roles[TransportRole] = "transport";
|
||||
roles[SecurityRole] = "security";
|
||||
roles[FlowRole] = "flow";
|
||||
|
||||
// Security
|
||||
roles[FingerprintRole] = "fingerprint";
|
||||
roles[SniRole] = "sni";
|
||||
roles[AlpnRole] = "alpn";
|
||||
|
||||
// XHTTP
|
||||
roles[XhttpModeRole] = "xhttpMode";
|
||||
roles[XhttpHostRole] = "xhttpHost";
|
||||
roles[XhttpPathRole] = "xhttpPath";
|
||||
roles[XhttpHeadersTemplateRole] = "xhttpHeadersTemplate";
|
||||
roles[XhttpUplinkMethodRole] = "xhttpUplinkMethod";
|
||||
roles[XhttpDisableGrpcRole] = "xhttpDisableGrpc";
|
||||
roles[XhttpDisableSseRole] = "xhttpDisableSse";
|
||||
|
||||
roles[XhttpSessionPlacementRole] = "xhttpSessionPlacement";
|
||||
roles[XhttpSessionKeyRole] = "xhttpSessionKey";
|
||||
roles[XhttpSeqPlacementRole] = "xhttpSeqPlacement";
|
||||
roles[XhttpSeqKeyRole] = "xhttpSeqKey";
|
||||
roles[XhttpUplinkDataPlacementRole] = "xhttpUplinkDataPlacement";
|
||||
roles[XhttpUplinkDataKeyRole] = "xhttpUplinkDataKey";
|
||||
|
||||
roles[XhttpUplinkChunkSizeRole] = "xhttpUplinkChunkSize";
|
||||
roles[XhttpScMaxBufferedPostsRole] = "xhttpScMaxBufferedPosts";
|
||||
roles[XhttpScMaxEachPostBytesMinRole] = "xhttpScMaxEachPostBytesMin";
|
||||
roles[XhttpScMaxEachPostBytesMaxRole] = "xhttpScMaxEachPostBytesMax";
|
||||
roles[XhttpScMinPostsIntervalMsMinRole] = "xhttpScMinPostsIntervalMsMin";
|
||||
roles[XhttpScMinPostsIntervalMsMaxRole] = "xhttpScMinPostsIntervalMsMax";
|
||||
roles[XhttpScStreamUpServerSecsMinRole] = "xhttpScStreamUpServerSecsMin";
|
||||
roles[XhttpScStreamUpServerSecsMaxRole] = "xhttpScStreamUpServerSecsMax";
|
||||
|
||||
// mKCP
|
||||
roles[MkcpTtiRole] = "mkcpTti";
|
||||
roles[MkcpUplinkCapacityRole] = "mkcpUplinkCapacity";
|
||||
roles[MkcpDownlinkCapacityRole] = "mkcpDownlinkCapacity";
|
||||
roles[MkcpReadBufferSizeRole] = "mkcpReadBufferSize";
|
||||
roles[MkcpWriteBufferSizeRole] = "mkcpWriteBufferSize";
|
||||
roles[MkcpCongestionRole] = "mkcpCongestion";
|
||||
|
||||
// xPadding
|
||||
roles[XPaddingBytesMinRole] = "xPaddingBytesMin";
|
||||
roles[XPaddingBytesMaxRole] = "xPaddingBytesMax";
|
||||
roles[XPaddingObfsModeRole] = "xPaddingObfsMode";
|
||||
roles[XPaddingKeyRole] = "xPaddingKey";
|
||||
roles[XPaddingHeaderRole] = "xPaddingHeader";
|
||||
roles[XPaddingPlacementRole] = "xPaddingPlacement";
|
||||
roles[XPaddingMethodRole] = "xPaddingMethod";
|
||||
|
||||
// xmux
|
||||
roles[XmuxEnabledRole] = "xmuxEnabled";
|
||||
roles[XmuxMaxConcurrencyMinRole] = "xmuxMaxConcurrencyMin";
|
||||
roles[XmuxMaxConcurrencyMaxRole] = "xmuxMaxConcurrencyMax";
|
||||
roles[XmuxMaxConnectionsMinRole] = "xmuxMaxConnectionsMin";
|
||||
roles[XmuxMaxConnectionsMaxRole] = "xmuxMaxConnectionsMax";
|
||||
roles[XmuxCMaxReuseTimesMinRole] = "xmuxCMaxReuseTimesMin";
|
||||
roles[XmuxCMaxReuseTimesMaxRole] = "xmuxCMaxReuseTimesMax";
|
||||
roles[XmuxHMaxRequestTimesMinRole] = "xmuxHMaxRequestTimesMin";
|
||||
roles[XmuxHMaxRequestTimesMaxRole] = "xmuxHMaxRequestTimesMax";
|
||||
roles[XmuxHMaxReusableSecsMinRole] = "xmuxHMaxReusableSecsMin";
|
||||
roles[XmuxHMaxReusableSecsMaxRole] = "xmuxHMaxReusableSecsMax";
|
||||
roles[XmuxHKeepAlivePeriodRole] = "xmuxHKeepAlivePeriod";
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
void XrayConfigModel::resetToDefaults()
|
||||
{
|
||||
const bool wasUnsavedChanges = hasUnsavedChanges();
|
||||
|
||||
beginResetModel();
|
||||
m_protocolConfig.serverConfig = amnezia::XrayServerConfig{};
|
||||
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
|
||||
endResetModel();
|
||||
|
||||
if (wasUnsavedChanges != hasUnsavedChanges()) {
|
||||
emit hasUnsavedChangesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void XrayConfigModel::applyServerConfig(const amnezia::XrayServerConfig &serverConfig)
|
||||
{
|
||||
const bool wasUnsavedChanges = hasUnsavedChanges();
|
||||
|
||||
beginResetModel();
|
||||
m_protocolConfig.serverConfig = serverConfig;
|
||||
// Clear client config since server settings changed
|
||||
m_protocolConfig.clearClientConfig();
|
||||
m_originalProtocolConfig = m_protocolConfig;
|
||||
endResetModel();
|
||||
|
||||
if (wasUnsavedChanges != hasUnsavedChanges()) {
|
||||
emit hasUnsavedChangesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::flowOptions()
|
||||
{
|
||||
return {
|
||||
"", // Empty (no flow)
|
||||
"xtls-rprx-vision",
|
||||
"xtls-rprx-vision-udp443"
|
||||
};
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::securityOptions()
|
||||
{
|
||||
return { "none", "tls", "reality" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::transportOptions()
|
||||
{
|
||||
return { "raw", "xhttp", "mkcp" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::fingerprintOptions()
|
||||
{
|
||||
return { "chrome", "firefox", "safari", "ios", "android", "edge", "360", "qq", "random" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::alpnOptions()
|
||||
{
|
||||
return { "HTTP/2", "HTTP/1.1", "HTTP/2,HTTP/1.1" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpModeOptions()
|
||||
{
|
||||
return { "Auto", "Packet-up", "Stream-up", "Stream-one" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpHeadersTemplateOptions()
|
||||
{
|
||||
return { "HTTP", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpUplinkMethodOptions()
|
||||
{
|
||||
return { "POST", "PUT", "PATCH" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpSessionPlacementOptions()
|
||||
{
|
||||
return { "Path", "Header", "Cookie", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpSessionKeyOptions()
|
||||
{
|
||||
return { "Path", "Header", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpSeqPlacementOptions()
|
||||
{
|
||||
return { "Path", "Header", "Cookie", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpUplinkDataPlacementOptions()
|
||||
{
|
||||
// Matches splithttp uplink payload placement (packet-up / advanced)
|
||||
return { "Body", "Auto", "Header", "Cookie" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xPaddingPlacementOptions()
|
||||
{
|
||||
// Xray-core: cookie | header | query | queryInHeader (not "body")
|
||||
return { "Cookie", "Header", "Query", "Query in header" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xPaddingMethodOptions()
|
||||
{
|
||||
return { "Repeat-x", "Tokenish" };
|
||||
}
|
||||
|
||||
QString XrayConfigModel::mkcpDefaultTti()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpTti);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::mkcpDefaultUplinkCapacity()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpUplinkCapacity);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::mkcpDefaultDownlinkCapacity()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpDownlinkCapacity);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::mkcpDefaultReadBufferSize()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpReadBufferSize);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::mkcpDefaultWriteBufferSize()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpWriteBufferSize);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define XRAYCONFIGMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QStringList>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
@@ -11,23 +12,122 @@
|
||||
class XrayConfigModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool hasUnsavedChanges READ hasUnsavedChanges NOTIFY hasUnsavedChangesChanged)
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
SiteRole,
|
||||
PortRole
|
||||
enum Roles
|
||||
{
|
||||
// ── Main page ─────────────────────────────────────────────────
|
||||
SiteRole = Qt::UserRole + 1,
|
||||
PortRole,
|
||||
TransportRole, // "raw" | "xhttp" | "mkcp" (display in main page row)
|
||||
SecurityRole, // "none" | "tls" | "reality" (display in main page row)
|
||||
FlowRole, // "" | "xtls-rprx-vision" | "xtls-rprx-vision-udp443"
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────
|
||||
FingerprintRole,
|
||||
SniRole,
|
||||
AlpnRole,
|
||||
|
||||
// ── Transport — XHTTP ─────────────────────────────────────────
|
||||
XhttpModeRole,
|
||||
XhttpHostRole,
|
||||
XhttpPathRole,
|
||||
XhttpHeadersTemplateRole,
|
||||
XhttpUplinkMethodRole,
|
||||
XhttpDisableGrpcRole,
|
||||
XhttpDisableSseRole,
|
||||
|
||||
// Session & Sequence
|
||||
XhttpSessionPlacementRole,
|
||||
XhttpSessionKeyRole,
|
||||
XhttpSeqPlacementRole,
|
||||
XhttpSeqKeyRole,
|
||||
XhttpUplinkDataPlacementRole,
|
||||
XhttpUplinkDataKeyRole,
|
||||
|
||||
// Traffic Shaping
|
||||
XhttpUplinkChunkSizeRole,
|
||||
XhttpScMaxBufferedPostsRole,
|
||||
XhttpScMaxEachPostBytesMinRole,
|
||||
XhttpScMaxEachPostBytesMaxRole,
|
||||
XhttpScMinPostsIntervalMsMinRole,
|
||||
XhttpScMinPostsIntervalMsMaxRole,
|
||||
XhttpScStreamUpServerSecsMinRole,
|
||||
XhttpScStreamUpServerSecsMaxRole,
|
||||
|
||||
// ── Transport — mKCP ──────────────────────────────────────────
|
||||
MkcpTtiRole,
|
||||
MkcpUplinkCapacityRole,
|
||||
MkcpDownlinkCapacityRole,
|
||||
MkcpReadBufferSizeRole,
|
||||
MkcpWriteBufferSizeRole,
|
||||
MkcpCongestionRole,
|
||||
|
||||
// ── xPadding ──────────────────────────────────────────────────
|
||||
XPaddingBytesMinRole,
|
||||
XPaddingBytesMaxRole,
|
||||
XPaddingObfsModeRole,
|
||||
XPaddingKeyRole,
|
||||
XPaddingHeaderRole,
|
||||
XPaddingPlacementRole,
|
||||
XPaddingMethodRole,
|
||||
|
||||
// ── xmux ──────────────────────────────────────────────────────
|
||||
XmuxEnabledRole,
|
||||
XmuxMaxConcurrencyMinRole,
|
||||
XmuxMaxConcurrencyMaxRole,
|
||||
XmuxMaxConnectionsMinRole,
|
||||
XmuxMaxConnectionsMaxRole,
|
||||
XmuxCMaxReuseTimesMinRole,
|
||||
XmuxCMaxReuseTimesMaxRole,
|
||||
XmuxHMaxRequestTimesMinRole,
|
||||
XmuxHMaxRequestTimesMaxRole,
|
||||
XmuxHMaxReusableSecsMinRole,
|
||||
XmuxHMaxReusableSecsMaxRole,
|
||||
XmuxHKeepAlivePeriodRole,
|
||||
};
|
||||
|
||||
explicit XrayConfigModel(QObject *parent = nullptr);
|
||||
explicit XrayConfigModel(QObject* parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
bool setData(const QModelIndex& index, const QVariant& value, int role) override;
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
// ── Static option lists (for QML DropDown models) ─────────────────
|
||||
Q_INVOKABLE static QStringList flowOptions();
|
||||
Q_INVOKABLE static QStringList securityOptions();
|
||||
Q_INVOKABLE static QStringList transportOptions();
|
||||
Q_INVOKABLE static QStringList fingerprintOptions();
|
||||
Q_INVOKABLE static QStringList alpnOptions();
|
||||
Q_INVOKABLE static QStringList xhttpModeOptions();
|
||||
Q_INVOKABLE static QStringList xhttpHeadersTemplateOptions();
|
||||
Q_INVOKABLE static QStringList xhttpUplinkMethodOptions();
|
||||
Q_INVOKABLE static QStringList xhttpSessionPlacementOptions();
|
||||
Q_INVOKABLE static QStringList xhttpSessionKeyOptions();
|
||||
Q_INVOKABLE static QStringList xhttpSeqPlacementOptions();
|
||||
Q_INVOKABLE static QStringList xhttpUplinkDataPlacementOptions();
|
||||
Q_INVOKABLE static QStringList xPaddingPlacementOptions();
|
||||
Q_INVOKABLE static QStringList xPaddingMethodOptions();
|
||||
|
||||
// mKCP display defaults (protocolConstants.h — must match xrayConfigurator empty-field behavior)
|
||||
Q_INVOKABLE static QString mkcpDefaultTti();
|
||||
Q_INVOKABLE static QString mkcpDefaultUplinkCapacity();
|
||||
Q_INVOKABLE static QString mkcpDefaultDownlinkCapacity();
|
||||
Q_INVOKABLE static QString mkcpDefaultReadBufferSize();
|
||||
Q_INVOKABLE static QString mkcpDefaultWriteBufferSize();
|
||||
|
||||
public slots:
|
||||
void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig &protocolConfig);
|
||||
void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig);
|
||||
amnezia::XrayProtocolConfig getProtocolConfig();
|
||||
bool isServerSettingsEqual() const;
|
||||
bool hasUnsavedChanges() const;
|
||||
void resetToDefaults();
|
||||
void applyServerConfig(const amnezia::XrayServerConfig &serverConfig);
|
||||
|
||||
signals:
|
||||
void hasUnsavedChangesChanged();
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
@@ -36,7 +136,7 @@ private:
|
||||
amnezia::DockerContainer m_container;
|
||||
amnezia::XrayProtocolConfig m_protocolConfig;
|
||||
amnezia::XrayProtocolConfig m_originalProtocolConfig;
|
||||
|
||||
|
||||
void applyDefaultsToServerConfig(amnezia::XrayServerConfig& config);
|
||||
};
|
||||
|
||||
|
||||
216
client/ui/models/protocols/xrayConfigSnapshotsModel.cpp
Normal file
216
client/ui/models/protocols/xrayConfigSnapshotsModel.cpp
Normal file
@@ -0,0 +1,216 @@
|
||||
#include "xrayConfigSnapshotsModel.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QUuid>
|
||||
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
|
||||
QJsonObject XrayConfigSnapshot::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj["id"] = id;
|
||||
obj["displayName"] = displayName;
|
||||
obj["createdAt"] = createdAt.toString(Qt::ISODate);
|
||||
obj["serverConfig"] = serverConfig.toJson();
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayConfigSnapshot XrayConfigSnapshot::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayConfigSnapshot s;
|
||||
s.id = json.value("id").toString();
|
||||
s.displayName = json.value("displayName").toString();
|
||||
s.createdAt = QDateTime::fromString(json.value("createdAt").toString(), Qt::ISODate);
|
||||
s.serverConfig = amnezia::XrayServerConfig::fromJson(json.value("serverConfig").toObject());
|
||||
return s;
|
||||
}
|
||||
|
||||
XrayConfigSnapshotsModel::XrayConfigSnapshotsModel(SecureAppSettingsRepository *appSettings,
|
||||
XrayConfigModel *xrayConfigModel, QObject *parent)
|
||||
: QAbstractListModel(parent), m_appSettings(appSettings), m_xrayConfigModel(xrayConfigModel)
|
||||
{
|
||||
loadAll();
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::loadAll()
|
||||
{
|
||||
m_configs.clear();
|
||||
QByteArray raw = m_appSettings->xraySavedConfigs();
|
||||
if (raw.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray arr = QJsonDocument::fromJson(raw).array();
|
||||
for (const QJsonValue &v : arr) {
|
||||
m_configs.append(XrayConfigSnapshot::fromJson(v.toObject()));
|
||||
}
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::persistAll()
|
||||
{
|
||||
QJsonArray arr;
|
||||
for (const XrayConfigSnapshot &s : m_configs) {
|
||||
arr.append(s.toJson());
|
||||
}
|
||||
m_appSettings->setXraySavedConfigs(QJsonDocument(arr).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
int XrayConfigSnapshotsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return m_configs.size();
|
||||
}
|
||||
|
||||
QVariant XrayConfigSnapshotsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= m_configs.size()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
const XrayConfigSnapshot &s = m_configs.at(index.row());
|
||||
|
||||
switch (role) {
|
||||
case IdRole: {
|
||||
return s.id;
|
||||
}
|
||||
case DisplayNameRole: {
|
||||
return s.displayName;
|
||||
}
|
||||
case CreatedAtRole: {
|
||||
return s.createdAt.toString("dd.MM.yyyy HH:mm");
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> XrayConfigSnapshotsModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[IdRole] = "configId";
|
||||
roles[DisplayNameRole] = "configName";
|
||||
roles[CreatedAtRole] = "configDate";
|
||||
return roles;
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::reload()
|
||||
{
|
||||
beginResetModel();
|
||||
loadAll();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::createFromCurrent(const amnezia::XrayServerConfig &serverConfig)
|
||||
{
|
||||
XrayConfigSnapshot snapshot;
|
||||
snapshot.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
snapshot.displayName = buildDisplayName(serverConfig);
|
||||
snapshot.createdAt = QDateTime::currentDateTime();
|
||||
snapshot.serverConfig = serverConfig;
|
||||
|
||||
beginInsertRows(QModelIndex(), m_configs.size(), m_configs.size());
|
||||
m_configs.append(snapshot);
|
||||
endInsertRows();
|
||||
|
||||
persistAll();
|
||||
}
|
||||
|
||||
amnezia::XrayServerConfig XrayConfigSnapshotsModel::applyConfig(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_configs.size()) {
|
||||
return amnezia::XrayServerConfig {};
|
||||
}
|
||||
|
||||
return m_configs.at(index).serverConfig;
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::removeConfig(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_configs.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
beginRemoveRows(QModelIndex(), index, index);
|
||||
m_configs.removeAt(index);
|
||||
endRemoveRows();
|
||||
|
||||
persistAll();
|
||||
emit configRemoved(index);
|
||||
}
|
||||
|
||||
QString XrayConfigSnapshotsModel::exportToJson(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_configs.size()) {
|
||||
return {};
|
||||
}
|
||||
return QString::fromUtf8(QJsonDocument(m_configs.at(index).toJson()).toJson(QJsonDocument::Indented));
|
||||
}
|
||||
|
||||
bool XrayConfigSnapshotsModel::importFromJson(const QString &jsonString)
|
||||
{
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
emit importFailed(tr("Invalid JSON format"));
|
||||
return false;
|
||||
}
|
||||
|
||||
XrayConfigSnapshot snapshot = XrayConfigSnapshot::fromJson(doc.object());
|
||||
if (snapshot.id.isEmpty()) {
|
||||
snapshot.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
}
|
||||
if (snapshot.displayName.isEmpty()) {
|
||||
snapshot.displayName = buildDisplayName(snapshot.serverConfig);
|
||||
}
|
||||
snapshot.createdAt = QDateTime::currentDateTime();
|
||||
|
||||
beginInsertRows(QModelIndex(), m_configs.size(), m_configs.size());
|
||||
m_configs.append(snapshot);
|
||||
endInsertRows();
|
||||
|
||||
persistAll();
|
||||
return true;
|
||||
}
|
||||
|
||||
QString XrayConfigSnapshotsModel::buildDisplayName(const amnezia::XrayServerConfig &cfg)
|
||||
{
|
||||
// Build a human-readable name: "XHTTP TLS Reality", "RAW Reality", etc.
|
||||
QString transport;
|
||||
if (cfg.transport == "xhttp") {
|
||||
transport = "XHTTP";
|
||||
} else if (cfg.transport == "mkcp") {
|
||||
transport = "mKCP";
|
||||
} else {
|
||||
transport = "RAW (TCP)";
|
||||
}
|
||||
|
||||
QString security;
|
||||
if (cfg.security == "tls") {
|
||||
security = "TLS";
|
||||
} else if (cfg.security == "reality") {
|
||||
security = "Reality";
|
||||
} else {
|
||||
security = "None";
|
||||
}
|
||||
|
||||
return QString("%1 %2").arg(transport, security).trimmed();
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::createFromCurrentModel()
|
||||
{
|
||||
if (!m_xrayConfigModel) {
|
||||
return;
|
||||
}
|
||||
createFromCurrent(m_xrayConfigModel->getProtocolConfig().serverConfig);
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::applyConfigToCurrentModel(int index)
|
||||
{
|
||||
if (!m_xrayConfigModel) {
|
||||
return;
|
||||
}
|
||||
amnezia::XrayServerConfig cfg = applyConfig(index);
|
||||
if (cfg.port.isEmpty()) {
|
||||
return; // guard against invalid index
|
||||
}
|
||||
m_xrayConfigModel->applyServerConfig(cfg);
|
||||
}
|
||||
76
client/ui/models/protocols/xrayConfigSnapshotsModel.h
Normal file
76
client/ui/models/protocols/xrayConfigSnapshotsModel.h
Normal file
@@ -0,0 +1,76 @@
|
||||
#ifndef XRAYCONFIGSMODEL_H
|
||||
#define XRAYCONFIGSMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QDateTime>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "ui/models/protocols/xrayConfigModel.h"
|
||||
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
struct XrayConfigSnapshot
|
||||
{
|
||||
QString id;
|
||||
QString displayName; // auto-generated: "XHTTP TLS Reality", "RAW Reality", etc.
|
||||
QDateTime createdAt;
|
||||
amnezia::XrayServerConfig serverConfig;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayConfigSnapshot fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
class XrayConfigSnapshotsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
IdRole = Qt::UserRole + 1,
|
||||
DisplayNameRole,
|
||||
CreatedAtRole, // "dd.MM.yyyy HH:mm"
|
||||
};
|
||||
|
||||
explicit XrayConfigSnapshotsModel(SecureAppSettingsRepository *appSettings, XrayConfigModel *xrayConfigModel,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
public slots:
|
||||
void reload();
|
||||
|
||||
Q_INVOKABLE void createFromCurrent(const amnezia::XrayServerConfig &serverConfig);
|
||||
Q_INVOKABLE amnezia::XrayServerConfig applyConfig(int index) const;
|
||||
Q_INVOKABLE void removeConfig(int index);
|
||||
|
||||
Q_INVOKABLE QString exportToJson(int index) const;
|
||||
Q_INVOKABLE bool importFromJson(const QString &jsonString);
|
||||
|
||||
// Convenience: create snapshot from live model, apply snapshot back to model
|
||||
Q_INVOKABLE void createFromCurrentModel();
|
||||
Q_INVOKABLE void applyConfigToCurrentModel(int index);
|
||||
|
||||
signals:
|
||||
void configApplied(int index);
|
||||
void configRemoved(int index);
|
||||
void importFailed(const QString &errorMessage);
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
SecureAppSettingsRepository *m_appSettings;
|
||||
XrayConfigModel *m_xrayConfigModel;
|
||||
QVector<XrayConfigSnapshot> m_configs;
|
||||
|
||||
void persistAll();
|
||||
void loadAll();
|
||||
static QString buildDisplayName(const amnezia::XrayServerConfig &cfg);
|
||||
};
|
||||
|
||||
#endif // XRAYCONFIGSMODEL_H
|
||||
@@ -42,6 +42,8 @@ QHash<int, QByteArray> ProtocolsModel::roleNames() const
|
||||
roles[IsSftpRole] = "isSftp";
|
||||
roles[IsIpsecRole] = "isIpsec";
|
||||
roles[IsSocks5ProxyRole] = "isSocks5Proxy";
|
||||
roles[IsMtProxyRole] = "isMtProxy";
|
||||
roles[IsTelemtRole] = "isTelemt";
|
||||
|
||||
return roles;
|
||||
}
|
||||
@@ -71,6 +73,8 @@ QVariant ProtocolsModel::data(const QModelIndex &index, int role) const
|
||||
case IsSftpRole: return proto == Proto::Sftp;
|
||||
case IsIpsecRole: return proto == Proto::Ikev2;
|
||||
case IsSocks5ProxyRole: return proto == Proto::Socks5Proxy;
|
||||
case IsMtProxyRole: return proto == Proto::MtProxy;
|
||||
case IsTelemtRole: return proto == Proto::Telemt;
|
||||
case RawConfigRole:
|
||||
return getRawConfig();
|
||||
case IsClientProtocolExistsRole:
|
||||
@@ -124,6 +128,8 @@ PageLoader::PageEnum ProtocolsModel::serverProtocolPage(Proto protocol) const
|
||||
case Proto::Dns: return PageLoader::PageEnum::PageServiceDnsSettings;
|
||||
case Proto::Sftp: return PageLoader::PageEnum::PageServiceSftpSettings;
|
||||
case Proto::Socks5Proxy: return PageLoader::PageEnum::PageServiceSocksProxySettings;
|
||||
case Proto::MtProxy: return PageLoader::PageEnum::PageServiceMtProxySettings;
|
||||
case Proto::Telemt: return PageLoader::PageEnum::PageServiceTelemtSettings;
|
||||
default: return PageLoader::PageEnum::PageProtocolOpenVpnSettings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ public:
|
||||
IsXrayRole,
|
||||
IsSftpRole,
|
||||
IsIpsecRole,
|
||||
IsSocks5ProxyRole
|
||||
IsSocks5ProxyRole,
|
||||
IsMtProxyRole,
|
||||
IsTelemtRole,
|
||||
};
|
||||
|
||||
explicit ProtocolsModel(QObject *parent = nullptr);
|
||||
|
||||
714
client/ui/models/services/mtProxyConfigModel.cpp
Normal file
714
client/ui/models/services/mtProxyConfigModel.cpp
Normal file
@@ -0,0 +1,714 @@
|
||||
#include "mtProxyConfigModel.h"
|
||||
|
||||
#include "ui/models/utils/mtproxy_public_host_input.h"
|
||||
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/qrCodeUtils.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "qrcodegen.hpp"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
#include <QHostAddress>
|
||||
#include <QRegExp>
|
||||
#include <QRegularExpression>
|
||||
#include <QtGlobal>
|
||||
#include <qqml.h>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
MtProxyConfigModel::MtProxyConfigModel(QObject *parent) : QAbstractListModel(parent) {
|
||||
qmlRegisterType<PublicHostInputValidator>("MtProxyConfig", 1, 0, "PublicHostInputValidator");
|
||||
}
|
||||
|
||||
int MtProxyConfigModel::rowCount(const QModelIndex &parent) const {
|
||||
Q_UNUSED(parent);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::setData(const QModelIndex &index, const QVariant &value, int role) {
|
||||
if (!index.isValid() || index.row() != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (role) {
|
||||
case Roles::PortRole: {
|
||||
m_protocolConfig.port = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::SecretRole: {
|
||||
m_protocolConfig.secret = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::TagRole: {
|
||||
const QString tag = sanitizeMtProxyTagFieldText(value.toString());
|
||||
if (!isValidMtProxyTag(tag)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.tag = tag;
|
||||
break;
|
||||
}
|
||||
case Roles::IsEnabledRole: {
|
||||
m_protocolConfig.isEnabled = value.toBool();
|
||||
break;
|
||||
}
|
||||
case Roles::PublicHostRole: {
|
||||
const QString h = value.toString().trimmed();
|
||||
if (!isValidPublicHost(h)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.publicHost = h;
|
||||
break;
|
||||
}
|
||||
case Roles::TransportModeRole: {
|
||||
m_protocolConfig.transportMode = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::TlsDomainRole: {
|
||||
const QString d = value.toString().trimmed();
|
||||
if (!isValidFakeTlsDomain(d)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.tlsDomain = d;
|
||||
break;
|
||||
}
|
||||
case Roles::AdditionalSecretsRole: {
|
||||
m_protocolConfig.additionalSecrets = value.toStringList();
|
||||
break;
|
||||
}
|
||||
case Roles::WorkersModeRole: {
|
||||
m_protocolConfig.workersMode = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::WorkersRole: {
|
||||
m_protocolConfig.workers = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::NatEnabledRole: {
|
||||
m_protocolConfig.natEnabled = value.toBool();
|
||||
break;
|
||||
}
|
||||
case Roles::NatInternalIpRole: {
|
||||
const QString ip = value.toString().trimmed();
|
||||
if (!isValidOptionalIpv4(ip)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.natInternalIp = ip;
|
||||
break;
|
||||
}
|
||||
case Roles::NatExternalIpRole: {
|
||||
const QString ip = value.toString().trimmed();
|
||||
if (!isValidOptionalIpv4(ip)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.natExternalIp = ip;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
emit dataChanged(index, index, QList{role});
|
||||
return true;
|
||||
}
|
||||
|
||||
QVariant MtProxyConfigModel::data(const QModelIndex &index, int role) const {
|
||||
if (!index.isValid() || index.row() != 0) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
switch (role) {
|
||||
case Roles::PortRole: {
|
||||
return m_protocolConfig.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : m_protocolConfig.port;
|
||||
}
|
||||
case Roles::SecretRole: {
|
||||
return m_protocolConfig.secret;
|
||||
}
|
||||
case Roles::TagRole: {
|
||||
return m_protocolConfig.tag;
|
||||
}
|
||||
case Roles::TgLinkRole: {
|
||||
return m_protocolConfig.tgLink;
|
||||
}
|
||||
case Roles::TmeLinkRole: {
|
||||
return m_protocolConfig.tmeLink;
|
||||
}
|
||||
case Roles::IsEnabledRole: {
|
||||
return m_protocolConfig.isEnabled;
|
||||
}
|
||||
case Roles::PublicHostRole: {
|
||||
return m_protocolConfig.publicHost.isEmpty()
|
||||
? m_fullConfig.value(configKey::hostName).toString()
|
||||
: m_protocolConfig.publicHost;
|
||||
}
|
||||
case Roles::TransportModeRole: {
|
||||
return m_protocolConfig.transportMode.isEmpty()
|
||||
? QString(protocols::mtProxy::transportModeStandard)
|
||||
: m_protocolConfig.transportMode;
|
||||
}
|
||||
case Roles::TlsDomainRole: {
|
||||
return m_protocolConfig.tlsDomain;
|
||||
}
|
||||
case Roles::AdditionalSecretsRole: {
|
||||
return m_protocolConfig.additionalSecrets;
|
||||
}
|
||||
case Roles::WorkersModeRole: {
|
||||
return m_protocolConfig.workersMode.isEmpty()
|
||||
? QString(protocols::mtProxy::workersModeAuto)
|
||||
: m_protocolConfig.workersMode;
|
||||
}
|
||||
case Roles::WorkersRole: {
|
||||
return m_protocolConfig.workers.isEmpty() ? QString(protocols::mtProxy::defaultWorkers)
|
||||
: m_protocolConfig.workers;
|
||||
}
|
||||
case Roles::NatEnabledRole: {
|
||||
return m_protocolConfig.natEnabled;
|
||||
}
|
||||
case Roles::NatInternalIpRole: {
|
||||
return m_protocolConfig.natInternalIp;
|
||||
}
|
||||
case Roles::NatExternalIpRole: {
|
||||
return m_protocolConfig.natExternalIp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::updateModel(amnezia::DockerContainer container,
|
||||
const amnezia::MtProxyProtocolConfig &protocolConfig) {
|
||||
beginResetModel();
|
||||
m_container = container;
|
||||
m_protocolConfig = protocolConfig;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::updateModel(const QJsonObject &config) {
|
||||
beginResetModel();
|
||||
|
||||
m_fullConfig = config;
|
||||
m_protocolConfig = MtProxyProtocolConfig::fromJson(config.value(configKey::mtproxy).toObject());
|
||||
if (m_protocolConfig.port.isEmpty()) m_protocolConfig.port = protocols::mtProxy::defaultPort;
|
||||
if (m_protocolConfig.transportMode.isEmpty()) m_protocolConfig.transportMode = protocols::mtProxy::transportModeStandard;
|
||||
if (m_protocolConfig.workersMode.isEmpty()) m_protocolConfig.workersMode = protocols::mtProxy::workersModeAuto;
|
||||
if (m_protocolConfig.workers.isEmpty()) m_protocolConfig.workers = protocols::mtProxy::defaultWorkers;
|
||||
{
|
||||
QString tagIn = sanitizeMtProxyTagFieldText(m_protocolConfig.tag);
|
||||
if (!isValidMtProxyTag(tagIn)) {
|
||||
tagIn.clear();
|
||||
}
|
||||
m_protocolConfig.tag = tagIn;
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QJsonObject MtProxyConfigModel::getConfig() {
|
||||
m_fullConfig.insert(configKey::mtproxy, m_protocolConfig.toJson());
|
||||
return m_fullConfig;
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::generateSecret() {
|
||||
// Generate 16 random bytes = 32 hex chars
|
||||
QString secret;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
quint32 byte = QRandomGenerator::global()->bounded(256);
|
||||
secret += QString("%1").arg(byte, 2, 16, QChar('0'));
|
||||
}
|
||||
|
||||
m_protocolConfig.secret = secret;
|
||||
emit dataChanged(index(0), index(0), QList<int>{SecretRole});
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setSecret(const QString &secret) {
|
||||
if (secret.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), secret, SecretRole);
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::validateAndSetSecret(const QString &rawSecret) {
|
||||
if (!QRegularExpression("^[0-9a-fA-F]{32}$").match(rawSecret).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
setData(index(0), rawSecret, SecretRole);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setPort(const QString &port) {
|
||||
setData(index(0), port, PortRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setTag(const QString &tag) {
|
||||
setData(index(0), tag, TagRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setPublicHost(const QString &host) {
|
||||
const QString t = host.trimmed();
|
||||
if (!isValidPublicHost(t)) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), t, PublicHostRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setTransportMode(const QString &mode) {
|
||||
setData(index(0), mode, TransportModeRole);
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::getTransportMode() const {
|
||||
return m_protocolConfig.transportMode.isEmpty()
|
||||
? QString(protocols::mtProxy::transportModeStandard)
|
||||
: m_protocolConfig.transportMode;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::getTlsDomain() const {
|
||||
return m_protocolConfig.tlsDomain.isEmpty()
|
||||
? QString(protocols::mtProxy::defaultTlsDomain)
|
||||
: m_protocolConfig.tlsDomain;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::getPublicHost() const {
|
||||
return m_protocolConfig.publicHost;
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setTlsDomain(const QString &domain) {
|
||||
const QString t = domain.trimmed();
|
||||
if (!isValidFakeTlsDomain(t)) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), t, TlsDomainRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setWorkersMode(const QString &mode) {
|
||||
setData(index(0), mode, WorkersModeRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setWorkers(const QString &workers) {
|
||||
setData(index(0), workers, WorkersRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setNatEnabled(bool enabled) {
|
||||
setData(index(0), enabled, NatEnabledRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setNatInternalIp(const QString &ip) {
|
||||
const QString t = ip.trimmed();
|
||||
if (!isValidOptionalIpv4(t)) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), t, NatInternalIpRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setNatExternalIp(const QString &ip) {
|
||||
const QString t = ip.trimmed();
|
||||
if (!isValidOptionalIpv4(t)) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), t, NatExternalIpRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::addAdditionalSecret() {
|
||||
QString newSecret;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
quint32 byte = QRandomGenerator::global()->bounded(256);
|
||||
newSecret += QString("%1").arg(byte, 2, 16, QChar('0'));
|
||||
}
|
||||
|
||||
m_protocolConfig.additionalSecrets.append(newSecret);
|
||||
emit dataChanged(index(0), index(0), QList<int>{AdditionalSecretsRole});
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::removeAdditionalSecret(int idx) {
|
||||
if (idx < 0 || idx >= m_protocolConfig.additionalSecrets.size()) {
|
||||
return;
|
||||
}
|
||||
m_protocolConfig.additionalSecrets.removeAt(idx);
|
||||
emit dataChanged(index(0), index(0), QList<int>{AdditionalSecretsRole});
|
||||
}
|
||||
|
||||
QVariantList MtProxyConfigModel::additionalSecretsList() const {
|
||||
QVariantList out;
|
||||
out.reserve(m_protocolConfig.additionalSecrets.size());
|
||||
for (const auto &s: m_protocolConfig.additionalSecrets) {
|
||||
if (!s.isEmpty()) {
|
||||
out.append(s);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setEnabled(bool enabled) {
|
||||
m_protocolConfig.isEnabled = enabled;
|
||||
emit dataChanged(index(0), index(0), QList<int>{IsEnabledRole});
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::generateQrCode(const QString &text) {
|
||||
if (text.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
auto qr = qrCodeUtils::generateQrCode(text.toUtf8());
|
||||
return qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::defaultTlsDomain() const {
|
||||
return protocols::mtProxy::defaultTlsDomain;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::defaultPort() const {
|
||||
return protocols::mtProxy::defaultPort;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::defaultWorkers() const {
|
||||
return protocols::mtProxy::defaultWorkers;
|
||||
}
|
||||
|
||||
int MtProxyConfigModel::maxWorkers() const {
|
||||
return protocols::mtProxy::maxWorkers;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::transportModeStandard() const {
|
||||
return protocols::mtProxy::transportModeStandard;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::transportModeFakeTLS() const {
|
||||
return protocols::mtProxy::transportModeFakeTLS;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::workersModeAuto() const {
|
||||
return protocols::mtProxy::workersModeAuto;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::workersModeManual() const {
|
||||
return protocols::mtProxy::workersModeManual;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isValidPublicHost(const QString &host) const {
|
||||
const QString t = host.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (t.length() > 253) {
|
||||
return false;
|
||||
}
|
||||
QHostAddress a(t);
|
||||
if (a.protocol() == QHostAddress::IPv4Protocol) {
|
||||
return NetworkUtilities::checkIPv4Format(t);
|
||||
}
|
||||
if (a.protocol() == QHostAddress::IPv6Protocol) {
|
||||
return true;
|
||||
}
|
||||
static const QRegularExpression onlyAsciiDigits(QStringLiteral(R"(^\d+$)"));
|
||||
if (onlyAsciiDigits.match(t).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
return NetworkUtilities::domainRegExp().exactMatch(t);
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isPublicHostInputAllowed(const QString &text) const {
|
||||
return mtproxyPublicHostInputAllowed(text);
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isPublicHostTypingIncomplete(const QString &text) const {
|
||||
const QString t = text.trimmed();
|
||||
if (isValidPublicHost(t)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static const QRegularExpression onlyDigitDot(QStringLiteral(R"(^[0-9.]+$)"));
|
||||
if (onlyDigitDot.match(t).hasMatch()) {
|
||||
if (t.endsWith(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
const QStringList parts = t.split(QLatin1Char('.'), Qt::KeepEmptyParts);
|
||||
if (parts.size() < 4) {
|
||||
return true;
|
||||
}
|
||||
for (const QString &part: parts) {
|
||||
if (part.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (t.contains(QLatin1Char(':'))) {
|
||||
if (t.contains(QLatin1String(":::"))) {
|
||||
return false;
|
||||
}
|
||||
if (t.endsWith(QLatin1Char(':'))) {
|
||||
return true;
|
||||
}
|
||||
QHostAddress a(t);
|
||||
if (a.protocol() == QHostAddress::IPv6Protocol) {
|
||||
return false;
|
||||
}
|
||||
if (!t.contains(QLatin1String("::")) && t.count(QLatin1Char(':')) < 7 && !t.contains(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!t.contains(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isValidMtProxyTag(const QString &tag) const {
|
||||
if (tag.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
static const QRegularExpression re(
|
||||
QStringLiteral("^([0-9a-fA-F]{%1})$").arg(protocols::mtProxy::botTagHexLength));
|
||||
return re.match(tag).hasMatch();
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isMtProxyTagTypingIncomplete(const QString &text) const {
|
||||
const QString t = text.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
static const QRegularExpression hexOnly(QStringLiteral(R"(^[0-9a-fA-F]*$)"));
|
||||
if (!hexOnly.match(t).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
return t.size() < protocols::mtProxy::botTagHexLength;
|
||||
}
|
||||
|
||||
int MtProxyConfigModel::mtProxyBotTagHexLength() const {
|
||||
return protocols::mtProxy::botTagHexLength;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isValidFakeTlsDomain(const QString &domain) const {
|
||||
const QString t = domain.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (t.length() > 253) {
|
||||
return false;
|
||||
}
|
||||
QHostAddress addr;
|
||||
if (addr.setAddress(t)) {
|
||||
return false;
|
||||
}
|
||||
static const QRegularExpression onlyAsciiDigits(QStringLiteral(R"(^\d+$)"));
|
||||
if (onlyAsciiDigits.match(t).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
QRegExp re(NetworkUtilities::domainRegExp());
|
||||
re.setCaseSensitivity(Qt::CaseInsensitive);
|
||||
if (!re.exactMatch(t)) {
|
||||
return false;
|
||||
}
|
||||
// ee + 32 hex (base secret) + hex(UTF-8 domain); keep headroom under typical client limits.
|
||||
if (t.toUtf8().size() > 111) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::clipboardText() const {
|
||||
if (QClipboard *c = QGuiApplication::clipboard()) {
|
||||
return c->text();
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizeFakeTlsDomainFieldText(const QString &input) const {
|
||||
const QString t = normalizeFakeTlsDomainInput(input);
|
||||
QString out;
|
||||
out.reserve(t.size());
|
||||
for (const QChar &c: t) {
|
||||
const ushort u = c.unicode();
|
||||
const bool letter = (u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z');
|
||||
const bool digit = (u >= '0' && u <= '9');
|
||||
if (letter || digit || u == '.' || u == '-') {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
if (out.size() > 253) {
|
||||
out.truncate(253);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isFakeTlsDomainInputAllowed(const QString &text) const {
|
||||
if (text.length() > 253) {
|
||||
return false;
|
||||
}
|
||||
static const QRegularExpression re(QStringLiteral(R"(^[a-zA-Z0-9.-]*$)"));
|
||||
return re.match(text).hasMatch();
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizePublicHostFieldText(const QString &input) const {
|
||||
QString out;
|
||||
const int cap = qMin(input.size(), 253);
|
||||
out.reserve(cap);
|
||||
for (const QChar &c: input) {
|
||||
if (out.size() >= 253) {
|
||||
break;
|
||||
}
|
||||
const ushort u = c.unicode();
|
||||
if ((u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9') || u == '.' || u == ':' ||
|
||||
u == '-') {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizePortFieldText(const QString &input) const {
|
||||
QString out;
|
||||
out.reserve(qMin(input.size(), 5));
|
||||
for (const QChar &c: input) {
|
||||
const ushort u = c.unicode();
|
||||
if (u >= '0' && u <= '9' && out.size() < 5) {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizeMtProxyTagFieldText(const QString &input) const {
|
||||
QString trimmed = input.trimmed();
|
||||
if (trimmed.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
|
||||
trimmed = trimmed.mid(2).trimmed();
|
||||
}
|
||||
// Prefer a contiguous 32-hex run (paste from bot message with extra text).
|
||||
static const QRegularExpression runHex(QStringLiteral(R"(([0-9a-fA-F]{32}))"));
|
||||
const QRegularExpressionMatch m = runHex.match(trimmed);
|
||||
if (m.hasMatch()) {
|
||||
return m.captured(1);
|
||||
}
|
||||
const int cap = protocols::mtProxy::botTagHexLength;
|
||||
QString out;
|
||||
out.reserve(qMin(trimmed.size(), cap));
|
||||
for (const QChar &c: trimmed) {
|
||||
if (out.size() >= cap) {
|
||||
break;
|
||||
}
|
||||
const ushort u = c.unicode();
|
||||
if ((u >= '0' && u <= '9') || (u >= 'a' && u <= 'f') || (u >= 'A' && u <= 'F')) {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizeWorkersFieldText(const QString &input) const {
|
||||
QString out;
|
||||
out.reserve(qMin(input.size(), 3));
|
||||
for (const QChar &c: input) {
|
||||
const ushort u = c.unicode();
|
||||
if (u >= '0' && u <= '9' && out.size() < 3) {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizeOptionalIpv4FieldText(const QString &input) const {
|
||||
QString out;
|
||||
out.reserve(qMin(input.size(), 15));
|
||||
for (const QChar &c: input) {
|
||||
if (out.size() >= 15) {
|
||||
break;
|
||||
}
|
||||
const ushort u = c.unicode();
|
||||
if ((u >= '0' && u <= '9') || u == '.') {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::normalizeFakeTlsDomainInput(const QString &input) const {
|
||||
QString t = input.trimmed();
|
||||
if (t.startsWith(QLatin1String("https://"), Qt::CaseInsensitive)) {
|
||||
t = t.mid(8);
|
||||
} else if (t.startsWith(QLatin1String("http://"), Qt::CaseInsensitive)) {
|
||||
t = t.mid(7);
|
||||
}
|
||||
if (const int slash = t.indexOf(QLatin1Char('/')); slash >= 0) {
|
||||
t = t.left(slash);
|
||||
}
|
||||
if (const int at = t.indexOf(QLatin1Char('@')); at >= 0) {
|
||||
t = t.mid(at + 1);
|
||||
}
|
||||
if (const int colon = t.indexOf(QLatin1Char(':')); colon >= 0) {
|
||||
t = t.left(colon);
|
||||
}
|
||||
if (t.startsWith(QLatin1String("www."), Qt::CaseInsensitive)) {
|
||||
const QString rest = t.mid(4);
|
||||
if (rest.contains(QLatin1Char('.'))) {
|
||||
t = rest;
|
||||
}
|
||||
}
|
||||
return t.trimmed();
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isFakeTlsDomainTypingIncomplete(const QString &text) const {
|
||||
const QString t = text.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (isValidFakeTlsDomain(t)) {
|
||||
return false;
|
||||
}
|
||||
if (t.contains(QLatin1Char('/')) || t.contains(QLatin1Char(':')) || t.contains(QLatin1Char('@'))
|
||||
|| t.contains(QLatin1Char(' '))) {
|
||||
return false;
|
||||
}
|
||||
if (t.contains(QLatin1String(".."))) {
|
||||
return false;
|
||||
}
|
||||
if (!t.contains(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
if (t.endsWith(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
static const QRegularExpression legalPartial(QStringLiteral(R"(^[a-zA-Z0-9.-]*$)"));
|
||||
if (!legalPartial.match(t).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isValidOptionalIpv4(const QString &ip) const {
|
||||
const QString t = ip.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return NetworkUtilities::checkIPv4Format(t);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> MtProxyConfigModel::roleNames() const {
|
||||
QHash<int, QByteArray> roles;
|
||||
|
||||
roles[PortRole] = "port";
|
||||
roles[SecretRole] = "secret";
|
||||
roles[TagRole] = "tag";
|
||||
roles[TgLinkRole] = "tgLink";
|
||||
roles[TmeLinkRole] = "tmeLink";
|
||||
roles[IsEnabledRole] = "isEnabled";
|
||||
roles[PublicHostRole] = "publicHost";
|
||||
roles[TransportModeRole] = "transportMode";
|
||||
roles[TlsDomainRole] = "tlsDomain";
|
||||
roles[AdditionalSecretsRole] = "additionalSecrets";
|
||||
roles[WorkersModeRole] = "workersMode";
|
||||
roles[WorkersRole] = "workers";
|
||||
roles[NatEnabledRole] = "natEnabled";
|
||||
roles[NatInternalIpRole] = "natInternalIp";
|
||||
roles[NatExternalIpRole] = "natExternalIp";
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
amnezia::MtProxyProtocolConfig MtProxyConfigModel::getProtocolConfig() {
|
||||
return m_protocolConfig;
|
||||
}
|
||||
156
client/ui/models/services/mtProxyConfigModel.h
Normal file
156
client/ui/models/services/mtProxyConfigModel.h
Normal file
@@ -0,0 +1,156 @@
|
||||
#ifndef MTPROXYCONFIGMODEL_H
|
||||
#define MTPROXYCONFIGMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QRandomGenerator>
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
|
||||
class MtProxyConfigModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
PortRole = Qt::UserRole + 1,
|
||||
SecretRole,
|
||||
TagRole,
|
||||
TgLinkRole,
|
||||
TmeLinkRole,
|
||||
IsEnabledRole,
|
||||
PublicHostRole,
|
||||
TransportModeRole,
|
||||
TlsDomainRole,
|
||||
AdditionalSecretsRole,
|
||||
WorkersModeRole,
|
||||
WorkersRole,
|
||||
NatEnabledRole,
|
||||
NatInternalIpRole,
|
||||
NatExternalIpRole
|
||||
};
|
||||
|
||||
explicit MtProxyConfigModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
|
||||
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
public slots:
|
||||
|
||||
void updateModel(amnezia::DockerContainer container, const amnezia::MtProxyProtocolConfig &protocolConfig);
|
||||
|
||||
void updateModel(const QJsonObject &config);
|
||||
|
||||
QJsonObject getConfig();
|
||||
|
||||
amnezia::MtProxyProtocolConfig getProtocolConfig();
|
||||
|
||||
Q_INVOKABLE void generateSecret();
|
||||
|
||||
Q_INVOKABLE void setSecret(const QString &secret);
|
||||
|
||||
Q_INVOKABLE bool validateAndSetSecret(const QString &rawSecret);
|
||||
|
||||
Q_INVOKABLE void setPort(const QString &port);
|
||||
|
||||
Q_INVOKABLE void setTag(const QString &tag);
|
||||
|
||||
Q_INVOKABLE void setPublicHost(const QString &host);
|
||||
|
||||
Q_INVOKABLE void setTransportMode(const QString &mode);
|
||||
|
||||
Q_INVOKABLE QString getTransportMode() const;
|
||||
|
||||
Q_INVOKABLE QString getTlsDomain() const;
|
||||
|
||||
Q_INVOKABLE QString getPublicHost() const;
|
||||
|
||||
Q_INVOKABLE void setTlsDomain(const QString &domain);
|
||||
|
||||
Q_INVOKABLE void setWorkersMode(const QString &mode);
|
||||
|
||||
Q_INVOKABLE void setWorkers(const QString &workers);
|
||||
|
||||
Q_INVOKABLE void setNatEnabled(bool enabled);
|
||||
|
||||
Q_INVOKABLE void setNatInternalIp(const QString &ip);
|
||||
|
||||
Q_INVOKABLE void setNatExternalIp(const QString &ip);
|
||||
|
||||
Q_INVOKABLE void addAdditionalSecret();
|
||||
|
||||
Q_INVOKABLE void removeAdditionalSecret(int idx);
|
||||
/// Current `mtproxy_additional_secrets` list from in-memory config (for QML snapshot vs. unsaved adds).
|
||||
Q_INVOKABLE QVariantList additionalSecretsList() const;
|
||||
|
||||
Q_INVOKABLE QString generateQrCode(const QString &text);
|
||||
|
||||
Q_INVOKABLE void setEnabled(bool enabled);
|
||||
|
||||
Q_INVOKABLE QString defaultTlsDomain() const;
|
||||
|
||||
Q_INVOKABLE QString defaultPort() const;
|
||||
|
||||
Q_INVOKABLE QString defaultWorkers() const;
|
||||
|
||||
Q_INVOKABLE int maxWorkers() const;
|
||||
|
||||
Q_INVOKABLE QString transportModeStandard() const;
|
||||
|
||||
Q_INVOKABLE QString transportModeFakeTLS() const;
|
||||
|
||||
Q_INVOKABLE QString workersModeAuto() const;
|
||||
|
||||
Q_INVOKABLE QString workersModeManual() const;
|
||||
|
||||
Q_INVOKABLE bool isValidPublicHost(const QString &host) const;
|
||||
|
||||
Q_INVOKABLE bool isPublicHostInputAllowed(const QString &text) const;
|
||||
|
||||
Q_INVOKABLE bool isPublicHostTypingIncomplete(const QString &text) const;
|
||||
|
||||
Q_INVOKABLE bool isValidMtProxyTag(const QString &tag) const;
|
||||
|
||||
Q_INVOKABLE bool isMtProxyTagTypingIncomplete(const QString &text) const;
|
||||
|
||||
Q_INVOKABLE int mtProxyBotTagHexLength() const;
|
||||
|
||||
Q_INVOKABLE bool isValidFakeTlsDomain(const QString &domain) const;
|
||||
|
||||
Q_INVOKABLE QString normalizeFakeTlsDomainInput(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE QString sanitizeFakeTlsDomainFieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE bool isFakeTlsDomainInputAllowed(const QString &text) const;
|
||||
|
||||
Q_INVOKABLE QString clipboardText() const;
|
||||
|
||||
Q_INVOKABLE QString sanitizePublicHostFieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE QString sanitizePortFieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE QString sanitizeMtProxyTagFieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE QString sanitizeWorkersFieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE QString sanitizeOptionalIpv4FieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE bool isFakeTlsDomainTypingIncomplete(const QString &text) const;
|
||||
|
||||
Q_INVOKABLE bool isValidOptionalIpv4(const QString &ip) const;
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
amnezia::DockerContainer m_container;
|
||||
QJsonObject m_fullConfig;
|
||||
amnezia::MtProxyProtocolConfig m_protocolConfig;
|
||||
};
|
||||
|
||||
#endif // MTPROXYCONFIGMODEL_H
|
||||
406
client/ui/models/services/telemtConfigModel.cpp
Normal file
406
client/ui/models/services/telemtConfigModel.cpp
Normal file
@@ -0,0 +1,406 @@
|
||||
#include "telemtConfigModel.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include "core/utils/qrCodeUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "qrcodegen.hpp"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
TelemtConfigModel::TelemtConfigModel(QObject *parent) : QAbstractListModel(parent) {}
|
||||
|
||||
void TelemtConfigModel::applyDefaults(TelemtProtocolConfig &c) {
|
||||
if (c.port.isEmpty()) {
|
||||
c.port = QString::fromUtf8(protocols::telemt::defaultPort);
|
||||
}
|
||||
if (c.transportMode.isEmpty()) {
|
||||
c.transportMode = QString::fromUtf8(protocols::telemt::transportModeStandard);
|
||||
}
|
||||
if (c.workersMode.isEmpty()) {
|
||||
c.workersMode = QString::fromUtf8(protocols::telemt::workersModeAuto);
|
||||
}
|
||||
if (c.workers.isEmpty()) {
|
||||
c.workers = QString::fromUtf8(protocols::telemt::defaultWorkers);
|
||||
}
|
||||
if (c.userName.isEmpty()) {
|
||||
c.userName = QString::fromUtf8(protocols::telemt::defaultUserName);
|
||||
}
|
||||
}
|
||||
|
||||
int TelemtConfigModel::rowCount(const QModelIndex &parent) const {
|
||||
Q_UNUSED(parent);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool TelemtConfigModel::setData(const QModelIndex &index, const QVariant &value, int role) {
|
||||
if (!index.isValid() || index.row() != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (role) {
|
||||
case Roles::PortRole: {
|
||||
m_protocolConfig.port = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::SecretRole: {
|
||||
m_protocolConfig.secret = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::TagRole: {
|
||||
m_protocolConfig.tag = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::IsEnabledRole: {
|
||||
m_protocolConfig.isEnabled = value.toBool();
|
||||
break;
|
||||
}
|
||||
case Roles::PublicHostRole: {
|
||||
m_protocolConfig.publicHost = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::TransportModeRole: {
|
||||
m_protocolConfig.transportMode = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::TlsDomainRole: {
|
||||
m_protocolConfig.tlsDomain = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::AdditionalSecretsRole: {
|
||||
m_protocolConfig.additionalSecrets = value.toStringList();
|
||||
break;
|
||||
}
|
||||
case Roles::WorkersModeRole: {
|
||||
m_protocolConfig.workersMode = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::WorkersRole: {
|
||||
m_protocolConfig.workers = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::NatEnabledRole: {
|
||||
m_protocolConfig.natEnabled = value.toBool();
|
||||
break;
|
||||
}
|
||||
case Roles::NatInternalIpRole: {
|
||||
m_protocolConfig.natInternalIp = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::NatExternalIpRole: {
|
||||
m_protocolConfig.natExternalIp = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::MaskEnabledRole: {
|
||||
m_protocolConfig.maskEnabled = value.toBool();
|
||||
break;
|
||||
}
|
||||
case Roles::UseMiddleProxyRole: {
|
||||
m_protocolConfig.useMiddleProxy = value.toBool();
|
||||
break;
|
||||
}
|
||||
case Roles::TlsEmulationRole: {
|
||||
m_protocolConfig.tlsEmulation = value.toBool();
|
||||
break;
|
||||
}
|
||||
case Roles::UserNameRole: {
|
||||
m_protocolConfig.userName = value.toString();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
emit dataChanged(index, index, QList{role});
|
||||
return true;
|
||||
}
|
||||
|
||||
QVariant TelemtConfigModel::data(const QModelIndex &index, int role) const {
|
||||
if (!index.isValid() || index.row() != 0) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
switch (role) {
|
||||
case Roles::PortRole: {
|
||||
return m_protocolConfig.port.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultPort)
|
||||
: m_protocolConfig.port;
|
||||
}
|
||||
case Roles::SecretRole: {
|
||||
return m_protocolConfig.secret;
|
||||
}
|
||||
case Roles::TagRole: {
|
||||
return m_protocolConfig.tag;
|
||||
}
|
||||
case Roles::TgLinkRole: {
|
||||
return m_protocolConfig.tgLink;
|
||||
}
|
||||
case Roles::TmeLinkRole: {
|
||||
return m_protocolConfig.tmeLink;
|
||||
}
|
||||
case Roles::IsEnabledRole: {
|
||||
return m_protocolConfig.isEnabled;
|
||||
}
|
||||
case Roles::PublicHostRole: {
|
||||
return m_protocolConfig.publicHost.isEmpty() ? m_fullConfig.value(QString(configKey::hostName)).toString()
|
||||
: m_protocolConfig.publicHost;
|
||||
}
|
||||
case Roles::TransportModeRole: {
|
||||
return m_protocolConfig.transportMode.isEmpty() ? QString::fromUtf8(
|
||||
protocols::telemt::transportModeStandard)
|
||||
: m_protocolConfig.transportMode;
|
||||
}
|
||||
case Roles::TlsDomainRole: {
|
||||
return m_protocolConfig.tlsDomain;
|
||||
}
|
||||
case Roles::AdditionalSecretsRole: {
|
||||
return m_protocolConfig.additionalSecrets;
|
||||
}
|
||||
case Roles::WorkersModeRole: {
|
||||
return m_protocolConfig.workersMode.isEmpty() ? QString::fromUtf8(protocols::telemt::workersModeAuto)
|
||||
: m_protocolConfig.workersMode;
|
||||
}
|
||||
case Roles::WorkersRole: {
|
||||
return m_protocolConfig.workers.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultWorkers)
|
||||
: m_protocolConfig.workers;
|
||||
}
|
||||
case Roles::NatEnabledRole: {
|
||||
return m_protocolConfig.natEnabled;
|
||||
}
|
||||
case Roles::NatInternalIpRole: {
|
||||
return m_protocolConfig.natInternalIp;
|
||||
}
|
||||
case Roles::NatExternalIpRole: {
|
||||
return m_protocolConfig.natExternalIp;
|
||||
}
|
||||
case Roles::MaskEnabledRole: {
|
||||
return m_protocolConfig.maskEnabled;
|
||||
}
|
||||
case Roles::UseMiddleProxyRole: {
|
||||
return m_protocolConfig.useMiddleProxy;
|
||||
}
|
||||
case Roles::TlsEmulationRole: {
|
||||
return m_protocolConfig.tlsEmulation;
|
||||
}
|
||||
case Roles::UserNameRole: {
|
||||
return m_protocolConfig.userName.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultUserName)
|
||||
: m_protocolConfig.userName;
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void TelemtConfigModel::updateModel(DockerContainer container, const TelemtProtocolConfig &protocolConfig) {
|
||||
beginResetModel();
|
||||
m_container = container;
|
||||
m_protocolConfig = protocolConfig;
|
||||
applyDefaults(m_protocolConfig);
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void TelemtConfigModel::updateModel(const QJsonObject &config) {
|
||||
beginResetModel();
|
||||
|
||||
m_fullConfig = config;
|
||||
m_protocolConfig = TelemtProtocolConfig::fromJson(config.value(QString(configKey::telemt)).toObject());
|
||||
applyDefaults(m_protocolConfig);
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QJsonObject TelemtConfigModel::getConfig() {
|
||||
m_fullConfig.insert(QString(configKey::telemt), m_protocolConfig.toJson());
|
||||
return m_fullConfig;
|
||||
}
|
||||
|
||||
TelemtProtocolConfig TelemtConfigModel::getProtocolConfig() {
|
||||
return m_protocolConfig;
|
||||
}
|
||||
|
||||
void TelemtConfigModel::generateSecret() {
|
||||
QString secret;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
quint32 byte = QRandomGenerator::global()->bounded(256);
|
||||
secret += QString("%1").arg(byte, 2, 16, QChar('0'));
|
||||
}
|
||||
|
||||
m_protocolConfig.secret = secret;
|
||||
emit dataChanged(index(0), index(0), QList<int>{SecretRole});
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setSecret(const QString &secret) {
|
||||
if (secret.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), secret, SecretRole);
|
||||
}
|
||||
|
||||
bool TelemtConfigModel::validateAndSetSecret(const QString &rawSecret) {
|
||||
if (!QRegularExpression(QStringLiteral("^[0-9a-fA-F]{32}$")).match(rawSecret).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
setData(index(0), rawSecret, SecretRole);
|
||||
return true;
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setPort(const QString &port) {
|
||||
setData(index(0), port, PortRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setTag(const QString &tag) {
|
||||
setData(index(0), tag, TagRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setPublicHost(const QString &host) {
|
||||
setData(index(0), host, PublicHostRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setTransportMode(const QString &mode) {
|
||||
setData(index(0), mode, TransportModeRole);
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::getTransportMode() const {
|
||||
return m_protocolConfig.transportMode.isEmpty() ? QString::fromUtf8(protocols::telemt::transportModeStandard)
|
||||
: m_protocolConfig.transportMode;
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::getTlsDomain() const {
|
||||
return m_protocolConfig.tlsDomain.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultTlsDomain)
|
||||
: m_protocolConfig.tlsDomain;
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::getPublicHost() const {
|
||||
return m_protocolConfig.publicHost;
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setTlsDomain(const QString &domain) {
|
||||
setData(index(0), domain, TlsDomainRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setWorkersMode(const QString &mode) {
|
||||
setData(index(0), mode, WorkersModeRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setWorkers(const QString &workers) {
|
||||
setData(index(0), workers, WorkersRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setNatEnabled(bool enabled) {
|
||||
setData(index(0), enabled, NatEnabledRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setNatInternalIp(const QString &ip) {
|
||||
setData(index(0), ip, NatInternalIpRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setNatExternalIp(const QString &ip) {
|
||||
setData(index(0), ip, NatExternalIpRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setMaskEnabled(bool enabled) {
|
||||
setData(index(0), enabled, MaskEnabledRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setUseMiddleProxy(bool enabled) {
|
||||
setData(index(0), enabled, UseMiddleProxyRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setTlsEmulation(bool enabled) {
|
||||
setData(index(0), enabled, TlsEmulationRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setUserName(const QString &name) {
|
||||
setData(index(0), name, UserNameRole);
|
||||
}
|
||||
|
||||
void TelemtConfigModel::addAdditionalSecret() {
|
||||
QString newSecret;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
quint32 byte = QRandomGenerator::global()->bounded(256);
|
||||
newSecret += QString("%1").arg(byte, 2, 16, QChar('0'));
|
||||
}
|
||||
|
||||
m_protocolConfig.additionalSecrets.append(newSecret);
|
||||
emit dataChanged(index(0), index(0), QList<int>{AdditionalSecretsRole});
|
||||
}
|
||||
|
||||
void TelemtConfigModel::removeAdditionalSecret(int idx) {
|
||||
if (idx < 0 || idx >= m_protocolConfig.additionalSecrets.size()) {
|
||||
return;
|
||||
}
|
||||
m_protocolConfig.additionalSecrets.removeAt(idx);
|
||||
emit dataChanged(index(0), index(0), QList<int>{AdditionalSecretsRole});
|
||||
}
|
||||
|
||||
void TelemtConfigModel::setEnabled(bool enabled) {
|
||||
m_protocolConfig.isEnabled = enabled;
|
||||
emit dataChanged(index(0), index(0), QList<int>{IsEnabledRole});
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::generateQrCode(const QString &text) {
|
||||
if (text.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
auto qr = qrCodeUtils::generateQrCode(text.toUtf8());
|
||||
return qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::defaultTlsDomain() const {
|
||||
return QString::fromUtf8(protocols::telemt::defaultTlsDomain);
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::defaultPort() const {
|
||||
return QString::fromUtf8(protocols::telemt::defaultPort);
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::defaultWorkers() const {
|
||||
return QString::fromUtf8(protocols::telemt::defaultWorkers);
|
||||
}
|
||||
|
||||
int TelemtConfigModel::maxWorkers() const {
|
||||
return protocols::telemt::maxWorkers;
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::transportModeStandard() const {
|
||||
return QString::fromUtf8(protocols::telemt::transportModeStandard);
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::transportModeFakeTLS() const {
|
||||
return QString::fromUtf8(protocols::telemt::transportModeFakeTLS);
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::workersModeAuto() const {
|
||||
return QString::fromUtf8(protocols::telemt::workersModeAuto);
|
||||
}
|
||||
|
||||
QString TelemtConfigModel::workersModeManual() const {
|
||||
return QString::fromUtf8(protocols::telemt::workersModeManual);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> TelemtConfigModel::roleNames() const {
|
||||
QHash<int, QByteArray> roles;
|
||||
|
||||
roles[PortRole] = "port";
|
||||
roles[SecretRole] = "secret";
|
||||
roles[TagRole] = "tag";
|
||||
roles[TgLinkRole] = "tgLink";
|
||||
roles[TmeLinkRole] = "tmeLink";
|
||||
roles[IsEnabledRole] = "isEnabled";
|
||||
roles[PublicHostRole] = "publicHost";
|
||||
roles[TransportModeRole] = "transportMode";
|
||||
roles[TlsDomainRole] = "tlsDomain";
|
||||
roles[AdditionalSecretsRole] = "additionalSecrets";
|
||||
roles[WorkersModeRole] = "workersMode";
|
||||
roles[WorkersRole] = "workers";
|
||||
roles[NatEnabledRole] = "natEnabled";
|
||||
roles[NatInternalIpRole] = "natInternalIp";
|
||||
roles[NatExternalIpRole] = "natExternalIp";
|
||||
roles[MaskEnabledRole] = "maskEnabled";
|
||||
roles[UseMiddleProxyRole] = "useMiddleProxy";
|
||||
roles[TlsEmulationRole] = "tlsEmulation";
|
||||
roles[UserNameRole] = "userName";
|
||||
|
||||
return roles;
|
||||
}
|
||||
130
client/ui/models/services/telemtConfigModel.h
Normal file
130
client/ui/models/services/telemtConfigModel.h
Normal file
@@ -0,0 +1,130 @@
|
||||
#ifndef TELEMTCONFIGMODEL_H
|
||||
#define TELEMTCONFIGMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonObject>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
|
||||
class TelemtConfigModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
PortRole = Qt::UserRole + 1,
|
||||
SecretRole,
|
||||
TagRole,
|
||||
TgLinkRole,
|
||||
TmeLinkRole,
|
||||
IsEnabledRole,
|
||||
PublicHostRole,
|
||||
TransportModeRole,
|
||||
TlsDomainRole,
|
||||
AdditionalSecretsRole,
|
||||
WorkersModeRole,
|
||||
WorkersRole,
|
||||
NatEnabledRole,
|
||||
NatInternalIpRole,
|
||||
NatExternalIpRole,
|
||||
MaskEnabledRole,
|
||||
UseMiddleProxyRole,
|
||||
TlsEmulationRole,
|
||||
UserNameRole
|
||||
};
|
||||
|
||||
explicit TelemtConfigModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
|
||||
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
public slots:
|
||||
|
||||
void updateModel(amnezia::DockerContainer container, const amnezia::TelemtProtocolConfig &protocolConfig);
|
||||
|
||||
void updateModel(const QJsonObject &config);
|
||||
|
||||
QJsonObject getConfig();
|
||||
|
||||
amnezia::TelemtProtocolConfig getProtocolConfig();
|
||||
|
||||
Q_INVOKABLE void generateSecret();
|
||||
|
||||
Q_INVOKABLE void setSecret(const QString &secret);
|
||||
|
||||
Q_INVOKABLE bool validateAndSetSecret(const QString &rawSecret);
|
||||
|
||||
Q_INVOKABLE void setPort(const QString &port);
|
||||
|
||||
Q_INVOKABLE void setTag(const QString &tag);
|
||||
|
||||
Q_INVOKABLE void setPublicHost(const QString &host);
|
||||
|
||||
Q_INVOKABLE void setTransportMode(const QString &mode);
|
||||
|
||||
Q_INVOKABLE QString getTransportMode() const;
|
||||
|
||||
Q_INVOKABLE QString getTlsDomain() const;
|
||||
|
||||
Q_INVOKABLE QString getPublicHost() const;
|
||||
|
||||
Q_INVOKABLE void setTlsDomain(const QString &domain);
|
||||
|
||||
Q_INVOKABLE void setWorkersMode(const QString &mode);
|
||||
|
||||
Q_INVOKABLE void setWorkers(const QString &workers);
|
||||
|
||||
Q_INVOKABLE void setNatEnabled(bool enabled);
|
||||
|
||||
Q_INVOKABLE void setNatInternalIp(const QString &ip);
|
||||
|
||||
Q_INVOKABLE void setNatExternalIp(const QString &ip);
|
||||
|
||||
Q_INVOKABLE void addAdditionalSecret();
|
||||
|
||||
Q_INVOKABLE void removeAdditionalSecret(int idx);
|
||||
|
||||
Q_INVOKABLE QString generateQrCode(const QString &text);
|
||||
|
||||
Q_INVOKABLE void setEnabled(bool enabled);
|
||||
|
||||
Q_INVOKABLE void setMaskEnabled(bool enabled);
|
||||
|
||||
Q_INVOKABLE void setUseMiddleProxy(bool enabled);
|
||||
|
||||
Q_INVOKABLE void setTlsEmulation(bool enabled);
|
||||
|
||||
Q_INVOKABLE void setUserName(const QString &name);
|
||||
|
||||
Q_INVOKABLE QString defaultTlsDomain() const;
|
||||
|
||||
Q_INVOKABLE QString defaultPort() const;
|
||||
|
||||
Q_INVOKABLE QString defaultWorkers() const;
|
||||
|
||||
Q_INVOKABLE int maxWorkers() const;
|
||||
|
||||
Q_INVOKABLE QString transportModeStandard() const;
|
||||
|
||||
Q_INVOKABLE QString transportModeFakeTLS() const;
|
||||
|
||||
Q_INVOKABLE QString workersModeAuto() const;
|
||||
|
||||
Q_INVOKABLE QString workersModeManual() const;
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
static void applyDefaults(amnezia::TelemtProtocolConfig &c);
|
||||
|
||||
amnezia::DockerContainer m_container = amnezia::DockerContainer::None;
|
||||
QJsonObject m_fullConfig;
|
||||
amnezia::TelemtProtocolConfig m_protocolConfig;
|
||||
};
|
||||
|
||||
#endif // TELEMTCONFIGMODEL_H
|
||||
127
client/ui/models/utils/mtproxy_public_host_input.cpp
Normal file
127
client/ui/models/utils/mtproxy_public_host_input.cpp
Normal file
@@ -0,0 +1,127 @@
|
||||
#include "mtproxy_public_host_input.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
namespace {
|
||||
|
||||
bool ipv4OctetTokenOk(const QString &s) {
|
||||
static const QRegularExpression re(QStringLiteral(R"(^\d{1,3}$)"));
|
||||
if (!re.match(s).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
bool ok = false;
|
||||
const int n = s.toInt(&ok);
|
||||
return ok && n >= 0 && n <= 255;
|
||||
}
|
||||
|
||||
// Reject labels like "312edweqwe" (digits >255 then letters).
|
||||
bool labelHasInvalidOctetLikePrefixBeforeLetters(const QString &label) {
|
||||
static const QRegularExpression re(QStringLiteral(R"(^(\d+)([a-zA-Z].*)$)"));
|
||||
const QRegularExpressionMatch m = re.match(label);
|
||||
if (!m.hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
const QString digits = m.captured(1);
|
||||
if (digits.length() > 3) {
|
||||
return true;
|
||||
}
|
||||
bool ok = false;
|
||||
const int n = digits.toInt(&ok);
|
||||
if (!ok) {
|
||||
return true;
|
||||
}
|
||||
if (n > 255) {
|
||||
return true;
|
||||
}
|
||||
// Do not restrict n≤255 + letters here (e.g. "123mlkjh.example.com"); four-segment IPv4+junk is handled below.
|
||||
return false;
|
||||
}
|
||||
|
||||
// "123.123wqqweqweqweqwe" — first label is a real octet, second looks like an octet glued to letters (not "123.45").
|
||||
bool looksLikeTwoSegmentOctetThenDigitLetterGlue(const QString &text) {
|
||||
const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts);
|
||||
if (parts.size() != 2) {
|
||||
return false;
|
||||
}
|
||||
if (!ipv4OctetTokenOk(parts.at(0))) {
|
||||
return false;
|
||||
}
|
||||
const QString &p1 = parts.at(1);
|
||||
static const QRegularExpression digitThenLetter(QStringLiteral(R"(^\d+[a-zA-Z])"));
|
||||
if (!digitThenLetter.match(p1).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
return !ipv4OctetTokenOk(p1);
|
||||
}
|
||||
|
||||
// "a.b.c.djunk" where first three parts are pure octets and last part has digits then letters (e.g. "123wdqweqweqwe").
|
||||
bool looksLikeFourOctetIpv4WithGarbageInLastSegment(const QString &text) {
|
||||
const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts);
|
||||
if (parts.size() != 4) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (!ipv4OctetTokenOk(parts.at(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
static const QRegularExpression digitThenLetter(QStringLiteral(R"(^\d+[a-zA-Z])"));
|
||||
return digitThenLetter.match(parts.at(3)).hasMatch();
|
||||
}
|
||||
|
||||
bool hostLabelsRejectBrokenDigitLetterMix(const QString &text) {
|
||||
if (looksLikeTwoSegmentOctetThenDigitLetterGlue(text)) {
|
||||
return false;
|
||||
}
|
||||
if (looksLikeFourOctetIpv4WithGarbageInLastSegment(text)) {
|
||||
return false;
|
||||
}
|
||||
const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts);
|
||||
for (const QString &part: parts) {
|
||||
if (labelHasInvalidOctetLikePrefixBeforeLetters(part)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool mtproxyPublicHostInputAllowed(const QString &text) {
|
||||
if (text.length() > 253) {
|
||||
return false;
|
||||
}
|
||||
static const QRegularExpression allowed(QStringLiteral(R"(^[a-zA-Z0-9.:\-]*$)"));
|
||||
if (!allowed.match(text).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
static const QRegularExpression onlyDigits(QStringLiteral(R"(^\d+$)"));
|
||||
if (onlyDigits.match(text).hasMatch() && text.length() > 3) {
|
||||
return false;
|
||||
}
|
||||
static const QRegularExpression onlyDigitDot(QStringLiteral(R"(^[0-9.]+$)"));
|
||||
if (!text.isEmpty() && onlyDigitDot.match(text).hasMatch()) {
|
||||
static const QRegularExpression ipv4Partial(QStringLiteral(R"(^(\d{1,3}\.){0,3}\d{0,3}$)"));
|
||||
return ipv4Partial.match(text).hasMatch();
|
||||
}
|
||||
if (text.contains(QLatin1Char(':'))) {
|
||||
static const QRegularExpression ipv6Chars(QStringLiteral(R"(^[0-9a-fA-F:.]*$)"));
|
||||
if (!ipv6Chars.match(text).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
if (text.size() > 45) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!hostLabelsRejectBrokenDigitLetterMix(text)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
PublicHostInputValidator::PublicHostInputValidator(QObject *parent) : QValidator(parent) {}
|
||||
|
||||
QValidator::State PublicHostInputValidator::validate(QString &input, int &pos) const {
|
||||
Q_UNUSED(pos)
|
||||
return mtproxyPublicHostInputAllowed(input) ? Acceptable : Invalid;
|
||||
}
|
||||
20
client/ui/models/utils/mtproxy_public_host_input.h
Normal file
20
client/ui/models/utils/mtproxy_public_host_input.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef MTPROXY_PUBLIC_HOST_INPUT_H
|
||||
#define MTPROXY_PUBLIC_HOST_INPUT_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <QValidator>
|
||||
|
||||
/// Shared rules for public host field (IPv4 dotted partial, IPv6 hex, FQDN ASCII).
|
||||
bool mtproxyPublicHostInputAllowed(const QString &text);
|
||||
|
||||
class PublicHostInputValidator : public QValidator {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PublicHostInputValidator(QObject *parent = nullptr);
|
||||
|
||||
QValidator::State validate(QString &input, int &pos) const override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -45,6 +45,12 @@ ListViewType {
|
||||
PageController.goToPage(PageEnum.PageProtocolRaw)
|
||||
} else if (isDns) {
|
||||
PageController.goToPage(PageEnum.PageServiceDnsSettings)
|
||||
} else if (isMtProxy) {
|
||||
MtProxyConfigModel.updateModel(config)
|
||||
PageController.goToPage(PageEnum.PageServiceMtProxySettings)
|
||||
} else if (isTelemt) {
|
||||
TelemtConfigModel.updateModel(config)
|
||||
PageController.goToPage(PageEnum.PageServiceTelemtSettings)
|
||||
} else {
|
||||
InstallController.updateProtocols(ServersUiController.getServerId(ServersUiController.processedServerIndex), containerIndex)
|
||||
PageController.goToPage(PageEnum.PageSettingsServerProtocol)
|
||||
|
||||
@@ -31,6 +31,9 @@ ListViewType {
|
||||
|
||||
function triggerCurrentItem() {
|
||||
var item = root.itemAtIndex(selectedIndex)
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
item.selectable.clicked()
|
||||
}
|
||||
|
||||
|
||||
61
client/ui/qml/Controls2/MinMaxRowType.qml
Normal file
61
client/ui/qml/Controls2/MinMaxRowType.qml
Normal file
@@ -0,0 +1,61 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Style 1.0
|
||||
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
|
||||
// MinMaxRowType — two side-by-side labeled text fields: Min / Max
|
||||
// Usage:
|
||||
// MinMaxRowType {
|
||||
// minValue: "0"
|
||||
// maxValue: "0"
|
||||
// onMinChanged: someProperty = val
|
||||
// onMaxChanged: someProperty = val
|
||||
// }
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string minValue: "0"
|
||||
property string maxValue: "0"
|
||||
|
||||
signal minChanged(string val)
|
||||
signal maxChanged(string val)
|
||||
|
||||
implicitHeight: row.implicitHeight
|
||||
implicitWidth: row.implicitWidth
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
// Min field
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
headerText: qsTr("Min")
|
||||
textField.text: root.minValue
|
||||
textField.validator: IntValidator { bottom: 0 }
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== root.minValue) {
|
||||
root.minChanged(textField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Max field
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
headerText: qsTr("Max")
|
||||
textField.text: root.maxValue
|
||||
textField.validator: IntValidator { bottom: 0 }
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== root.maxValue) {
|
||||
root.maxChanged(textField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ Item {
|
||||
id: root
|
||||
|
||||
property string headerText
|
||||
property string subtitleText // optional line under header (e.g. default value hint)
|
||||
property string headerTextDisabledColor: AmneziaStyle.color.charcoalGray
|
||||
property string headerTextColor: AmneziaStyle.color.mutedGray
|
||||
|
||||
@@ -84,6 +85,15 @@ Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SmallTextType {
|
||||
text: root.subtitleText
|
||||
visible: root.subtitleText !== ""
|
||||
color: AmneziaStyle.color.charcoalGray
|
||||
font.pixelSize: 13
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: visible ? 2 : 0
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: textField
|
||||
|
||||
|
||||
125
client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml
Normal file
125
client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml
Normal file
@@ -0,0 +1,125 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("Flow")
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("Empty")
|
||||
checked: flow === ""
|
||||
onClicked: flow = ""
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: "xtls-rprx-vision"
|
||||
checked: flow === "xtls-rprx-vision"
|
||||
onClicked: flow = "xtls-rprx-vision"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: "xtls-rprx-vision-udp443"
|
||||
checked: flow === "xtls-rprx-vision-udp443"
|
||||
onClicked: flow = "xtls-rprx-vision-udp443"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
|
||||
return
|
||||
}
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
||||
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
|
||||
}
|
||||
var noButtonFunction = function () {
|
||||
if (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
292
client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml
Normal file
292
client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml
Normal file
@@ -0,0 +1,292 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("Security")
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("None")
|
||||
checked: security === "none"
|
||||
onClicked: security = "none"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("TLS")
|
||||
checked: security === "tls"
|
||||
onClicked: security = "tls"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("Reality")
|
||||
checked: security === "reality"
|
||||
onClicked: security = "reality"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── TLS fields ────────────────────────────────────────────
|
||||
ColumnLayout {
|
||||
visible: security === "tls"
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
DropDownType {
|
||||
id: tlsAlpnDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: alpn
|
||||
descriptionText: qsTr("ALPN")
|
||||
headerText: qsTr("ALPN")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.alpnOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
alpn = selectedText
|
||||
tlsAlpnDropDown.text = selectedText
|
||||
tlsAlpnDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === alpn) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
tlsAlpnDropDown.text = alpn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: tlsFingerprintDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: fingerprint
|
||||
descriptionText: qsTr("Fingerprint")
|
||||
headerText: qsTr("Fingerprint")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.fingerprintOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
fingerprint = selectedText
|
||||
tlsFingerprintDropDown.text = selectedText
|
||||
tlsFingerprintDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === fingerprint) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
tlsFingerprintDropDown.text = fingerprint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Server Name (SNI)")
|
||||
textField.text: sni
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== sni) sni = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reality fields ────────────────────────────────────────
|
||||
ColumnLayout {
|
||||
visible: security === "reality"
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
DropDownType {
|
||||
id: realityFingerprintDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: fingerprint
|
||||
descriptionText: qsTr("Fingerprint")
|
||||
headerText: qsTr("Fingerprint")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.fingerprintOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
fingerprint = selectedText
|
||||
realityFingerprintDropDown.text = selectedText
|
||||
realityFingerprintDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === fingerprint) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
realityFingerprintDropDown.text = fingerprint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Server Name (SNI)")
|
||||
textField.text: sni
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== sni) sni = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
|
||||
return
|
||||
}
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
||||
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
|
||||
}
|
||||
var noButtonFunction = function () {
|
||||
if (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,20 @@ import "../Components"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
function formatTransport(value) {
|
||||
if (value === "raw") return "RAW (TCP)"
|
||||
if (value === "xhttp") return "XHTTP"
|
||||
if (value === "mkcp") return "mKCP"
|
||||
return value
|
||||
}
|
||||
|
||||
function formatSecurity(value) {
|
||||
if (value === "none") return "None"
|
||||
if (value === "tls") return "TLS"
|
||||
if (value === "reality") return "Reality"
|
||||
return value
|
||||
}
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
@@ -50,88 +64,125 @@ PageType {
|
||||
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
headerText: qsTr("XRay settings")
|
||||
Layout.topMargin: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
headerText: qsTr("XRay VLESS settings")
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignRight
|
||||
implicitWidth: 40
|
||||
implicitHeight: 40
|
||||
image: "qrc:/images/controls/more-vertical.svg"
|
||||
imageColor: AmneziaStyle.color.paleGray
|
||||
onClicked: PageController.goToPage(PageEnum.PageProtocolXraySnapshots)
|
||||
}
|
||||
}
|
||||
|
||||
LabelTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 4
|
||||
text: qsTr("More about settings")
|
||||
color: AmneziaStyle.color.goldenApricot
|
||||
font.pixelSize: 16
|
||||
lineHeight: 24 + LanguageUiController.getLineHeightAppend()
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: Qt.openUrlExternally("https://docs.amnezia.org")
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: textFieldWithHeaderType
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 32
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
enabled: listView.enabled
|
||||
|
||||
headerText: qsTr("Disguised as traffic from")
|
||||
textField.text: site
|
||||
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== site) {
|
||||
var tmpText = textField.text
|
||||
tmpText = tmpText.toLocaleLowerCase()
|
||||
|
||||
if (tmpText.startsWith("https://")) {
|
||||
tmpText = textField.text.substring(8)
|
||||
site = tmpText
|
||||
} else {
|
||||
site = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkEmptyText: true
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: portTextField
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
enabled: listView.enabled
|
||||
|
||||
headerText: qsTr("Port")
|
||||
textField.text: port
|
||||
textField.maximumLength: 5
|
||||
textField.validator: IntValidator { bottom: 1; top: 65535 }
|
||||
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== port) {
|
||||
port = textField.text
|
||||
}
|
||||
textField.validator: IntValidator {
|
||||
bottom: 1; top: 65535
|
||||
}
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== port) port = textField.text
|
||||
}
|
||||
|
||||
checkEmptyText: true
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
text: qsTr("Transport")
|
||||
descriptionText: root.formatTransport(transport)
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
enabled: listView.enabled
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayTransportSettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Security")
|
||||
descriptionText: root.formatSecurity(security)
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
enabled: listView.enabled
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageProtocolXraySecuritySettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Flow")
|
||||
descriptionText: flow
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
enabled: listView.enabled
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayFlowSettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true; Layout.preferredHeight: 24
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
enabled: portTextField.errorText === ""
|
||||
|
||||
// Show Save immediately while user edits port, even before focus loss.
|
||||
visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || textFieldWithHeaderType.textField.text !== port)
|
||||
enabled: visible && textFieldWithHeaderType.errorText === ""
|
||||
text: qsTr("Save")
|
||||
|
||||
onClicked: function() {
|
||||
forceActiveFocus()
|
||||
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
|
||||
var yesButtonFunction = function() {
|
||||
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
|
||||
@@ -142,16 +193,32 @@ PageType {
|
||||
InstallController.updateContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
|
||||
}
|
||||
var noButtonFunction = function() {
|
||||
if (!GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
if (!GC.isMobile()) saveButton.forceActiveFocus()
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: saveButton.clicked()
|
||||
Keys.onReturnPressed: saveButton.clicked()
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Reset settings")
|
||||
textColor: AmneziaStyle.color.vibrantRed
|
||||
visible: listView.enabled
|
||||
clickedFunction: function() {
|
||||
var yesButtonFunction = function() {
|
||||
XrayConfigModel.resetToDefaults()
|
||||
}
|
||||
showQuestionDrawer(qsTr("Reset settings?"), qsTr("All XRay settings will be restored to defaults."),
|
||||
qsTr("Reset"), qsTr("Cancel"), yesButtonFunction, function() {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true; Layout.preferredHeight: 32
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
291
client/ui/qml/Pages2/PageProtocolXraySnapshots.qml
Normal file
291
client/ui/qml/Pages2/PageProtocolXraySnapshots.qml
Normal file
@@ -0,0 +1,291 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
import Qt.labs.platform 1.1
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property string selectedConfigName: ""
|
||||
property int selectedConfigIndex: -1
|
||||
|
||||
// Reload the list every time we open this page
|
||||
Component.onCompleted: XrayConfigSnapshotsModel.reload()
|
||||
|
||||
// ── Save xray config snapshot to file ────────────────────────────
|
||||
function saveConfigToFile(json) {
|
||||
var fileName = ""
|
||||
if (GC.isMobile()) {
|
||||
fileName = "amnezia_xray_config.json"
|
||||
} else {
|
||||
fileName = SystemController.getFileName(
|
||||
qsTr("Save XRay configuration"),
|
||||
qsTr("JSON files (*.json)"),
|
||||
StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/amnezia_xray_config",
|
||||
true,
|
||||
".json")
|
||||
}
|
||||
if (fileName !== "") {
|
||||
PageController.showBusyIndicator(true)
|
||||
ExportController.setConfigFromString(json, fileName)
|
||||
PageController.showBusyIndicator(false)
|
||||
PageController.showNotificationMessage(qsTr("Configuration saved"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
model: XrayConfigSnapshotsModel
|
||||
|
||||
header: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("XRay Configurations")
|
||||
}
|
||||
|
||||
// ── Create from current settings ──────────────────────────
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Create configuration based on current settings")
|
||||
textMaximumLineCount: 2
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
XrayConfigSnapshotsModel.createFromCurrentModel()
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Export ────────────────────────────────────────────────
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Export settings")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
var idx = root.selectedConfigIndex >= 0 ? root.selectedConfigIndex : 0
|
||||
if (listView.count > 0) {
|
||||
var json = XrayConfigSnapshotsModel.exportToJson(idx)
|
||||
saveConfigToFile(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Import ────────────────────────────────────────────────
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Import settings")
|
||||
descriptionText: qsTr("In JSON format")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
var filePath = SystemController.getFileName(
|
||||
qsTr("Open XRay configuration"),
|
||||
qsTr("JSON files (*.json)"))
|
||||
if (filePath !== "") {
|
||||
var jsonContent = ImportController.readTextFile(filePath)
|
||||
if (jsonContent !== "") {
|
||||
if (!XrayConfigSnapshotsModel.importFromJson(jsonContent)) {
|
||||
PageController.showNotificationMessage(qsTr("Failed to import configuration"))
|
||||
} else {
|
||||
PageController.showNotificationMessage(qsTr("Configuration imported successfully"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Section label ─────────────────────────────────────────
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("Configurations")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
visible: listView.count > 0
|
||||
}
|
||||
}
|
||||
|
||||
// ── Empty state ───────────────────────────────────────────────
|
||||
footer: ColumnLayout {
|
||||
width: listView.width
|
||||
visible: listView.count === 0
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 32
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("No saved configurations yet.\nCreate one from the current settings.")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
// ── Config list items ─────────────────────────────────────────
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: configName
|
||||
descriptionText: configDate
|
||||
rightImageSource: "qrc:/images/controls/more-vertical.svg"
|
||||
clickedFunction: function () {
|
||||
root.selectedConfigName = configName
|
||||
root.selectedConfigIndex = index
|
||||
configActionsDrawer.openTriggered()
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Import result handler ─────────────────────────────────────────
|
||||
Connections {
|
||||
target: XrayConfigSnapshotsModel
|
||||
|
||||
function onImportFailed(errorMessage) {
|
||||
PageController.showNotificationMessage(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Per-config actions drawer ─────────────────────────────────────
|
||||
DrawerType2 {
|
||||
id: configActionsDrawer
|
||||
parent: root
|
||||
anchors.fill: parent
|
||||
expandedHeight: root.height * 0.35
|
||||
|
||||
expandedStateContent: ColumnLayout {
|
||||
id: drawerContent
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: 0
|
||||
|
||||
onImplicitHeightChanged: {
|
||||
configActionsDrawer.expandedHeight = drawerContent.implicitHeight + 32
|
||||
}
|
||||
|
||||
BackButtonType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
backButtonFunction: function () {
|
||||
configActionsDrawer.closeTriggered()
|
||||
}
|
||||
}
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
Layout.bottomMargin: 16
|
||||
headerText: root.selectedConfigName
|
||||
}
|
||||
|
||||
// Apply
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Apply configuration")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
configActionsDrawer.closeTriggered()
|
||||
XrayConfigSnapshotsModel.applyConfigToCurrentModel(root.selectedConfigIndex)
|
||||
PageController.closePage()
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// Export this config
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Export configuration")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
configActionsDrawer.closeTriggered()
|
||||
var json = XrayConfigSnapshotsModel.exportToJson(root.selectedConfigIndex)
|
||||
saveConfigToFile(json)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// Delete
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Delete configuration")
|
||||
textColor: AmneziaStyle.color.vibrantRed
|
||||
clickedFunction: function () {
|
||||
configActionsDrawer.closeTriggered()
|
||||
var yesButtonFunction = function () {
|
||||
XrayConfigSnapshotsModel.removeConfig(root.selectedConfigIndex)
|
||||
root.selectedConfigIndex = -1
|
||||
root.selectedConfigName = ""
|
||||
}
|
||||
showQuestionDrawer(
|
||||
qsTr("Delete configuration?"),
|
||||
qsTr("This action cannot be undone."),
|
||||
qsTr("Delete"), qsTr("Cancel"),
|
||||
yesButtonFunction, function () {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
755
client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml
Normal file
755
client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml
Normal file
@@ -0,0 +1,755 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("Transport")
|
||||
}
|
||||
|
||||
// ── Radio buttons ─────────────────────────────────────────
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("RAW (TCP)")
|
||||
checked: transport === "raw"
|
||||
onToggled: if (checked && transport !== "raw") transport = "raw"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("XHTTP")
|
||||
descriptionText: qsTr("Advanced users")
|
||||
checked: transport === "xhttp"
|
||||
onToggled: if (checked && transport !== "xhttp") transport = "xhttp"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("mKCP")
|
||||
checked: transport === "mkcp"
|
||||
onToggled: if (checked && transport !== "mkcp") transport = "mkcp"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// mKCP Settings
|
||||
// ══════════════════════════════════════════════════════════
|
||||
ColumnLayout {
|
||||
visible: transport === "mkcp"
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("mKCP Settings")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("TTI")
|
||||
subtitleText: qsTr("Default: %1 ms", "mKCP TTI").arg(XrayConfigModel.mkcpDefaultTti())
|
||||
textField.text: mkcpTti
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpTti) mkcpTti = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("uplinkCapacity")
|
||||
subtitleText: qsTr("Default: %1 Mbit/s", "mKCP uplink").arg(XrayConfigModel.mkcpDefaultUplinkCapacity())
|
||||
textField.text: mkcpUplinkCapacity
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpUplinkCapacity) mkcpUplinkCapacity = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("downlinkCapacity")
|
||||
subtitleText: qsTr("Default: %1 Mbit/s", "mKCP downlink").arg(XrayConfigModel.mkcpDefaultDownlinkCapacity())
|
||||
textField.text: mkcpDownlinkCapacity
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpDownlinkCapacity) mkcpDownlinkCapacity = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("readBufferSize")
|
||||
subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultReadBufferSize())
|
||||
textField.text: mkcpReadBufferSize
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpReadBufferSize) mkcpReadBufferSize = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("writeBufferSize")
|
||||
subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize())
|
||||
textField.text: mkcpWriteBufferSize
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpWriteBufferSize) mkcpWriteBufferSize = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
Layout.topMargin: 8
|
||||
text: qsTr("Congestion")
|
||||
checked: mkcpCongestion
|
||||
onToggled: mkcpCongestion = checked
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// XHTTP Settings
|
||||
// ══════════════════════════════════════════════════════════
|
||||
ColumnLayout {
|
||||
visible: transport === "xhttp"
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
DropDownType {
|
||||
id: modeDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpMode
|
||||
descriptionText: qsTr("Mode")
|
||||
headerText: qsTr("Mode")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpModeOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpMode = selectedText
|
||||
modeDropDown.text = selectedText
|
||||
modeDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpMode) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
modeDropDown.text = xhttpMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("HTTP Profile")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Host")
|
||||
textField.text: xhttpHost
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpHost) xhttpHost = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Path")
|
||||
textField.text: xhttpPath
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpPath) xhttpPath = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: headersDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpHeadersTemplate
|
||||
descriptionText: qsTr("Headers template")
|
||||
headerText: qsTr("Headers template")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpHeadersTemplateOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpHeadersTemplate = selectedText
|
||||
headersDropDown.text = selectedText
|
||||
headersDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpHeadersTemplate) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
headersDropDown.text = xhttpHeadersTemplate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: uplinkMethodDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpUplinkMethod
|
||||
descriptionText: qsTr("UplinkHTTPMethod")
|
||||
headerText: qsTr("UplinkHTTPMethod")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpUplinkMethodOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpUplinkMethod = selectedText
|
||||
uplinkMethodDropDown.text = selectedText
|
||||
uplinkMethodDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpUplinkMethod) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
uplinkMethodDropDown.text = xhttpUplinkMethod
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
Layout.topMargin: 16
|
||||
text: qsTr("Disable gRPC Header")
|
||||
descriptionText: qsTr("noGRPCHeader")
|
||||
checked: xhttpDisableGrpc
|
||||
onToggled: xhttpDisableGrpc = checked
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
text: qsTr("Disable SSE Header")
|
||||
descriptionText: qsTr("noSSEHeader")
|
||||
checked: xhttpDisableSse
|
||||
onToggled: xhttpDisableSse = checked
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Session & Sequence ────────────────────────────────
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("Session & Sequence")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: sessionPlacementDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpSessionPlacement
|
||||
descriptionText: qsTr("SessionPlacement")
|
||||
headerText: qsTr("SessionPlacement")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpSessionPlacementOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpSessionPlacement = selectedText
|
||||
sessionPlacementDropDown.text = selectedText
|
||||
sessionPlacementDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpSessionPlacement) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
sessionPlacementDropDown.text = xhttpSessionPlacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: sessionKeyDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpSessionKey
|
||||
descriptionText: qsTr("SessionKey")
|
||||
headerText: qsTr("SessionKey")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpSessionKeyOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpSessionKey = selectedText
|
||||
sessionKeyDropDown.text = selectedText
|
||||
sessionKeyDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpSessionKey) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
sessionKeyDropDown.text = xhttpSessionKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: seqPlacementDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpSeqPlacement
|
||||
descriptionText: qsTr("SeqPlacement")
|
||||
headerText: qsTr("SeqPlacement")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpSeqPlacementOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpSeqPlacement = selectedText
|
||||
seqPlacementDropDown.text = selectedText
|
||||
seqPlacementDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpSeqPlacement) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
seqPlacementDropDown.text = xhttpSeqPlacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("SeqKey")
|
||||
textField.text: xhttpSeqKey
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpSeqKey) xhttpSeqKey = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: uplinkDataPlacementDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpUplinkDataPlacement
|
||||
descriptionText: qsTr("UplinkDataPlacement")
|
||||
headerText: qsTr("UplinkDataPlacement")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpUplinkDataPlacementOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpUplinkDataPlacement = selectedText
|
||||
uplinkDataPlacementDropDown.text = selectedText
|
||||
uplinkDataPlacementDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpUplinkDataPlacement) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
uplinkDataPlacementDropDown.text = xhttpUplinkDataPlacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("UplinkDataKey")
|
||||
textField.text: xhttpUplinkDataKey
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpUplinkDataKey) xhttpUplinkDataKey = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
// ── Traffic Shaping ───────────────────────────────────
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("Traffic Shaping")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("UplinkChunkSize")
|
||||
textField.text: xhttpUplinkChunkSize
|
||||
textField.validator: IntValidator {
|
||||
bottom: 0
|
||||
}
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpUplinkChunkSize) xhttpUplinkChunkSize = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("scMaxBufferedPosts")
|
||||
textField.text: xhttpScMaxBufferedPosts
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpScMaxBufferedPosts) xhttpScMaxBufferedPosts = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("scMaxEachPostBytes")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xhttpScMaxEachPostBytesMin
|
||||
maxValue: xhttpScMaxEachPostBytesMax
|
||||
onMinChanged: xhttpScMaxEachPostBytesMin = val
|
||||
onMaxChanged: xhttpScMaxEachPostBytesMax = val
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("scStreamUpServerSecs")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xhttpScStreamUpServerSecsMin
|
||||
maxValue: xhttpScStreamUpServerSecsMax
|
||||
onMinChanged: xhttpScStreamUpServerSecsMin = val
|
||||
onMaxChanged: xhttpScStreamUpServerSecsMax = val
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("scMinPostsIntervalMs")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xhttpScMinPostsIntervalMsMin
|
||||
maxValue: xhttpScMinPostsIntervalMsMax
|
||||
onMinChanged: xhttpScMinPostsIntervalMsMin = val
|
||||
onMaxChanged: xhttpScMinPostsIntervalMsMax = val
|
||||
}
|
||||
|
||||
// ── Padding and multiplexing ──────────────────────────
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("Padding and multiplexing")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("xPadding")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayXPaddingSettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("XMux")
|
||||
descriptionText: xmuxEnabled ? qsTr("On") : qsTr("Off")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayXmuxSettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
|
||||
return
|
||||
}
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
||||
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
|
||||
}
|
||||
var noButtonFunction = function () {
|
||||
if (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
108
client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml
Normal file
108
client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml
Normal file
@@ -0,0 +1,108 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("xPaddingBytes")
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("Range")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xPaddingBytesMin
|
||||
maxValue: xPaddingBytesMax
|
||||
onMinChanged: xPaddingBytesMin = val
|
||||
onMaxChanged: xPaddingBytesMax = val
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
|
||||
return
|
||||
}
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
||||
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
|
||||
}
|
||||
var noButtonFunction = function () {
|
||||
if (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
224
client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml
Normal file
224
client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml
Normal file
@@ -0,0 +1,224 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("xPadding")
|
||||
}
|
||||
|
||||
// xPaddingBytes — min/max display row
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("xPaddingBytes")
|
||||
descriptionText: (xPaddingBytesMin !== "" ? xPaddingBytesMin : "0") + "—" + (xPaddingBytesMax !== "" ? xPaddingBytesMax : "0")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayXPaddingBytesSettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
text: qsTr("xPaddingObfsMode")
|
||||
checked: xPaddingObfsMode
|
||||
onToggled: xPaddingObfsMode = checked
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
headerText: qsTr("xPaddingKey")
|
||||
textField.text: xPaddingKey
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xPaddingKey) xPaddingKey = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("xPaddingHeader")
|
||||
textField.text: xPaddingHeader
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xPaddingHeader) xPaddingHeader = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: placementDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xPaddingPlacement
|
||||
descriptionText: qsTr("xPaddingPlacement")
|
||||
headerText: qsTr("xPaddingPlacement")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xPaddingPlacementOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xPaddingPlacement = selectedText
|
||||
placementDropDown.text = selectedText
|
||||
placementDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xPaddingPlacement) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
placementDropDown.text = xPaddingPlacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: methodDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xPaddingMethod
|
||||
descriptionText: qsTr("xPaddingMethod")
|
||||
headerText: qsTr("xPaddingMethod")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xPaddingMethodOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xPaddingMethod = selectedText
|
||||
methodDropDown.text = selectedText
|
||||
methodDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xPaddingMethod) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
methodDropDown.text = xPaddingMethod
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
|
||||
return
|
||||
}
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
||||
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
|
||||
}
|
||||
var noButtonFunction = function () {
|
||||
if (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
222
client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml
Normal file
222
client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml
Normal file
@@ -0,0 +1,222 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("xmux")
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
text: qsTr("xmux")
|
||||
checked: xmuxEnabled
|
||||
onToggled: xmuxEnabled = checked
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
enabled: xmuxEnabled
|
||||
|
||||
// maxConcurrency
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("maxConcurrency")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxMaxConcurrencyMin
|
||||
maxValue: xmuxMaxConcurrencyMax
|
||||
onMinChanged: xmuxMaxConcurrencyMin = val
|
||||
onMaxChanged: xmuxMaxConcurrencyMax = val
|
||||
}
|
||||
|
||||
// maxConnections
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("maxConnections")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxMaxConnectionsMin
|
||||
maxValue: xmuxMaxConnectionsMax
|
||||
onMinChanged: xmuxMaxConnectionsMin = val
|
||||
onMaxChanged: xmuxMaxConnectionsMax = val
|
||||
}
|
||||
|
||||
// cMaxReuseTimes
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("cMaxReuseTimes")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxCMaxReuseTimesMin
|
||||
maxValue: xmuxCMaxReuseTimesMax
|
||||
onMinChanged: xmuxCMaxReuseTimesMin = val
|
||||
onMaxChanged: xmuxCMaxReuseTimesMax = val
|
||||
}
|
||||
|
||||
// hMaxRequestTimes
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("hMaxRequestTimes")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxHMaxRequestTimesMin
|
||||
maxValue: xmuxHMaxRequestTimesMax
|
||||
onMinChanged: xmuxHMaxRequestTimesMin = val
|
||||
onMaxChanged: xmuxHMaxRequestTimesMax = val
|
||||
}
|
||||
|
||||
// hMaxReusableSecs
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("hMaxReusableSecs")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxHMaxReusableSecsMin
|
||||
maxValue: xmuxHMaxReusableSecsMax
|
||||
onMinChanged: xmuxHMaxReusableSecsMin = val
|
||||
onMaxChanged: xmuxHMaxReusableSecsMax = val
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
headerText: qsTr("hKeepAlivePeriod")
|
||||
textField.text: xmuxHKeepAlivePeriod
|
||||
textField.validator: IntValidator {
|
||||
bottom: 0
|
||||
}
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xmuxHKeepAlivePeriod) xmuxHKeepAlivePeriod = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
var yesButtonFunction = function () {
|
||||
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
|
||||
return
|
||||
}
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
||||
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
|
||||
}
|
||||
var noButtonFunction = function () {
|
||||
if (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1885
client/ui/qml/Pages2/PageServiceMtProxySettings.qml
Normal file
1885
client/ui/qml/Pages2/PageServiceMtProxySettings.qml
Normal file
File diff suppressed because it is too large
Load Diff
1447
client/ui/qml/Pages2/PageServiceTelemtSettings.qml
Normal file
1447
client/ui/qml/Pages2/PageServiceTelemtSettings.qml
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user