mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-28 13:56:16 +03:00
Compare commits
28 Commits
fix/androi
...
server_scr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
201e4063ed | ||
|
|
211bf51f1d | ||
|
|
7e0c35ba29 | ||
|
|
bcee58b08b | ||
|
|
52de1acebf | ||
|
|
027a12a1df | ||
|
|
0a659a2d74 | ||
|
|
6f119cd083 | ||
|
|
1753aed3fc | ||
|
|
c714d98bd1 | ||
|
|
4787f3915b | ||
|
|
7a383116b2 | ||
|
|
d3de5f0f48 | ||
|
|
8749d683e3 | ||
|
|
9de9d082bc | ||
|
|
a4233fef41 | ||
|
|
4890dd1d74 | ||
|
|
564630827e | ||
|
|
fbe15d965b | ||
|
|
b29515c380 | ||
|
|
0658a8f565 | ||
|
|
482ec04b4a | ||
|
|
d40d24fcf9 | ||
|
|
fb5666057b | ||
|
|
a49892c7e7 | ||
|
|
277b295fd8 | ||
|
|
8c33779fc3 | ||
|
|
f0299ca9fe |
76
.github/workflows/deploy.yml
vendored
76
.github/workflows/deploy.yml
vendored
@@ -18,11 +18,11 @@ jobs:
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
base: ${{ github.event.before }}
|
||||
filters: |
|
||||
recipes:
|
||||
- 'recipes/**'
|
||||
- 'conanfile.py'
|
||||
- '.github/workflows/deploy.yml'
|
||||
|
||||
Bake-Prebuilts-Linux:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -40,7 +40,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 +50,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 +100,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 +120,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 +151,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 +233,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 +246,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 +287,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 +354,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 +386,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 +404,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 +431,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 +517,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,20 +533,24 @@ 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
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
Bake-Prebuilts-MacOS-NE:
|
||||
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
|
||||
@@ -548,15 +567,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 +656,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 +692,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 +717,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 +735,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 +829,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:
|
||||
@@ -894,3 +917,4 @@ jobs:
|
||||
run: |
|
||||
echo "Pull request:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[[#${{ fromJSON(steps.pull_request.outputs.data)[0].number }}] ${{ fromJSON(steps.pull_request.outputs.data)[0].title }}](${{ fromJSON(steps.pull_request.outputs.data)[0].html_url }})" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
|
||||
@@ -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.9.0.1)
|
||||
|
||||
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")
|
||||
|
||||
@@ -193,10 +193,6 @@ elseif(APPLE)
|
||||
include(cmake/macos.cmake)
|
||||
endif()
|
||||
|
||||
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
list(APPEND SOURCES ${CMAKE_CURRENT_LIST_DIR}/main.cpp)
|
||||
|
||||
target_link_libraries(${PROJECT} PRIVATE ${LIBS})
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QThread>
|
||||
#include <QUuid>
|
||||
#include "logger.h"
|
||||
|
||||
@@ -20,183 +21,642 @@
|
||||
#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;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::uploadServerConfigJson(const ServerCredentials &credentials, DockerContainer container,
|
||||
const DnsSettings &dnsSettings, const QJsonObject &serverConfig) const
|
||||
{
|
||||
const QString updatedConfig = QJsonDocument(serverConfig).toJson();
|
||||
ErrorCode errorCode = m_sshSession->uploadTextFileToContainer(
|
||||
container, credentials, updatedConfig, amnezia::protocols::xray::serverConfigPath,
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to upload updated config";
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
const QString restartScript = QStringLiteral("sudo docker restart $CONTAINER_NAME");
|
||||
errorCode = m_sshSession->runScript(
|
||||
credentials,
|
||||
m_sshSession->replaceVars(restartScript,
|
||||
amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns,
|
||||
dnsSettings.secondaryDns)));
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to restart container";
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::readRealityKeyFiles(const DockerContainer container, const ServerCredentials &credentials,
|
||||
QString &outPublicKey, QString &outShortId) const
|
||||
{
|
||||
outPublicKey.clear();
|
||||
outShortId.clear();
|
||||
|
||||
auto readKeyFile = [&](const QString &path, QString &out) -> ErrorCode {
|
||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||
ErrorCode fileError = ErrorCode::NoError;
|
||||
out = QString::fromUtf8(m_sshSession->getTextFileFromContainer(container, credentials, path, fileError));
|
||||
out.replace(QLatin1Char('\n'), QString());
|
||||
out.replace(QLatin1Char('\r'), QString());
|
||||
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
logger.error() << "Xray readRealityKeyFiles: failed path=" << path;
|
||||
return ErrorCode::XrayRealityKeysReadFailed;
|
||||
};
|
||||
|
||||
ErrorCode errorCode = readKeyFile(QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), outPublicKey);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
return readKeyFile(QString::fromLatin1(amnezia::protocols::xray::shortidPath), outShortId);
|
||||
}
|
||||
|
||||
QJsonObject XrayConfigurator::mergeStreamSettingsForServerInbound(const XrayServerConfig &srv,
|
||||
const QJsonObject &existingStreamSettings) const
|
||||
{
|
||||
QJsonObject streamSettings = buildStreamSettings(srv, QString());
|
||||
|
||||
if (srv.security != QLatin1String("reality")) {
|
||||
return streamSettings;
|
||||
}
|
||||
|
||||
const QJsonObject newRs = streamSettings[amnezia::protocols::xray::realitySettings].toObject();
|
||||
QJsonObject oldRs = existingStreamSettings[amnezia::protocols::xray::realitySettings].toObject();
|
||||
QJsonObject merged = oldRs.isEmpty() ? newRs : oldRs;
|
||||
|
||||
const QString siteEff = srv.site.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultSite) : srv.site;
|
||||
const QString sniEff = srv.sni.isEmpty() ? siteEff : srv.sni;
|
||||
|
||||
if (newRs.contains(amnezia::protocols::xray::fingerprint)) {
|
||||
merged[amnezia::protocols::xray::fingerprint] = newRs[amnezia::protocols::xray::fingerprint];
|
||||
}
|
||||
merged[amnezia::protocols::xray::serverNames] = QJsonArray { sniEff };
|
||||
if (!merged.contains(QStringLiteral("dest"))) {
|
||||
merged[QStringLiteral("dest")] = siteEff + QStringLiteral(":443");
|
||||
}
|
||||
|
||||
streamSettings[amnezia::protocols::xray::realitySettings] = merged;
|
||||
return streamSettings;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials &credentials, DockerContainer container,
|
||||
ContainerConfig &containerConfig, const DnsSettings &dnsSettings,
|
||||
bool appendNewClient, QString *outClientId)
|
||||
{
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
|
||||
if (!xrayCfg) {
|
||||
logger.error() << "Xray applyServerSettings: missing XrayProtocolConfig";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
const XrayServerConfig &srv = xrayCfg->serverConfig;
|
||||
if (srv.isThirdPartyConfig) {
|
||||
logger.info() << "Xray applyServerSettings: skipped (third-party/native profile)";
|
||||
if (outClientId && xrayCfg->hasClientConfig()) {
|
||||
*outClientId = xrayCfg->clientConfig->id;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
logger.info() << "Xray applyServerSettings: start"
|
||||
<< "container=" << static_cast<int>(container) << "host=" << credentials.hostName
|
||||
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port
|
||||
<< "appendClient=" << appendNewClient;
|
||||
QString flowValue = srv.flow;
|
||||
if (flowValue.isEmpty() && srv.security == QLatin1String("reality")) {
|
||||
flowValue = QStringLiteral("xtls-rprx-vision");
|
||||
}
|
||||
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
errorCode = readRealityKeyFiles(container, credentials, realityPublicKey, realityShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: readRealityKeyFiles failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
QString currentConfig = m_sshSession->getTextFileFromContainer(
|
||||
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: getTextFileFromContainer failed, error="
|
||||
<< static_cast<int>(errorCode) << "path=" << amnezia::protocols::xray::serverConfigPath;
|
||||
return errorCode;
|
||||
}
|
||||
logger.info() << "Xray applyServerSettings: read server config, bytes=" << currentConfig.size();
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
logger.error() << "Failed to parse server config JSON";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
QJsonObject serverConfig = doc.object();
|
||||
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
|
||||
logger.error() << "Server config missing 'inbounds' field";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
QJsonArray inbounds = serverConfig[amnezia::protocols::xray::inbounds].toArray();
|
||||
if (inbounds.isEmpty()) {
|
||||
logger.error() << "Server config has empty 'inbounds' array";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
QJsonObject inbound = inbounds[0].toObject();
|
||||
if (!inbound.contains(amnezia::protocols::xray::settings)) {
|
||||
logger.error() << "Inbound missing 'settings' field";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
const QJsonObject existingStream = inbound[amnezia::protocols::xray::streamSettings].toObject();
|
||||
inbound[amnezia::protocols::xray::streamSettings] = mergeStreamSettingsForServerInbound(srv, existingStream);
|
||||
|
||||
if (!srv.port.isEmpty()) {
|
||||
inbound[amnezia::protocols::xray::port] = srv.port.toInt();
|
||||
}
|
||||
|
||||
QJsonObject settings = inbound[amnezia::protocols::xray::settings].toObject();
|
||||
if (!settings.contains(amnezia::protocols::xray::clients)) {
|
||||
settings[amnezia::protocols::xray::clients] = QJsonArray {};
|
||||
}
|
||||
|
||||
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
|
||||
QString clientId;
|
||||
|
||||
if (appendNewClient) {
|
||||
clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
QJsonObject clientEntry;
|
||||
clientEntry[amnezia::protocols::xray::id] = clientId;
|
||||
if (!flowValue.isEmpty()) {
|
||||
clientEntry[amnezia::protocols::xray::flow] = flowValue;
|
||||
}
|
||||
clients.append(clientEntry);
|
||||
} else {
|
||||
if (clients.isEmpty()) {
|
||||
logger.error() << "Server config has no VLESS clients";
|
||||
return ErrorCode::XrayServerNoVlessClients;
|
||||
}
|
||||
clientId = clients[0].toObject()[amnezia::protocols::xray::id].toString();
|
||||
if (clientId.isEmpty()) {
|
||||
logger.error() << "Server config VLESS client has empty id";
|
||||
return ErrorCode::XrayServerNoVlessClients;
|
||||
}
|
||||
QJsonArray updatedClients;
|
||||
for (const QJsonValue &v : clients) {
|
||||
QJsonObject c = v.toObject();
|
||||
if (flowValue.isEmpty()) {
|
||||
c.remove(amnezia::protocols::xray::flow);
|
||||
} else {
|
||||
c[amnezia::protocols::xray::flow] = flowValue;
|
||||
}
|
||||
updatedClients.append(c);
|
||||
}
|
||||
clients = updatedClients;
|
||||
}
|
||||
|
||||
settings[amnezia::protocols::xray::clients] = clients;
|
||||
inbound[amnezia::protocols::xray::settings] = settings;
|
||||
inbounds[0] = inbound;
|
||||
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
|
||||
|
||||
errorCode = uploadServerConfigJson(credentials, container, dnsSettings, serverConfig);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: upload/restart failed, error=" << static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
logger.info() << "Xray applyServerSettings: server config uploaded and container restarted";
|
||||
|
||||
if (outClientId) {
|
||||
*outClientId = clientId;
|
||||
}
|
||||
|
||||
XrayProtocolConfig updated =
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: buildClientProtocolConfig failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
containerConfig.protocolConfig = updated;
|
||||
logger.info() << "Xray applyServerSettings: done, clientId=" << clientId;
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
ErrorCode &errorCode)
|
||||
{
|
||||
// Generate new UUID for client
|
||||
QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
|
||||
// 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 "";
|
||||
ContainerConfig mutableConfig = containerConfig;
|
||||
QString clientId;
|
||||
const ErrorCode applyError =
|
||||
applyServerSettingsToRemote(credentials, container, mutableConfig, dnsSettings, true, &clientId);
|
||||
errorCode = applyError;
|
||||
if (applyError != ErrorCode::NoError || clientId.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
// Parse current config as JSON
|
||||
QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
logger.error() << "Failed to parse server config JSON";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
QJsonObject serverConfig = doc.object();
|
||||
|
||||
// Validate server config structure
|
||||
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
|
||||
logger.error() << "Server config missing 'inbounds' field";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
QJsonArray inbounds = serverConfig[amnezia::protocols::xray::inbounds].toArray();
|
||||
if (inbounds.isEmpty()) {
|
||||
logger.error() << "Server config has empty 'inbounds' array";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
QJsonObject inbound = inbounds[0].toObject();
|
||||
if (!inbound.contains(amnezia::protocols::xray::settings)) {
|
||||
logger.error() << "Inbound missing 'settings' field";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
QJsonObject settings = inbound[amnezia::protocols::xray::settings].toObject();
|
||||
if (!settings.contains(amnezia::protocols::xray::clients)) {
|
||||
logger.error() << "Settings missing 'clients' field";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
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"}
|
||||
};
|
||||
|
||||
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,
|
||||
updatedConfig,
|
||||
amnezia::protocols::xray::serverConfigPath,
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting
|
||||
);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to upload updated config";
|
||||
return "";
|
||||
}
|
||||
|
||||
// Restart container
|
||||
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
|
||||
errorCode = m_sshSession->runScript(
|
||||
credentials,
|
||||
m_sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns))
|
||||
);
|
||||
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to restart container";
|
||||
return "";
|
||||
}
|
||||
|
||||
return clientId;
|
||||
}
|
||||
|
||||
ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
ErrorCode &errorCode)
|
||||
XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCredentials &credentials,
|
||||
DockerContainer container,
|
||||
const XrayServerConfig &srv, const QString &clientId,
|
||||
ErrorCode &errorCode,
|
||||
const QString &prefetchedRealityPublicKey,
|
||||
const QString &prefetchedRealityShortId) const
|
||||
{
|
||||
const XrayServerConfig* serverConfig = nullptr;
|
||||
if (auto* xrayConfig = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
serverConfig = &xrayConfig->serverConfig;
|
||||
QString xrayPublicKey = prefetchedRealityPublicKey;
|
||||
QString xrayShortId = prefetchedRealityShortId;
|
||||
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
if (xrayPublicKey.isEmpty() || xrayShortId.isEmpty()) {
|
||||
errorCode = readRealityKeyFiles(container, credentials, xrayPublicKey, xrayShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
QJsonObject userObj;
|
||||
userObj[amnezia::protocols::xray::id] = clientId;
|
||||
userObj[amnezia::protocols::xray::encryption] = QStringLiteral("none");
|
||||
if (!srv.flow.isEmpty()) {
|
||||
userObj[amnezia::protocols::xray::flow] = srv.flow;
|
||||
}
|
||||
|
||||
QJsonObject vnextEntry;
|
||||
vnextEntry[amnezia::protocols::xray::address] = credentials.hostName;
|
||||
vnextEntry[amnezia::protocols::xray::port] =
|
||||
srv.port.isEmpty() ? QString(amnezia::protocols::xray::defaultPort).toInt() : srv.port.toInt();
|
||||
vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj };
|
||||
|
||||
QJsonObject outboundSettings;
|
||||
outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry };
|
||||
|
||||
QJsonObject outbound;
|
||||
outbound[QStringLiteral("protocol")] = QStringLiteral("vless");
|
||||
outbound[amnezia::protocols::xray::settings] = outboundSettings;
|
||||
|
||||
QJsonObject streamObj = buildStreamSettings(srv, clientId);
|
||||
if (srv.security == QLatin1String("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] = QString();
|
||||
streamObj[amnezia::protocols::xray::realitySettings] = rs;
|
||||
}
|
||||
|
||||
outbound[amnezia::protocols::xray::streamSettings] = streamObj;
|
||||
|
||||
QJsonObject inboundObj;
|
||||
inboundObj[QStringLiteral("listen")] = amnezia::protocols::xray::defaultLocalListenAddr;
|
||||
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort;
|
||||
inboundObj[QStringLiteral("protocol")] = QStringLiteral("socks");
|
||||
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { QStringLiteral("udp"), true } };
|
||||
|
||||
QJsonObject clientJson;
|
||||
clientJson[QStringLiteral("log")] = QJsonObject { { QStringLiteral("loglevel"), QStringLiteral("error") } };
|
||||
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj };
|
||||
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound };
|
||||
|
||||
const QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
|
||||
|
||||
XrayProtocolConfig protocolConfig;
|
||||
protocolConfig.serverConfig = srv;
|
||||
|
||||
XrayClientConfig clientConfig;
|
||||
clientConfig.nativeConfig = config;
|
||||
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
clientConfig.id = clientId;
|
||||
protocolConfig.setClientConfig(clientConfig);
|
||||
|
||||
return protocolConfig;
|
||||
}
|
||||
|
||||
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
|
||||
{
|
||||
QJsonObject streamSettings;
|
||||
const auto &xhttp = srv.xhttp;
|
||||
const auto &mkcp = srv.mkcp;
|
||||
namespace px = amnezia::protocols::xray;
|
||||
|
||||
QString networkValue = QStringLiteral("tcp");
|
||||
if (srv.transport == QLatin1String("xhttp"))
|
||||
networkValue = QStringLiteral("xhttp");
|
||||
else if (srv.transport == QLatin1String("mkcp"))
|
||||
networkValue = QStringLiteral("kcp");
|
||||
streamSettings[px::network] = networkValue;
|
||||
|
||||
streamSettings[px::security] = srv.security;
|
||||
|
||||
if (srv.security == QLatin1String("tls")) {
|
||||
QJsonObject tlsSettings;
|
||||
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
|
||||
tlsSettings[px::serverName] = sniEff;
|
||||
const QString alpnEff = srv.alpn.isEmpty() ? QString::fromLatin1(px::defaultAlpn) : srv.alpn;
|
||||
QJsonArray alpnArray;
|
||||
for (const QString &a : alpnEff.split(QLatin1Char(','))) {
|
||||
const QString t = a.trimmed();
|
||||
if (!t.isEmpty())
|
||||
alpnArray.append(t);
|
||||
}
|
||||
if (!alpnArray.isEmpty())
|
||||
tlsSettings[QStringLiteral("alpn")] = alpnArray;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
tlsSettings[px::fingerprint] = fpEff;
|
||||
streamSettings[QStringLiteral("tlsSettings")] = tlsSettings;
|
||||
}
|
||||
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
QJsonObject realSettings;
|
||||
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
|
||||
realSettings[px::fingerprint] = fpEff;
|
||||
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
|
||||
realSettings[px::serverName] = sniEff;
|
||||
streamSettings[px::realitySettings] = realSettings;
|
||||
}
|
||||
|
||||
// XHTTP — JSON must match Xray-core SplitHTTPConfig (flat xPadding fields, see transport_internet.go)
|
||||
if (srv.transport == QLatin1String("xhttp")) {
|
||||
QJsonObject xo;
|
||||
const QString hostEff = xhttp.host.isEmpty() ? QString::fromLatin1(px::defaultXhttpHost) : xhttp.host;
|
||||
xo[QStringLiteral("host")] = hostEff;
|
||||
if (!xhttp.path.isEmpty())
|
||||
xo[QStringLiteral("path")] = xhttp.path;
|
||||
xo[QStringLiteral("mode")] = normalizeXhttpMode(xhttp.mode);
|
||||
|
||||
if (xhttp.headersTemplate.compare(QLatin1String("HTTP"), Qt::CaseInsensitive) == 0) {
|
||||
QJsonObject headers;
|
||||
headers[QStringLiteral("Host")] = hostEff;
|
||||
xo[QStringLiteral("headers")] = headers;
|
||||
}
|
||||
|
||||
const QString methodEff =
|
||||
xhttp.uplinkMethod.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkMethod) : xhttp.uplinkMethod;
|
||||
xo[QStringLiteral("uplinkHTTPMethod")] = methodEff.toUpper();
|
||||
|
||||
xo[QStringLiteral("noGRPCHeader")] = xhttp.disableGrpc;
|
||||
xo[QStringLiteral("noSSEHeader")] = xhttp.disableSse;
|
||||
|
||||
const QString sessPl = normalizeSessionSeqPlacement(xhttp.sessionPlacement);
|
||||
if (!sessPl.isEmpty())
|
||||
xo[QStringLiteral("sessionPlacement")] = sessPl;
|
||||
const QString seqPl = normalizeSessionSeqPlacement(xhttp.seqPlacement);
|
||||
if (!seqPl.isEmpty())
|
||||
xo[QStringLiteral("seqPlacement")] = seqPl;
|
||||
if (!xhttp.sessionKey.isEmpty())
|
||||
xo[QStringLiteral("sessionKey")] = xhttp.sessionKey;
|
||||
if (!xhttp.seqKey.isEmpty())
|
||||
xo[QStringLiteral("seqKey")] = xhttp.seqKey;
|
||||
|
||||
xo[QStringLiteral("uplinkDataPlacement")] = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement);
|
||||
if (!xhttp.uplinkDataKey.isEmpty())
|
||||
xo[QStringLiteral("uplinkDataKey")] = xhttp.uplinkDataKey;
|
||||
|
||||
const QString ucs = xhttp.uplinkChunkSize.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkChunkSize)
|
||||
: xhttp.uplinkChunkSize;
|
||||
if (!ucs.isEmpty() && ucs != QLatin1String("0")) {
|
||||
const int v = ucs.toInt();
|
||||
QJsonObject chunkR;
|
||||
chunkR[QStringLiteral("from")] = v;
|
||||
chunkR[QStringLiteral("to")] = v;
|
||||
xo[QStringLiteral("uplinkChunkSize")] = chunkR;
|
||||
}
|
||||
|
||||
if (!xhttp.scMaxBufferedPosts.isEmpty())
|
||||
xo[QStringLiteral("scMaxBufferedPosts")] = xhttp.scMaxBufferedPosts.toLongLong();
|
||||
|
||||
putIntRangeIfAny(xo, "scMaxEachPostBytes", xhttp.scMaxEachPostBytesMin, xhttp.scMaxEachPostBytesMax,
|
||||
px::defaultXhttpScMaxEachPostBytesMin, px::defaultXhttpScMaxEachPostBytesMax);
|
||||
putIntRangeIfAny(xo, "scMinPostsIntervalMs", xhttp.scMinPostsIntervalMsMin, xhttp.scMinPostsIntervalMsMax,
|
||||
px::defaultXhttpScMinPostsIntervalMsMin, px::defaultXhttpScMinPostsIntervalMsMax);
|
||||
putIntRangeIfAny(xo, "scStreamUpServerSecs", xhttp.scStreamUpServerSecsMin, xhttp.scStreamUpServerSecsMax,
|
||||
px::defaultXhttpScStreamUpServerSecsMin, px::defaultXhttpScStreamUpServerSecsMax);
|
||||
|
||||
const auto &pad = xhttp.xPadding;
|
||||
xo[QStringLiteral("xPaddingObfsMode")] = pad.obfsMode;
|
||||
if (pad.obfsMode) {
|
||||
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
|
||||
QJsonObject br;
|
||||
br[QStringLiteral("from")] = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt();
|
||||
br[QStringLiteral("to")] = pad.bytesMax.isEmpty() ? (pad.bytesMin.isEmpty() ? 256 : pad.bytesMin.toInt())
|
||||
: pad.bytesMax.toInt();
|
||||
xo[QStringLiteral("xPaddingBytes")] = br;
|
||||
}
|
||||
xo[QStringLiteral("xPaddingKey")] = pad.key.isEmpty() ? QStringLiteral("x_padding") : pad.key;
|
||||
xo[QStringLiteral("xPaddingHeader")] = pad.header.isEmpty() ? QStringLiteral("X-Padding") : pad.header;
|
||||
xo[QStringLiteral("xPaddingPlacement")] = normalizeXPaddingPlacement(
|
||||
pad.placement.isEmpty() ? QString::fromLatin1(px::defaultXPaddingPlacement) : pad.placement);
|
||||
xo[QStringLiteral("xPaddingMethod")] = normalizeXPaddingMethod(
|
||||
pad.method.isEmpty() ? QString::fromLatin1(px::defaultXPaddingMethod) : pad.method);
|
||||
}
|
||||
|
||||
// xmux: Xray has no "enabled" flag; omit object when UI disables multiplex tuning.
|
||||
if (xhttp.xmux.enabled) {
|
||||
QJsonObject mux;
|
||||
auto addMuxRange = [&](const char *key, const QString &a, const QString &b) {
|
||||
if (a.isEmpty() && b.isEmpty())
|
||||
return;
|
||||
QJsonObject r;
|
||||
r[QStringLiteral("from")] = a.isEmpty() ? 0 : a.toInt();
|
||||
r[QStringLiteral("to")] = b.isEmpty() ? 0 : b.toInt();
|
||||
mux[QString::fromUtf8(key)] = r;
|
||||
};
|
||||
addMuxRange("maxConcurrency", xhttp.xmux.maxConcurrencyMin, xhttp.xmux.maxConcurrencyMax);
|
||||
addMuxRange("maxConnections", xhttp.xmux.maxConnectionsMin, xhttp.xmux.maxConnectionsMax);
|
||||
addMuxRange("cMaxReuseTimes", xhttp.xmux.cMaxReuseTimesMin, xhttp.xmux.cMaxReuseTimesMax);
|
||||
addMuxRange("hMaxRequestTimes", xhttp.xmux.hMaxRequestTimesMin, xhttp.xmux.hMaxRequestTimesMax);
|
||||
addMuxRange("hMaxReusableSecs", xhttp.xmux.hMaxReusableSecsMin, xhttp.xmux.hMaxReusableSecsMax);
|
||||
if (!xhttp.xmux.hKeepAlivePeriod.isEmpty())
|
||||
mux[QStringLiteral("hKeepAlivePeriod")] = xhttp.xmux.hKeepAlivePeriod.toLongLong();
|
||||
if (!mux.isEmpty())
|
||||
xo[QStringLiteral("xmux")] = mux;
|
||||
}
|
||||
|
||||
streamSettings[QStringLiteral("xhttpSettings")] = xo;
|
||||
}
|
||||
|
||||
if (srv.transport == QLatin1String("mkcp")) {
|
||||
QJsonObject kcpObj;
|
||||
const QString ttiEff = mkcp.tti.isEmpty() ? QString::fromLatin1(px::defaultMkcpTti) : mkcp.tti;
|
||||
const QString upEff = mkcp.uplinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpUplinkCapacity)
|
||||
: mkcp.uplinkCapacity;
|
||||
const QString downEff = mkcp.downlinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpDownlinkCapacity)
|
||||
: mkcp.downlinkCapacity;
|
||||
const QString rbufEff = mkcp.readBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpReadBufferSize)
|
||||
: mkcp.readBufferSize;
|
||||
const QString wbufEff = mkcp.writeBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpWriteBufferSize)
|
||||
: mkcp.writeBufferSize;
|
||||
kcpObj[QStringLiteral("tti")] = ttiEff.toInt();
|
||||
kcpObj[QStringLiteral("uplinkCapacity")] = upEff.toInt();
|
||||
kcpObj[QStringLiteral("downlinkCapacity")] = downEff.toInt();
|
||||
kcpObj[QStringLiteral("readBufferSize")] = rbufEff.toInt();
|
||||
kcpObj[QStringLiteral("writeBufferSize")] = wbufEff.toInt();
|
||||
kcpObj[QStringLiteral("congestion")] = mkcp.congestion;
|
||||
streamSettings[QStringLiteral("kcpSettings")] = kcpObj;
|
||||
}
|
||||
|
||||
return streamSettings;
|
||||
}
|
||||
|
||||
ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
ErrorCode &errorCode)
|
||||
{
|
||||
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
if (xrayCfg->serverConfig.isThirdPartyConfig && xrayCfg->hasClientConfig()) {
|
||||
logger.info() << "Xray createConfig: returning existing third-party client config without server SSH";
|
||||
return *xrayCfg;
|
||||
}
|
||||
}
|
||||
|
||||
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{};
|
||||
}
|
||||
|
||||
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{};
|
||||
}
|
||||
|
||||
config.replace("$XRAY_CLIENT_ID", xrayClientId);
|
||||
config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey);
|
||||
config.replace("$XRAY_SHORT_ID", xrayShortId);
|
||||
|
||||
XrayProtocolConfig protocolConfig;
|
||||
if (serverConfig) {
|
||||
protocolConfig.serverConfig = *serverConfig;
|
||||
}
|
||||
|
||||
XrayClientConfig clientConfig;
|
||||
clientConfig.nativeConfig = config;
|
||||
clientConfig.localPort = "";
|
||||
clientConfig.id = xrayClientId;
|
||||
|
||||
protocolConfig.setClientConfig(clientConfig);
|
||||
|
||||
return protocolConfig;
|
||||
}
|
||||
return buildClientProtocolConfig(credentials, container, srv, xrayClientId, errorCode);
|
||||
}
|
||||
@@ -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,42 @@ public:
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
amnezia::ErrorCode &errorCode) override;
|
||||
|
||||
amnezia::ProtocolConfig processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
|
||||
amnezia::ProtocolConfig protocolConfig) override;
|
||||
|
||||
amnezia::ErrorCode applyServerSettingsToRemote(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
bool appendNewClient,
|
||||
QString *outClientId = nullptr);
|
||||
|
||||
private:
|
||||
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
amnezia::ErrorCode &errorCode);
|
||||
|
||||
amnezia::ErrorCode uploadServerConfigJson(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container,
|
||||
const amnezia::DnsSettings &dnsSettings, const QJsonObject &serverConfig) const;
|
||||
|
||||
amnezia::XrayProtocolConfig buildClientProtocolConfig(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
const amnezia::XrayServerConfig &srv,
|
||||
const QString &clientId,
|
||||
amnezia::ErrorCode &errorCode,
|
||||
const QString &prefetchedRealityPublicKey = {},
|
||||
const QString &prefetchedRealityShortId = {}) const;
|
||||
|
||||
amnezia::ErrorCode readRealityKeyFiles(amnezia::DockerContainer container,
|
||||
const amnezia::ServerCredentials &credentials,
|
||||
QString &outPublicKey,
|
||||
QString &outShortId) const;
|
||||
|
||||
QJsonObject mergeStreamSettingsForServerInbound(const amnezia::XrayServerConfig &srv,
|
||||
const QJsonObject &existingStreamSettings) const;
|
||||
|
||||
QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
|
||||
const QString &clientId) const;
|
||||
};
|
||||
|
||||
#endif // XRAY_CONFIGURATOR_H
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QEventLoop>
|
||||
#include <QFutureWatcher>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QPromise>
|
||||
#include <QSet>
|
||||
#include <QSysInfo>
|
||||
@@ -216,7 +217,8 @@ ErrorCode SubscriptionController::executeRequest(const QString &endpoint, const
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData)
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
CaptchaInfo &captchaInfo)
|
||||
{
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
@@ -233,6 +235,19 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
|
||||
|
||||
if (errorCode == ErrorCode::ApiCaptchaRequiredError) {
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
captchaInfo.captchaId = jsonObj.value("captcha_id").toString();
|
||||
captchaInfo.captchaImageBase64 = jsonObj.value("captcha_image").toString();
|
||||
captchaInfo.hint = jsonObj.value("hint").toString();
|
||||
captchaInfo.isRequired = true;
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
@@ -242,9 +257,9 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
|
||||
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
|
||||
|
||||
|
||||
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
@@ -460,6 +475,7 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
|
||||
if (apiV2->nameOverriddenByUser) {
|
||||
newApiV2->name = apiV2->name;
|
||||
newApiV2->displayName = apiV2->displayName;
|
||||
newApiV2->nameOverriddenByUser = true;
|
||||
}
|
||||
|
||||
@@ -955,3 +971,74 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(const
|
||||
return promise->future();
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::resolveImportServiceCaptcha(const QString &userCountryCode,
|
||||
const QString &serviceType,
|
||||
const QString &serviceProtocol,
|
||||
const ProtocolData &protocolData,
|
||||
const QString &captchaId,
|
||||
const QString &captchaSolution,
|
||||
CaptchaInfo *retryCaptchaOut)
|
||||
{
|
||||
GatewayRequestData gatewayRequestData{QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
|
||||
m_appSettingsRepository->getInstallationUuid(true),
|
||||
userCountryCode,
|
||||
"",
|
||||
serviceType,
|
||||
serviceProtocol,
|
||||
QJsonObject()};
|
||||
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
|
||||
|
||||
apiPayload["captcha_id"] = captchaId;
|
||||
QString normalizedSolution;
|
||||
normalizedSolution.reserve(captchaSolution.size());
|
||||
for (const QChar &ch : captchaSolution) {
|
||||
const ushort u = ch.unicode();
|
||||
if (u >= '0' && u <= '9') {
|
||||
normalizedSolution += ch;
|
||||
} else if (u >= 0xFF10 && u <= 0xFF19) {
|
||||
normalizedSolution += QChar(static_cast<char16_t>(u - 0xFF10 + '0'));
|
||||
}
|
||||
}
|
||||
apiPayload["captcha_solution"] = normalizedSolution.isEmpty() ? captchaSolution.trimmed() : normalizedSolution;
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
if (retryCaptchaOut
|
||||
&& (errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|
||||
|| errorCode == ErrorCode::ApiCaptchaRequiredError)) {
|
||||
const QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
const QJsonObject jsonObj = jsonDoc.object();
|
||||
if (jsonObj.contains(QStringLiteral("captcha_id")) && jsonObj.contains(QStringLiteral("captcha_image"))) {
|
||||
retryCaptchaOut->captchaId = jsonObj.value(QStringLiteral("captcha_id")).toString();
|
||||
retryCaptchaOut->captchaImageBase64 = jsonObj.value(QStringLiteral("captcha_image")).toString();
|
||||
retryCaptchaOut->hint = jsonObj.value(QStringLiteral("hint")).toString();
|
||||
retryCaptchaOut->isRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
QJsonObject serverConfigJson;
|
||||
errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
|
||||
|
||||
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(serverConfigJson);
|
||||
m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(),
|
||||
serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson()));
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,13 @@ public:
|
||||
QJsonObject toJsonObject() const;
|
||||
};
|
||||
|
||||
struct CaptchaInfo {
|
||||
QString captchaId;
|
||||
QString captchaImageBase64;
|
||||
QString hint;
|
||||
bool isRequired = false;
|
||||
};
|
||||
|
||||
explicit SubscriptionController(SecureServersRepository* serversRepository,
|
||||
SecureAppSettingsRepository* appSettingsRepository);
|
||||
|
||||
@@ -49,7 +56,8 @@ public:
|
||||
void appendProtocolDataToApiPayload(const QString &protocol, const ProtocolData &protocolData, QJsonObject &apiPayload);
|
||||
|
||||
ErrorCode importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData);
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
CaptchaInfo &captchaInfo);
|
||||
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &email);
|
||||
|
||||
@@ -98,6 +106,11 @@ public:
|
||||
AppStoreRestoreResult processAppStoreRestore(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol);
|
||||
|
||||
ErrorCode resolveImportServiceCaptcha(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
const QString &captchaId, const QString &captchaSolution,
|
||||
CaptchaInfo *retryCaptchaOut = nullptr);
|
||||
|
||||
private:
|
||||
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
|
||||
bool isApiKeyExpired(const QString &serverId) const;
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
#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/utilities.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "version.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
@@ -67,13 +65,15 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
|
||||
bool isApiConfig = false;
|
||||
|
||||
const auto kind = m_serversRepository->serverKind(serverId);
|
||||
const QString primaryDns = m_appSettingsRepository->primaryDns();
|
||||
const QString secondaryDns = m_appSettingsRepository->secondaryDns();
|
||||
switch (kind) {
|
||||
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
|
||||
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!cfg.has_value()) return ErrorCode::InternalError;
|
||||
container = cfg->defaultContainer;
|
||||
containerConfigModel = cfg->containerConfig(container);
|
||||
dns = { cfg->dns1, cfg->dns2 };
|
||||
dns = cfg->getDnsPair(m_appSettingsRepository->useAmneziaDns(), primaryDns, secondaryDns);
|
||||
hostName = cfg->hostName;
|
||||
description = cfg->description;
|
||||
break;
|
||||
@@ -83,7 +83,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
|
||||
if (!cfg.has_value()) return ErrorCode::InternalError;
|
||||
container = cfg->defaultContainer;
|
||||
containerConfigModel = cfg->containerConfig(container);
|
||||
dns = { cfg->dns1, cfg->dns2 };
|
||||
dns = cfg->getDnsPair(primaryDns, secondaryDns);
|
||||
hostName = cfg->hostName;
|
||||
description = cfg->description;
|
||||
break;
|
||||
@@ -93,7 +93,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
|
||||
if (!cfg.has_value()) return ErrorCode::InternalError;
|
||||
container = cfg->defaultContainer;
|
||||
containerConfigModel = cfg->containerConfig(container);
|
||||
dns = { cfg->dns1, cfg->dns2 };
|
||||
dns = cfg->getDnsPair(primaryDns, secondaryDns);
|
||||
hostName = cfg->hostName;
|
||||
description = cfg->description;
|
||||
break;
|
||||
@@ -105,7 +105,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
|
||||
if (!cfg.has_value()) return ErrorCode::InternalError;
|
||||
container = cfg->defaultContainer;
|
||||
containerConfigModel = cfg->containerConfig(container);
|
||||
dns = { cfg->dns1, cfg->dns2 };
|
||||
dns = cfg->getDnsPair(primaryDns, secondaryDns);
|
||||
hostName = cfg->hostName;
|
||||
description = cfg->description;
|
||||
configVersion = serverConfigUtils::ConfigSource::AmneziaGateway;
|
||||
@@ -123,16 +123,6 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
|
||||
if (!isContainerSupported(container)) {
|
||||
return ErrorCode::NotSupportedOnThisPlatform;
|
||||
}
|
||||
if (dns.first.isEmpty() || !NetworkUtilities::checkIPv4Format(dns.first)) {
|
||||
if (m_appSettingsRepository->useAmneziaDns()) {
|
||||
dns.first = protocols::dns::amneziaDnsIp;
|
||||
} else {
|
||||
dns.first = m_appSettingsRepository->primaryDns();
|
||||
}
|
||||
}
|
||||
if (dns.second.isEmpty() || !NetworkUtilities::checkIPv4Format(dns.second)) {
|
||||
dns.second = m_appSettingsRepository->secondaryDns();
|
||||
}
|
||||
|
||||
vpnConfiguration = createConnectionConfiguration(dns, isApiConfig, hostName, description, configVersion,
|
||||
containerConfigModel, container);
|
||||
|
||||
@@ -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,8 @@ void CoreController::initControllers()
|
||||
#ifdef Q_OS_WINDOWS
|
||||
m_ikev2ConfigModel,
|
||||
#endif
|
||||
m_sftpConfigModel, m_socks5ConfigModel, this);
|
||||
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel,
|
||||
m_connectionController, this);
|
||||
setQmlContextProperty("InstallController", m_installUiController);
|
||||
|
||||
m_importController = new ImportUiController(m_importCoreController, this);
|
||||
@@ -202,12 +212,17 @@ void CoreController::initControllers()
|
||||
m_systemController = new SystemController(this);
|
||||
setQmlContextProperty("SystemController", m_systemController);
|
||||
|
||||
m_networkReachabilityController = new NetworkReachabilityController(this);
|
||||
setQmlContextProperty("NetworkReachabilityController", m_networkReachabilityController);
|
||||
setQmlContextProperty("NetworkReachability", m_networkReachabilityController);
|
||||
|
||||
m_servicesCatalogUiController = new ServicesCatalogUiController(m_servicesCatalogController, m_apiServicesModel, this);
|
||||
setQmlContextProperty("ServicesCatalogUiController", m_servicesCatalogUiController);
|
||||
|
||||
m_subscriptionUiController = new SubscriptionUiController(m_serversController, m_apiServicesModel, m_servicesCatalogController, m_subscriptionController,
|
||||
m_apiSubscriptionPlansModel, m_apiBenefitsModel, m_apiAccountInfoModel,
|
||||
m_apiCountryModel, m_apiDevicesModel, m_settingsController, this);
|
||||
m_apiCountryModel, m_apiDevicesModel, m_settingsController,
|
||||
m_connectionController, this);
|
||||
setQmlContextProperty("SubscriptionUiController", m_subscriptionUiController);
|
||||
|
||||
m_apiNewsUiController = new ApiNewsUiController(m_newsModel, m_newsController, this);
|
||||
@@ -329,9 +344,6 @@ void CoreController::openConnectionByIndex(int serverIndex)
|
||||
if (serverId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (m_serversModel) {
|
||||
m_serversModel->setProcessedServerIndex(serverIndex);
|
||||
}
|
||||
if (m_serversController) {
|
||||
m_serversController->setDefaultServer(serverId);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -77,33 +82,11 @@
|
||||
#endif
|
||||
|
||||
class CoreSignalHandlers;
|
||||
class TestMultipleImports;
|
||||
class TestAdminSelfHostedExport;
|
||||
class TestServerEdit;
|
||||
class TestDefaultServerChange;
|
||||
class TestServerEdgeCases;
|
||||
class TestSignalOrder;
|
||||
class TestServersModelSync;
|
||||
class TestComplexOperations;
|
||||
class TestSettingsSignals;
|
||||
class TestUiServersModelAndController;
|
||||
class TestSelfHostedServerSetup;
|
||||
|
||||
class CoreController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
friend class CoreSignalHandlers;
|
||||
friend class TestMultipleImports;
|
||||
friend class TestAdminSelfHostedExport;
|
||||
friend class TestServerEdit;
|
||||
friend class TestDefaultServerChange;
|
||||
friend class TestServerEdgeCases;
|
||||
friend class TestSignalOrder;
|
||||
friend class TestServersModelSync;
|
||||
friend class TestComplexOperations;
|
||||
friend class TestSettingsSignals;
|
||||
friend class TestUiServersModelAndController;
|
||||
friend class TestSelfHostedServerSetup;
|
||||
|
||||
public:
|
||||
explicit CoreController(const QSharedPointer<VpnConnection> &vpnConnection, SecureQSettings* settings,
|
||||
@@ -120,6 +103,36 @@ signals:
|
||||
void translationsUpdated();
|
||||
void websiteUrlChanged(const QString &newUrl);
|
||||
|
||||
protected:
|
||||
SecureServersRepository* serversRepositoryProtected() const { return m_serversRepository; }
|
||||
SecureAppSettingsRepository* appSettingsRepositoryProtected() const { return m_appSettingsRepository; }
|
||||
ServersModel* serversModelProtected() const { return m_serversModel; }
|
||||
ContainersModel* containersModelProtected() const { return m_containersModel; }
|
||||
ApiServicesModel* apiServicesModelProtected() const { return m_apiServicesModel; }
|
||||
NewsModel* newsModelProtected() const { return m_newsModel; }
|
||||
AllowedDnsModel* allowedDnsModelProtected() const { return m_allowedDnsModel; }
|
||||
AppSplitTunnelingModel* appSplitTunnelingModelProtected() const { return m_appSplitTunnelingModel; }
|
||||
IpSplitTunnelingModel* ipSplitTunnelingModelProtected() const { return m_ipSplitTunnelingModel; }
|
||||
LanguageModel* languageModelProtected() const { return m_languageModel; }
|
||||
|
||||
InstallUiController* installUiControllerProtected() const { return m_installUiController; }
|
||||
ImportController* importCoreControllerProtected() const { return m_importCoreController; }
|
||||
ExportController* exportControllerProtected() const { return m_exportController; }
|
||||
InstallController* installControllerProtected() const { return m_installController; }
|
||||
ServersController* serversControllerProtected() const { return m_serversController; }
|
||||
SettingsUiController* settingsUiControllerProtected() const { return m_settingsUiController; }
|
||||
SettingsController* settingsControllerProtected() const { return m_settingsController; }
|
||||
AllowedDnsUiController* allowedDnsUiControllerProtected() const { return m_allowedDnsUiController; }
|
||||
AllowedDnsController* allowedDnsControllerProtected() const { return m_allowedDnsController; }
|
||||
LanguageUiController* languageUiControllerProtected() const { return m_languageUiController; }
|
||||
IpSplitTunnelingController* ipSplitTunnelingControllerProtected() const { return m_ipSplitTunnelingController; }
|
||||
IpSplitTunnelingUiController* ipSplitTunnelingUiControllerProtected() const { return m_ipSplitTunnelingUiController; }
|
||||
AppSplitTunnelingController* appSplitTunnelingControllerProtected() const { return m_appSplitTunnelingController; }
|
||||
AppSplitTunnelingUiController* appSplitTunnelingUiControllerProtected() const { return m_appSplitTunnelingUiController; }
|
||||
ServersUiController* serversUiControllerProtected() const { return m_serversUiController; }
|
||||
ServicesCatalogUiController* servicesCatalogUiControllerProtected() const { return m_servicesCatalogUiController; }
|
||||
ApiNewsUiController* apiNewsUiControllerProtected() const { return m_apiNewsUiController; }
|
||||
|
||||
private:
|
||||
void initRepositories();
|
||||
void initCoreControllers();
|
||||
@@ -156,6 +169,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 +214,7 @@ private:
|
||||
|
||||
OpenVpnConfigModel* m_openVpnConfigModel;
|
||||
XrayConfigModel* m_xrayConfigModel;
|
||||
XrayConfigSnapshotsModel* m_xrayConfigSnapshotsModel;
|
||||
TorConfigModel* m_torConfigModel;
|
||||
WireGuardConfigModel* m_wireGuardConfigModel;
|
||||
AwgConfigModel* m_awgConfigModel;
|
||||
@@ -208,6 +223,8 @@ private:
|
||||
#endif
|
||||
SftpConfigModel* m_sftpConfigModel;
|
||||
Socks5ProxyConfigModel* m_socks5ConfigModel;
|
||||
MtProxyConfigModel* m_mtProxyConfigModel;
|
||||
TelemtConfigModel* m_telemtConfigModel;
|
||||
|
||||
CoreSignalHandlers* m_signalHandlers;
|
||||
};
|
||||
|
||||
@@ -125,9 +125,9 @@ void CoreSignalHandlers::initInstallControllerHandler()
|
||||
{
|
||||
connect(m_coreController->m_installController, &InstallController::serverIsBusy, m_coreController->m_installUiController, &InstallUiController::serverIsBusy);
|
||||
connect(m_coreController->m_installUiController, &InstallUiController::cancelInstallation, m_coreController->m_installController, &InstallController::cancelInstallation);
|
||||
connect(m_coreController->m_serversUiController, &ServersUiController::processedServerIndexChanged,
|
||||
m_coreController->m_installUiController, [this](int serverIndex) {
|
||||
if (serverIndex >= 0) {
|
||||
connect(m_coreController->m_serversUiController, &ServersUiController::processedServerIdChanged,
|
||||
m_coreController->m_installUiController, [this](const QString &serverId) {
|
||||
if (!serverId.isEmpty()) {
|
||||
m_coreController->m_installUiController->clearProcessedServerCredentials();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -30,6 +30,8 @@ namespace
|
||||
constexpr QLatin1String errorResponsePattern1("No active configuration found for");
|
||||
constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for");
|
||||
constexpr QLatin1String errorResponsePattern3("Account not found.");
|
||||
constexpr QLatin1String errorResponsePatternQrSessionNotFound("QR session not found");
|
||||
constexpr QLatin1String errorResponsePatternSessionNotFound("Session not found");
|
||||
|
||||
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
|
||||
|
||||
@@ -37,6 +39,7 @@ namespace
|
||||
constexpr int httpStatusCodeConflict = 409;
|
||||
constexpr int httpStatusCodeNotImplemented = 501;
|
||||
constexpr int httpStatusCodePaymentRequired = 402;
|
||||
constexpr int httpStatusCodeRequestTimeout = 408;
|
||||
constexpr int httpStatusCodeUnprocessableEntity = 422;
|
||||
|
||||
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
|
||||
@@ -206,8 +209,9 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
||||
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
|
||||
}
|
||||
|
||||
auto errorCode =
|
||||
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, decryptionResult.decryptedBody);
|
||||
responseBody = decryptionResult.decryptedBody;
|
||||
const auto errorCode =
|
||||
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, responseBody);
|
||||
if (errorCode) {
|
||||
return errorCode;
|
||||
}
|
||||
@@ -217,7 +221,6 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
||||
return ErrorCode::ApiConfigDecryptionError;
|
||||
}
|
||||
|
||||
responseBody = decryptionResult.decryptedBody;
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
@@ -256,7 +259,7 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
|
||||
decryptionResult.decryptedBody);
|
||||
if (errorCode) {
|
||||
promise->addResult(qMakePair(errorCode, QByteArray()));
|
||||
promise->addResult(qMakePair(errorCode, decryptionResult.decryptedBody));
|
||||
promise->finish();
|
||||
return;
|
||||
}
|
||||
@@ -459,15 +462,19 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
|
||||
qDebug() << "the response contains an html tag";
|
||||
return true;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeRequestTimeout) {
|
||||
return false;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeNotFound) {
|
||||
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|
||||
|| responseBody.contains(errorResponsePattern3)) {
|
||||
|| responseBody.contains(errorResponsePattern3) || responseBody.contains(errorResponsePatternQrSessionNotFound)
|
||||
|| responseBody.contains(errorResponsePatternSessionNotFound)) {
|
||||
return false;
|
||||
} else {
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeNotImplemented) {
|
||||
if (responseBody.contains(updateRequestResponsePattern)) {
|
||||
return false;
|
||||
|
||||
@@ -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,9 @@
|
||||
#include "core/installers/openvpnInstaller.h"
|
||||
#include "core/installers/sftpInstaller.h"
|
||||
#include "core/installers/socks5Installer.h"
|
||||
#include "core/installers/mtProxyInstaller.h"
|
||||
#include "core/configurators/xrayConfigurator.h"
|
||||
#include "core/installers/telemtInstaller.h"
|
||||
#include "core/installers/torInstaller.h"
|
||||
#include "core/installers/wireguardInstaller.h"
|
||||
#include "core/installers/xrayInstaller.h"
|
||||
@@ -34,6 +37,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 +57,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,
|
||||
@@ -101,9 +120,14 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
return e;
|
||||
qDebug().noquote() << "InstallController::setupContainer prepareHostWorker finished";
|
||||
|
||||
amnezia::ScriptVars removeContainerVars =
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
if (!isUpdate) {
|
||||
removeContainerVars.append({ { "$REMOVE_CONTAINER_DATA", QStringLiteral("1") } });
|
||||
}
|
||||
sshSession.runScript(credentials,
|
||||
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container),
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString())));
|
||||
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container),
|
||||
removeContainerVars));
|
||||
qDebug().noquote() << "InstallController::setupContainer removeContainer finished";
|
||||
|
||||
qDebug().noquote() << "buildContainerWorker start";
|
||||
@@ -136,6 +160,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;
|
||||
@@ -154,6 +187,16 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
|
||||
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
|
||||
qDebug() << "InstallController::updateContainer for container" << container << "reinstall required is" << reinstallRequired;
|
||||
|
||||
bool xrayServerSettingsChanged = false;
|
||||
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
|
||||
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
|
||||
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
if (oldXrayConfig && newXrayConfig) {
|
||||
xrayServerSettingsChanged =
|
||||
!oldXrayConfig->serverConfig.hasEqualServerSettings(newXrayConfig->serverConfig);
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
if (reinstallRequired) {
|
||||
errorCode = setupContainer(credentials, container, newConfig, true);
|
||||
@@ -164,7 +207,27 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
|
||||
}
|
||||
}
|
||||
|
||||
const bool skipXrayInboundSync =
|
||||
newConfig.getXrayProtocolConfig() && newConfig.getXrayProtocolConfig()->serverConfig.isThirdPartyConfig;
|
||||
|
||||
if (errorCode == ErrorCode::NoError && xrayServerSettingsChanged && !skipXrayInboundSync) {
|
||||
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
|
||||
XrayConfigurator xrayConfigurator(&sshSession);
|
||||
qDebug() << "InstallController::updateContainer applying Xray server inbound sync, reinstall="
|
||||
<< reinstallRequired;
|
||||
errorCode = xrayConfigurator.applyServerSettingsToRemote(credentials, container, newConfig, dnsSettings, false);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
qDebug() << "InstallController::updateContainer Xray inbound sync failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -184,9 +247,9 @@ void InstallController::clearCachedProfile(const QString &serverId, DockerContai
|
||||
return;
|
||||
}
|
||||
|
||||
adminConfig->clearCachedClientProfile(container);
|
||||
const ContainerConfig containerConfigModel = adminConfig->containerConfig(container);
|
||||
|
||||
adminConfig->clearCachedClientProfile(container);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
|
||||
emit clientRevocationRequested(serverId, containerConfigModel, container);
|
||||
@@ -194,38 +257,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 +508,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)
|
||||
@@ -554,12 +669,92 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
|
||||
const auto* oldXrayConfig = oldConfig.getXrayProtocolConfig();
|
||||
const auto* newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
|
||||
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
|
||||
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
|
||||
if (oldXrayConfig && newXrayConfig) {
|
||||
if (oldXrayConfig->serverConfig.port != newXrayConfig->serverConfig.port)
|
||||
const QString oldPort = oldXrayConfig->serverConfig.port.isEmpty()
|
||||
? QString(protocols::xray::defaultPort)
|
||||
: oldXrayConfig->serverConfig.port;
|
||||
const QString newPort = newXrayConfig->serverConfig.port.isEmpty()
|
||||
? QString(protocols::xray::defaultPort)
|
||||
: newXrayConfig->serverConfig.port;
|
||||
if (oldPort != newPort) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,7 +849,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;
|
||||
@@ -785,10 +980,12 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession(this);
|
||||
amnezia::ScriptVars removeContainerVars =
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
removeContainerVars.append({ { "$REMOVE_CONTAINER_DATA", QStringLiteral("1") } });
|
||||
ErrorCode errorCode = sshSession.runScript(
|
||||
credentials,
|
||||
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container),
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString())));
|
||||
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container), removeContainerVars));
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
QMap<DockerContainer, ContainerConfig> containers = adminConfig->containers;
|
||||
@@ -823,6 +1020,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 +1060,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 +1377,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 +1511,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);
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
|
||||
auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!cfg.has_value()) return false;
|
||||
cfg->description = name;
|
||||
cfg->displayName = name;
|
||||
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
|
||||
return true;
|
||||
}
|
||||
@@ -51,6 +52,7 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
|
||||
auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
|
||||
if (!cfg.has_value()) return false;
|
||||
cfg->description = name;
|
||||
cfg->displayName = name;
|
||||
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
|
||||
return true;
|
||||
}
|
||||
@@ -58,6 +60,7 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
|
||||
auto cfg = m_serversRepository->nativeConfig(serverId);
|
||||
if (!cfg.has_value()) return false;
|
||||
cfg->description = name;
|
||||
cfg->displayName = name;
|
||||
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
|
||||
return true;
|
||||
}
|
||||
@@ -67,6 +70,7 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
|
||||
auto cfg = m_serversRepository->apiV2Config(serverId);
|
||||
if (!cfg.has_value()) return false;
|
||||
cfg->name = name;
|
||||
cfg->displayName = name;
|
||||
cfg->nameOverriddenByUser = true;
|
||||
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
|
||||
return true;
|
||||
|
||||
@@ -217,6 +217,11 @@ void SettingsController::toggleAutoStart(bool enable)
|
||||
|
||||
bool SettingsController::isStartMinimizedEnabled() const
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
if (!isAutoStartEnabled()) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
return m_appSettingsRepository->isStartMinimized();
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
#elif defined(Q_OS_MACOS) && !defined(MACOS_NE)
|
||||
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);
|
||||
@@ -184,7 +184,7 @@ void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QSt
|
||||
logger.error() << QString("Network error occurred while fetching %1: %2 %3")
|
||||
.arg(operation, reply->errorString(), QString::number(error));
|
||||
});
|
||||
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::sslErrors, [operation](const QList<QSslError> &errors) {
|
||||
QStringList errorStrings;
|
||||
for (const QSslError &err : errors) {
|
||||
@@ -196,21 +196,13 @@ void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QSt
|
||||
|
||||
void UpdateController::handleNetworkError(QNetworkReply* reply, const QString& operation)
|
||||
{
|
||||
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
|
||||
logger.error() << errorString(ErrorCode::ApiConfigTimeoutError);
|
||||
} else {
|
||||
QString err = reply->errorString();
|
||||
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
|
||||
logger.error() << "Error message:" << err;
|
||||
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
logger.error() << errorString(ErrorCode::ApiConfigDownloadError);
|
||||
}
|
||||
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
|
||||
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
}
|
||||
|
||||
QString UpdateController::composeDownloadUrl() const
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
const QString fileName = QString(kInstallerRemoteFileNamePattern).arg(m_version);
|
||||
return m_baseUrl + "/" + fileName;
|
||||
#else
|
||||
@@ -220,7 +212,7 @@ QString UpdateController::composeDownloadUrl() const
|
||||
|
||||
void UpdateController::runInstaller()
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
if (m_downloadUrl.isEmpty()) {
|
||||
logger.error() << "Download URL is empty";
|
||||
return;
|
||||
@@ -252,7 +244,7 @@ void UpdateController::runInstaller()
|
||||
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
runWindowsInstaller(kInstallerLocalPath);
|
||||
#elif defined(Q_OS_MACOS)
|
||||
#elif defined(Q_OS_MACOS) && !defined(MACOS_NE)
|
||||
runMacInstaller(kInstallerLocalPath);
|
||||
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
runLinuxInstaller(kInstallerLocalPath);
|
||||
@@ -292,7 +284,7 @@ int UpdateController::runWindowsInstaller(const QString &installerPath)
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_MACOS)
|
||||
#if defined(Q_OS_MACOS) && !defined(MACOS_NE)
|
||||
int UpdateController::runMacInstaller(const QString &installerPath)
|
||||
{
|
||||
// Create temporary directory for extraction
|
||||
|
||||
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
|
||||
@@ -1,15 +1,17 @@
|
||||
#include "socks5Installer.h"
|
||||
|
||||
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
|
||||
#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"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/utils/utilities.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
using namespace amnezia;
|
||||
using namespace ProtocolUtils;
|
||||
|
||||
@@ -33,10 +35,29 @@ ContainerConfig Socks5Installer::generateConfig(DockerContainer container, int p
|
||||
ErrorCode Socks5Installer::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession* sshSession, ContainerConfig &config)
|
||||
{
|
||||
Q_UNUSED(container);
|
||||
Q_UNUSED(credentials);
|
||||
Q_UNUSED(sshSession);
|
||||
Q_UNUSED(config);
|
||||
if (container != DockerContainer::Socks5Proxy || !sshSession) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
Socks5ProxyProtocolConfig *socks5Config = config.getSocks5ProxyProtocolConfig();
|
||||
if (!socks5Config) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode readError = ErrorCode::NoError;
|
||||
const QByteArray configRaw = sshSession->getTextFileFromContainer(
|
||||
container, credentials, QString::fromUtf8(protocols::socks5Proxy::proxyConfigPath), readError);
|
||||
if (readError != ErrorCode::NoError || configRaw.trimmed().isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
const QString proxyConfig = QString::fromUtf8(configRaw);
|
||||
static const QRegularExpression usernameAndPasswordRegExp(QStringLiteral("users (\\w+):CL:(\\w+)"));
|
||||
const QRegularExpressionMatch usernameAndPasswordMatch = usernameAndPasswordRegExp.match(proxyConfig);
|
||||
if (usernameAndPasswordMatch.hasMatch()) {
|
||||
socks5Config->userName = usernameAndPasswordMatch.captured(1);
|
||||
socks5Config->password = usernameAndPasswordMatch.captured(2);
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/models/api/apiConfig.h"
|
||||
#include "core/models/api/authData.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -67,6 +68,20 @@ ContainerConfig ApiV2ServerConfig::containerConfig(DockerContainer container) co
|
||||
return containers.value(container);
|
||||
}
|
||||
|
||||
QPair<QString, QString> ApiV2ServerConfig::getDnsPair(const QString &primaryDns, const QString &secondaryDns) const
|
||||
{
|
||||
QString d1 = dns1;
|
||||
QString d2 = dns2;
|
||||
|
||||
if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) {
|
||||
d1 = primaryDns;
|
||||
}
|
||||
if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) {
|
||||
d2 = secondaryDns;
|
||||
}
|
||||
return { d1, d2 };
|
||||
}
|
||||
|
||||
QJsonObject ApiV2ServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
@@ -80,9 +95,6 @@ QJsonObject ApiV2ServerConfig::toJson() const
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = description;
|
||||
}
|
||||
if (!displayName.isEmpty()) {
|
||||
obj[configKey::displayName] = displayName;
|
||||
}
|
||||
|
||||
obj[configKey::configVersion] = configVersion;
|
||||
|
||||
@@ -134,7 +146,6 @@ ApiV2ServerConfig ApiV2ServerConfig::fromJson(const QJsonObject& json)
|
||||
config.name = json.value(configKey::name).toString();
|
||||
config.nameOverriddenByUser = json.value(configKey::nameOverriddenByUser).toBool(false);
|
||||
config.description = json.value(configKey::description).toString();
|
||||
config.displayName = json.value(configKey::displayName).toString();
|
||||
config.configVersion = json.value(configKey::configVersion).toInt(2);
|
||||
config.hostName = json.value(configKey::hostName).toString();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QPair>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
@@ -43,6 +44,9 @@ struct ApiV2ServerConfig {
|
||||
bool isExternalPremium() const;
|
||||
bool hasContainers() const;
|
||||
ContainerConfig containerConfig(DockerContainer container) const;
|
||||
|
||||
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static ApiV2ServerConfig fromJson(const QJsonObject& json);
|
||||
};
|
||||
|
||||
@@ -23,9 +23,7 @@ LegacyApiServerConfig LegacyApiServerConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
LegacyApiServerConfig config;
|
||||
|
||||
config.name = json.value(configKey::name).toString();
|
||||
config.description = json.value(configKey::description).toString();
|
||||
config.displayName = json.value(configKey::displayName).toString();
|
||||
config.hostName = json.value(configKey::hostName).toString();
|
||||
|
||||
config.crc = json.value(configKey::crc).toInt(0);
|
||||
|
||||
@@ -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,267 @@
|
||||
#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;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
XrayXhttpConfig clearedXhttpConfig()
|
||||
{
|
||||
XrayXhttpConfig c;
|
||||
c.mode = QString();
|
||||
c.host = QString();
|
||||
c.path = QString();
|
||||
c.headersTemplate = QString();
|
||||
c.uplinkMethod = QString();
|
||||
c.disableGrpc = false;
|
||||
c.disableSse = false;
|
||||
c.sessionPlacement = QString();
|
||||
c.sessionKey = QString();
|
||||
c.seqPlacement = QString();
|
||||
c.seqKey = QString();
|
||||
c.uplinkDataPlacement = QString();
|
||||
c.uplinkDataKey = QString();
|
||||
c.uplinkChunkSize = QString();
|
||||
c.scMaxBufferedPosts = QString();
|
||||
c.scMaxEachPostBytesMin = QString();
|
||||
c.scMaxEachPostBytesMax = QString();
|
||||
c.scMinPostsIntervalMsMin = QString();
|
||||
c.scMinPostsIntervalMsMax = QString();
|
||||
c.scStreamUpServerSecsMin = QString();
|
||||
c.scStreamUpServerSecsMax = QString();
|
||||
return c;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
if (json.isEmpty()) {
|
||||
return clearedXhttpConfig();
|
||||
}
|
||||
|
||||
XrayXhttpConfig c = clearedXhttpConfig();
|
||||
|
||||
if (json.contains(configKey::xhttpMode)) {
|
||||
c.mode = json.value(configKey::xhttpMode).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpHost)) {
|
||||
c.host = json.value(configKey::xhttpHost).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpPath)) {
|
||||
c.path = json.value(configKey::xhttpPath).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpHeadersTemplate)) {
|
||||
c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpUplinkMethod)) {
|
||||
c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpDisableGrpc)) {
|
||||
c.disableGrpc = json.value(configKey::xhttpDisableGrpc).toBool();
|
||||
}
|
||||
if (json.contains(configKey::xhttpDisableSse)) {
|
||||
c.disableSse = json.value(configKey::xhttpDisableSse).toBool();
|
||||
}
|
||||
if (json.contains(configKey::xhttpSessionPlacement)) {
|
||||
c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpSessionKey)) {
|
||||
c.sessionKey = json.value(configKey::xhttpSessionKey).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpSeqPlacement)) {
|
||||
c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpSeqKey)) {
|
||||
c.seqKey = json.value(configKey::xhttpSeqKey).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpUplinkDataPlacement)) {
|
||||
c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpUplinkDataKey)) {
|
||||
c.uplinkDataKey = json.value(configKey::xhttpUplinkDataKey).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpUplinkChunkSize)) {
|
||||
c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMaxBufferedPosts)) {
|
||||
c.scMaxBufferedPosts = json.value(configKey::xhttpScMaxBufferedPosts).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMaxEachPostBytesMin)) {
|
||||
c.scMaxEachPostBytesMin = json.value(configKey::xhttpScMaxEachPostBytesMin).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMaxEachPostBytesMax)) {
|
||||
c.scMaxEachPostBytesMax = json.value(configKey::xhttpScMaxEachPostBytesMax).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMinPostsIntervalMsMin)) {
|
||||
c.scMinPostsIntervalMsMin = json.value(configKey::xhttpScMinPostsIntervalMsMin).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMinPostsIntervalMsMax)) {
|
||||
c.scMinPostsIntervalMsMax = json.value(configKey::xhttpScMinPostsIntervalMsMax).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScStreamUpServerSecsMin)) {
|
||||
c.scStreamUpServerSecsMin = json.value(configKey::xhttpScStreamUpServerSecsMin).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScStreamUpServerSecsMax)) {
|
||||
c.scStreamUpServerSecsMax = json.value(configKey::xhttpScStreamUpServerSecsMax).toString();
|
||||
}
|
||||
|
||||
if (json.contains(QLatin1String("xPadding"))) {
|
||||
c.xPadding = XrayXPaddingConfig::fromJson(json.value(QLatin1String("xPadding")).toObject());
|
||||
}
|
||||
if (json.contains(QLatin1String("xmux"))) {
|
||||
c.xmux = XrayXmuxConfig::fromJson(json.value(QLatin1String("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;
|
||||
if (json.isEmpty()) {
|
||||
return c;
|
||||
}
|
||||
if (json.contains(configKey::mkcpTti)) {
|
||||
c.tti = json.value(configKey::mkcpTti).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpUplinkCapacity)) {
|
||||
c.uplinkCapacity = json.value(configKey::mkcpUplinkCapacity).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpDownlinkCapacity)) {
|
||||
c.downlinkCapacity = json.value(configKey::mkcpDownlinkCapacity).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpReadBufferSize)) {
|
||||
c.readBufferSize = json.value(configKey::mkcpReadBufferSize).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpWriteBufferSize)) {
|
||||
c.writeBufferSize = json.value(configKey::mkcpWriteBufferSize).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpCongestion)) {
|
||||
c.congestion = json.value(configKey::mkcpCongestion).toBool();
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
QJsonObject XrayServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
|
||||
|
||||
// Existing fields
|
||||
if (!port.isEmpty()) {
|
||||
obj[configKey::port] = port;
|
||||
}
|
||||
@@ -29,60 +276,124 @@ 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;
|
||||
}
|
||||
const QJsonObject xhttpObj = xhttp.toJson();
|
||||
if (!xhttpObj.isEmpty()) {
|
||||
obj[QStringLiteral("xhttp")] = xhttpObj;
|
||||
}
|
||||
const QJsonObject mkcpObj = mkcp.toJson();
|
||||
if (!mkcpObj.isEmpty()) {
|
||||
obj[QStringLiteral("mkcp")] = mkcpObj;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (json.contains(configKey::xraySecurity)) {
|
||||
c.security = json.value(configKey::xraySecurity).toString();
|
||||
}
|
||||
if (json.contains(configKey::xrayFlow)) {
|
||||
c.flow = json.value(configKey::xrayFlow).toString();
|
||||
}
|
||||
if (json.contains(configKey::xrayFingerprint)) {
|
||||
c.fingerprint = json.value(configKey::xrayFingerprint).toString();
|
||||
if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
}
|
||||
}
|
||||
if (json.contains(configKey::xraySni)) {
|
||||
c.sni = json.value(configKey::xraySni).toString();
|
||||
}
|
||||
if (json.contains(configKey::xrayAlpn)) {
|
||||
c.alpn = json.value(configKey::xrayAlpn).toString();
|
||||
}
|
||||
if (json.contains(configKey::xrayTransport)) {
|
||||
c.transport = json.value(configKey::xrayTransport).toString();
|
||||
}
|
||||
if (json.contains(QLatin1String("xhttp"))) {
|
||||
const QJsonObject xhttpJson = json.value(QLatin1String("xhttp")).toObject();
|
||||
if (!xhttpJson.isEmpty()) {
|
||||
c.xhttp = XrayXhttpConfig::fromJson(xhttpJson);
|
||||
}
|
||||
}
|
||||
if (json.contains(QLatin1String("mkcp"))) {
|
||||
const QJsonObject mkcpJson = json.value(QLatin1String("mkcp")).toObject();
|
||||
if (!mkcpJson.isEmpty()) {
|
||||
c.mkcp = XrayMkcpConfig::fromJson(mkcpJson);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
&& alpn == other.alpn
|
||||
&& xhttp.toJson() == other.xhttp.toJson()
|
||||
&& mkcp.toJson() == other.mkcp.toJson();
|
||||
}
|
||||
|
||||
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 +411,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 +422,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 +440,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 +466,159 @@ 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;
|
||||
|
||||
c.needsClientHydration =
|
||||
c.hasClientConfig()
|
||||
&& (!json.contains(configKey::xrayTransport) || c.serverConfig.isThirdPartyConfig);
|
||||
if (c.needsClientHydration) {
|
||||
c.hydrateServerConfigFromClientNative();
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
bool XrayProtocolConfig::hydrateServerConfigFromClientNative()
|
||||
{
|
||||
if (!clientConfig.has_value() || clientConfig->nativeConfig.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(clientConfig->nativeConfig.toUtf8());
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonObject root = doc.object();
|
||||
const QJsonArray outbounds = root.value(protocols::xray::outbounds).toArray();
|
||||
if (outbounds.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonObject outbound = outbounds[0].toObject();
|
||||
const QJsonObject streamSettings = outbound.value(protocols::xray::streamSettings).toObject();
|
||||
if (streamSettings.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
XrayServerConfig &srv = serverConfig;
|
||||
|
||||
const QJsonObject settings = outbound.value(protocols::xray::settings).toObject();
|
||||
const QJsonArray vnext = settings.value(protocols::xray::vnext).toArray();
|
||||
if (!vnext.isEmpty()) {
|
||||
const QJsonObject vnextEntry = vnext[0].toObject();
|
||||
if (vnextEntry.contains(protocols::xray::port)) {
|
||||
srv.port = QString::number(vnextEntry.value(protocols::xray::port).toInt());
|
||||
}
|
||||
const QJsonArray users = vnextEntry.value(protocols::xray::users).toArray();
|
||||
if (!users.isEmpty()) {
|
||||
srv.flow = users[0].toObject().value(protocols::xray::flow).toString();
|
||||
}
|
||||
}
|
||||
|
||||
const QString networkVal = streamSettings.value(protocols::xray::network).toString(QStringLiteral("tcp"));
|
||||
if (networkVal == QLatin1String("xhttp")) {
|
||||
srv.transport = QStringLiteral("xhttp");
|
||||
} else if (networkVal == QLatin1String("kcp")) {
|
||||
srv.transport = QStringLiteral("mkcp");
|
||||
} else {
|
||||
srv.transport = QStringLiteral("raw");
|
||||
}
|
||||
|
||||
if (streamSettings.contains(protocols::xray::security)) {
|
||||
srv.security = streamSettings.value(protocols::xray::security).toString();
|
||||
}
|
||||
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
const QJsonObject rs = streamSettings.value(protocols::xray::realitySettings).toObject();
|
||||
srv.sni = rs.value(protocols::xray::serverName).toString();
|
||||
srv.site = srv.sni.isEmpty() ? srv.site : srv.sni;
|
||||
const QString fp = rs.value(protocols::xray::fingerprint).toString();
|
||||
if (!fp.isEmpty()) {
|
||||
srv.fingerprint = fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)
|
||||
? QString::fromLatin1(protocols::xray::defaultFingerprint)
|
||||
: fp;
|
||||
}
|
||||
}
|
||||
|
||||
if (srv.security == QLatin1String("tls")) {
|
||||
const QJsonObject tls = streamSettings.value(QStringLiteral("tlsSettings")).toObject();
|
||||
srv.sni = tls.value(protocols::xray::serverName).toString();
|
||||
const QString fp = tls.value(protocols::xray::fingerprint).toString();
|
||||
if (!fp.isEmpty()) {
|
||||
srv.fingerprint = fp;
|
||||
}
|
||||
QStringList alpnList;
|
||||
for (const QJsonValue &v : tls.value(QStringLiteral("alpn")).toArray()) {
|
||||
alpnList << v.toString();
|
||||
}
|
||||
if (!alpnList.isEmpty()) {
|
||||
srv.alpn = alpnList.join(QLatin1Char(','));
|
||||
}
|
||||
}
|
||||
|
||||
if (srv.transport == QLatin1String("xhttp")) {
|
||||
const QJsonObject xhttpObj = streamSettings.value(QStringLiteral("xhttpSettings")).toObject();
|
||||
QJsonObject xhttpJson;
|
||||
const QString mode = xhttpObj.value(QStringLiteral("mode")).toString();
|
||||
if (!mode.isEmpty()) {
|
||||
if (mode == QLatin1String("auto")) {
|
||||
xhttpJson[configKey::xhttpMode] = QStringLiteral("Auto");
|
||||
} else if (mode == QLatin1String("packet-up")) {
|
||||
xhttpJson[configKey::xhttpMode] = QStringLiteral("Packet-up");
|
||||
} else if (mode == QLatin1String("stream-up")) {
|
||||
xhttpJson[configKey::xhttpMode] = QStringLiteral("Stream-up");
|
||||
} else if (mode == QLatin1String("stream-one")) {
|
||||
xhttpJson[configKey::xhttpMode] = QStringLiteral("Stream-one");
|
||||
} else {
|
||||
xhttpJson[configKey::xhttpMode] = mode;
|
||||
}
|
||||
}
|
||||
if (xhttpObj.contains(QStringLiteral("host"))) {
|
||||
xhttpJson[configKey::xhttpHost] = xhttpObj.value(QStringLiteral("host")).toString();
|
||||
}
|
||||
if (xhttpObj.contains(QStringLiteral("path"))) {
|
||||
xhttpJson[configKey::xhttpPath] = xhttpObj.value(QStringLiteral("path")).toString();
|
||||
}
|
||||
if (xhttpObj.contains(QStringLiteral("uplinkHTTPMethod"))) {
|
||||
xhttpJson[configKey::xhttpUplinkMethod] = xhttpObj.value(QStringLiteral("uplinkHTTPMethod")).toString();
|
||||
}
|
||||
xhttpJson[configKey::xhttpDisableGrpc] = xhttpObj.value(QStringLiteral("noGRPCHeader")).toBool(true);
|
||||
xhttpJson[configKey::xhttpDisableSse] = xhttpObj.value(QStringLiteral("noSSEHeader")).toBool(true);
|
||||
srv.xhttp = XrayXhttpConfig::fromJson(xhttpJson);
|
||||
}
|
||||
|
||||
if (srv.transport == QLatin1String("mkcp")) {
|
||||
const QJsonObject kcpObj = streamSettings.value(QStringLiteral("kcpSettings")).toObject();
|
||||
XrayMkcpConfig mk;
|
||||
if (kcpObj.contains(QStringLiteral("tti"))) {
|
||||
mk.tti = QString::number(kcpObj.value(QStringLiteral("tti")).toInt());
|
||||
}
|
||||
if (kcpObj.contains(QStringLiteral("uplinkCapacity"))) {
|
||||
mk.uplinkCapacity = QString::number(kcpObj.value(QStringLiteral("uplinkCapacity")).toInt());
|
||||
}
|
||||
if (kcpObj.contains(QStringLiteral("downlinkCapacity"))) {
|
||||
mk.downlinkCapacity = QString::number(kcpObj.value(QStringLiteral("downlinkCapacity")).toInt());
|
||||
}
|
||||
if (kcpObj.contains(QStringLiteral("readBufferSize"))) {
|
||||
mk.readBufferSize = QString::number(kcpObj.value(QStringLiteral("readBufferSize")).toInt());
|
||||
}
|
||||
if (kcpObj.contains(QStringLiteral("writeBufferSize"))) {
|
||||
mk.writeBufferSize = QString::number(kcpObj.value(QStringLiteral("writeBufferSize")).toInt());
|
||||
}
|
||||
if (kcpObj.contains(QStringLiteral("congestion"))) {
|
||||
mk.congestion = kcpObj.value(QStringLiteral("congestion")).toBool(true);
|
||||
}
|
||||
srv.mkcp = mk;
|
||||
}
|
||||
|
||||
needsClientHydration = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XrayProtocolConfig::hasClientConfig() const
|
||||
@@ -173,7 +626,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 +637,3 @@ void XrayProtocolConfig::clearClientConfig()
|
||||
}
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
|
||||
@@ -2,47 +2,148 @@
|
||||
#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;
|
||||
/// Reads only keys present in JSON (no Amnezia UI defaults). Use XrayConfigModel::applyDefaultsToServerConfig for UI.
|
||||
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;
|
||||
|
||||
|
||||
QString security;
|
||||
QString flow;
|
||||
QString fingerprint;
|
||||
QString sni;
|
||||
QString alpn;
|
||||
|
||||
QString transport;
|
||||
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();
|
||||
|
||||
bool needsClientHydration = false;
|
||||
|
||||
bool hydrateServerConfigFromClientNative();
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // XRAYPROTOCOLCONFIG_H
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -28,6 +29,20 @@ ContainerConfig NativeServerConfig::containerConfig(DockerContainer container) c
|
||||
return containers.value(container);
|
||||
}
|
||||
|
||||
QPair<QString, QString> NativeServerConfig::getDnsPair(const QString &primaryDns, const QString &secondaryDns) const
|
||||
{
|
||||
QString d1 = dns1;
|
||||
QString d2 = dns2;
|
||||
|
||||
if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) {
|
||||
d1 = primaryDns;
|
||||
}
|
||||
if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) {
|
||||
d2 = secondaryDns;
|
||||
}
|
||||
return { d1, d2 };
|
||||
}
|
||||
|
||||
QJsonObject NativeServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
@@ -35,9 +50,6 @@ QJsonObject NativeServerConfig::toJson() const
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = this->description;
|
||||
}
|
||||
if (!displayName.isEmpty()) {
|
||||
obj[configKey::displayName] = displayName;
|
||||
}
|
||||
if (!hostName.isEmpty()) {
|
||||
obj[configKey::hostName] = hostName;
|
||||
}
|
||||
@@ -70,7 +82,6 @@ NativeServerConfig NativeServerConfig::fromJson(const QJsonObject& json)
|
||||
NativeServerConfig config;
|
||||
|
||||
config.description = json.value(configKey::description).toString();
|
||||
config.displayName = json.value(configKey::displayName).toString();
|
||||
config.hostName = json.value(configKey::hostName).toString();
|
||||
|
||||
QJsonArray containersArray = json.value(configKey::containers).toArray();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QPair>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
@@ -25,6 +26,9 @@ struct NativeServerConfig {
|
||||
|
||||
bool hasContainers() const;
|
||||
ContainerConfig containerConfig(DockerContainer container) const;
|
||||
|
||||
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static NativeServerConfig fromJson(const QJsonObject& json);
|
||||
};
|
||||
|
||||
@@ -87,9 +87,6 @@ QJsonObject SelfHostedAdminServerConfig::toJson() const
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = this->description;
|
||||
}
|
||||
if (!displayName.isEmpty()) {
|
||||
obj[configKey::displayName] = displayName;
|
||||
}
|
||||
if (!hostName.isEmpty()) {
|
||||
obj[configKey::hostName] = hostName;
|
||||
}
|
||||
@@ -132,7 +129,6 @@ SelfHostedAdminServerConfig SelfHostedAdminServerConfig::fromJson(const QJsonObj
|
||||
SelfHostedAdminServerConfig config;
|
||||
|
||||
config.description = json.value(configKey::description).toString();
|
||||
config.displayName = json.value(configKey::displayName).toString();
|
||||
config.hostName = json.value(configKey::hostName).toString();
|
||||
|
||||
QJsonArray containersArray = json.value(configKey::containers).toArray();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -42,6 +43,21 @@ ContainerConfig SelfHostedUserServerConfig::containerConfig(DockerContainer cont
|
||||
return containers.value(container);
|
||||
}
|
||||
|
||||
QPair<QString, QString> SelfHostedUserServerConfig::getDnsPair(const QString &primaryDns,
|
||||
const QString &secondaryDns) const
|
||||
{
|
||||
QString d1 = dns1;
|
||||
QString d2 = dns2;
|
||||
|
||||
if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) {
|
||||
d1 = primaryDns;
|
||||
}
|
||||
if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) {
|
||||
d2 = secondaryDns;
|
||||
}
|
||||
return { d1, d2 };
|
||||
}
|
||||
|
||||
QJsonObject SelfHostedUserServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
@@ -49,9 +65,6 @@ QJsonObject SelfHostedUserServerConfig::toJson() const
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = this->description;
|
||||
}
|
||||
if (!displayName.isEmpty()) {
|
||||
obj[configKey::displayName] = displayName;
|
||||
}
|
||||
if (!hostName.isEmpty()) {
|
||||
obj[configKey::hostName] = hostName;
|
||||
}
|
||||
@@ -84,7 +97,6 @@ SelfHostedUserServerConfig SelfHostedUserServerConfig::fromJson(const QJsonObjec
|
||||
SelfHostedUserServerConfig config;
|
||||
|
||||
config.description = json.value(configKey::description).toString();
|
||||
config.displayName = json.value(configKey::displayName).toString();
|
||||
config.hostName = json.value(configKey::hostName).toString();
|
||||
|
||||
QJsonArray containersArray = json.value(configKey::containers).toArray();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QPair>
|
||||
#include <optional>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
@@ -30,6 +31,9 @@ struct SelfHostedUserServerConfig {
|
||||
std::optional<ServerCredentials> credentials() const;
|
||||
bool hasContainers() const;
|
||||
ContainerConfig containerConfig(DockerContainer container) const;
|
||||
|
||||
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static SelfHostedUserServerConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
@@ -85,15 +84,14 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
const int httpStatusCodeNotFound = 404;
|
||||
const int httpStatusCodeNotImplemented = 501;
|
||||
const int httpStatusCodePaymentRequired = 402;
|
||||
const int httpStatusCodeTooManyRequests = 429;
|
||||
const int httpStatusCodeRequestTimeout = 408;
|
||||
const int httpStatusCodeUnprocessableEntity = 422;
|
||||
|
||||
if (!sslErrors.empty()) {
|
||||
qDebug().noquote() << sslErrors;
|
||||
return amnezia::ErrorCode::ApiConfigSslError;
|
||||
}
|
||||
if (replyError == QNetworkReply::NoError) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
}
|
||||
if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||
qDebug() << replyError;
|
||||
@@ -104,14 +102,14 @@ 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();
|
||||
const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
|
||||
|
||||
if (httpStatusFromBody == httpStatusCodeTooManyRequests) {
|
||||
return amnezia::ErrorCode::ApiRateLimitError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeConflict) {
|
||||
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
|
||||
@@ -121,6 +119,9 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
if (httpStatusFromBody == httpStatusCodeNotFound) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeRequestTimeout) {
|
||||
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
}
|
||||
@@ -131,9 +132,28 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
|
||||
const QString message = apiErrorMessageFromJson(jsonObj);
|
||||
if (message.contains(QLatin1String("refresh_captcha"), Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaRefreshError;
|
||||
}
|
||||
if (message.contains(QLatin1String("invalid_captcha"), Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaInvalidError;
|
||||
}
|
||||
if (jsonObj.contains(QStringLiteral("captcha_id")) || jsonObj.contains(QStringLiteral("captcha_image"))
|
||||
|| message.compare(QLatin1String("rate_limit_exceeded"), Qt::CaseInsensitive) == 0
|
||||
|| message.contains(QLatin1String("rate_limit_exceeded"), Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaRequiredError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
|
||||
if (httpStatusFromBody >= 300) {
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
}
|
||||
|
||||
if (replyError == QNetworkReply::NoError) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
}
|
||||
|
||||
qDebug() << "something went wrong";
|
||||
@@ -233,18 +253,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ namespace amnezia
|
||||
ServerCgroupMountpoint = 212,
|
||||
DockerPullRateLimit = 213,
|
||||
ServerLinuxKernelTooOld = 214,
|
||||
XrayServerConfigInvalid = 215,
|
||||
XrayServerNoVlessClients = 216,
|
||||
XrayRealityKeysReadFailed = 217,
|
||||
|
||||
// Ssh connection errors
|
||||
SshRequestDeniedError = 300,
|
||||
@@ -98,6 +101,10 @@ namespace amnezia
|
||||
ApiSubscriptionNotActiveError = 1114,
|
||||
ApiNoPurchasedSubscriptionsError = 1115,
|
||||
ApiTrialAlreadyUsedError = 1116,
|
||||
ApiCaptchaRequiredError = 1117,
|
||||
ApiCaptchaInvalidError = 1118,
|
||||
ApiCaptchaRefreshError = 1119,
|
||||
ApiRateLimitError = 1120,
|
||||
|
||||
// QFile errors
|
||||
OpenError = 1200,
|
||||
|
||||
@@ -30,6 +30,15 @@ QString errorString(ErrorCode code) {
|
||||
case(ErrorCode::ServerCgroupMountpoint): errorMessage = QObject::tr("Server error: cgroup mountpoint does not exist"); break;
|
||||
case(ErrorCode::DockerPullRateLimit): errorMessage = QObject::tr("Docker error: The pull rate limit has been reached"); break;
|
||||
case(ErrorCode::ServerLinuxKernelTooOld): errorMessage = QObject::tr("Server error: Linux kernel is too old"); break;
|
||||
case(ErrorCode::XrayServerConfigInvalid):
|
||||
errorMessage = QObject::tr("Server error: invalid or unreadable XRay server configuration");
|
||||
break;
|
||||
case(ErrorCode::XrayServerNoVlessClients):
|
||||
errorMessage = QObject::tr("Server error: XRay server has no VLESS clients");
|
||||
break;
|
||||
case(ErrorCode::XrayRealityKeysReadFailed):
|
||||
errorMessage = QObject::tr("Server error: failed to read XRay Reality keys from the server");
|
||||
break;
|
||||
|
||||
// Libssh errors
|
||||
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
|
||||
@@ -84,6 +93,10 @@ QString errorString(ErrorCode code) {
|
||||
case (ErrorCode::ApiSubscriptionNotActiveError): errorMessage = QObject::tr("No active subscription found"); break;
|
||||
case (ErrorCode::ApiNoPurchasedSubscriptionsError): errorMessage = QObject::tr("No purchased subscriptions found. Please purchase a subscription first"); break;
|
||||
case (ErrorCode::ApiTrialAlreadyUsedError): errorMessage = QObject::tr("This email address has already been used to activate a trial"); break;
|
||||
case (ErrorCode::ApiCaptchaRequiredError): errorMessage = QObject::tr("CAPTCHA verification is required"); break;
|
||||
case (ErrorCode::ApiCaptchaInvalidError): errorMessage = QObject::tr("CAPTCHA was incorrect. Please try again"); break;
|
||||
case (ErrorCode::ApiCaptchaRefreshError): errorMessage = QObject::tr("CAPTCHA refreshed. Please try again"); break;
|
||||
case (ErrorCode::ApiRateLimitError): errorMessage = QObject::tr("Too many requests. Please try again later"); break;
|
||||
|
||||
// QFile errors
|
||||
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
||||
|
||||
@@ -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,90 @@ 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_REGENERATE_SECRET",
|
||||
c.secret.isEmpty() ? QStringLiteral("1") : QStringLiteral("0")}});
|
||||
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_REGENERATE_SECRET",
|
||||
c.secret.isEmpty() ? QStringLiteral("1") : QStringLiteral("0") } });
|
||||
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 +395,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,7 +1,8 @@
|
||||
if which apt-get > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/dpkg/lock-frontend";\
|
||||
elif which dnf > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/cache/dnf/* /var/run/dnf/* /var/lib/dnf/* /var/lib/rpm/*";\
|
||||
elif which yum > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/yum.pid";\
|
||||
elif which zypper > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/zypp.pid";\
|
||||
elif which pacman > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/pacman/db.lck";\
|
||||
else echo "Packet manager not found"; echo "Internal error"; exit 1; fi;\
|
||||
if command -v $LOCK_CMD > /dev/null 2>&1; then sudo $LOCK_CMD $LOCK_FILE 2>/dev/null; else echo "$LOCK_CMD not installed"; fi
|
||||
if which apt-get > /dev/null 2>&1 || command -v apt-get > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/dpkg/lock-frontend";\
|
||||
elif which dnf > /dev/null 2>&1 || command -v dnf > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/cache/dnf/* /var/run/dnf/* /var/lib/dnf/* /var/lib/rpm/*";\
|
||||
elif which yum > /dev/null 2>&1 || command -v yum > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/yum.pid";\
|
||||
elif which zypper > /dev/null 2>&1 || command -v zypper > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/zypp.pid";\
|
||||
elif which pacman > /dev/null 2>&1 || command -v pacman > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/pacman/db.lck";\
|
||||
else echo "Packet manager not found"; echo "Internal error"; exit 1;\
|
||||
fi;\
|
||||
if which $LOCK_CMD > /dev/null 2>&1 || command -v $LOCK_CMD > /dev/null 2>&1; then sudo $LOCK_CMD $LOCK_FILE 2>/dev/null; else echo "$LOCK_CMD not installed"; fi
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); opt="--version";\
|
||||
elif which dnf > /dev/null 2>&1; then pm=$(which dnf); opt="--version";\
|
||||
elif which yum > /dev/null 2>&1; then pm=$(which yum); opt="--version";\
|
||||
elif which zypper > /dev/null 2>&1; then pm=$(which zypper); opt="--version";\
|
||||
elif which pacman > /dev/null 2>&1; then pm=$(which pacman); opt="--version";\
|
||||
if pm=$(which apt-get 2>/dev/null || command -v apt-get 2>/dev/null); then opt="--version";\
|
||||
elif pm=$(which dnf 2>/dev/null || command -v dnf 2>/dev/null); then opt="--version";\
|
||||
elif pm=$(which yum 2>/dev/null || command -v yum 2>/dev/null); then opt="--version";\
|
||||
elif pm=$(which zypper 2>/dev/null || command -v zypper 2>/dev/null); then opt="--version";\
|
||||
elif pm=$(which pacman 2>/dev/null || command -v pacman 2>/dev/null); then opt="--version";\
|
||||
else pm="uname"; opt="-a";\
|
||||
fi;\
|
||||
CUR_USER=$(whoami 2>/dev/null || echo $HOME | sed 's/.*\///');\
|
||||
|
||||
@@ -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 [""]
|
||||
62
client/server_scripts/mtproxy/configure_container.sh
Normal file
62
client/server_scripts/mtproxy/configure_container.sh
Normal file
@@ -0,0 +1,62 @@
|
||||
#!/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: regenerate (fresh install) -> env var -> saved file -> generate new
|
||||
if [ "$MTPROXY_REGENERATE_SECRET" = "1" ]; then
|
||||
SECRET=$(openssl rand -hex 16)
|
||||
elif [ -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
|
||||
@@ -1,3 +1,4 @@
|
||||
sudo docker stop $CONTAINER_NAME;\
|
||||
sudo docker rm -fv $CONTAINER_NAME;\
|
||||
sudo docker rmi $CONTAINER_NAME
|
||||
sudo docker rmi $CONTAINER_NAME;\
|
||||
test "$REMOVE_CONTAINER_DATA" = "1" && sudo docker volume rm -f ${CONTAINER_NAME}-data 2>/dev/null || true
|
||||
|
||||
@@ -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 [""]
|
||||
75
client/server_scripts/telemt/configure_container.sh
Normal file
75
client/server_scripts/telemt/configure_container.sh
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/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: regenerate (fresh install) -> env var -> saved file -> openssl
|
||||
if [ "$TELEMT_REGENERATE_SECRET" = "1" ]; then
|
||||
SECRET=$(openssl rand -hex 16)
|
||||
elif [ -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
|
||||
@@ -1,145 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.25.0)
|
||||
|
||||
project(AmneziaVPN_Tests)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Test)
|
||||
|
||||
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
qt6_add_resources(TEST_QRC
|
||||
${CLIENT_ROOT_DIR}/server_scripts/serverScripts.qrc
|
||||
)
|
||||
|
||||
add_library(test_common OBJECT
|
||||
${SOURCES}
|
||||
${HEADERS}
|
||||
${TEST_QRC}
|
||||
)
|
||||
|
||||
qt_add_repc_replicas(test_common
|
||||
${CLIENT_ROOT_DIR}/../ipc/ipc_interface.rep
|
||||
${CLIENT_ROOT_DIR}/../ipc/ipc_process_interface.rep
|
||||
)
|
||||
|
||||
target_link_libraries(test_common PUBLIC
|
||||
${LIBS}
|
||||
)
|
||||
|
||||
target_include_directories(test_common PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/..
|
||||
${CMAKE_CURRENT_BINARY_DIR}/..
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
|
||||
add_executable(test_import_export
|
||||
testAdminSelfHostedExport.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_import_export PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_multiple_imports
|
||||
testMultipleImports.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_multiple_imports PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_server_edit
|
||||
testServerEdit.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_server_edit PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_default_server_change
|
||||
testDefaultServerChange.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_default_server_change PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_server_edge_cases
|
||||
testServerEdgeCases.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_server_edge_cases PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_signal_order
|
||||
testSignalOrder.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_signal_order PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_servers_model_sync
|
||||
testServersModelSync.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_servers_model_sync PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_complex_operations
|
||||
testComplexOperations.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_complex_operations PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_settings_signals
|
||||
testSettingsSignals.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_settings_signals PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_ui_servers_model_and_controller
|
||||
testUiServersModelAndController.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_ui_servers_model_and_controller PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_self_hosted_server_setup
|
||||
testSelfHostedServerSetup.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_self_hosted_server_setup PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
enable_testing()
|
||||
add_test(NAME ImportExportTest COMMAND test_import_export)
|
||||
add_test(NAME MultipleImportsTest COMMAND test_multiple_imports)
|
||||
add_test(NAME ServerEditTest COMMAND test_server_edit)
|
||||
add_test(NAME DefaultServerChangeTest COMMAND test_default_server_change)
|
||||
add_test(NAME ServerEdgeCasesTest COMMAND test_server_edge_cases)
|
||||
add_test(NAME SignalOrderTest COMMAND test_signal_order)
|
||||
add_test(NAME ServersModelSyncTest COMMAND test_servers_model_sync)
|
||||
add_test(NAME ComplexOperationsTest COMMAND test_complex_operations)
|
||||
add_test(NAME SettingsSignalsTest COMMAND test_settings_signals)
|
||||
add_test(NAME UiServersModelAndControllerTest COMMAND test_ui_servers_model_and_controller)
|
||||
add_test(NAME SelfHostedServerSetupTest COMMAND test_self_hosted_server_setup)
|
||||
@@ -1,147 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QFile>
|
||||
#include <QDebug>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
class TestAdminSelfHostedExport : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
QJsonObject decodeVpnKey(const QString &vpnKey) {
|
||||
QString key = vpnKey;
|
||||
key.replace("vpn://", "");
|
||||
|
||||
QByteArray ba = QByteArray::fromBase64(
|
||||
key.toUtf8(),
|
||||
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals
|
||||
);
|
||||
|
||||
qDebug() << "Base64 decoded size:" << ba.size();
|
||||
|
||||
QJsonDocument testDoc = QJsonDocument::fromJson(ba);
|
||||
if (!testDoc.isNull()) {
|
||||
qDebug() << "Data is not compressed, using as-is";
|
||||
return testDoc.object();
|
||||
}
|
||||
|
||||
QByteArray baUncompressed = qUncompress(ba);
|
||||
if (!baUncompressed.isEmpty()) {
|
||||
qDebug() << "Data was compressed, uncompressed size:" << baUncompressed.size();
|
||||
ba = baUncompressed;
|
||||
} else {
|
||||
qDebug() << "qUncompress failed or data is not compressed";
|
||||
}
|
||||
|
||||
return QJsonDocument::fromJson(ba).object();
|
||||
}
|
||||
|
||||
QJsonObject sortContainers(const QJsonObject &config) {
|
||||
QJsonObject sorted = config;
|
||||
|
||||
if (!config.contains("containers")) {
|
||||
return sorted;
|
||||
}
|
||||
|
||||
QJsonArray containers = config["containers"].toArray();
|
||||
QVector<QJsonObject> containerVec;
|
||||
|
||||
for (const QJsonValue &val : containers) {
|
||||
containerVec.append(val.toObject());
|
||||
}
|
||||
|
||||
std::sort(containerVec.begin(), containerVec.end(), [](const QJsonObject &a, const QJsonObject &b) {
|
||||
return a["container"].toString() < b["container"].toString();
|
||||
});
|
||||
|
||||
QJsonArray sortedContainers;
|
||||
for (const QJsonObject &obj : containerVec) {
|
||||
sortedContainers.append(obj);
|
||||
}
|
||||
|
||||
sorted["containers"] = sortedContainers;
|
||||
return sorted;
|
||||
}
|
||||
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
}
|
||||
|
||||
void testAdminSelfHostedExport() {
|
||||
QString vpnKey = "vpn://AAABTXjarZIxT8MwEIX_Cro5jbDjQunKUhhYyoZQZZKjRGpsy3baQtT_zp2bJh3oACLLPfvz3bOe00FpTdS1QR9g_tKB3q1h3sFCwBzEdf9N5ElBBgtJqBiQOkcFoemAbs6RInQ7oNkZemAvrrKvRV9VX6fH-lhSVSwavU9GSdcmXZX0UqSbseJRMqlioDxuSsJZH1mKWTrhvI22tJvVljKoLU-TtB3aN4NxpavKYwhpSD7LRc4t0WsTeMwqNRNsKweHbAyTtnRj8KvWE0pUEut-hNah2TpDM0-Kwu8vKMSd-ttFLrntao_rVvuKWkc9OnIk4n8t915_Ulcqo5FSxa9tYsk2rxlU-K7bTby_lDWfCKWvXTy-5jOGeLVET-9L7MOG-KQbJEBx57jXjdtgXtqG_wUdws5yJhCpa1iefhopM2gD-n4An-ElHL4BvzD6nw";
|
||||
|
||||
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
qDebug() << "IMPORTED KEY:" << vpnKey;
|
||||
|
||||
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(vpnKey);
|
||||
|
||||
QVERIFY2(importResult.errorCode == ErrorCode::NoError, "Import should succeed");
|
||||
QVERIFY2(!importResult.config.isEmpty(), "Config should not be empty");
|
||||
|
||||
QJsonObject importedConfig = importResult.config;
|
||||
|
||||
m_coreController->m_importCoreController->importConfig(importedConfig);
|
||||
|
||||
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted");
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged signal should NOT be emitted (default is already 0)");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added");
|
||||
|
||||
const QString serverId = m_coreController->m_serversRepository->defaultServerId();
|
||||
auto exportResult = m_coreController->m_exportController->generateFullAccessConfig(serverId);
|
||||
|
||||
QVERIFY2(exportResult.errorCode == ErrorCode::NoError, "Export should succeed");
|
||||
QVERIFY2(!exportResult.config.isEmpty(), "Exported config should not be empty");
|
||||
|
||||
qDebug() << "EXPORTED KEY:" << exportResult.config;
|
||||
|
||||
QJsonObject exportedConfig = decodeVpnKey(exportResult.config);
|
||||
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(exportResult.config);
|
||||
QVERIFY2(importResult2.errorCode == ErrorCode::NoError, "Re-import should succeed");
|
||||
|
||||
QJsonObject sortedImported = sortContainers(importedConfig);
|
||||
QJsonObject sortedExported = sortContainers(importResult2.config);
|
||||
|
||||
QString importedJson = QJsonDocument(sortedImported).toJson(QJsonDocument::Compact);
|
||||
QString exportedJson = QJsonDocument(sortedExported).toJson(QJsonDocument::Compact);
|
||||
|
||||
qDebug() << "IMPORTED JSON:" << importedJson;
|
||||
qDebug() << "EXPORTED JSON:" << exportedJson;
|
||||
|
||||
QCOMPARE(exportedJson, importedJson);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestAdminSelfHostedExport)
|
||||
#include "testAdminSelfHostedExport.moc"
|
||||
@@ -1,111 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "tests/testServerRepositoryHelpers.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestComplexOperations : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testComplexOperationSequence() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
|
||||
QString wgKey = "vpn://AAAAwXjahY89a8NADIb_StDsHLFDIHjt0C1LhgwlBNWnpgfx3SHp6hDj_15dacnYTS_Po68ZhhQVQyQW6N_mZ4QecIz0CLieAtO1IHto4Fn3M-TEat6u3XetMSnvkfSC3jOJjYN24_audRtjyhil-pfMSZPB4jMsy7kBTx9Ybvryz2ZPMnDIGlI042TktZLVkfjLmhr4TKIHHMnodHV0xzHfyA1pNJZRZEr1alAS_Yvbin6e6LoGihD_DqhSjbB8AyB_ZI8";
|
||||
|
||||
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
|
||||
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
|
||||
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
|
||||
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult1.config);
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult2.config);
|
||||
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult3.config);
|
||||
|
||||
QVERIFY2(importFinishedSpy.count() == 3, "importFinished should be emitted 3 times");
|
||||
QVERIFY2(serverAddedSpy.count() == 3, "serverAdded should be emitted 3 times");
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 2, "defaultServerChanged should be emitted 2 times (0->1, 1->2, first import doesn't emit as default is already 0)");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 3, "Should have 3 servers");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
|
||||
|
||||
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversController->getServerId(0),
|
||||
QStringLiteral("Edited First Server"));
|
||||
|
||||
QVERIFY2(serverEditedSpy.count() == 1, "serverEdited should be emitted");
|
||||
QString editedDesc0 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(0));
|
||||
QVERIFY2(editedDesc0 == "Edited First Server", "First server should be edited");
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(1));
|
||||
|
||||
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved should be emitted");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "Should have 2 servers");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default should be index 1 (was 2, removed 1)");
|
||||
|
||||
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
|
||||
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 4, "defaultServerChanged should be emitted again");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default should be index 0");
|
||||
|
||||
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversController->getServerId(0),
|
||||
QStringLiteral("Final Edited Server"));
|
||||
|
||||
QVERIFY2(serverEditedSpy.count() == 2, "serverEdited should be emitted again");
|
||||
QString finalDesc0 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(0));
|
||||
QVERIFY2(finalDesc0 == "Final Edited Server", "First server should be edited again");
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "Final servers count should be 2");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Final default index should be 0");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 2, "Model should have 2 rows");
|
||||
QString modelDesc0 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
|
||||
QVERIFY2(modelDesc0 == "Final Edited Server", "Model should reflect final edited name");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestComplexOperations)
|
||||
#include "testComplexOperations.moc"
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "tests/testServerRepositoryHelpers.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestDefaultServerChange : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
m_coreController->m_serversRepository->invalidateCache();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testSetDefaultServerIndex() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
|
||||
QString wgKey = "vpn://AAAAwXjahY89a8NADIb_StDsHLFDIHjt0C1LhgwlBNWnpgfx3SHp6hDj_15dacnYTS_Po68ZhhQVQyQW6N_mZ4QecIz0CLieAtO1IHto4Fn3M-TEat6u3XetMSnvkfSC3jOJjYN24_audRtjyhil-pfMSZPB4jMsy7kBTx9Ybvryz2ZPMnDIGlI042TktZLVkfjLmhr4TKIHHMnodHV0xzHfyA1pNJZRZEr1alAS_Yvbin6e6LoGihD_DqhSjbB8AyB_ZI8";
|
||||
|
||||
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult1.config);
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult2.config);
|
||||
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult3.config);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 3, "Should have 3 servers");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
|
||||
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted");
|
||||
QVERIFY2(defaultServerChangedSpy.at(0).at(0).toString() == m_coreController->m_serversController->getServerId(0),
|
||||
"defaultServerChanged should emit new default server id");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
int modelDefaultIndex = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::IsDefaultRole).toBool() ? 0 : -1;
|
||||
QVERIFY2(modelDefaultIndex == 0, "Model should reflect default server");
|
||||
}
|
||||
|
||||
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(2));
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 2, "defaultServerChanged signal should be emitted again");
|
||||
QVERIFY2(defaultServerChangedSpy.at(1).at(0).toString() == m_coreController->m_serversController->getServerId(2),
|
||||
"defaultServerChanged should emit new default server id");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default server index should be 2");
|
||||
}
|
||||
|
||||
void testDefaultServerChangeOnRemoveEdgeCases() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
|
||||
QString wgKey = "vpn://AAAAwXjahY89a8NADIb_StDsHLFDIHjt0C1LhgwlBNWnpgfx3SHp6hDj_15dacnYTS_Po68ZhhQVQyQW6N_mZ4QecIz0CLieAtO1IHto4Fn3M-TEat6u3XetMSnvkfSC3jOJjYN24_audRtjyhil-pfMSZPB4jMsy7kBTx9Ybvryz2ZPMnDIGlI042TktZLVkfjLmhr4TKIHHMnodHV0xzHfyA1pNJZRZEr1alAS_Yvbin6e6LoGihD_DqhSjbB8AyB_ZI8";
|
||||
|
||||
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult1.config);
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult2.config);
|
||||
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult3.config);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 3, "Should have 3 servers");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
|
||||
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
|
||||
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "Should have 2 servers");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default should be index 1 (was 2, removed 0)");
|
||||
|
||||
QString desc1 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(0));
|
||||
QString desc2 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(1));
|
||||
QVERIFY2(desc1 == "Xray Server", "First remaining server should be Xray");
|
||||
QVERIFY2(desc2 == "WireGuard Server", "Second remaining server should be WireGuard");
|
||||
|
||||
defaultServerChangedSpy.clear();
|
||||
serverRemovedSpy.clear();
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
|
||||
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Should have 1 server");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default should be index 0 (was 1, removed 0)");
|
||||
|
||||
QString lastDesc = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(0));
|
||||
QVERIFY2(lastDesc == "WireGuard Server", "Last server should be WireGuard");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestDefaultServerChange)
|
||||
#include "testDefaultServerChange.moc"
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QDebug>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "tests/testServerRepositoryHelpers.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestMultipleImports : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
m_coreController->m_serversRepository->invalidateCache();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testMultipleImports() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
|
||||
QString wgKey = "vpn://AAAAwXjahY89a8NADIb_StDsHLFDIHjt0C1LhgwlBNWnpgfx3SHp6hDj_15dacnYTS_Po68ZhhQVQyQW6N_mZ4QecIz0CLieAtO1IHto4Fn3M-TEat6u3XetMSnvkfSC3jOJjYN24_audRtjyhil-pfMSZPB4jMsy7kBTx9Ybvryz2ZPMnDIGlI042TktZLVkfjLmhr4TKIHHMnodHV0xzHfyA1pNJZRZEr1alAS_Yvbin6e6LoGihD_DqhSjbB8AyB_ZI8";
|
||||
|
||||
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Initial servers count should be 0");
|
||||
if (m_coreController->m_serversModel) {
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "Initial model row count should be 0");
|
||||
}
|
||||
|
||||
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
QVERIFY2(importResult1.errorCode == ErrorCode::NoError, "First import should succeed");
|
||||
|
||||
m_coreController->m_importCoreController->importConfig(importResult1.config);
|
||||
|
||||
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted once");
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged signal should NOT be emitted (default is already 0)");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "After first import servers count should be 1");
|
||||
if (m_coreController->m_serversModel) {
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 1, "After first import model row count should be 1");
|
||||
}
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "First server should be default");
|
||||
|
||||
QString desc1 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(0));
|
||||
QVERIFY2(desc1 == "AWG Server", "First server description should match");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
QString modelDesc1 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
|
||||
QVERIFY2(modelDesc1 == "AWG Server", "First server description in model should match");
|
||||
}
|
||||
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
|
||||
QVERIFY2(importResult2.errorCode == ErrorCode::NoError, "Second import should succeed");
|
||||
|
||||
m_coreController->m_importCoreController->importConfig(importResult2.config);
|
||||
|
||||
QVERIFY2(importFinishedSpy.count() == 2, "importFinished signal should be emitted twice");
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted once (0->1, first import doesn't emit)");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "After second import servers count should be 2");
|
||||
if (m_coreController->m_serversModel) {
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 2, "After second import model row count should be 2");
|
||||
}
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Second server should be default");
|
||||
|
||||
QString desc2 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(1));
|
||||
QVERIFY2(desc2 == "Xray Server", "Second server description should match");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
QString modelDesc2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(1, 0), ServersModel::NameRole).toString();
|
||||
QVERIFY2(modelDesc2 == "Xray Server", "Second server description in model should match");
|
||||
}
|
||||
|
||||
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
|
||||
QVERIFY2(importResult3.errorCode == ErrorCode::NoError, "Third import should succeed");
|
||||
|
||||
m_coreController->m_importCoreController->importConfig(importResult3.config);
|
||||
|
||||
QVERIFY2(importFinishedSpy.count() == 3, "importFinished signal should be emitted three times");
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 2, "defaultServerChanged signal should be emitted twice (0->1, 1->2, first import doesn't emit)");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 3, "After third import servers count should be 3");
|
||||
if (m_coreController->m_serversModel) {
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 3, "After third import model row count should be 3");
|
||||
}
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Third server should be default");
|
||||
|
||||
QString desc3 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(2));
|
||||
QVERIFY2(desc3 == "WireGuard Server", "Third server description should match");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
QString modelDesc3 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(2, 0), ServersModel::NameRole).toString();
|
||||
QVERIFY2(modelDesc3 == "WireGuard Server", "Third server description in model should match");
|
||||
}
|
||||
}
|
||||
|
||||
void testMultipleImportsRemoval() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
|
||||
|
||||
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Initial servers count should be 0");
|
||||
|
||||
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
QVERIFY2(importResult1.errorCode == ErrorCode::NoError, "First import should succeed");
|
||||
m_coreController->m_importCoreController->importConfig(importResult1.config);
|
||||
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
|
||||
QVERIFY2(importResult2.errorCode == ErrorCode::NoError, "Second import should succeed");
|
||||
m_coreController->m_importCoreController->importConfig(importResult2.config);
|
||||
|
||||
QVERIFY2(importFinishedSpy.count() == 2, "importFinished signal should be emitted twice");
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted once (0->1, first import doesn't emit)");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "After two imports servers count should be 2");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Second server should be default");
|
||||
|
||||
QString desc0 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(0));
|
||||
QString desc1 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(1));
|
||||
QVERIFY2(desc0 == "AWG Server", "First server description should match");
|
||||
QVERIFY2(desc1 == "Xray Server", "Second server description should match");
|
||||
|
||||
defaultServerChangedSpy.clear();
|
||||
serverRemovedSpy.clear();
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
|
||||
|
||||
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
|
||||
QVERIFY2(serverRemovedSpy.at(0).at(1).toInt() == 0, "serverRemoved should emit removed index 0");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "After removing first server, servers count should be 1");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "After removing first server, default index should be 0");
|
||||
|
||||
QString remainingDesc = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(0));
|
||||
QVERIFY2(remainingDesc == "Xray Server", "Remaining server should be Xray Server");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 1, "After removing first server, model row count should be 1");
|
||||
QString modelDesc = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
|
||||
QVERIFY2(modelDesc == "Xray Server", "Remaining server description in model should match");
|
||||
}
|
||||
|
||||
defaultServerChangedSpy.clear();
|
||||
serverRemovedSpy.clear();
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
|
||||
|
||||
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
|
||||
QVERIFY2(serverRemovedSpy.at(0).at(1).toInt() == 0, "serverRemoved should emit removed index 0");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "After removing last server, servers count should be 0");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "After removing last server, default index should be 0");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "After removing last server, model row count should be 0");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestMultipleImports)
|
||||
#include "testMultipleImports.moc"
|
||||
@@ -1,385 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QDebug>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/models/protocols/awgProtocolConfig.h"
|
||||
#include "core/models/protocols/dnsProtocolConfig.h"
|
||||
#include "core/utils/commonStructs.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/errorCodes.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestSelfHostedServerSetup : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
ServerCredentials getCredentialsFromEnv() {
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
|
||||
QString hostName = env.value("TEST_SERVER_HOST");
|
||||
QString userName = env.value("TEST_SERVER_USER");
|
||||
QString password = env.value("TEST_SERVER_PASSWORD");
|
||||
QString portStr = env.value("TEST_SERVER_PORT", "22");
|
||||
int port = portStr.toInt();
|
||||
|
||||
ServerCredentials credentials;
|
||||
credentials.hostName = hostName;
|
||||
credentials.userName = userName;
|
||||
credentials.secretData = password;
|
||||
credentials.port = port;
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
void verifySshConnection(const ServerCredentials& credentials) {
|
||||
QString sshOutput;
|
||||
ErrorCode sshError = m_coreController->m_installController->checkSshConnection(credentials, sshOutput);
|
||||
QVERIFY2(sshError == ErrorCode::NoError,
|
||||
QString("SSH connection should succeed. Error: %1, Output: %2")
|
||||
.arg(static_cast<int>(sshError))
|
||||
.arg(sshOutput)
|
||||
.toUtf8().constData());
|
||||
qDebug() << "SSH connection successful. Output:" << sshOutput;
|
||||
}
|
||||
|
||||
void verifyAdminAccess(int serverIndex)
|
||||
{
|
||||
const QString serverId = m_coreController->m_serversRepository->serverIdAt(serverIndex);
|
||||
const auto adminCfg = m_coreController->m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
QVERIFY2(adminCfg.has_value(), "Server config should be SelfHostedAdminServerConfig");
|
||||
|
||||
const SelfHostedAdminServerConfig &selfHosted = *adminCfg;
|
||||
|
||||
QVERIFY2(selfHosted.hasCredentials(),
|
||||
"Server should have credentials (admin access)");
|
||||
|
||||
QVERIFY2(!selfHosted.userName.isEmpty(),
|
||||
"Server should have userName for admin access");
|
||||
|
||||
QVERIFY2(!selfHosted.password.isEmpty(),
|
||||
"Server should have password for admin access");
|
||||
|
||||
QVERIFY2(!selfHosted.isReadOnly(),
|
||||
"Server should not be read-only (should have admin access)");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
bool hasWriteAccess = m_coreController->m_serversModel->data(
|
||||
m_coreController->m_serversModel->index(serverIndex, 0),
|
||||
ServersModel::HasWriteAccessRole
|
||||
).toBool();
|
||||
|
||||
QVERIFY2(hasWriteAccess,
|
||||
"Server should have write access (admin access) according to ServersModel");
|
||||
}
|
||||
|
||||
qDebug() << "Admin access verified for server at index:" << serverIndex;
|
||||
}
|
||||
|
||||
void verifyClientConfig(const ContainerConfig& containerConfig, DockerContainer container) {
|
||||
QString containerName = ContainerUtils::containerToString(container);
|
||||
qDebug() << "Checking container:" << containerName;
|
||||
|
||||
if (ContainerUtils::containerService(container) != ServiceType::Other) {
|
||||
bool hasClientConfig = containerConfig.protocolConfig.hasClientConfig();
|
||||
|
||||
QVERIFY2(hasClientConfig,
|
||||
QString("Container %1 should have client config initialized")
|
||||
.arg(containerName)
|
||||
.toUtf8().constData());
|
||||
|
||||
if (container == DockerContainer::Awg) {
|
||||
const AwgProtocolConfig* awgProtocolConfig = containerConfig.protocolConfig.as<AwgProtocolConfig>();
|
||||
QVERIFY2(awgProtocolConfig != nullptr, "Protocol config should be AwgProtocolConfig");
|
||||
QVERIFY2(awgProtocolConfig->hasClientConfig(), "AwgProtocolConfig should have client config");
|
||||
const std::optional<AwgClientConfig>& clientCfgOpt = awgProtocolConfig->clientConfig;
|
||||
QVERIFY2(clientCfgOpt.has_value(), "Awg client config should exist");
|
||||
|
||||
const AwgClientConfig& awgClientConfig = *clientCfgOpt;
|
||||
QVERIFY2(!awgClientConfig.hostName.isEmpty(), "Awg client config should have hostName");
|
||||
QVERIFY2(awgClientConfig.port > 0, "Awg client config should have valid port");
|
||||
QVERIFY2(!awgClientConfig.clientPrivateKey.isEmpty(), "Awg client config should have clientPrivateKey");
|
||||
QVERIFY2(!awgClientConfig.clientPublicKey.isEmpty(), "Awg client config should have clientPublicKey");
|
||||
QVERIFY2(!awgClientConfig.serverPublicKey.isEmpty(), "Awg client config should have serverPublicKey");
|
||||
QVERIFY2(!awgClientConfig.clientId.isEmpty(), "Awg client config should have clientId");
|
||||
QVERIFY2(!awgClientConfig.nativeConfig.isEmpty(), "Awg client config should have nativeConfig");
|
||||
}
|
||||
|
||||
qDebug() << "Container" << containerName << "has valid client config initialized";
|
||||
} else {
|
||||
qDebug() << "Container" << containerName << "is service type Other, skipping client config check";
|
||||
}
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testSelfHostedServerSetup() {
|
||||
ServerCredentials credentials = getCredentialsFromEnv();
|
||||
|
||||
if (credentials.hostName.isEmpty() || credentials.userName.isEmpty() || credentials.secretData.isEmpty()) {
|
||||
QSKIP("Test requires TEST_SERVER_HOST, TEST_SERVER_USER, TEST_SERVER_PASSWORD environment variables");
|
||||
}
|
||||
|
||||
QVERIFY2(credentials.isValid(), "Server credentials should be valid");
|
||||
qDebug() << "Using server:" << credentials.hostName << "user:" << credentials.userName << "port:" << credentials.port;
|
||||
|
||||
verifySshConnection(credentials);
|
||||
|
||||
int awgPort = 55424;
|
||||
TransportProto awgTransportProto = TransportProto::Udp;
|
||||
bool wasAwgInstalled = false;
|
||||
|
||||
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
|
||||
ErrorCode installServerError = m_coreController->m_installController->installServer(
|
||||
credentials, DockerContainer::Awg, awgPort, awgTransportProto, wasAwgInstalled);
|
||||
|
||||
QVERIFY2(installServerError == ErrorCode::NoError,
|
||||
QString("installServer for Awg should succeed. Error: %1")
|
||||
.arg(static_cast<int>(installServerError))
|
||||
.toUtf8().constData());
|
||||
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added");
|
||||
|
||||
int serverIndex = m_coreController->m_serversRepository->serversCount() - 1;
|
||||
qDebug() << "Server with Awg container added at index:" << serverIndex;
|
||||
|
||||
const auto adminAfterAwg = m_coreController->m_serversRepository->selfHostedAdminConfig(
|
||||
m_coreController->m_serversRepository->serverIdAt(serverIndex));
|
||||
QVERIFY2(adminAfterAwg.has_value(), "Server should be self-hosted (admin)");
|
||||
const SelfHostedAdminServerConfig *selfHostedAfterAwg = &(*adminAfterAwg);
|
||||
QVERIFY2(selfHostedAfterAwg->defaultContainer == DockerContainer::Awg, "Default container should be Awg");
|
||||
QVERIFY2(selfHostedAfterAwg->containers.contains(DockerContainer::Awg), "Server should have Awg container");
|
||||
|
||||
ContainerConfig awgConfig = selfHostedAfterAwg->containers.value(DockerContainer::Awg);
|
||||
QVERIFY2(awgConfig.container == DockerContainer::Awg, "Awg container config should be valid");
|
||||
QVERIFY2(selfHostedAfterAwg->containers.size() == 1,
|
||||
QString("Server should have exactly 1 container after Awg installation, but has %1")
|
||||
.arg(selfHostedAfterAwg->containers.size())
|
||||
.toUtf8().constData());
|
||||
verifyClientConfig(awgConfig, DockerContainer::Awg);
|
||||
|
||||
qDebug() << "Awg container installed and configured successfully with valid client config";
|
||||
|
||||
int dnsPort = 53;
|
||||
TransportProto dnsTransportProto = TransportProto::Udp;
|
||||
bool wasDnsInstalled = false;
|
||||
|
||||
const QString serverIdForOps = m_coreController->m_serversRepository->serverIdAt(serverIndex);
|
||||
ErrorCode installContainerError = m_coreController->m_installController->installContainer(
|
||||
serverIdForOps, DockerContainer::Dns, dnsPort, dnsTransportProto, wasDnsInstalled);
|
||||
|
||||
QVERIFY2(installContainerError == ErrorCode::NoError,
|
||||
QString("installContainer for Dns should succeed. Error: %1")
|
||||
.arg(static_cast<int>(installContainerError))
|
||||
.toUtf8().constData());
|
||||
qDebug() << "Dns container installed:" << wasDnsInstalled;
|
||||
|
||||
const auto adminAfterDns = m_coreController->m_serversRepository->selfHostedAdminConfig(
|
||||
m_coreController->m_serversRepository->serverIdAt(serverIndex));
|
||||
QVERIFY2(adminAfterDns.has_value(), "Server config should be SelfHostedAdminServerConfig");
|
||||
const SelfHostedAdminServerConfig *selfHostedAfterDns = &(*adminAfterDns);
|
||||
QVERIFY2(selfHostedAfterDns->containers.contains(DockerContainer::Awg), "Server should still have Awg container");
|
||||
QVERIFY2(selfHostedAfterDns->containers.contains(DockerContainer::Dns), "Server should have Dns container");
|
||||
QVERIFY2(selfHostedAfterDns->containers.size() == 2,
|
||||
QString("Server should have exactly 2 containers after Dns installation, but has %1")
|
||||
.arg(selfHostedAfterDns->containers.size())
|
||||
.toUtf8().constData());
|
||||
|
||||
ContainerConfig dnsConfig = selfHostedAfterDns->containers.value(DockerContainer::Dns);
|
||||
QVERIFY2(dnsConfig.container == DockerContainer::Dns, "Dns container config should be valid");
|
||||
|
||||
const DnsProtocolConfig* dnsProtocolConfig = dnsConfig.protocolConfig.as<DnsProtocolConfig>();
|
||||
QVERIFY2(dnsProtocolConfig != nullptr, "Protocol config should be DnsProtocolConfig");
|
||||
|
||||
qDebug() << "Dns container installed and configured successfully";
|
||||
|
||||
verifyAdminAccess(serverIndex);
|
||||
|
||||
qDebug() << "Test completed successfully. Server setup with Awg and Dns containers is complete.";
|
||||
}
|
||||
|
||||
void testSelfHostedServerEmptyRecover() {
|
||||
ServerCredentials credentials = getCredentialsFromEnv();
|
||||
|
||||
if (credentials.hostName.isEmpty() || credentials.userName.isEmpty() || credentials.secretData.isEmpty()) {
|
||||
QSKIP("Test requires TEST_SERVER_HOST, TEST_SERVER_USER, TEST_SERVER_PASSWORD environment variables");
|
||||
}
|
||||
|
||||
QVERIFY2(credentials.isValid(), "Server credentials should be valid");
|
||||
qDebug() << "Using server:" << credentials.hostName << "user:" << credentials.userName << "port:" << credentials.port;
|
||||
|
||||
verifySshConnection(credentials);
|
||||
|
||||
SelfHostedAdminServerConfig serverConfig;
|
||||
serverConfig.hostName = credentials.hostName;
|
||||
serverConfig.userName = credentials.userName;
|
||||
serverConfig.password = credentials.secretData;
|
||||
serverConfig.port = credentials.port;
|
||||
serverConfig.description = m_coreController->m_appSettingsRepository->nextAvailableServerName();
|
||||
serverConfig.displayName = serverConfig.description.isEmpty() ? serverConfig.hostName : serverConfig.description;
|
||||
serverConfig.defaultContainer = DockerContainer::None;
|
||||
|
||||
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
|
||||
m_coreController->m_serversRepository->addServer(QString(), serverConfig.toJson(),
|
||||
serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
|
||||
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added");
|
||||
|
||||
int serverIndex = m_coreController->m_serversRepository->serversCount() - 1;
|
||||
qDebug() << "Empty server added at index:" << serverIndex;
|
||||
|
||||
const auto addedAdmin = m_coreController->m_serversRepository->selfHostedAdminConfig(
|
||||
m_coreController->m_serversRepository->serverIdAt(serverIndex));
|
||||
QVERIFY2(addedAdmin.has_value(), "Added server should be self-hosted admin");
|
||||
const SelfHostedAdminServerConfig *selfHosted = &(*addedAdmin);
|
||||
QVERIFY2(selfHosted->containers.isEmpty(), "Server should have no containers initially");
|
||||
QVERIFY2(selfHosted->defaultContainer == DockerContainer::None, "Default container should be None");
|
||||
|
||||
const QString scanServerId = m_coreController->m_serversRepository->serverIdAt(serverIndex);
|
||||
ErrorCode scanError = m_coreController->m_installController->scanServerForInstalledContainers(scanServerId);
|
||||
QVERIFY2(scanError == ErrorCode::NoError,
|
||||
QString("Server scan should succeed. Error: %1")
|
||||
.arg(static_cast<int>(scanError))
|
||||
.toUtf8().constData());
|
||||
qDebug() << "Server scan completed successfully";
|
||||
|
||||
const auto scannedAdmin = m_coreController->m_serversRepository->selfHostedAdminConfig(
|
||||
m_coreController->m_serversRepository->serverIdAt(serverIndex));
|
||||
QVERIFY2(scannedAdmin.has_value(), "Scanned server config should be SelfHostedAdminServerConfig");
|
||||
const SelfHostedAdminServerConfig *scannedSelfHosted = &(*scannedAdmin);
|
||||
|
||||
QMap<DockerContainer, ContainerConfig> containers = scannedSelfHosted->containers;
|
||||
int containersCount = containers.size();
|
||||
qDebug() << "Found containers count:" << containersCount;
|
||||
|
||||
QVERIFY2(containersCount >= 0,
|
||||
QString("Containers count should be non-negative, but got %1")
|
||||
.arg(containersCount)
|
||||
.toUtf8().constData());
|
||||
|
||||
if (containersCount > 0) {
|
||||
qDebug() << "Server has" << containersCount << "installed container(s)";
|
||||
} else {
|
||||
qDebug() << "Server has no installed containers";
|
||||
}
|
||||
|
||||
for (auto it = containers.begin(); it != containers.end(); ++it) {
|
||||
verifyClientConfig(it.value(), it.key());
|
||||
}
|
||||
|
||||
QVERIFY2(scannedSelfHosted->containers.size() == containersCount,
|
||||
QString("Scanned containers count should match. Expected: %1, Actual: %2")
|
||||
.arg(containersCount)
|
||||
.arg(scannedSelfHosted->containers.size())
|
||||
.toUtf8().constData());
|
||||
|
||||
verifyAdminAccess(serverIndex);
|
||||
|
||||
qDebug() << "Test completed successfully. Server has admin access and all containers are initialized.";
|
||||
}
|
||||
|
||||
void testRemoveAllContainers() {
|
||||
ServerCredentials credentials = getCredentialsFromEnv();
|
||||
|
||||
if (credentials.hostName.isEmpty() || credentials.userName.isEmpty() || credentials.secretData.isEmpty()) {
|
||||
QSKIP("Test requires TEST_SERVER_HOST, TEST_SERVER_USER, TEST_SERVER_PASSWORD environment variables");
|
||||
}
|
||||
|
||||
QVERIFY2(credentials.isValid(), "Server credentials should be valid");
|
||||
qDebug() << "Using server:" << credentials.hostName << "user:" << credentials.userName << "port:" << credentials.port;
|
||||
|
||||
verifySshConnection(credentials);
|
||||
|
||||
int awgPort = 55424;
|
||||
TransportProto awgTransportProto = TransportProto::Udp;
|
||||
bool wasAwgInstalled = false;
|
||||
|
||||
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
|
||||
ErrorCode installServerError = m_coreController->m_installController->installServer(
|
||||
credentials, DockerContainer::Awg, awgPort, awgTransportProto, wasAwgInstalled);
|
||||
|
||||
QVERIFY2(installServerError == ErrorCode::NoError,
|
||||
QString("installServer for Awg should succeed. Error: %1")
|
||||
.arg(static_cast<int>(installServerError))
|
||||
.toUtf8().constData());
|
||||
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
|
||||
|
||||
int serverIndex = m_coreController->m_serversRepository->serversCount() - 1;
|
||||
qDebug() << "Server with Awg container added at index:" << serverIndex;
|
||||
|
||||
const auto adminBeforeRemoval = m_coreController->m_serversRepository->selfHostedAdminConfig(
|
||||
m_coreController->m_serversRepository->serverIdAt(serverIndex));
|
||||
QVERIFY2(adminBeforeRemoval.has_value(), "Server config should be SelfHostedAdminServerConfig");
|
||||
const SelfHostedAdminServerConfig *selfHostedBeforeRemoval = &(*adminBeforeRemoval);
|
||||
QVERIFY2(!selfHostedBeforeRemoval->containers.isEmpty(), "Server should have containers before removal");
|
||||
QVERIFY2(selfHostedBeforeRemoval->defaultContainer != DockerContainer::None, "Server should have default container before removal");
|
||||
|
||||
qDebug() << "Containers before removal:" << selfHostedBeforeRemoval->containers.size();
|
||||
|
||||
const QString removeServerId = m_coreController->m_serversRepository->serverIdAt(serverIndex);
|
||||
ErrorCode removeError = m_coreController->m_installController->removeAllContainers(removeServerId);
|
||||
QVERIFY2(removeError == ErrorCode::NoError,
|
||||
QString("removeAllContainers should succeed. Error: %1")
|
||||
.arg(static_cast<int>(removeError))
|
||||
.toUtf8().constData());
|
||||
qDebug() << "All containers removed successfully";
|
||||
|
||||
const auto adminAfterRemoval = m_coreController->m_serversRepository->selfHostedAdminConfig(
|
||||
m_coreController->m_serversRepository->serverIdAt(serverIndex));
|
||||
QVERIFY2(adminAfterRemoval.has_value(), "Server config should be SelfHostedAdminServerConfig");
|
||||
const SelfHostedAdminServerConfig *selfHostedAfterRemoval = &(*adminAfterRemoval);
|
||||
|
||||
QVERIFY2(selfHostedAfterRemoval->containers.isEmpty(),
|
||||
"Server should have no containers after removal");
|
||||
QVERIFY2(selfHostedAfterRemoval->defaultContainer == DockerContainer::None,
|
||||
"Default container should be None after removal");
|
||||
|
||||
qDebug() << "Containers after removal:" << selfHostedAfterRemoval->containers.size();
|
||||
|
||||
verifyAdminAccess(serverIndex);
|
||||
|
||||
qDebug() << "Test completed successfully. All containers removed and server is empty.";
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestSelfHostedServerSetup)
|
||||
#include "testSelfHostedServerSetup.moc"
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestServerEdgeCases : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
m_coreController->m_serversRepository->invalidateCache();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testInvalidIndexOperations() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
|
||||
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult.config);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Should have 1 server");
|
||||
|
||||
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
|
||||
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(-1));
|
||||
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index");
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(10));
|
||||
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index");
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(100));
|
||||
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Server count should remain 1");
|
||||
|
||||
const QString validServerId = m_coreController->m_serversController->getServerId(0);
|
||||
const serverConfigUtils::ConfigType editKind =
|
||||
m_coreController->m_serversRepository->serverKind(validServerId);
|
||||
|
||||
m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(-1),
|
||||
QJsonObject(), editKind);
|
||||
QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for invalid index");
|
||||
|
||||
m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(10),
|
||||
QJsonObject(), editKind);
|
||||
QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for invalid index");
|
||||
|
||||
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(-1));
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for invalid index");
|
||||
|
||||
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(10));
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for invalid index");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should remain 0");
|
||||
}
|
||||
|
||||
void testEmptyRepositoryOperations() {
|
||||
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
|
||||
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Should start with 0 servers");
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
|
||||
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for empty repository");
|
||||
|
||||
m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(0),
|
||||
SelfHostedAdminServerConfig {}.toJson(),
|
||||
serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for empty repository");
|
||||
|
||||
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for empty repository");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0 for empty repository");
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Server count should remain 0");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestServerEdgeCases)
|
||||
#include "testServerEdgeCases.moc"
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "tests/testServerRepositoryHelpers.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestServerEdit : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
m_coreController->m_serversRepository->invalidateCache();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testServerEditTriggersHandlers() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
|
||||
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
|
||||
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult.config);
|
||||
QVERIFY2(importFinishedSpy.count() == 1, "Import should succeed");
|
||||
|
||||
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
|
||||
|
||||
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversController->getServerId(0),
|
||||
QStringLiteral("Edited AWG Server"));
|
||||
|
||||
QVERIFY2(serverEditedSpy.count() == 1, "serverEdited signal should be emitted");
|
||||
QVERIFY2(serverEditedSpy.at(0).at(0).toString() == m_coreController->m_serversRepository->serverIdAt(0),
|
||||
"serverEdited should emit edited server id");
|
||||
|
||||
const QString editedDesc = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(0));
|
||||
QVERIFY2(editedDesc == "Edited AWG Server", "Server description should be updated");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
QString modelDesc = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
|
||||
QVERIFY2(modelDesc == "Edited AWG Server", "Server description in model should be updated");
|
||||
}
|
||||
}
|
||||
|
||||
void testServerEditPreservesDefault() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
|
||||
|
||||
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult1.config);
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult2.config);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default server should be index 1");
|
||||
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversController->getServerId(1),
|
||||
QStringLiteral("Edited Default Server"));
|
||||
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted when editing default server");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default server index should remain 1");
|
||||
|
||||
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversController->getServerId(0),
|
||||
QStringLiteral("Edited Non-Default Server"));
|
||||
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted when editing non-default server");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default server index should remain 1");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestServerEdit)
|
||||
#include "testServerEdit.moc"
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
#ifndef TESTSERVERREPOSITORYHELPERS_H
|
||||
#define TESTSERVERREPOSITORYHELPERS_H
|
||||
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
|
||||
namespace amnezia::test
|
||||
{
|
||||
|
||||
inline QString serverDescription(SecureServersRepository *repo, const QString &serverId)
|
||||
{
|
||||
switch (repo->serverKind(serverId)) {
|
||||
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
|
||||
const auto cfg = repo->selfHostedAdminConfig(serverId);
|
||||
return cfg.has_value() ? cfg->description : QString();
|
||||
}
|
||||
case serverConfigUtils::ConfigType::SelfHostedUser: {
|
||||
const auto cfg = repo->selfHostedUserConfig(serverId);
|
||||
return cfg.has_value() ? cfg->description : QString();
|
||||
}
|
||||
case serverConfigUtils::ConfigType::Native: {
|
||||
const auto cfg = repo->nativeConfig(serverId);
|
||||
return cfg.has_value() ? cfg->description : QString();
|
||||
}
|
||||
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
|
||||
case serverConfigUtils::ConfigType::AmneziaFreeV3:
|
||||
case serverConfigUtils::ConfigType::ExternalPremium: {
|
||||
const auto cfg = repo->apiV2Config(serverId);
|
||||
return cfg.has_value() ? cfg->description : QString();
|
||||
}
|
||||
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
|
||||
case serverConfigUtils::ConfigType::AmneziaFreeV2: {
|
||||
const auto cfg = repo->legacyApiConfig(serverId);
|
||||
return cfg.has_value() ? cfg->description : QString();
|
||||
}
|
||||
case serverConfigUtils::ConfigType::Invalid:
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
inline void setServerDescription(SecureServersRepository *repo, const QString &serverId, const QString &description)
|
||||
{
|
||||
const serverConfigUtils::ConfigType kind = repo->serverKind(serverId);
|
||||
switch (kind) {
|
||||
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
|
||||
auto cfg = repo->selfHostedAdminConfig(serverId);
|
||||
if (!cfg.has_value()) return;
|
||||
cfg->description = description;
|
||||
cfg->displayName = description;
|
||||
repo->editServer(serverId, cfg->toJson(), kind);
|
||||
return;
|
||||
}
|
||||
case serverConfigUtils::ConfigType::SelfHostedUser: {
|
||||
auto cfg = repo->selfHostedUserConfig(serverId);
|
||||
if (!cfg.has_value()) return;
|
||||
cfg->description = description;
|
||||
cfg->displayName = description;
|
||||
repo->editServer(serverId, cfg->toJson(), kind);
|
||||
return;
|
||||
}
|
||||
case serverConfigUtils::ConfigType::Native: {
|
||||
auto cfg = repo->nativeConfig(serverId);
|
||||
if (!cfg.has_value()) return;
|
||||
cfg->description = description;
|
||||
cfg->displayName = description;
|
||||
repo->editServer(serverId, cfg->toJson(), kind);
|
||||
return;
|
||||
}
|
||||
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
|
||||
case serverConfigUtils::ConfigType::AmneziaFreeV3:
|
||||
case serverConfigUtils::ConfigType::ExternalPremium: {
|
||||
auto cfg = repo->apiV2Config(serverId);
|
||||
if (!cfg.has_value()) return;
|
||||
cfg->description = description;
|
||||
cfg->displayName = description;
|
||||
repo->editServer(serverId, cfg->toJson(), kind);
|
||||
return;
|
||||
}
|
||||
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
|
||||
case serverConfigUtils::ConfigType::AmneziaFreeV2:
|
||||
case serverConfigUtils::ConfigType::Invalid:
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace amnezia::test
|
||||
|
||||
#endif
|
||||
@@ -1,112 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "tests/testServerRepositoryHelpers.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestServersModelSync : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testServersModelSyncOnOperations() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
|
||||
if (!m_coreController->m_serversModel) {
|
||||
QSKIP("ServersModel not available");
|
||||
}
|
||||
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "Initial model row count should be 0");
|
||||
|
||||
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult.config);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 1, "Model should have 1 row after import");
|
||||
QString modelDesc1 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
|
||||
QVERIFY2(modelDesc1 == "AWG Server", "Model should have correct server name");
|
||||
|
||||
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversController->getServerId(0),
|
||||
QStringLiteral("Edited AWG Server"));
|
||||
|
||||
QString modelDesc2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
|
||||
QVERIFY2(modelDesc2 == "Edited AWG Server", "Model should be updated after edit");
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "Model should have 0 rows after removal");
|
||||
}
|
||||
|
||||
void testServersModelDefaultIndexSync() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
|
||||
QString wgKey = "vpn://AAAAwXjahY89a8NADIb_StDsHLFDIHjt0C1LhgwlBNWnpgfx3SHp6hDj_15dacnYTS_Po68ZhhQVQyQW6N_mZ4QecIz0CLieAtO1IHto4Fn3M-TEat6u3XetMSnvkfSC3jOJjYN24_audRtjyhil-pfMSZPB4jMsy7kBTx9Ybvryz2ZPMnDIGlI042TktZLVkfjLmhr4TKIHHMnodHV0xzHfyA1pNJZRZEr1alAS_Yvbin6e6LoGihD_DqhSjbB8AyB_ZI8";
|
||||
|
||||
if (!m_coreController->m_serversModel) {
|
||||
QSKIP("ServersModel not available");
|
||||
}
|
||||
|
||||
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult1.config);
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult2.config);
|
||||
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult3.config);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 3, "Model should have 3 rows");
|
||||
|
||||
bool isDefault0 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::IsDefaultRole).toBool();
|
||||
bool isDefault1 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(1, 0), ServersModel::IsDefaultRole).toBool();
|
||||
bool isDefault2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(2, 0), ServersModel::IsDefaultRole).toBool();
|
||||
|
||||
QVERIFY2(!isDefault0, "Server 0 should not be default");
|
||||
QVERIFY2(!isDefault1, "Server 1 should not be default");
|
||||
QVERIFY2(isDefault2, "Server 2 should be default");
|
||||
|
||||
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
|
||||
|
||||
isDefault0 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::IsDefaultRole).toBool();
|
||||
isDefault2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(2, 0), ServersModel::IsDefaultRole).toBool();
|
||||
|
||||
QVERIFY2(isDefault0, "Server 0 should be default after change");
|
||||
QVERIFY2(!isDefault2, "Server 2 should not be default after change");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestServersModelSync)
|
||||
#include "testServersModelSync.moc"
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
#include <QLocale>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "ui/controllers/settingsUiController.h"
|
||||
#include "ui/controllers/languageUiController.h"
|
||||
#include "ui/models/allowedDnsModel.h"
|
||||
#include "ui/models/ipSplitTunnelingModel.h"
|
||||
#include "ui/models/appSplitTunnelingModel.h"
|
||||
#include "ui/models/languageModel.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
class TestSettingsSignals : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
}
|
||||
|
||||
void testDnsSettingsSignals() {
|
||||
QSignalSpy primaryDnsChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::primaryDnsChanged);
|
||||
QSignalSpy secondaryDnsChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::secondaryDnsChanged);
|
||||
QSignalSpy allowedDnsServersChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::allowedDnsServersChanged);
|
||||
|
||||
QString primaryDns = "8.8.8.8";
|
||||
QString secondaryDns = "8.8.4.4";
|
||||
|
||||
m_coreController->m_settingsUiController->setPrimaryDns(primaryDns);
|
||||
QVERIFY2(primaryDnsChangedSpy.count() == 1, "primaryDnsChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_settingsController->getPrimaryDns() == primaryDns, "Primary DNS should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->getPrimaryDns() == primaryDns, "Primary DNS should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->primaryDns() == primaryDns, "Primary DNS should be available in SecureAppSettingsRepository");
|
||||
|
||||
m_coreController->m_settingsUiController->setSecondaryDns(secondaryDns);
|
||||
QVERIFY2(secondaryDnsChangedSpy.count() == 1, "secondaryDnsChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_settingsController->getSecondaryDns() == secondaryDns, "Secondary DNS should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->getSecondaryDns() == secondaryDns, "Secondary DNS should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->secondaryDns() == secondaryDns, "Secondary DNS should be available in SecureAppSettingsRepository");
|
||||
|
||||
QStringList dnsList = {"1.1.1.1", "1.0.0.1"};
|
||||
m_coreController->m_allowedDnsController->addDnsList(dnsList, true);
|
||||
QVERIFY2(allowedDnsServersChangedSpy.count() == 1, "allowedDnsServersChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->getAllowedDnsServers() == dnsList, "Allowed DNS servers should be updated in SecureAppSettingsRepository");
|
||||
QVERIFY2(m_coreController->m_allowedDnsController->getCurrentDnsServers() == dnsList, "Allowed DNS servers should be available in AllowedDnsController");
|
||||
|
||||
QVERIFY2(m_coreController->m_allowedDnsUiController != nullptr, "AllowedDnsUiController should exist");
|
||||
QVERIFY2(m_coreController->m_allowedDnsModel != nullptr, "AllowedDnsModel should exist");
|
||||
|
||||
QStringList modelDnsList;
|
||||
for (int i = 0; i < m_coreController->m_allowedDnsModel->rowCount(); ++i) {
|
||||
modelDnsList.append(m_coreController->m_allowedDnsModel->data(m_coreController->m_allowedDnsModel->index(i, 0), AllowedDnsModel::IpRole).toString());
|
||||
}
|
||||
QVERIFY2(modelDnsList == dnsList, "Allowed DNS servers should be available in AllowedDnsModel");
|
||||
}
|
||||
|
||||
void testAmneziaDnsToggleSignal() {
|
||||
QSignalSpy amneziaDnsToggledSpy(m_coreController->m_settingsUiController, &SettingsUiController::amneziaDnsToggled);
|
||||
QSignalSpy useAmneziaDnsChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::useAmneziaDnsChanged);
|
||||
|
||||
bool initialValue = m_coreController->m_settingsController->isAmneziaDnsEnabled();
|
||||
|
||||
m_coreController->m_settingsUiController->toggleAmneziaDns(!initialValue);
|
||||
QVERIFY2(amneziaDnsToggledSpy.count() == 1, "amneziaDnsToggled signal should be emitted");
|
||||
QVERIFY2(amneziaDnsToggledSpy.at(0).at(0).toBool() == !initialValue, "amneziaDnsToggled should emit correct value");
|
||||
QVERIFY2(useAmneziaDnsChangedSpy.count() == 1, "useAmneziaDnsChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_settingsController->isAmneziaDnsEnabled() == !initialValue, "Amnezia DNS state should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isAmneziaDnsEnabled() == !initialValue, "Amnezia DNS state should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->useAmneziaDns() == !initialValue, "Amnezia DNS state should be available in SecureAppSettingsRepository");
|
||||
|
||||
m_coreController->m_settingsUiController->toggleAmneziaDns(initialValue);
|
||||
QVERIFY2(amneziaDnsToggledSpy.count() == 2, "amneziaDnsToggled signal should be emitted again");
|
||||
QVERIFY2(useAmneziaDnsChangedSpy.count() == 2, "useAmneziaDnsChanged signal should be emitted again");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isAmneziaDnsEnabled() == initialValue, "Amnezia DNS state should be restored in SettingsUiController");
|
||||
}
|
||||
|
||||
void testLoggingSignals() {
|
||||
QSignalSpy loggingStateChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::loggingStateChanged);
|
||||
QSignalSpy saveLogsChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::saveLogsChanged);
|
||||
|
||||
bool initialLogging = m_coreController->m_settingsController->isLoggingEnabled();
|
||||
|
||||
m_coreController->m_settingsUiController->toggleLogging(!initialLogging);
|
||||
QVERIFY2(loggingStateChangedSpy.count() == 1, "loggingStateChanged signal should be emitted");
|
||||
QVERIFY2(saveLogsChangedSpy.count() == 1, "saveLogsChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_settingsController->isLoggingEnabled() == !initialLogging, "Logging state should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isLoggingEnabled() == !initialLogging, "Logging state should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->isSaveLogs() == !initialLogging, "Logging state should be available in SecureAppSettingsRepository");
|
||||
|
||||
m_coreController->m_settingsUiController->toggleLogging(initialLogging);
|
||||
QVERIFY2(loggingStateChangedSpy.count() == 2, "loggingStateChanged signal should be emitted again");
|
||||
QVERIFY2(saveLogsChangedSpy.count() == 2, "saveLogsChanged signal should be emitted again");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isLoggingEnabled() == initialLogging, "Logging state should be restored in SettingsUiController");
|
||||
}
|
||||
|
||||
void testScreenshotsSignals() {
|
||||
QSignalSpy screenshotsEnabledChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::screenshotsEnabledChanged);
|
||||
|
||||
bool initialScreenshots = m_coreController->m_settingsController->isScreenshotsEnabled();
|
||||
|
||||
m_coreController->m_settingsUiController->toggleScreenshotsEnabled(!initialScreenshots);
|
||||
QVERIFY2(screenshotsEnabledChangedSpy.count() == 1, "screenshotsEnabledChanged signal should be emitted");
|
||||
QVERIFY2(screenshotsEnabledChangedSpy.at(0).at(0).toBool() == !initialScreenshots, "screenshotsEnabledChanged should emit correct value");
|
||||
QVERIFY2(m_coreController->m_settingsController->isScreenshotsEnabled() == !initialScreenshots, "Screenshots state should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isScreenshotsEnabled() == !initialScreenshots, "Screenshots state should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->isScreenshotsEnabled() == !initialScreenshots, "Screenshots state should be available in SecureAppSettingsRepository");
|
||||
}
|
||||
|
||||
void testStartMinimizedSignals() {
|
||||
QSignalSpy startMinimizedChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::startMinimizedChanged);
|
||||
|
||||
bool initialStartMinimized = m_coreController->m_settingsController->isStartMinimizedEnabled();
|
||||
|
||||
m_coreController->m_settingsUiController->toggleStartMinimized(!initialStartMinimized);
|
||||
QVERIFY2(startMinimizedChangedSpy.count() == 1, "startMinimizedChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_settingsController->isStartMinimizedEnabled() == !initialStartMinimized, "Start minimized state should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isStartMinimizedEnabled() == !initialStartMinimized, "Start minimized state should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->isStartMinimized() == !initialStartMinimized, "Start minimized state should be available in SecureAppSettingsRepository");
|
||||
}
|
||||
|
||||
void testAutoConnectSignals() {
|
||||
bool initialAutoConnect = m_coreController->m_settingsController->isAutoConnectEnabled();
|
||||
|
||||
m_coreController->m_settingsUiController->toggleAutoConnect(!initialAutoConnect);
|
||||
QVERIFY2(m_coreController->m_settingsController->isAutoConnectEnabled() == !initialAutoConnect, "Auto connect state should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isAutoConnectEnabled() == !initialAutoConnect, "Auto connect state should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->isAutoConnect() == !initialAutoConnect, "Auto connect state should be available in SecureAppSettingsRepository");
|
||||
|
||||
m_coreController->m_settingsUiController->toggleAutoConnect(initialAutoConnect);
|
||||
QVERIFY2(m_coreController->m_settingsController->isAutoConnectEnabled() == initialAutoConnect, "Auto connect state should be restored in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isAutoConnectEnabled() == initialAutoConnect, "Auto connect state should be restored in SettingsUiController");
|
||||
}
|
||||
|
||||
void testLanguageChangeSignals() {
|
||||
QSignalSpy appLanguageChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appLanguageChanged);
|
||||
QSignalSpy translationsUpdatedSpy(m_coreController->m_languageUiController, &LanguageUiController::translationsUpdated);
|
||||
|
||||
QLocale initialLocale = m_coreController->m_settingsController->getAppLanguage();
|
||||
QLocale newLocale = (initialLocale.language() == QLocale::English) ? QLocale::Russian : QLocale::English;
|
||||
|
||||
m_coreController->m_settingsController->setAppLanguage(newLocale);
|
||||
QVERIFY2(appLanguageChangedSpy.count() == 1, "appLanguageChanged signal should be emitted");
|
||||
QVERIFY2(appLanguageChangedSpy.at(0).at(0).value<QLocale>() == newLocale, "appLanguageChanged should emit correct locale");
|
||||
QVERIFY2(m_coreController->m_settingsController->getAppLanguage() == newLocale, "App language should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->getAppLanguage() == newLocale, "App language should be available in SecureAppSettingsRepository");
|
||||
|
||||
if (m_coreController->m_languageModel) {
|
||||
QString newLanguageName = m_coreController->m_languageUiController->getCurrentLanguageName();
|
||||
QVERIFY2(!newLanguageName.isEmpty(), "Language name should be available in LanguageUiController");
|
||||
}
|
||||
}
|
||||
|
||||
void testGatewayEndpointSignals() {
|
||||
QSignalSpy gatewayEndpointChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::gatewayEndpointChanged);
|
||||
QSignalSpy devGatewayEnvChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::devGatewayEnvChanged);
|
||||
|
||||
QString initialEndpoint = m_coreController->m_settingsController->getGatewayEndpoint();
|
||||
QString newEndpoint = "https://test-gateway.example.com";
|
||||
|
||||
m_coreController->m_settingsUiController->setGatewayEndpoint(newEndpoint);
|
||||
QVERIFY2(gatewayEndpointChangedSpy.count() == 1, "gatewayEndpointChanged signal should be emitted");
|
||||
QVERIFY2(gatewayEndpointChangedSpy.at(0).at(0).toString() == newEndpoint, "gatewayEndpointChanged should emit correct endpoint");
|
||||
QVERIFY2(m_coreController->m_settingsController->getGatewayEndpoint() == newEndpoint, "Gateway endpoint should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->getGatewayEndpoint() == newEndpoint, "Gateway endpoint should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->getGatewayEndpoint() == newEndpoint, "Gateway endpoint should be available in SecureAppSettingsRepository");
|
||||
|
||||
bool initialDevEnv = m_coreController->m_settingsController->isDevGatewayEnv();
|
||||
m_coreController->m_settingsUiController->toggleDevGatewayEnv(!initialDevEnv);
|
||||
QVERIFY2(devGatewayEnvChangedSpy.count() == 1, "devGatewayEnvChanged signal should be emitted");
|
||||
QVERIFY2(devGatewayEnvChangedSpy.at(0).at(0).toBool() == !initialDevEnv, "devGatewayEnvChanged should emit correct value");
|
||||
QVERIFY2(m_coreController->m_settingsController->isDevGatewayEnv() == !initialDevEnv, "Dev gateway env state should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isDevGatewayEnv() == !initialDevEnv, "Dev gateway env state should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->isDevGatewayEnv() == !initialDevEnv, "Dev gateway env state should be available in SecureAppSettingsRepository");
|
||||
}
|
||||
|
||||
void testSettingsClearedSignal() {
|
||||
QSignalSpy settingsClearedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::settingsCleared);
|
||||
|
||||
m_coreController->m_settingsController->clearSettings();
|
||||
QVERIFY2(settingsClearedSpy.count() == 1, "settingsCleared signal should be emitted");
|
||||
}
|
||||
|
||||
void testSplitTunnelingSignals() {
|
||||
QSignalSpy siteSplitTunnelingToggledSpy(m_coreController->m_settingsController, &SettingsController::siteSplitTunnelingToggled);
|
||||
QSignalSpy appSplitTunnelingToggledSpy(m_coreController->m_settingsController, &SettingsController::appSplitTunnelingToggled);
|
||||
QSignalSpy sitesSplitTunnelingEnabledChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::sitesSplitTunnelingEnabledChanged);
|
||||
QSignalSpy appsSplitTunnelingEnabledChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appsSplitTunnelingEnabledChanged);
|
||||
QSignalSpy routeModeChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::routeModeChanged);
|
||||
QSignalSpy appsRouteModeChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appsRouteModeChanged);
|
||||
QSignalSpy sitesChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::sitesChanged);
|
||||
QSignalSpy appsChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appsChanged);
|
||||
|
||||
bool initialSitesSplitTunneling = m_coreController->m_ipSplitTunnelingController->isSplitTunnelingEnabled();
|
||||
m_coreController->m_ipSplitTunnelingController->toggleSplitTunneling(!initialSitesSplitTunneling);
|
||||
QVERIFY2(sitesSplitTunnelingEnabledChangedSpy.count() == 1, "sitesSplitTunnelingEnabledChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_ipSplitTunnelingController->isSplitTunnelingEnabled() == !initialSitesSplitTunneling, "Sites split tunneling should be updated in IpSplitTunnelingController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->isSitesSplitTunnelingEnabled() == !initialSitesSplitTunneling, "Sites split tunneling should be available in SecureAppSettingsRepository");
|
||||
|
||||
bool initialAppsSplitTunneling = m_coreController->m_appSplitTunnelingController->isSplitTunnelingEnabled();
|
||||
m_coreController->m_appSplitTunnelingController->toggleSplitTunneling(!initialAppsSplitTunneling);
|
||||
QVERIFY2(appsSplitTunnelingEnabledChangedSpy.count() == 1, "appsSplitTunnelingEnabledChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_appSplitTunnelingController->isSplitTunnelingEnabled() == !initialAppsSplitTunneling, "Apps split tunneling should be updated in AppSplitTunnelingController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->isAppsSplitTunnelingEnabled() == !initialAppsSplitTunneling, "Apps split tunneling should be available in SecureAppSettingsRepository");
|
||||
|
||||
RouteMode initialRouteMode = m_coreController->m_ipSplitTunnelingController->getRouteMode();
|
||||
RouteMode newRouteMode = (initialRouteMode == RouteMode::VpnOnlyForwardSites)
|
||||
? RouteMode::VpnAllExceptSites
|
||||
: RouteMode::VpnOnlyForwardSites;
|
||||
m_coreController->m_ipSplitTunnelingController->setRouteMode(newRouteMode);
|
||||
QVERIFY2(routeModeChangedSpy.count() == 1, "routeModeChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_ipSplitTunnelingController->getRouteMode() == newRouteMode, "Route mode should be updated in IpSplitTunnelingController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->routeMode() == newRouteMode, "Route mode should be available in SecureAppSettingsRepository");
|
||||
|
||||
AppsRouteMode initialAppsRouteMode = m_coreController->m_appSplitTunnelingController->getRouteMode();
|
||||
AppsRouteMode newAppsRouteMode = (initialAppsRouteMode == AppsRouteMode::VpnAllExceptApps)
|
||||
? AppsRouteMode::VpnAllApps
|
||||
: AppsRouteMode::VpnAllExceptApps;
|
||||
m_coreController->m_appSplitTunnelingController->setRouteMode(newAppsRouteMode);
|
||||
QVERIFY2(appsRouteModeChangedSpy.count() == 1, "appsRouteModeChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_appSplitTunnelingController->getRouteMode() == newAppsRouteMode, "Apps route mode should be updated in AppSplitTunnelingController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->appsRouteMode() == newAppsRouteMode, "Apps route mode should be available in SecureAppSettingsRepository");
|
||||
|
||||
QMap<QString, QString> sitesMap{{"example.com", "1.2.3.4"}};
|
||||
m_coreController->m_ipSplitTunnelingController->addSites(sitesMap, true);
|
||||
QVERIFY2(sitesChangedSpy.count() >= 1, "sitesChanged signal should be emitted");
|
||||
QVector<QPair<QString, QString>> currentSites = m_coreController->m_ipSplitTunnelingController->getCurrentSites();
|
||||
QVERIFY2(currentSites.size() >= 1, "Sites should be available in IpSplitTunnelingController");
|
||||
|
||||
QVERIFY2(m_coreController->m_ipSplitTunnelingUiController != nullptr, "IpSplitTunnelingUiController should exist");
|
||||
QVERIFY2(m_coreController->m_ipSplitTunnelingModel != nullptr, "IpSplitTunnelingModel should exist");
|
||||
|
||||
m_coreController->m_ipSplitTunnelingUiController->updateModel();
|
||||
QVERIFY2(m_coreController->m_ipSplitTunnelingModel->rowCount() >= 1, "Sites should be available in IpSplitTunnelingModel");
|
||||
QString modelUrl = m_coreController->m_ipSplitTunnelingModel->data(m_coreController->m_ipSplitTunnelingModel->index(0, 0), IpSplitTunnelingModel::UrlRole).toString();
|
||||
QVERIFY2(modelUrl == "example.com", "Site URL should be available in IpSplitTunnelingModel");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestSettingsSignals)
|
||||
#include "testSettingsSignals.moc"
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestSignalOrder : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
m_coreController->m_serversRepository->invalidateCache();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testSignalOrderOnImport() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
|
||||
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
|
||||
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult.config);
|
||||
|
||||
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted");
|
||||
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged signal should NOT be emitted (default is already 0)");
|
||||
|
||||
QVERIFY2(serverAddedSpy.at(0).count() > 0, "serverAdded should have arguments");
|
||||
}
|
||||
|
||||
void testSignalOrderOnRemoveDefault() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
|
||||
|
||||
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult1.config);
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult2.config);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default should be index 1");
|
||||
|
||||
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(1));
|
||||
|
||||
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted when removing default server");
|
||||
QVERIFY2(defaultServerChangedSpy.at(0).at(0).toString() == m_coreController->m_serversRepository->defaultServerId(),
|
||||
"defaultServerChanged should emit new default server id");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestSignalOrder)
|
||||
#include "testSignalOrder.moc"
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QDebug>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
#include <QModelIndex>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "core/controllers/selfhosted/importController.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "ui/models/containersModel.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
|
||||
using namespace amnezia;
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace {
|
||||
int defaultServerRow(const QVector<ServerDescription> &descriptions, const QString &defaultServerId)
|
||||
{
|
||||
for (int i = 0; i < descriptions.size(); ++i) {
|
||||
if (descriptions.at(i).serverId == defaultServerId) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class TestUiServersModelAndController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
QJsonObject createAwg2Config()
|
||||
{
|
||||
QJsonObject clientConfig;
|
||||
clientConfig[configKey::mtu] = protocols::awg::defaultMtu;
|
||||
clientConfig[configKey::junkPacketCount] = protocols::awg::defaultJunkPacketCount;
|
||||
clientConfig[configKey::junkPacketMinSize] = protocols::awg::defaultJunkPacketMinSize;
|
||||
clientConfig[configKey::junkPacketMaxSize] = protocols::awg::defaultJunkPacketMaxSize;
|
||||
clientConfig[configKey::specialJunk1] = protocols::awg::defaultSpecialJunk1;
|
||||
clientConfig[configKey::specialJunk2] = protocols::awg::defaultSpecialJunk2;
|
||||
clientConfig[configKey::specialJunk3] = protocols::awg::defaultSpecialJunk3;
|
||||
clientConfig[configKey::specialJunk4] = protocols::awg::defaultSpecialJunk4;
|
||||
clientConfig[configKey::specialJunk5] = protocols::awg::defaultSpecialJunk5;
|
||||
clientConfig[configKey::clientPrivKey] = "test_client_private_key";
|
||||
clientConfig[configKey::clientPubKey] = "test_client_public_key";
|
||||
clientConfig[configKey::serverPubKey] = "test_server_public_key";
|
||||
clientConfig[configKey::pskKey] = "test_psk_key";
|
||||
clientConfig[configKey::clientIp] = "10.8.1.2";
|
||||
clientConfig[configKey::allowedIps] = QJsonArray::fromStringList({"0.0.0.0/0"});
|
||||
|
||||
QJsonObject awgConfig;
|
||||
awgConfig[configKey::lastConfig] = QString(QJsonDocument(clientConfig).toJson());
|
||||
awgConfig[configKey::port] = protocols::awg::defaultPort;
|
||||
awgConfig[configKey::transportProto] = "udp";
|
||||
awgConfig[configKey::protocolVersion] = protocols::awg::awgV2;
|
||||
awgConfig[configKey::subnetAddress] = protocols::wireguard::defaultSubnetAddress;
|
||||
awgConfig[configKey::junkPacketCount] = protocols::awg::defaultJunkPacketCount;
|
||||
awgConfig[configKey::junkPacketMinSize] = protocols::awg::defaultJunkPacketMinSize;
|
||||
awgConfig[configKey::junkPacketMaxSize] = protocols::awg::defaultJunkPacketMaxSize;
|
||||
awgConfig[configKey::initPacketJunkSize] = protocols::awg::defaultInitPacketJunkSize;
|
||||
awgConfig[configKey::responsePacketJunkSize] = protocols::awg::defaultResponsePacketJunkSize;
|
||||
awgConfig[configKey::cookieReplyPacketJunkSize] = protocols::awg::defaultCookieReplyPacketJunkSize;
|
||||
awgConfig[configKey::transportPacketJunkSize] = protocols::awg::defaultTransportPacketJunkSize;
|
||||
awgConfig[configKey::initPacketMagicHeader] = protocols::awg::defaultInitPacketMagicHeader;
|
||||
awgConfig[configKey::responsePacketMagicHeader] = protocols::awg::defaultResponsePacketMagicHeader;
|
||||
awgConfig[configKey::underloadPacketMagicHeader] = protocols::awg::defaultUnderloadPacketMagicHeader;
|
||||
awgConfig[configKey::transportPacketMagicHeader] = protocols::awg::defaultTransportPacketMagicHeader;
|
||||
awgConfig[configKey::specialJunk1] = protocols::awg::defaultSpecialJunk1;
|
||||
awgConfig[configKey::specialJunk2] = protocols::awg::defaultSpecialJunk2;
|
||||
awgConfig[configKey::specialJunk3] = protocols::awg::defaultSpecialJunk3;
|
||||
awgConfig[configKey::specialJunk4] = protocols::awg::defaultSpecialJunk4;
|
||||
awgConfig[configKey::specialJunk5] = protocols::awg::defaultSpecialJunk5;
|
||||
awgConfig[configKey::isThirdPartyConfig] = true;
|
||||
|
||||
QJsonObject container;
|
||||
container[configKey::container] = "amnezia-awg";
|
||||
container[configKey::awg] = awgConfig;
|
||||
|
||||
QJsonArray containers;
|
||||
containers.append(container);
|
||||
|
||||
QJsonObject config;
|
||||
config[configKey::containers] = containers;
|
||||
config[configKey::defaultContainer] = "amnezia-awg";
|
||||
config[configKey::description] = "AWG2 Test Server";
|
||||
config[configKey::hostName] = "test.example.com";
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
QJsonObject createServerDescriptionTestConfig(bool withAmneziaDns)
|
||||
{
|
||||
QJsonObject config = createAwg2Config();
|
||||
config[configKey::description] = "Server 1";
|
||||
if (withAmneziaDns) {
|
||||
config[configKey::dns1] = protocols::dns::amneziaDnsIp;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testUiServersModelAndControllerRoles() {
|
||||
QJsonObject testConfig = createAwg2Config();
|
||||
|
||||
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
|
||||
|
||||
m_coreController->m_importCoreController->importConfig(testConfig);
|
||||
|
||||
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Server should be imported");
|
||||
|
||||
int serverIndex = m_coreController->m_serversRepository->defaultServerIndex();
|
||||
QVERIFY2(serverIndex == 0, "Default server index should be 0");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 1, "ServersModel should have 1 row");
|
||||
|
||||
QModelIndex serverModelIndex = m_coreController->m_serversModel->index(0, 0);
|
||||
QVERIFY2(serverModelIndex.isValid(), "Server model index should be valid");
|
||||
|
||||
QString serverName = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::NameRole).toString();
|
||||
QVERIFY2(serverName == "AWG2 Test Server", QString("Server name should be 'AWG2 Test Server', got '%1'").arg(serverName).toUtf8().constData());
|
||||
|
||||
QString serverDescription = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::ServerDescriptionRole).toString();
|
||||
QVERIFY2(serverDescription.contains("test.example.com"), QString("Server description should contain hostname, got '%1'").arg(serverDescription).toUtf8().constData());
|
||||
|
||||
QString hostName = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::HostNameRole).toString();
|
||||
QVERIFY2(hostName == "test.example.com", "Host name should match");
|
||||
|
||||
bool isDefault = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::IsDefaultRole).toBool();
|
||||
QVERIFY2(isDefault == true, "Server should be default");
|
||||
|
||||
bool hasInstalledContainers = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::HasInstalledContainers).toBool();
|
||||
QVERIFY2(hasInstalledContainers == true, "Server should have installed containers");
|
||||
|
||||
bool hasWriteAccess = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::HasWriteAccessRole).toBool();
|
||||
QVERIFY2(hasWriteAccess == false, "Server should not have write access for imported config");
|
||||
|
||||
int defaultContainerRole = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::DefaultContainerRole).toInt();
|
||||
DockerContainer expectedContainer = DockerContainer::Awg;
|
||||
QVERIFY2(defaultContainerRole == static_cast<int>(expectedContainer), "Default container should be Awg");
|
||||
}
|
||||
|
||||
if (m_coreController->m_serversUiController) {
|
||||
m_coreController->m_serversUiController->setProcessedServerId(
|
||||
m_coreController->m_serversUiController->getServerId(0));
|
||||
|
||||
QString hostName = "test.example.com";
|
||||
|
||||
QString collapsedDescription = m_coreController->m_serversUiController->getDefaultServerDescriptionCollapsed();
|
||||
QString expectedCollapsed = "AmneziaWG (version 2) | " + hostName;
|
||||
QVERIFY2(collapsedDescription == expectedCollapsed,
|
||||
QString("Collapsed description should be '%1', got '%2'").arg(expectedCollapsed, collapsedDescription).toUtf8().constData());
|
||||
|
||||
QString expandedDescription = m_coreController->m_serversUiController->getDefaultServerDescriptionExpanded();
|
||||
QString expectedExpanded = hostName;
|
||||
QVERIFY2(expandedDescription == expectedExpanded,
|
||||
QString("Expanded description should be '%1', got '%2'").arg(expectedExpanded, expandedDescription).toUtf8().constData());
|
||||
}
|
||||
|
||||
if (m_coreController->m_containersModel) {
|
||||
|
||||
int awgContainerIndex = -1;
|
||||
for (int i = 0; i < ContainerUtils::allContainers().size(); ++i) {
|
||||
DockerContainer container = ContainerUtils::allContainers().at(i);
|
||||
if (container == DockerContainer::Awg) {
|
||||
awgContainerIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QVERIFY2(awgContainerIndex >= 0, "Awg container index should be found");
|
||||
|
||||
QModelIndex containerModelIndex = m_coreController->m_containersModel->index(awgContainerIndex, 0);
|
||||
QVERIFY2(containerModelIndex.isValid(), "Container model index should be valid");
|
||||
|
||||
bool isInstalled = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsInstalledRole).toBool();
|
||||
QVERIFY2(isInstalled == true, "Awg container should be installed");
|
||||
|
||||
bool isVpnContainer = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsVpnContainerRole).toBool();
|
||||
QVERIFY2(isVpnContainer == true, "Awg container should be VPN container");
|
||||
|
||||
QString containerName = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::NameRole).toString();
|
||||
QString expectedContainerName = ContainerUtils::containerHumanNames().value(DockerContainer::Awg);
|
||||
QVERIFY2(containerName == expectedContainerName, QString("Container name should be '%1', got '%2'").arg(expectedContainerName, containerName).toUtf8().constData());
|
||||
|
||||
QString containerDescription = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::DescriptionRole).toString();
|
||||
QString expectedDescription = ContainerUtils::containerDescriptions().value(DockerContainer::Awg);
|
||||
QVERIFY2(containerDescription == expectedDescription, QString("Container description should match, got '%1'").arg(containerDescription).toUtf8().constData());
|
||||
|
||||
QString detailedDescription = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::DetailedDescriptionRole).toString();
|
||||
QString expectedDetailedDescription = ContainerUtils::containerDetailedDescriptions().value(DockerContainer::Awg);
|
||||
QVERIFY2(detailedDescription == expectedDetailedDescription, QString("Container detailed description should match, got '%1'").arg(detailedDescription).toUtf8().constData());
|
||||
|
||||
int serviceType = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::ServiceTypeRole).toInt();
|
||||
QVERIFY2(serviceType == static_cast<int>(ProtocolEnumNS::ServiceType::Vpn), "Service type should be Vpn");
|
||||
|
||||
bool isSupported = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsSupportedRole).toBool();
|
||||
QVERIFY2(isSupported == true, "Container should be supported");
|
||||
|
||||
bool isShareable = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsShareableRole).toBool();
|
||||
QVERIFY2(isShareable == true, "Container should be shareable");
|
||||
|
||||
QJsonObject containerConfig = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::ConfigRole).toJsonObject();
|
||||
QVERIFY2(!containerConfig.isEmpty(), "Container config should not be empty");
|
||||
QVERIFY2(containerConfig.value(configKey::container).toString() == "amnezia-awg", "Container config should have correct container type");
|
||||
|
||||
QJsonObject awgProtocolConfig = containerConfig.value(configKey::awg).toObject();
|
||||
QVERIFY2(!awgProtocolConfig.isEmpty(), "AWG protocol config should not be empty");
|
||||
|
||||
QString protocolVersion = awgProtocolConfig.value(configKey::protocolVersion).toString();
|
||||
QVERIFY2(protocolVersion == protocols::awg::awgV2, QString("Protocol version should be '%1', got '%2'").arg(protocols::awg::awgV2, protocolVersion).toUtf8().constData());
|
||||
|
||||
QString port = awgProtocolConfig.value(configKey::port).toString();
|
||||
QVERIFY2(port == protocols::awg::defaultPort, QString("Port should be '%1', got '%2'").arg(protocols::awg::defaultPort, port).toUtf8().constData());
|
||||
|
||||
QString subnetAddress = awgProtocolConfig.value(configKey::subnetAddress).toString();
|
||||
QVERIFY2(subnetAddress == protocols::wireguard::defaultSubnetAddress, QString("Subnet address should be '%1', got '%2'").arg(protocols::wireguard::defaultSubnetAddress, subnetAddress).toUtf8().constData());
|
||||
|
||||
bool isThirdParty = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsThirdPartyConfigRole).toBool();
|
||||
QVERIFY2(isThirdParty == true, "Imported config should be third party config");
|
||||
|
||||
DockerContainer dockerContainer = static_cast<DockerContainer>(m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::DockerContainerRole).toInt());
|
||||
QVERIFY2(dockerContainer == DockerContainer::Awg, "Docker container should be Awg");
|
||||
|
||||
QString containerString = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::ContainerStringRole).toString();
|
||||
QVERIFY2(containerString == "amnezia-awg", "Container string should be amnezia-awg");
|
||||
}
|
||||
}
|
||||
|
||||
void testServerDescriptionFormat() {
|
||||
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
|
||||
|
||||
QJsonObject configNoDns = createServerDescriptionTestConfig(false);
|
||||
m_coreController->m_importCoreController->importConfig(configNoDns);
|
||||
QVERIFY2(importFinishedSpy.count() == 1, "importFinished should be emitted");
|
||||
m_coreController->m_appSettingsRepository->setUseAmneziaDns(false);
|
||||
QVector<ServerDescription> descriptionsNoDns = m_coreController->m_serversController->buildServerDescriptions(
|
||||
m_coreController->m_appSettingsRepository->useAmneziaDns());
|
||||
const QString defIdNoDns = m_coreController->m_serversRepository->defaultServerId();
|
||||
m_coreController->m_serversModel->updateModel(descriptionsNoDns, defaultServerRow(descriptionsNoDns, defIdNoDns));
|
||||
|
||||
QString descNoDns = m_coreController->m_serversModel->data(
|
||||
m_coreController->m_serversModel->index(0, 0), ServersModel::ServerDescriptionRole).toString();
|
||||
QVERIFY2(descNoDns == "test.example.com",
|
||||
QString("Without Amnezia DNS expected 'test.example.com', got '%1'").arg(descNoDns).toUtf8().constData());
|
||||
|
||||
m_coreController->m_serversRepository->clearServers();
|
||||
if (m_coreController->m_serversRepository->serversCount() > 0) {
|
||||
m_coreController->m_serversRepository->setDefaultServer(m_coreController->m_serversRepository->serverIdAt(0));
|
||||
}
|
||||
|
||||
QJsonObject configWithDns = createServerDescriptionTestConfig(true);
|
||||
m_coreController->m_importCoreController->importConfig(configWithDns);
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Server should be imported");
|
||||
m_coreController->m_appSettingsRepository->setUseAmneziaDns(true);
|
||||
QVector<ServerDescription> descriptionsWithDns = m_coreController->m_serversController->buildServerDescriptions(
|
||||
m_coreController->m_appSettingsRepository->useAmneziaDns());
|
||||
const QString defIdWithDns = m_coreController->m_serversRepository->defaultServerId();
|
||||
m_coreController->m_serversModel->updateModel(descriptionsWithDns, defaultServerRow(descriptionsWithDns, defIdWithDns));
|
||||
|
||||
QString descWithDns = m_coreController->m_serversModel->data(
|
||||
m_coreController->m_serversModel->index(0, 0), ServersModel::ServerDescriptionRole).toString();
|
||||
QVERIFY2(descWithDns == "Amnezia DNS | test.example.com",
|
||||
QString("With Amnezia DNS expected 'Amnezia DNS | test.example.com', got '%1'").arg(descWithDns).toUtf8().constData());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestUiServersModelAndController)
|
||||
#include "testUiServersModelAndController.moc"
|
||||
@@ -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>
|
||||
|
||||
@@ -65,6 +65,7 @@ SubscriptionUiController::SubscriptionUiController(ServersController* serversCon
|
||||
ApiCountryModel* apiCountryModel,
|
||||
ApiDevicesModel* apiDevicesModel,
|
||||
SettingsController* settingsController,
|
||||
ConnectionController* connectionController,
|
||||
QObject *parent)
|
||||
: QObject(parent),
|
||||
m_serversController(serversController),
|
||||
@@ -76,13 +77,34 @@ SubscriptionUiController::SubscriptionUiController(ServersController* serversCon
|
||||
m_apiAccountInfoModel(apiAccountInfoModel),
|
||||
m_apiCountryModel(apiCountryModel),
|
||||
m_apiDevicesModel(apiDevicesModel),
|
||||
m_settingsController(settingsController)
|
||||
m_settingsController(settingsController),
|
||||
m_connectionController(connectionController)
|
||||
{
|
||||
connect(m_apiServicesModel, &ApiServicesModel::serviceSelectionChanged, this, [this]() {
|
||||
ApiServicesModel::ApiServicesData selectedServiceData = m_apiServicesModel->selectedServiceData();
|
||||
m_apiSubscriptionPlansModel->updateModel(selectedServiceData.subscriptionPlansJson);
|
||||
m_apiBenefitsModel->updateModel(selectedServiceData.benefits);
|
||||
});
|
||||
|
||||
connect(this, &SubscriptionUiController::installServerFromApiFinished, this,
|
||||
[this](const QString &, int preferredDefaultServerIndex) {
|
||||
if (m_connectionController->isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int selectedServerIndex = preferredDefaultServerIndex >= 0
|
||||
? preferredDefaultServerIndex
|
||||
: (m_serversController->getServersCount() - 1);
|
||||
const QString serverId = m_serversController->getServerId(selectedServerIndex);
|
||||
if (!serverId.isEmpty()) {
|
||||
m_serversController->setDefaultServer(serverId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool SubscriptionUiController::isCaptchaAwaitingUser() const
|
||||
{
|
||||
return m_captchaState.isPending;
|
||||
}
|
||||
|
||||
bool SubscriptionUiController::exportVpnKey(const QString &serverId, const QString &fileName)
|
||||
@@ -271,18 +293,105 @@ bool SubscriptionUiController::importFreeFromGateway()
|
||||
}
|
||||
|
||||
SubscriptionController::ProtocolData protocolData = m_subscriptionController->generateProtocolData(serviceProtocol);
|
||||
SubscriptionController::CaptchaInfo captchaInfo;
|
||||
|
||||
ErrorCode errorCode = m_subscriptionController->importServiceFromGateway(userCountryCode, serviceType,
|
||||
serviceProtocol, protocolData);
|
||||
serviceProtocol, protocolData,
|
||||
captchaInfo);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
|
||||
return true;
|
||||
} else if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfo.isRequired) {
|
||||
m_captchaState.userCountryCode = userCountryCode;
|
||||
m_captchaState.serviceType = serviceType;
|
||||
m_captchaState.serviceProtocol = serviceProtocol;
|
||||
m_captchaState.openvpnPrivKey = protocolData.certPrivKey;
|
||||
m_captchaState.wireguardClientPrivKey = protocolData.wireGuardClientPrivKey;
|
||||
m_captchaState.wireguardClientPubKey = protocolData.wireGuardClientPubKey;
|
||||
m_captchaState.xrayUuid = protocolData.xrayUuid;
|
||||
m_captchaState.isPending = true;
|
||||
|
||||
emit captchaRequired(captchaInfo.captchaId, captchaInfo.captchaImageBase64,
|
||||
captchaInfo.hint.isEmpty() ? tr("Enter the digits from the image to continue") : captchaInfo.hint);
|
||||
return false;
|
||||
} else {
|
||||
emit errorOccurred(errorCode);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void SubscriptionUiController::onCaptchaSolved(const QString &captchaId, const QString &solution)
|
||||
{
|
||||
if (!m_captchaState.isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
SubscriptionController::ProtocolData protocolData;
|
||||
protocolData.certPrivKey = m_captchaState.openvpnPrivKey;
|
||||
protocolData.wireGuardClientPrivKey = m_captchaState.wireguardClientPrivKey;
|
||||
protocolData.wireGuardClientPubKey = m_captchaState.wireguardClientPubKey;
|
||||
protocolData.xrayUuid = m_captchaState.xrayUuid;
|
||||
|
||||
SubscriptionController::CaptchaInfo retryCaptcha;
|
||||
ErrorCode errorCode = m_subscriptionController->resolveImportServiceCaptcha(
|
||||
m_captchaState.userCountryCode,
|
||||
m_captchaState.serviceType,
|
||||
m_captchaState.serviceProtocol,
|
||||
protocolData,
|
||||
captchaId,
|
||||
solution,
|
||||
&retryCaptcha);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
m_captchaState.isPending = false;
|
||||
emit captchaFlowDismissRequested();
|
||||
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
|
||||
return;
|
||||
}
|
||||
|
||||
if ((errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|
||||
|| errorCode == ErrorCode::ApiCaptchaRequiredError)
|
||||
&& retryCaptcha.isRequired) {
|
||||
emit captchaRequired(retryCaptcha.captchaId, retryCaptcha.captchaImageBase64,
|
||||
retryCaptcha.hint.isEmpty() ? tr("Enter the digits from the image to continue") : retryCaptcha.hint);
|
||||
return;
|
||||
}
|
||||
|
||||
m_captchaState.isPending = false;
|
||||
emit errorOccurred(errorCode);
|
||||
}
|
||||
|
||||
void SubscriptionUiController::onRefreshCaptchaRequested()
|
||||
{
|
||||
if (!m_captchaState.isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
SubscriptionController::ProtocolData protocolData;
|
||||
protocolData.certPrivKey = m_captchaState.openvpnPrivKey;
|
||||
protocolData.wireGuardClientPrivKey = m_captchaState.wireguardClientPrivKey;
|
||||
protocolData.wireGuardClientPubKey = m_captchaState.wireguardClientPubKey;
|
||||
protocolData.xrayUuid = m_captchaState.xrayUuid;
|
||||
|
||||
SubscriptionController::CaptchaInfo captchaInfo;
|
||||
|
||||
ErrorCode errorCode = m_subscriptionController->importServiceFromGateway(
|
||||
m_captchaState.userCountryCode,
|
||||
m_captchaState.serviceType,
|
||||
m_captchaState.serviceProtocol,
|
||||
protocolData,
|
||||
captchaInfo);
|
||||
|
||||
if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfo.isRequired) {
|
||||
emit captchaRequired(captchaInfo.captchaId, captchaInfo.captchaImageBase64,
|
||||
captchaInfo.hint.isEmpty() ? tr("Enter the digits from the image to continue") : captchaInfo.hint);
|
||||
} else if (errorCode != ErrorCode::NoError) {
|
||||
m_captchaState.isPending = false;
|
||||
emit errorOccurred(errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
bool SubscriptionUiController::importTrialFromGateway(const QString &email)
|
||||
{
|
||||
emit trialEmailError(QString());
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "core/controllers/serversController.h"
|
||||
#include "core/controllers/settingsController.h"
|
||||
#include "core/controllers/connectionController.h"
|
||||
#include "core/controllers/api/servicesCatalogController.h"
|
||||
#include "core/controllers/api/subscriptionController.h"
|
||||
#include "ui/models/api/apiSubscriptionPlansModel.h"
|
||||
@@ -28,6 +29,7 @@ public:
|
||||
ApiCountryModel* apiCountryModel,
|
||||
ApiDevicesModel* apiDevicesModel,
|
||||
SettingsController* settingsController,
|
||||
ConnectionController* connectionController,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
Q_PROPERTY(QList<QString> qrCodes READ getQrCodes NOTIFY vpnKeyExportReady)
|
||||
@@ -56,6 +58,10 @@ public slots:
|
||||
void setCurrentProtocol(const QString &serverId, const QString &protocolName);
|
||||
bool isVlessProtocol(const QString &serverId);
|
||||
|
||||
bool isCaptchaAwaitingUser() const;
|
||||
void onCaptchaSolved(const QString &captchaId, const QString &solution);
|
||||
void onRefreshCaptchaRequested();
|
||||
|
||||
void removeApiConfig(const QString &serverId);
|
||||
|
||||
void removeServer(const QString &serverId);
|
||||
@@ -83,9 +89,23 @@ signals:
|
||||
void apiServerRemoved(const QString &message);
|
||||
|
||||
void vpnKeyExportReady();
|
||||
void captchaRequired(const QString &captchaId, const QString &captchaImageBase64, const QString &hint);
|
||||
void captchaFlowDismissRequested();
|
||||
|
||||
void unsupportedConnectDrawerRequested();
|
||||
|
||||
private:
|
||||
struct CaptchaState {
|
||||
QString userCountryCode;
|
||||
QString serviceType;
|
||||
QString serviceProtocol;
|
||||
QString openvpnPrivKey;
|
||||
QString wireguardClientPrivKey;
|
||||
QString wireguardClientPubKey;
|
||||
QString xrayUuid;
|
||||
bool isPending = false;
|
||||
} m_captchaState;
|
||||
|
||||
private:
|
||||
QList<QString> getQrCodes();
|
||||
int getQrCodesCount();
|
||||
@@ -104,6 +124,7 @@ private:
|
||||
ApiCountryModel* m_apiCountryModel;
|
||||
ApiDevicesModel* m_apiDevicesModel;
|
||||
SettingsController* m_settingsController;
|
||||
ConnectionController* m_connectionController;
|
||||
};
|
||||
|
||||
#endif // SUBSCRIPTIONUICONTROLLER_H
|
||||
|
||||
@@ -44,7 +44,6 @@ signals:
|
||||
void connectionStateChanged();
|
||||
|
||||
void connectionErrorOccurred(ErrorCode errorCode);
|
||||
void reconnectWithUpdatedContainer(const QString &message);
|
||||
|
||||
void connectButtonClicked();
|
||||
void preparingConfig();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user